Merge remote-tracking branch 'origin/master' into reports
# Conflicts: # ereuse_devicehub/resources/device/views.py
This commit is contained in:
commit
ee231aecb9
20
README.md
20
README.md
|
@ -1,7 +1,8 @@
|
||||||
# Devicehub
|
# Devicehub
|
||||||
|
|
||||||
Devicehub is an IT Asset Management System focused in reusing devices,
|
Devicehub is a distributed IT Asset Management System focused in
|
||||||
created under the project [eReuse.org](https://www.ereuse.org).
|
reusing devices, created under the project
|
||||||
|
[eReuse.org](https://www.ereuse.org).
|
||||||
|
|
||||||
Our main objectives are:
|
Our main objectives are:
|
||||||
|
|
||||||
|
@ -35,10 +36,12 @@ call the new file ``app.py``.
|
||||||
Create a PostgreSQL database called *devicehub* by running
|
Create a PostgreSQL database called *devicehub* by running
|
||||||
[create-db](examples/create-db.sh):
|
[create-db](examples/create-db.sh):
|
||||||
|
|
||||||
- In a Debian 9 terminal, execute the following two commands:
|
- In a Debian 9 bash terminal, execute the following two commands:
|
||||||
1. `sudo su - postgres`.
|
1. `sudo su - postgres`.
|
||||||
2. `bash examples/create-db.sh devicehub`.
|
2. `bash examples/create-db.sh devicehub dhub`,
|
||||||
- In MacOS: `examples/create-db.sh devicehub`.
|
and password `ereuse`.
|
||||||
|
- In MacOS: `bash examples/create-db.sh devicehub dhub`,
|
||||||
|
and password `ereuse`.
|
||||||
|
|
||||||
Create the tables in the database by executing in the same directory
|
Create the tables in the database by executing in the same directory
|
||||||
where `app.py` is:
|
where `app.py` is:
|
||||||
|
@ -85,7 +88,8 @@ To run the tests you will need to:
|
||||||
1. `git clone` this project.
|
1. `git clone` this project.
|
||||||
2. Create a database for testing executing `create-db.sh` like
|
2. Create a database for testing executing `create-db.sh` like
|
||||||
the normal installation but changing the first parameter
|
the normal installation but changing the first parameter
|
||||||
from `devicehub` to `dh_test`: `create-db.sh dh_test`.
|
from `devicehub` to `dh_test`: `create-db.sh dh_test dhub` and
|
||||||
|
password `ereuse`.
|
||||||
3. Execute at the root folder of the project `python3 setup.py test`.
|
3. Execute at the root folder of the project `python3 setup.py test`.
|
||||||
|
|
||||||
## Generating the docs
|
## Generating the docs
|
||||||
|
@ -94,3 +98,7 @@ To run the tests you will need to:
|
||||||
3. Execute `pip3 install -e .[docs]` in the project root folder.
|
3. Execute `pip3 install -e .[docs]` in the project root folder.
|
||||||
3. Go to `<project root folder>/docs` and execute `make html`.
|
3. Go to `<project root folder>/docs` and execute `make html`.
|
||||||
Repeat this step to generate new docs.
|
Repeat this step to generate new docs.
|
||||||
|
|
||||||
|
To auto-generate the docs do `pip3 install -e .[docs-auto]`, then
|
||||||
|
execute, in the root folder of the project
|
||||||
|
`sphinx-autobuild docs docs/_build/html`.
|
||||||
|
|
335
docs/actions.rst
335
docs/actions.rst
|
@ -38,43 +38,27 @@ to the `Swagger docs
|
||||||
|
|
||||||
Physical Actions
|
Physical Actions
|
||||||
****************
|
****************
|
||||||
The following actions describe and react on the physical condition
|
The following actions describe and react on the
|
||||||
|
:class:`ereuse_devicehub.resources.device.states.Physical` condition
|
||||||
of the devices.
|
of the devices.
|
||||||
|
|
||||||
ToPrepare, Prepare
|
ToPrepare, Prepare
|
||||||
==================
|
==================
|
||||||
Work has been performed to the device to a defined point of
|
.. autoclass:: ereuse_devicehub.resources.event.models.Prepare
|
||||||
acceptance. Users using this event have to agree what is this point
|
.. autoclass:: ereuse_devicehub.resources.event.models.ToPrepare
|
||||||
of acceptance; for some is when the device just works, for others
|
|
||||||
when some testing has been performed.
|
|
||||||
|
|
||||||
**Prepare** dictates that the device has been prepared, whereas
|
|
||||||
**ToPrepare** that the device has been selected to be prepared.
|
|
||||||
|
|
||||||
Usually **ToPrepare** is the next event done after registering the
|
|
||||||
device.
|
|
||||||
|
|
||||||
ToRepair, Repair
|
ToRepair, Repair
|
||||||
================
|
================
|
||||||
ToRepair is the act of selecting a device to be repaired, and
|
.. autoclass:: ereuse_devicehub.resources.event.models.Repair
|
||||||
Repair the act of performing the actual reparations. If a repair
|
.. autoclass:: ereuse_devicehub.resources.event.models.ToRepair
|
||||||
without an error is performed, it represents that the reparation
|
|
||||||
has been successful.
|
|
||||||
|
|
||||||
ReadyToUse
|
ReadyToUse
|
||||||
==========
|
==========
|
||||||
The device is ready to be used. This involves greater preparation
|
.. autoclass:: ereuse_devicehub.resources.event.models.ReadyToUse
|
||||||
from the ``Prepare`` event, and users should only use a device
|
|
||||||
after this event is performed.
|
|
||||||
|
|
||||||
Users usually require devices with this event before shipping them
|
|
||||||
to costumers.
|
|
||||||
|
|
||||||
Live
|
Live
|
||||||
====
|
====
|
||||||
A keep-alive from a device connected to the Internet with information
|
.. autoclass:: ereuse_devicehub.resources.event.models.Live
|
||||||
about its state (in the form of a ``Snapshot`` event) and usage
|
|
||||||
statistics.
|
|
||||||
|
|
||||||
DisposeWaste, Recover
|
DisposeWaste, Recover
|
||||||
=====================
|
=====================
|
||||||
|
@ -86,6 +70,8 @@ DisposeWaste, Recover
|
||||||
|
|
||||||
See `ToDisposeProduct, DisposeProduct`_.
|
See `ToDisposeProduct, DisposeProduct`_.
|
||||||
|
|
||||||
|
.. todo:: Events not developed yet.
|
||||||
|
|
||||||
Association actions
|
Association actions
|
||||||
*******************
|
*******************
|
||||||
Actions that change the associations users have with devices;
|
Actions that change the associations users have with devices;
|
||||||
|
@ -99,43 +85,29 @@ and **organize** actions.
|
||||||
|
|
||||||
Trade actions
|
Trade actions
|
||||||
=============
|
=============
|
||||||
Trade actions log the political exchange of devices between users,
|
Not fully developed.
|
||||||
stating **owner** xor **usufructuaree**. Every time a trade event
|
.. autoclass:: ereuse_devicehub.resources.event.models.Trade
|
||||||
is performed, the old user looses its political possession in favor
|
|
||||||
of another one.
|
|
||||||
|
|
||||||
Sell
|
Sell
|
||||||
----
|
----
|
||||||
The act of taking money from a buyer in exchange of a device.
|
.. autoclass:: ereuse_devicehub.resources.event.models.Sell
|
||||||
|
|
||||||
Donate
|
Donate
|
||||||
------
|
------
|
||||||
The act of giving devices without compensation.
|
.. autoclass:: ereuse_devicehub.resources.event.models.Donate
|
||||||
|
|
||||||
Rent
|
Rent
|
||||||
----
|
----
|
||||||
The act of giving money in return for temporary use, but not
|
.. autoclass:: ereuse_devicehub.resources.event.models.Rent
|
||||||
ownership, of a device.
|
|
||||||
|
|
||||||
CancelTrade
|
CancelTrade
|
||||||
-----------
|
-----------
|
||||||
The act of cancelling a `Sell`_, `Donate`_ or `Rent`_.
|
.. autoclass:: ereuse_devicehub.resources.event.models.CancelTrade
|
||||||
|
|
||||||
ToDisposeProduct, DisposeProduct
|
ToDisposeProduct, DisposeProduct
|
||||||
-------------------------
|
--------------------------------
|
||||||
``ToDispose`` and ``DisposeProduct`` manage the process of getting
|
.. autoclass:: ereuse_devicehub.resources.event.models.DisposeProduct
|
||||||
rid of devices by giving (selling, donating) to another organization
|
.. autoclass:: ereuse_devicehub.resources.event.models.ToDisposeProduct
|
||||||
like a waste manager.
|
|
||||||
|
|
||||||
``ToDispose`` marks a device for being disposed, and
|
|
||||||
``DisposeProduct`` dictates that the device has been disposed.
|
|
||||||
|
|
||||||
See `DisposeWaste, Recover`_ events for disposing without trading
|
|
||||||
the device.
|
|
||||||
|
|
||||||
.. note:: For usability purposes, users might not directly perform
|
|
||||||
``Dispose``, but this could automatically be done when
|
|
||||||
performing ``ToDispose`` + ``Receive`` to a ``RecyclingCenter``.
|
|
||||||
|
|
||||||
Transfer actions
|
Transfer actions
|
||||||
================
|
================
|
||||||
|
@ -143,34 +115,27 @@ The act of transferring/moving devices from one place to another.
|
||||||
|
|
||||||
Receive
|
Receive
|
||||||
-------
|
-------
|
||||||
The act of physically taking delivery of a device. The receiver
|
.. autoclass:: ereuse_devicehub.resources.event.models.Receive
|
||||||
confirms that the devices have arrived, and thus, they
|
.. autoclass:: ereuse_devicehub.resources.enums.ReceiverRole
|
||||||
**physically possess** them. Note that
|
:members:
|
||||||
there can only be one **physical possessor** per device, and
|
:undoc-members:
|
||||||
``Receive`` changes it.
|
.. autoattribute:: ereuse_devicehub.resources.device.models.Device.physical_possessor
|
||||||
|
|
||||||
The receiver can optionally take a role in the reception, giving
|
|
||||||
it meaning; an user that takes the ``FinalUser`` role in the
|
|
||||||
reception express that it will use the device, whereas a role
|
|
||||||
``Transporter`` is used by intermediaries in shipping.
|
|
||||||
|
|
||||||
.. todo:: how do we ensure users specify type of reception?
|
|
||||||
|
|
||||||
Organize actions
|
Organize actions
|
||||||
================
|
================
|
||||||
The act of manipulating/administering/supervising/controlling one or
|
.. autoclass:: ereuse_devicehub.resources.event.models.Organize
|
||||||
more devices.
|
|
||||||
|
|
||||||
Reserve, CancelReservation
|
Reserve, CancelReservation
|
||||||
--------------------------
|
-------------------------
|
||||||
The act of reserving devices and cancelling them.
|
Not fully developed.
|
||||||
|
|
||||||
After this event is performed, the user is the **reservee** of the
|
.. autoclass:: ereuse_devicehub.resources.event.models.Reserve
|
||||||
devices. There can only be one non-cancelled reservation for
|
.. autoclass:: ereuse_devicehub.resources.event.models.CancelReservation
|
||||||
a device, and a reservation can only have one reservee.
|
|
||||||
|
|
||||||
Assign, Accept, Reject
|
Assign, Accept, Reject
|
||||||
----------------------
|
----------------------
|
||||||
|
Not developed.
|
||||||
|
|
||||||
``Assign`` allocates devices to an user. The purpose or meaning
|
``Assign`` allocates devices to an user. The purpose or meaning
|
||||||
of the association is defined by the users.
|
of the association is defined by the users.
|
||||||
|
|
||||||
|
@ -179,9 +144,7 @@ assignments.
|
||||||
|
|
||||||
.. todo:: shall we add ``Deassign`` or make ``Assign``
|
.. todo:: shall we add ``Deassign`` or make ``Assign``
|
||||||
always define all active users?
|
always define all active users?
|
||||||
|
Assign won't be developed until further notice.
|
||||||
.. todo:: Assign won't be developed until further notice.
|
|
||||||
|
|
||||||
|
|
||||||
Internal state actions
|
Internal state actions
|
||||||
**********************
|
**********************
|
||||||
|
@ -190,254 +153,88 @@ their state.
|
||||||
|
|
||||||
Snapshot
|
Snapshot
|
||||||
========
|
========
|
||||||
The Snapshot sets the physical information of the device (S/N, model...)
|
.. autoclass:: ereuse_devicehub.resources.event.models.Snapshot
|
||||||
and updates it with erasures, benchmarks, ratings, and tests; updates the
|
|
||||||
composition of its components (adding / removing them), and links tags
|
|
||||||
to the device.
|
|
||||||
|
|
||||||
When receiving a Snapshot, the DeviceHub creates, adds and removes
|
|
||||||
components to match the Snapshot. For example, if a Snapshot of a computer
|
|
||||||
contains a new component, the system searches for the component in its
|
|
||||||
database and, if not found, its creates it; finally linking it to the
|
|
||||||
computer.
|
|
||||||
|
|
||||||
A Snapshot is used with Remove to represent changes in components for
|
|
||||||
a device:
|
|
||||||
|
|
||||||
1. ``Snapshot`` creates a device if it does not exist, and the same
|
|
||||||
for its components. This is all done in one ``Snapshot``.
|
|
||||||
2. If the device exists, it updates its component composition by
|
|
||||||
*adding* and *removing* them. If,
|
|
||||||
for example, this new Snasphot doesn't have a component, it means that
|
|
||||||
this component is not present anymore in the device, thus removing it
|
|
||||||
from it. Then we have that:
|
|
||||||
|
|
||||||
- Components that are added to the device: snapshot2.components -
|
|
||||||
snapshot1.components
|
|
||||||
- Components that are removed to the device: snapshot1.components -
|
|
||||||
snapshot2.components
|
|
||||||
|
|
||||||
When adding a component, there may be the case this component existed
|
|
||||||
before and it was inside another device. In such case, DeviceHub will
|
|
||||||
perform ``Remove`` on the old parent.
|
|
||||||
|
|
||||||
Snapshots from Workbench
|
|
||||||
------------------------
|
|
||||||
When processing a device from the Workbench, this one performs a Snapshot
|
|
||||||
and then performs more events (like testings, benchmarking...).
|
|
||||||
|
|
||||||
There are two ways of sending this information. In an async way,
|
|
||||||
this is, submitting events as soon as Workbench performs then, or
|
|
||||||
submitting only one Snapshot event with all the other events embedded.
|
|
||||||
|
|
||||||
Asynced
|
|
||||||
^^^^^^^
|
|
||||||
The use case, which is represented in the ``test_workbench_phases``,
|
|
||||||
is as follows:
|
|
||||||
|
|
||||||
1. In **T1**, WorkbenchServer (as the middleware from Workbench and
|
|
||||||
Devicehub) submits:
|
|
||||||
|
|
||||||
- A ``Snapshot`` event with the required information to **synchronize**
|
|
||||||
and **rate** the device. This is:
|
|
||||||
|
|
||||||
- Identification information about the device and components
|
|
||||||
(S/N, model, physical characteristics...)
|
|
||||||
- ``Tags`` in a ``tags`` property in the ``device``.
|
|
||||||
- ``Rate`` in an ``events`` property in the ``device``.
|
|
||||||
- ``Benchmarks`` in an ``events`` property in each ``component``
|
|
||||||
or ``device``.
|
|
||||||
- ``TestDataStorage`` as in ``Benchmarks``.
|
|
||||||
- An ordered set of **expected events**, defining which are the next
|
|
||||||
events that Workbench will perform to the device in ideal
|
|
||||||
conditions (device doesn't fail, no Internet drop...).
|
|
||||||
|
|
||||||
Devicehub **syncs** the device with the database and perform the
|
|
||||||
``Benchmark``, the ``TestDataStorage``, and finally the ``Rate``.
|
|
||||||
This leaves the Snapshot **open** to wait for the next events
|
|
||||||
to come.
|
|
||||||
2. Assuming that we expect all events, in **T2**, WorkbenchServer
|
|
||||||
submits a ``StressTest`` with a ``snapshot`` field containing the
|
|
||||||
ID of the Snapshot in 1, and Devicehub links the event with such
|
|
||||||
``Snapshot``.
|
|
||||||
3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot``
|
|
||||||
and ``component`` IDs from 1, linking it to them. It repeats
|
|
||||||
this for all the erased data storage devices; **T3+Tn** being
|
|
||||||
*n* the erased data storage devices.
|
|
||||||
4. WorkbenchServer does like in 3. but for the event ``Install``,
|
|
||||||
finishing in **T3+Tn+Tx**, being *x* the number of data storage
|
|
||||||
devices with an OS installed into.
|
|
||||||
5. In **T3+Tn+Tx**, when all *expected events* have been performed,
|
|
||||||
Devicehub **closes** the ``Snapshot`` from 1.
|
|
||||||
|
|
||||||
Synced
|
|
||||||
^^^^^^
|
|
||||||
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
|
||||||
the events in an ``events`` property inside each affected ``component``
|
|
||||||
or ``device``.
|
|
||||||
|
|
||||||
Add, Remove
|
Add, Remove
|
||||||
===========
|
===========
|
||||||
The act of adding and removing components of and from a device.
|
.. autoclass:: ereuse_devicehub.resources.event.models.Add
|
||||||
|
.. autoclass:: ereuse_devicehub.resources.event.models.Remove
|
||||||
These are usually used internally from `Snapshot`_, or manually, for
|
|
||||||
example, when removing a component (like a ``DataStorage`` unit) from
|
|
||||||
a broken computer.
|
|
||||||
|
|
||||||
EraseBasic, EraseSectors
|
EraseBasic, EraseSectors
|
||||||
========================
|
========================
|
||||||
An erasure attempt to a ``DataStorage``. The event contains
|
.. autoclass:: ereuse_devicehub.resources.event.models.EraseBasic
|
||||||
information about success and nature of the erasure.
|
.. autoclass:: ereuse_devicehub.resources.event.models.EraseSectors
|
||||||
|
.. autoclass:: ereuse_devicehub.resources.event.models.ErasePhysical
|
||||||
``EraseBasic`` is a fast non-secured way of erasing data storage, and
|
|
||||||
``EraseSectors`` is a slower secured, sector-by-sector, erasure
|
|
||||||
method.
|
|
||||||
|
|
||||||
Users can generate erasure certificates from successful erasures.
|
|
||||||
|
|
||||||
Erasures are an accumulation of **erasure steps**, that are performed
|
|
||||||
as separate actions, called ``StepRandom``, for an erasure step
|
|
||||||
that has overwritten data with random bits, and ``StepZero``,
|
|
||||||
for an erasure step that has overwritten data with zeros.
|
|
||||||
|
|
||||||
Install
|
Install
|
||||||
=======
|
=======
|
||||||
The action of install an Operative System to a data storage unit.
|
.. autoclass:: ereuse_devicehub.resources.event.models.Install
|
||||||
|
|
||||||
Test
|
Test
|
||||||
====
|
====
|
||||||
The act of testing the physical condition of a device and its
|
.. autoclass:: ereuse_devicehub.resources.event.models.Test
|
||||||
components.
|
|
||||||
|
|
||||||
TestDataStorage
|
TestDataStorage
|
||||||
---------------
|
---------------
|
||||||
The act of testing the data storage.
|
.. autoclass:: ereuse_devicehub.resources.event.models.TestDataStorage
|
||||||
|
|
||||||
Testing is done using the `S.M.A.R.T self test
|
|
||||||
<https://en.wikipedia.org/wiki/S.M.A.R.T.#Self-tests>`_. Note
|
|
||||||
that not all data storage units, specially some new PCIe ones, do not
|
|
||||||
support SMART testing.
|
|
||||||
|
|
||||||
The test takes to other SMART values indicators of the overall health
|
|
||||||
of the data storage.
|
|
||||||
|
|
||||||
StressTest
|
StressTest
|
||||||
----------
|
----------
|
||||||
The act of stressing (putting to the maximum capacity)
|
.. autoclass:: ereuse_devicehub.resources.event.models.StressTest
|
||||||
a device for an amount of minutes. If the device is not in great
|
|
||||||
condition won't probably survive such test.
|
|
||||||
|
|
||||||
Benchmark
|
Benchmark
|
||||||
=========
|
=========
|
||||||
The act of gauging the performance of a device.
|
.. autoclass:: ereuse_devicehub.resources.event.models.Benchmark
|
||||||
|
|
||||||
|
|
||||||
BenchmarkDataStorage
|
BenchmarkDataStorage
|
||||||
--------------------
|
--------------------
|
||||||
Benchmarks the data storage unit reading and writing speeds.
|
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkDataStorage
|
||||||
|
|
||||||
|
|
||||||
BenchmarkWithRate
|
BenchmarkWithRate
|
||||||
-----------------
|
-----------------
|
||||||
The act of benchmarking a device with a single rate.
|
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkWithRate
|
||||||
|
|
||||||
|
|
||||||
BenchmarkProcessor
|
BenchmarkProcessor
|
||||||
------------------
|
------------------
|
||||||
Benchmarks a processor by executing `BogoMips
|
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkProcessor
|
||||||
<https://en.wikipedia.org/wiki/BogoMips>`_. Note that this is not
|
|
||||||
a reliable way of rating processors and we keep it for compatibility
|
|
||||||
purposes.
|
|
||||||
|
|
||||||
BenchmarkProcessorSysbench
|
BenchmarkProcessorSysbench
|
||||||
--------------------------
|
--------------------------
|
||||||
Benchmarks a processor by using the processor benchmarking utility of
|
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkProcessorSysbench
|
||||||
`sysbench <https://github.com/akopytov/sysbench>`_.
|
|
||||||
|
|
||||||
|
|
||||||
|
BenchmarkRamSysbench
|
||||||
|
--------------------
|
||||||
|
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkRamSysbench
|
||||||
|
|
||||||
Rate
|
Rate
|
||||||
====
|
====
|
||||||
Devicehub generates an rating for a device taking into consideration the
|
.. autoclass:: ereuse_devicehub.resources.event.models.Rate
|
||||||
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 two rates: ``WorkbenchRate``
|
|
||||||
and ``PhotoboxRate``.
|
|
||||||
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.
|
|
||||||
|
|
||||||
There are three **types** of ``Rate``: ``WorkbenchRate``,
|
|
||||||
``AppRate``, and ``PhotoboxRate``. ``WorkbenchRate`` can have different
|
|
||||||
**software** algorithms, and each software algorithm can have several
|
|
||||||
**versions**. So, we have 3 dimensions for ``WorkbenchRate``:
|
|
||||||
type, software, version.
|
|
||||||
|
|
||||||
Devicehub generates a rate event for each software and version. So,
|
|
||||||
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),
|
|
||||||
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 technical Workflow in Devicehub is as follows:
|
|
||||||
|
|
||||||
1. In **T1**, the user 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
|
|
||||||
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.
|
|
||||||
2. In **T2**, the user takes pictures from the device through the
|
|
||||||
Photobox, and DeviceHub crates an ``ImageSet`` with multiple
|
|
||||||
``Image`` with information from the photobox.
|
|
||||||
3. In **T3**, an agent (user or AI) rates the pictures, creating a
|
|
||||||
``PhotoboxRate`` **for each** picture. When Devicehub receives the
|
|
||||||
first ``PhotoboxRate`` it creates an ``AggregateRating`` linked
|
|
||||||
to such ``PhotoboxRate``. So, the agent will perform as many
|
|
||||||
``PhotoboxRate`` as pictures are in the ``ImageSet``, and Devicehub
|
|
||||||
will link each ``PhotoboxRate`` to the same ``AggregateRating``.
|
|
||||||
This will end in **T3+Tn**, being *n* the number of photos to rate.
|
|
||||||
4. In **T3+Tn**, after the last photo is rated, Devicehub will generate
|
|
||||||
a new rate for the device: it takes the ``AggregateRating`` from 3.
|
|
||||||
and computes a rate from all the linked ``PhotoboxRate`` plus the
|
|
||||||
last available ``WorkbenchRate`` for that device.
|
|
||||||
|
|
||||||
If the agent in 3. is an user, Devicehub creates ``PhotoboxUserRate``
|
|
||||||
and if it is an AI it creates ``PhotoboxAIRate``.
|
|
||||||
|
|
||||||
The same ``ImageSet`` can be rated multiple times, generating a new
|
|
||||||
``AggregateRating`` each time.
|
|
||||||
|
|
||||||
Price
|
Price
|
||||||
=====
|
=====
|
||||||
Price states a selling price for the device, but not necessariliy the
|
.. autoclass:: ereuse_devicehub.resources.event.models.Price
|
||||||
final price this was sold (which is set in the Sell event).
|
|
||||||
|
|
||||||
Devicehub automatically computes a price from ``AggregateRating``
|
|
||||||
events. As in a **Rate**, price can have **software** and **version**,
|
|
||||||
and there is an **official** price that is used to automatically
|
|
||||||
compute the price from an ``AggregateRating``. Only the official price
|
|
||||||
is computed from an ``AggregateRating``.
|
|
||||||
|
|
||||||
Migrate
|
Migrate
|
||||||
=======
|
=======
|
||||||
Moves the devices to a new database/inventory. Devices cannot be
|
Not done.
|
||||||
modified anymore at the previous database.
|
|
||||||
|
|
||||||
Donation
|
|
||||||
========
|
|
||||||
.. todo:: nextcloud/eReuse/99. Tasks/224. Definir datos necesarios
|
|
||||||
configuración licencia
|
|
||||||
|
|
||||||
|
.. autoclass:: ereuse_devicehub.resources.event.models.Migrate
|
||||||
|
|
||||||
States
|
States
|
||||||
******
|
******
|
||||||
.. todo:: work on september.
|
.. autoclass:: ereuse_devicehub.resources.device.states.State
|
||||||
|
|
||||||
.. uml:: states.puml
|
.. uml:: states.puml
|
||||||
|
|
||||||
|
.. autoclass:: ereuse_devicehub.resources.device.states.Trading
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
.. autoclass:: ereuse_devicehub.resources.device.states.Physical
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
#
|
#
|
||||||
# import os
|
# import os
|
||||||
# import sys
|
# import sys
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
# sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
@ -42,7 +42,8 @@ extensions = [
|
||||||
'sphinx.ext.todo',
|
'sphinx.ext.todo',
|
||||||
'sphinx.ext.viewcode',
|
'sphinx.ext.viewcode',
|
||||||
'sphinxcontrib.plantuml',
|
'sphinxcontrib.plantuml',
|
||||||
'sphinx.ext.autosectionlabel'
|
'sphinx.ext.autosectionlabel',
|
||||||
|
'sphinx.ext.autodoc'
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
@ -159,7 +160,7 @@ texinfo_documents = [
|
||||||
# -- Options for intersphinx extension ---------------------------------------
|
# -- Options for intersphinx extension ---------------------------------------
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {'https://docs.python.org/': None}
|
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
||||||
|
|
||||||
# -- Options for todo extension ----------------------------------------------
|
# -- Options for todo extension ----------------------------------------------
|
||||||
|
|
||||||
|
@ -174,3 +175,4 @@ html_favicon = 'img/favicon.ico'
|
||||||
|
|
||||||
# autosectionlabel
|
# autosectionlabel
|
||||||
autosectionlabel_prefix_document = True
|
autosectionlabel_prefix_document = True
|
||||||
|
autodoc_member_order = 'bysource'
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
Inventory
|
Devices
|
||||||
#########
|
#########
|
||||||
|
|
||||||
Devicehub uses the same path to get devices and lots.
|
You can retrieve devices using ``GET /devices/``, or a specific
|
||||||
|
device by ``GET /devices/24``.
|
||||||
|
|
||||||
To get all devices and groups: ``GET /inventory`` or the devices of a
|
You can **filter** devices ``GET /devices/?filter={"type": "Computer"}``,
|
||||||
specific groups: ``GET /inventory/24``.
|
**sort** them ``GET /devices/?sort={"created": 1}``, and perform
|
||||||
|
natural search with ``GET /devices/?search=foo bar. Of course
|
||||||
You can **filter** devices ``GET /inventory/24?filter={"type": "Computer"}``,
|
you can combine them in the same query, returning devices that
|
||||||
and **sort** them ``GET /inventory?sort={"created": 1}``, and of course
|
only pass all conditions.
|
||||||
you can combine both in the same query. You only get the groups that
|
|
||||||
contain the devices that pass the filters. So, if a group contains
|
|
||||||
only one device that is filtered, you don't get that group neither.
|
|
||||||
|
|
||||||
Results are **paginated**; you get up to 30 devices and up to 30
|
Results are **paginated**; you get up to 30 devices and up to 30
|
||||||
groups in a page. Select the actual page by ``GET /inventory?page=3``.
|
groups in a page. Select the actual page by ``GET /inventory?page=3``.
|
||||||
|
@ -21,21 +19,10 @@ Query
|
||||||
The query consists of 4 optional params:
|
The query consists of 4 optional params:
|
||||||
|
|
||||||
- **search**: Filters devices by performing a full-text search over their
|
- **search**: Filters devices by performing a full-text search over their
|
||||||
physical properties, events, tags, and groups they are in:
|
physical properties, events, and tags. Search is a string.
|
||||||
|
|
||||||
- Device.type
|
|
||||||
- Device.serial_number
|
|
||||||
- Device.model
|
|
||||||
- Device.manufacturer
|
|
||||||
- Device.color
|
|
||||||
- Tag.id
|
|
||||||
- Tag.org
|
|
||||||
- Group.name
|
|
||||||
|
|
||||||
Search is a string.
|
|
||||||
- **filter**: Filters devices field-by-field. Each field can be
|
- **filter**: Filters devices field-by-field. Each field can be
|
||||||
filtered in different ways, see them in
|
filtered in different ways, see them in
|
||||||
:class:`ereuse_devicehub.resources.inventory.Filters`. Filter is
|
:class:`ereuse_devicehub.resources.devices.Filters`. Filter is
|
||||||
a JSON-encoded object whose keys are the filters. By default
|
a JSON-encoded object whose keys are the filters. By default
|
||||||
is empty (no filter applied).
|
is empty (no filter applied).
|
||||||
- **sort**: Sorts the devices. You can specify multiple sort clauses
|
- **sort**: Sorts the devices. You can specify multiple sort clauses
|
||||||
|
@ -59,3 +46,10 @@ The result is a JSON object with the following fields:
|
||||||
or ``1``.
|
or ``1``.
|
||||||
- **perPage**: How many devices are in every page, fixed to ``30``.
|
- **perPage**: How many devices are in every page, fixed to ``30``.
|
||||||
- **total**: How many total devices passed the filters.
|
- **total**: How many total devices passed the filters.
|
||||||
|
|
||||||
|
Models
|
||||||
|
******
|
||||||
|
|
||||||
|
.. automodule:: ereuse_devicehub.resources.device.models
|
||||||
|
:members:
|
||||||
|
:member-order: bysource
|
|
@ -14,7 +14,7 @@ This is the documentation and API of the `eReuse.org Devicehub
|
||||||
|
|
||||||
actions
|
actions
|
||||||
agents
|
agents
|
||||||
inventory
|
devices
|
||||||
tags
|
tags
|
||||||
lots
|
lots
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,15 @@ skinparam ranksep 1
|
||||||
[*] -> Registered
|
[*] -> Registered
|
||||||
|
|
||||||
state Attributes {
|
state Attributes {
|
||||||
|
|
||||||
state Broken : cannot turn on
|
state Broken : cannot turn on
|
||||||
state Owners
|
state Owners
|
||||||
state Usufructuarees
|
state Usufructuarees
|
||||||
state Reservees
|
state Reservees
|
||||||
state "Physical\nPossessor"
|
state "Physical\nPossessor"
|
||||||
state "Waste\n\Product"
|
state "Waste\n\Product"
|
||||||
|
state problems : List of current events \nwith Warn/Error
|
||||||
|
state privacy : Set of\ncurrent erasures
|
||||||
|
state working : List of current events\naffecting working
|
||||||
}
|
}
|
||||||
|
|
||||||
state Physical {
|
state Physical {
|
||||||
|
@ -44,10 +46,4 @@ state Trading {
|
||||||
Renting --> Cancelled : Cancel
|
Renting --> Cancelled : Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
state DataStoragePrivacyCompliance {
|
|
||||||
state Erased
|
|
||||||
state Destroyed
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
from distutils.version import StrictVersion
|
|
||||||
|
|
||||||
__version__ = '0.2.0a13'
|
|
||||||
version = StrictVersion(__version__)
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
from sqlalchemy import event
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
|
from sqlalchemy.sql import expression
|
||||||
|
from sqlalchemy_utils import view
|
||||||
from teal.db import SchemaSQLAlchemy
|
from teal.db import SchemaSQLAlchemy
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,4 +19,24 @@ class SQLAlchemy(SchemaSQLAlchemy):
|
||||||
self.drop_schema(schema='common')
|
self.drop_schema(schema='common')
|
||||||
|
|
||||||
|
|
||||||
|
def create_view(name, selectable):
|
||||||
|
"""Creates a view.
|
||||||
|
|
||||||
|
This is an adaptation from sqlalchemy_utils.view. See
|
||||||
|
`the test on sqlalchemy-utils <https://github.com/kvesteri/
|
||||||
|
sqlalchemy-utils/blob/master/tests/test_views.py>`_ for an
|
||||||
|
example on how to use.
|
||||||
|
"""
|
||||||
|
table = view.create_table_from_selectable(name, selectable)
|
||||||
|
|
||||||
|
# We need to ensure views are created / destroyed before / after
|
||||||
|
# SchemaSQLAlchemy's listeners execute
|
||||||
|
# That is why insert=True in 'after_create'
|
||||||
|
event.listen(db.metadata, 'after_create', view.CreateView(name, selectable), insert=True)
|
||||||
|
event.listen(db.metadata, 'before_drop', view.DropView(name))
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy(session_options={"autoflush": False})
|
db = SQLAlchemy(session_options={"autoflush": False})
|
||||||
|
f = db.func
|
||||||
|
exp = expression
|
||||||
|
|
|
@ -35,6 +35,7 @@ class Devicehub(Teal):
|
||||||
instance_relative_config, root_path, Auth)
|
instance_relative_config, root_path, Auth)
|
||||||
self.dummy = Dummy(self)
|
self.dummy = Dummy(self)
|
||||||
self.before_request(self.register_db_events_listeners)
|
self.before_request(self.register_db_events_listeners)
|
||||||
|
self.cli.command('regenerate-search')(self.regenerate_search)
|
||||||
|
|
||||||
def register_db_events_listeners(self):
|
def register_db_events_listeners(self):
|
||||||
"""Registers the SQLAlchemy event listeners."""
|
"""Registers the SQLAlchemy event listeners."""
|
||||||
|
@ -44,3 +45,9 @@ class Devicehub(Teal):
|
||||||
def _init_db(self):
|
def _init_db(self):
|
||||||
super()._init_db()
|
super()._init_db()
|
||||||
DeviceSearch.set_all_devices_tokens_if_empty(self.db.session)
|
DeviceSearch.set_all_devices_tokens_if_empty(self.db.session)
|
||||||
|
|
||||||
|
def regenerate_search(self):
|
||||||
|
"""Re-creates from 0 all the search tables."""
|
||||||
|
DeviceSearch.regenerate_search_table(self.db.session)
|
||||||
|
db.session.commit()
|
||||||
|
print('Done.')
|
||||||
|
|
|
@ -28,6 +28,7 @@ class Dummy:
|
||||||
ET = (
|
ET = (
|
||||||
('A0000000000001', 'DT-AAAAA'),
|
('A0000000000001', 'DT-AAAAA'),
|
||||||
('A0000000000002', 'DT-BBBBB'),
|
('A0000000000002', 'DT-BBBBB'),
|
||||||
|
('A0000000000003', 'DT-CCCCC'),
|
||||||
)
|
)
|
||||||
"""eTags to create."""
|
"""eTags to create."""
|
||||||
ORG = 'eReuse.org CAT', '-t', 'G-60437761', '-c', 'ES'
|
ORG = 'eReuse.org CAT', '-t', 'G-60437761', '-c', 'ES'
|
||||||
|
@ -118,9 +119,9 @@ class Dummy:
|
||||||
assert len(inventory['items'])
|
assert len(inventory['items'])
|
||||||
|
|
||||||
i, _ = user.get(res=Device, query=[('search', 'intel')])
|
i, _ = user.get(res=Device, query=[('search', 'intel')])
|
||||||
assert len(i['items']) == 11
|
|
||||||
i, _ = user.get(res=Device, query=[('search', 'pc')])
|
|
||||||
assert len(i['items']) == 12
|
assert len(i['items']) == 12
|
||||||
|
i, _ = user.get(res=Device, query=[('search', 'pc')])
|
||||||
|
assert len(i['items']) == 13
|
||||||
|
|
||||||
# Let's create a set of events for the pc device
|
# Let's create a set of events for the pc device
|
||||||
# Make device Ready
|
# Make device Ready
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"elapsed": 2
|
"elapsed": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"type": "StressTest",
|
"type": "StressTest",
|
||||||
"elapsed": 60
|
"elapsed": 60
|
||||||
},
|
},
|
||||||
|
@ -91,14 +91,14 @@
|
||||||
{
|
{
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"startTime": "2018-07-11T11:20:01.005336",
|
"startTime": "2018-07-11T11:20:01.005336",
|
||||||
"endTime": "2018-07-11T11:42:12.971177"
|
"endTime": "2018-07-11T11:42:12.971177"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"zeros": false,
|
"zeros": false,
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"type": "EraseBasic",
|
"type": "EraseBasic",
|
||||||
"endTime": "2018-07-11T11:42:12.975358",
|
"endTime": "2018-07-11T11:42:12.975358",
|
||||||
"startTime": "2018-07-11T11:20:01.004892"
|
"startTime": "2018-07-11T11:20:01.004892"
|
||||||
|
@ -111,7 +111,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"status": "Unspecified Error. Self-test not started.",
|
"status": "Unspecified Error. Self-test not started.",
|
||||||
"elapsed": 0
|
"elapsed": 0
|
||||||
|
|
|
@ -74,13 +74,13 @@
|
||||||
"type": "EraseBasic",
|
"type": "EraseBasic",
|
||||||
"zeros": false,
|
"zeros": false,
|
||||||
"endTime": "2018-07-11T11:56:52.390306",
|
"endTime": "2018-07-11T11:56:52.390306",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"startTime": "2018-07-11T10:49:31.998217",
|
"startTime": "2018-07-11T10:49:31.998217",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"endTime": "2018-07-11T11:56:52.386505",
|
"endTime": "2018-07-11T11:56:52.386505",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"startTime": "2018-07-11T10:49:31.998609"
|
"startTime": "2018-07-11T10:49:31.998609"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -89,7 +89,7 @@
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"status": "Unspecified Error. Self-test not started.",
|
"status": "Unspecified Error. Self-test not started.",
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"elapsed": 0
|
"elapsed": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -130,7 +130,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "StressTest",
|
"type": "StressTest",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"elapsed": 60
|
"elapsed": 60
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
{
|
{
|
||||||
"elapsed": 1,
|
"elapsed": 1,
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"status": "Unspecified Error. Self-test not started.",
|
"status": "Unspecified Error. Self-test not started.",
|
||||||
"length": "Short"
|
"length": "Short"
|
||||||
},
|
},
|
||||||
|
@ -83,13 +83,13 @@
|
||||||
"startTime": "2018-07-11T10:32:14.445306",
|
"startTime": "2018-07-11T10:32:14.445306",
|
||||||
"zeros": false,
|
"zeros": false,
|
||||||
"type": "EraseBasic",
|
"type": "EraseBasic",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"endTime": "2018-07-11T10:53:46.442123",
|
"endTime": "2018-07-11T10:53:46.442123",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"startTime": "2018-07-11T10:32:14.445496",
|
"startTime": "2018-07-11T10:32:14.445496",
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"endTime": "2018-07-11T10:53:46.438901"
|
"endTime": "2018-07-11T10:53:46.438901"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
{
|
{
|
||||||
"elapsed": 0,
|
"elapsed": 0,
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"status": "Unspecified Error. Self-test not started.",
|
"status": "Unspecified Error. Self-test not started.",
|
||||||
"length": "Short"
|
"length": "Short"
|
||||||
},
|
},
|
||||||
|
@ -115,13 +115,13 @@
|
||||||
"startTime": "2018-07-11T10:53:46.442187",
|
"startTime": "2018-07-11T10:53:46.442187",
|
||||||
"zeros": false,
|
"zeros": false,
|
||||||
"type": "EraseBasic",
|
"type": "EraseBasic",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"endTime": "2018-07-11T11:16:28.469899",
|
"endTime": "2018-07-11T11:16:28.469899",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"startTime": "2018-07-11T10:53:46.442343",
|
"startTime": "2018-07-11T10:53:46.442343",
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"endTime": "2018-07-11T11:16:28.463789"
|
"endTime": "2018-07-11T11:16:28.463789"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -157,7 +157,7 @@
|
||||||
"chassis": "Tower",
|
"chassis": "Tower",
|
||||||
"events": [
|
"events": [
|
||||||
{
|
{
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"elapsed": 60,
|
"elapsed": 60,
|
||||||
"type": "StressTest"
|
"type": "StressTest"
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
{
|
{
|
||||||
"elapsed": 60,
|
"elapsed": 60,
|
||||||
"type": "StressTest",
|
"type": "StressTest",
|
||||||
"error": false
|
"severity": "Info"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"elapsed": 1,
|
"elapsed": 1,
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
"elapsed": 15
|
"elapsed": 15
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"elapsed": 0,
|
"elapsed": 0,
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
|
@ -102,13 +102,13 @@
|
||||||
"startTime": "2018-07-11T13:28:07.319948",
|
"startTime": "2018-07-11T13:28:07.319948",
|
||||||
"type": "EraseBasic",
|
"type": "EraseBasic",
|
||||||
"endTime": "2018-07-11T14:04:04.864425",
|
"endTime": "2018-07-11T14:04:04.864425",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"startTime": "2018-07-11T13:28:07.320244",
|
"startTime": "2018-07-11T13:28:07.320244",
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"endTime": "2018-07-11T14:04:04.861590",
|
"endTime": "2018-07-11T14:04:04.861590",
|
||||||
"error": false
|
"severity": "Info"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"zeros": false
|
"zeros": false
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"manufacturer": "NEC Computers SAS",
|
"manufacturer": "NEC Computers SAS",
|
||||||
"events": [
|
"events": [
|
||||||
{
|
{
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"elapsed": 60,
|
"elapsed": 60,
|
||||||
"type": "StressTest"
|
"type": "StressTest"
|
||||||
},
|
},
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
"size": 305245,
|
"size": 305245,
|
||||||
"events": [
|
"events": [
|
||||||
{
|
{
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"endTime": "2018-07-11T11:33:41.531918",
|
"endTime": "2018-07-11T11:33:41.531918",
|
||||||
"startTime": "2018-07-11T10:30:35.643855",
|
"startTime": "2018-07-11T10:30:35.643855",
|
||||||
"zeros": false,
|
"zeros": false,
|
||||||
|
@ -111,7 +111,7 @@
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"endTime": "2018-07-11T11:33:41.529224",
|
"endTime": "2018-07-11T11:33:41.529224",
|
||||||
"startTime": "2018-07-11T10:30:35.644043",
|
"startTime": "2018-07-11T10:30:35.644043",
|
||||||
"error": false
|
"severity": "Info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"elapsed": 1,
|
"elapsed": 1,
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"status": "Unspecified Error. Self-test not started."
|
"status": "Unspecified Error. Self-test not started."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
{
|
||||||
|
"uuid": "de4f495e-c58b-40e1-a33e-46ab5e84767e",
|
||||||
|
"endTime": "2018-10-24T11:03:36.113006+00:00",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"speed": 1000,
|
||||||
|
"manufacturer": "Realtek Semiconductor Co., Ltd.",
|
||||||
|
"wireless": false,
|
||||||
|
"model": "RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller",
|
||||||
|
"serialNumber": "00:26:18:96:dc:af",
|
||||||
|
"type": "NetworkAdapter",
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speed": 1333.0,
|
||||||
|
"manufacturer": null,
|
||||||
|
"interface": "DDR",
|
||||||
|
"model": null,
|
||||||
|
"serialNumber": null,
|
||||||
|
"type": "RamModule",
|
||||||
|
"format": "DIMM",
|
||||||
|
"size": 2048,
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speed": 1333.0,
|
||||||
|
"manufacturer": null,
|
||||||
|
"interface": "DDR",
|
||||||
|
"model": null,
|
||||||
|
"serialNumber": null,
|
||||||
|
"type": "RamModule",
|
||||||
|
"format": "DIMM",
|
||||||
|
"size": 2048,
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speed": 1333.0,
|
||||||
|
"manufacturer": null,
|
||||||
|
"interface": "DDR",
|
||||||
|
"model": null,
|
||||||
|
"serialNumber": null,
|
||||||
|
"type": "RamModule",
|
||||||
|
"format": "DIMM",
|
||||||
|
"size": 2048,
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speed": 1333.0,
|
||||||
|
"manufacturer": null,
|
||||||
|
"interface": "DDR",
|
||||||
|
"model": null,
|
||||||
|
"serialNumber": null,
|
||||||
|
"type": "RamModule",
|
||||||
|
"format": "DIMM",
|
||||||
|
"size": 2048,
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Intel Corporation",
|
||||||
|
"model": "5 Series/3400 Series Chipset High Definition Audio",
|
||||||
|
"serialNumber": null,
|
||||||
|
"type": "SoundCard",
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speed": 2.5330000000000004,
|
||||||
|
"manufacturer": "Intel Corp.",
|
||||||
|
"cores": 4,
|
||||||
|
"address": 64,
|
||||||
|
"model": "Intel Core i7 CPU 860 @ 2.80GHz",
|
||||||
|
"serialNumber": null,
|
||||||
|
"type": "Processor",
|
||||||
|
"threads": 8,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"elapsed": 9,
|
||||||
|
"type": "BenchmarkProcessorSysbench",
|
||||||
|
"rate": 8.7418
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"elapsed": 0,
|
||||||
|
"type": "BenchmarkProcessor",
|
||||||
|
"rate": 44937.520000000004
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": null,
|
||||||
|
"interface": "ATA",
|
||||||
|
"model": "SAMSUNG HD103SJ",
|
||||||
|
"serialNumber": "S246J90Z406422",
|
||||||
|
"type": "HardDrive",
|
||||||
|
"size": 953869,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"elapsed": 120,
|
||||||
|
"lifetime": 14298,
|
||||||
|
"currentPendingSectorCount": 0,
|
||||||
|
"type": "TestDataStorage",
|
||||||
|
"status": "Completed without error",
|
||||||
|
"powerCycleCount": 693,
|
||||||
|
"assessment": true,
|
||||||
|
"offlineUncorrectable": 0,
|
||||||
|
"severity": "Info",
|
||||||
|
"length": "Short",
|
||||||
|
"reallocatedSectorCount": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"readSpeed": 136.0,
|
||||||
|
"elapsed": 9,
|
||||||
|
"type": "BenchmarkDataStorage",
|
||||||
|
"writeSpeed": 35.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "NVIDIA Corporation",
|
||||||
|
"model": "G84 GeForce 8600 GT",
|
||||||
|
"serialNumber": null,
|
||||||
|
"type": "GraphicCard",
|
||||||
|
"memory": 256.0,
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"firewire": 1,
|
||||||
|
"manufacturer": "ASUSTeK Computer INC.",
|
||||||
|
"usb": 2,
|
||||||
|
"model": "P7P55D",
|
||||||
|
"serialNumber": "101005570001137",
|
||||||
|
"type": "Motherboard",
|
||||||
|
"pcmcia": 0,
|
||||||
|
"slots": 4,
|
||||||
|
"serial": 1,
|
||||||
|
"events": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elapsed": 203,
|
||||||
|
"device": {
|
||||||
|
"manufacturer": null,
|
||||||
|
"model": null,
|
||||||
|
"chassis": "Tower",
|
||||||
|
"type": "Desktop",
|
||||||
|
"serialNumber": null,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"elapsed": 60,
|
||||||
|
"type": "StressTest",
|
||||||
|
"severity": "Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"elapsed": 1,
|
||||||
|
"type": "BenchmarkRamSysbench",
|
||||||
|
"rate": 0.8315
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
{"id": "A0000000000003", "type": "Tag"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": "11.0a6",
|
||||||
|
"expectedEvents": [
|
||||||
|
"Benchmark",
|
||||||
|
"TestDataStorage",
|
||||||
|
"StressTest"
|
||||||
|
],
|
||||||
|
"type": "Snapshot",
|
||||||
|
"closed": true,
|
||||||
|
"software": "Workbench"
|
||||||
|
}
|
|
@ -62,7 +62,7 @@
|
||||||
"assessment": true,
|
"assessment": true,
|
||||||
"currentPendingSectorCount": 0,
|
"currentPendingSectorCount": 0,
|
||||||
"elapsed": 134,
|
"elapsed": 134,
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"lifetime": 19549,
|
"lifetime": 19549,
|
||||||
"offlineUncorrectable": 0,
|
"offlineUncorrectable": 0,
|
||||||
|
@ -106,7 +106,7 @@
|
||||||
"events": [
|
"events": [
|
||||||
{
|
{
|
||||||
"elapsed": 60,
|
"elapsed": 60,
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"type": "StressTest"
|
"type": "StressTest"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -90,7 +90,7 @@
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"elapsed": 2,
|
"elapsed": 2,
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"status": "Unspecified Error. Self-test not started."
|
"status": "Unspecified Error. Self-test not started."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -99,12 +99,12 @@
|
||||||
{
|
{
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"startTime": "2018-07-03T09:15:22.257059",
|
"startTime": "2018-07-03T09:15:22.257059",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"endTime": "2018-07-03T10:32:11.843190"
|
"endTime": "2018-07-03T10:32:11.843190"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"startTime": "2018-07-03T09:15:22.256074",
|
"startTime": "2018-07-03T09:15:22.256074",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"zeros": false,
|
"zeros": false,
|
||||||
"endTime": "2018-07-03T10:32:11.848455"
|
"endTime": "2018-07-03T10:32:11.848455"
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "StressTest",
|
"type": "StressTest",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"elapsed": 60
|
"elapsed": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
"elapsed": 0,
|
"elapsed": 0,
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"status": "Unspecified Error. Self-test not started.",
|
"status": "Unspecified Error. Self-test not started.",
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"length": "Short"
|
"length": "Short"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"status": "Unspecified Error. Self-test not started.",
|
"status": "Unspecified Error. Self-test not started.",
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"elapsed": 1,
|
"elapsed": 1,
|
||||||
"length": Short
|
"length": Short
|
||||||
|
@ -142,7 +142,7 @@
|
||||||
{
|
{
|
||||||
"type": "StressTest",
|
"type": "StressTest",
|
||||||
"elapsed": 60,
|
"elapsed": 60,
|
||||||
"error": false
|
"severity": "Info"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rate": 0.9759,
|
"rate": 0.9759,
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
"status": "Unspecified Error. Self-test not started.",
|
"status": "Unspecified Error. Self-test not started.",
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"length": Short,
|
"length": Short,
|
||||||
"error": true
|
"severity": "Error"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "HardDrive",
|
"type": "HardDrive",
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
"events": [
|
"events": [
|
||||||
{
|
{
|
||||||
"type": "StressTest",
|
"type": "StressTest",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"elapsed": 120
|
"elapsed": 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
{
|
{
|
||||||
"type": "StressTest",
|
"type": "StressTest",
|
||||||
"elapsed": 300,
|
"elapsed": 300,
|
||||||
"error": false
|
"severity": "Info"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"serialNumber": "CZC0408YJG",
|
"serialNumber": "CZC0408YJG",
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
"offlineUncorrectable": 1,
|
"offlineUncorrectable": 1,
|
||||||
"powerCycleCount": 1838,
|
"powerCycleCount": 1838,
|
||||||
"assessment": true,
|
"assessment": true,
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"lifetime": 10546,
|
"lifetime": 10546,
|
||||||
"reallocatedSectorCount": 0,
|
"reallocatedSectorCount": 0,
|
||||||
|
|
|
@ -64,7 +64,7 @@ components:
|
||||||
elapsed: 21
|
elapsed: 21
|
||||||
- type: TestDataStorage
|
- type: TestDataStorage
|
||||||
elapsed: 233
|
elapsed: 233
|
||||||
error: False
|
severity: Info
|
||||||
status: Completed without error
|
status: Completed without error
|
||||||
length: Short
|
length: Short
|
||||||
lifetime: 99
|
lifetime: 99
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from teal.query import NestedQueryFlaskParser
|
||||||
|
from webargs.flaskparser import FlaskParser
|
||||||
|
|
||||||
|
|
||||||
|
class SearchQueryParser(NestedQueryFlaskParser):
|
||||||
|
|
||||||
|
def parse_querystring(self, req, name, field):
|
||||||
|
if name == 'search':
|
||||||
|
v = FlaskParser.parse_querystring(self, req, name, field)
|
||||||
|
else:
|
||||||
|
v = super().parse_querystring(req, name, field)
|
||||||
|
return v
|
|
@ -27,7 +27,7 @@ class JoinedTableMixin:
|
||||||
|
|
||||||
class Agent(Thing):
|
class Agent(Thing):
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||||
type = Column(Unicode, nullable=False)
|
type = Column(Unicode, nullable=False, index=True)
|
||||||
name = Column(CIText())
|
name = Column(CIText())
|
||||||
name.comment = """
|
name.comment = """
|
||||||
The name of the organization or person.
|
The name of the organization or person.
|
||||||
|
|
|
@ -276,6 +276,22 @@ class VideoconferenceDef(VideoDef):
|
||||||
SCHEMA = schemas.Videoconference
|
SCHEMA = schemas.Videoconference
|
||||||
|
|
||||||
|
|
||||||
|
class CookingDef(DeviceDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.Cooking
|
||||||
|
|
||||||
|
def __init__(self, app, import_name=__name__, 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()):
|
||||||
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
|
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||||
|
|
||||||
|
|
||||||
|
class Mixer(CookingDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.Mixer
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerDef(Resource):
|
class ManufacturerDef(Resource):
|
||||||
VIEW = ManufacturerView
|
VIEW = ManufacturerView
|
||||||
SCHEMA = schemas.Manufacturer
|
SCHEMA = schemas.Manufacturer
|
||||||
|
|
|
@ -8,6 +8,7 @@ from typing import Dict, List, Set
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from ereuse_utils.naming import Naming
|
from ereuse_utils.naming import Naming
|
||||||
|
from more_itertools import unique_everseen
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
||||||
Sequence, SmallInteger, Unicode, inspect, text
|
Sequence, SmallInteger, Unicode, inspect, text
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
@ -15,15 +16,15 @@ from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
from sqlalchemy_utils import ColorType
|
from sqlalchemy_utils import ColorType
|
||||||
from stdnum import imei, meid
|
from stdnum import imei, meid
|
||||||
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, check_lower, \
|
from teal.db import CASCADE_DEL, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, \
|
||||||
check_range
|
check_lower, check_range
|
||||||
from teal.enums import Layouts
|
from teal.enums import Layouts
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
from teal.resource import url_for_resource
|
from teal.resource import url_for_resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
|
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
||||||
DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface
|
PrinterTechnology, RamFormat, RamInterface, Severity
|
||||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,12 +32,13 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
EVENT_SORT_KEY = attrgetter('created')
|
||||||
|
|
||||||
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
|
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
|
||||||
id.comment = """
|
id.comment = """
|
||||||
The identifier of the device for this database.
|
The identifier of the device for this database.
|
||||||
"""
|
"""
|
||||||
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
type = Column(Unicode(STR_SM_SIZE), nullable=False, index=True)
|
||||||
hid = Column(Unicode(), check_lower('hid'), unique=True)
|
hid = Column(Unicode(), check_lower('hid'), unique=True)
|
||||||
hid.comment = """
|
hid.comment = """
|
||||||
The Hardware ID (HID) is the unique ID traceability systems
|
The Hardware ID (HID) is the unique ID traceability systems
|
||||||
|
@ -77,6 +79,11 @@ class Device(Thing):
|
||||||
'color'
|
'color'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kw) -> None:
|
||||||
|
super().__init__(**kw)
|
||||||
|
with suppress(TypeError):
|
||||||
|
self.hid = Naming.hid(self.manufacturer, self.serial_number, self.model)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def events(self) -> list:
|
def events(self) -> list:
|
||||||
"""
|
"""
|
||||||
|
@ -86,12 +93,25 @@ class Device(Thing):
|
||||||
|
|
||||||
Events are returned by ascending creation time.
|
Events are returned by ascending creation time.
|
||||||
"""
|
"""
|
||||||
return sorted(chain(self.events_multiple, self.events_one), key=attrgetter('created'))
|
return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY)
|
||||||
|
|
||||||
def __init__(self, **kw) -> None:
|
@property
|
||||||
super().__init__(**kw)
|
def problems(self):
|
||||||
with suppress(TypeError):
|
"""Current events with severity.Warning or higher.
|
||||||
self.hid = Naming.hid(self.manufacturer, self.serial_number, self.model)
|
|
||||||
|
There can be up to 3 events: current Snapshot,
|
||||||
|
current Physical event, current Trading event.
|
||||||
|
"""
|
||||||
|
from ereuse_devicehub.resources.device import states
|
||||||
|
from ereuse_devicehub.resources.event.models import Snapshot
|
||||||
|
events = set()
|
||||||
|
with suppress(LookupError, ValueError):
|
||||||
|
events.add(self.last_event_of(Snapshot))
|
||||||
|
with suppress(LookupError, ValueError):
|
||||||
|
events.add(self.last_event_of(*states.Physical.events()))
|
||||||
|
with suppress(LookupError, ValueError):
|
||||||
|
events.add(self.last_event_of(*states.Trading.events()))
|
||||||
|
return self._warning_events(events)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def physical_properties(self) -> Dict[str, object or None]:
|
def physical_properties(self) -> Dict[str, object or None]:
|
||||||
|
@ -158,12 +178,30 @@ class Device(Thing):
|
||||||
that has it physically. As an example, a transporter could
|
that has it physically. As an example, a transporter could
|
||||||
be a physical possessor of a device although it does not
|
be a physical possessor of a device although it does not
|
||||||
own it legally.
|
own it legally.
|
||||||
|
|
||||||
|
Note that there can only be one physical possessor per device,
|
||||||
|
and :class:`ereuse_devicehub.resources.event.models.Receive`
|
||||||
|
changes it.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.event.models import Receive
|
from ereuse_devicehub.resources.event.models import Receive
|
||||||
with suppress(LookupError):
|
with suppress(LookupError):
|
||||||
event = self.last_event_of(Receive)
|
event = self.last_event_of(Receive)
|
||||||
return event.agent
|
return event.agent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def working(self):
|
||||||
|
"""A list of the current tests with warning or errors. A
|
||||||
|
device is working if the list is empty.
|
||||||
|
|
||||||
|
This property returns, for the last test performed of each type,
|
||||||
|
the one with the worst severity of them, or `None` if no
|
||||||
|
test has been executed.
|
||||||
|
"""
|
||||||
|
from ereuse_devicehub.resources.event.models import Test
|
||||||
|
current_tests = unique_everseen((e for e in reversed(self.events) if isinstance(e, Test)),
|
||||||
|
key=attrgetter('type')) # last test of each type
|
||||||
|
return self._warning_events(current_tests)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def __mapper_args__(cls):
|
def __mapper_args__(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -188,6 +226,10 @@ class Device(Thing):
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise LookupError('{!r} does not contain events of types {}.'.format(self, types))
|
raise LookupError('{!r} does not contain events of types {}.'.format(self, types))
|
||||||
|
|
||||||
|
def _warning_events(self, events):
|
||||||
|
return sorted((ev for ev in events if ev.severity >= Severity.Warning),
|
||||||
|
key=self.EVENT_SORT_KEY)
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return self.id < other.id
|
return self.id < other.id
|
||||||
|
|
||||||
|
@ -255,7 +297,7 @@ class Computer(Device):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def events(self) -> list:
|
def events(self) -> list:
|
||||||
return sorted(chain(super().events, self.events_parent), key=attrgetter('created'))
|
return sorted(chain(super().events, self.events_parent), key=self.EVENT_SORT_KEY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ram_size(self) -> int:
|
def ram_size(self) -> int:
|
||||||
|
@ -294,6 +336,17 @@ class Computer(Device):
|
||||||
speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless] or 0)
|
speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless] or 0)
|
||||||
return speeds
|
return speeds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def privacy(self):
|
||||||
|
"""Returns the privacy of all DataStorage components when
|
||||||
|
it is None.
|
||||||
|
"""
|
||||||
|
return set(
|
||||||
|
privacy for privacy in
|
||||||
|
(hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage))
|
||||||
|
if privacy
|
||||||
|
)
|
||||||
|
|
||||||
def __format__(self, format_spec):
|
def __format__(self, format_spec):
|
||||||
if not format_spec:
|
if not format_spec:
|
||||||
return super().__format__(format_spec)
|
return super().__format__(format_spec)
|
||||||
|
@ -375,11 +428,11 @@ class Cellphone(Mobile):
|
||||||
class Component(Device):
|
class Component(Device):
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
|
|
||||||
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True)
|
||||||
parent = relationship(Computer,
|
parent = relationship(Computer,
|
||||||
backref=backref('components',
|
backref=backref('components',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
cascade=CASCADE,
|
cascade=CASCADE_DEL,
|
||||||
order_by=lambda: Component.id,
|
order_by=lambda: Component.id,
|
||||||
collection_class=OrderedSet),
|
collection_class=OrderedSet),
|
||||||
primaryjoin=parent_id == Computer.id)
|
primaryjoin=parent_id == Computer.id)
|
||||||
|
@ -405,7 +458,7 @@ class Component(Device):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def events(self) -> list:
|
def events(self) -> list:
|
||||||
return sorted(chain(super().events, self.events_components), key=attrgetter('created'))
|
return sorted(chain(super().events, self.events_components), key=self.EVENT_SORT_KEY)
|
||||||
|
|
||||||
|
|
||||||
class JoinedComponentTableMixin:
|
class JoinedComponentTableMixin:
|
||||||
|
@ -431,11 +484,12 @@ class DataStorage(JoinedComponentTableMixin, Component):
|
||||||
@property
|
@property
|
||||||
def privacy(self):
|
def privacy(self):
|
||||||
"""Returns the privacy compliance state of the data storage."""
|
"""Returns the privacy compliance state of the data storage."""
|
||||||
# todo add physical destruction event
|
|
||||||
from ereuse_devicehub.resources.event.models import EraseBasic
|
from ereuse_devicehub.resources.event.models import EraseBasic
|
||||||
with suppress(LookupError):
|
try:
|
||||||
erase = self.last_event_of(EraseBasic)
|
ev = self.last_event_of(EraseBasic)
|
||||||
return DataStoragePrivacyCompliance.from_erase(erase)
|
except LookupError:
|
||||||
|
ev = None
|
||||||
|
return ev
|
||||||
|
|
||||||
def __format__(self, format_spec):
|
def __format__(self, format_spec):
|
||||||
v = super().__format__(format_spec)
|
v = super().__format__(format_spec)
|
||||||
|
@ -589,6 +643,14 @@ class Videoconference(Video):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Cooking(Device):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Mixer(Cooking):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Manufacturer(db.Model):
|
class Manufacturer(db.Model):
|
||||||
__table_args__ = {'schema': 'common'}
|
__table_args__ = {'schema': 'common'}
|
||||||
CSV_DELIMITER = csv.get_dialect('excel').delimiter
|
CSV_DELIMITER = csv.get_dialect('excel').delimiter
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Set, Type, Union
|
from operator import attrgetter
|
||||||
|
from typing import Dict, Generator, Iterable, List, Optional, Set, Type
|
||||||
|
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from boltons.urlutils import URL
|
from boltons.urlutils import URL
|
||||||
|
@ -11,8 +12,8 @@ from teal.enums import Layouts
|
||||||
|
|
||||||
from ereuse_devicehub.resources.agent.models import Agent
|
from ereuse_devicehub.resources.agent.models import Agent
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
|
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
||||||
DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface
|
PrinterTechnology, RamFormat, RamInterface
|
||||||
from ereuse_devicehub.resources.event import models as e
|
from ereuse_devicehub.resources.event import models as e
|
||||||
from ereuse_devicehub.resources.image.models import ImageList
|
from ereuse_devicehub.resources.image.models import ImageList
|
||||||
from ereuse_devicehub.resources.lot.models import Lot
|
from ereuse_devicehub.resources.lot.models import Lot
|
||||||
|
@ -21,6 +22,8 @@ from ereuse_devicehub.resources.tag import Tag
|
||||||
|
|
||||||
|
|
||||||
class Device(Thing):
|
class Device(Thing):
|
||||||
|
EVENT_SORT_KEY = attrgetter('created')
|
||||||
|
|
||||||
id = ... # type: Column
|
id = ... # type: Column
|
||||||
type = ... # type: Column
|
type = ... # type: Column
|
||||||
hid = ... # type: Column
|
hid = ... # type: Column
|
||||||
|
@ -48,7 +51,6 @@ class Device(Thing):
|
||||||
self.height = ... # type: float
|
self.height = ... # type: float
|
||||||
self.depth = ... # type: float
|
self.depth = ... # type: float
|
||||||
self.color = ... # type: Color
|
self.color = ... # type: Color
|
||||||
self.events = ... # type: List[e.Event]
|
|
||||||
self.physical_properties = ... # type: Dict[str, object or None]
|
self.physical_properties = ... # type: Dict[str, object or None]
|
||||||
self.events_multiple = ... # type: Set[e.EventWithMultipleDevices]
|
self.events_multiple = ... # type: Set[e.EventWithMultipleDevices]
|
||||||
self.events_one = ... # type: Set[e.EventWithOneDevice]
|
self.events_one = ... # type: Set[e.EventWithOneDevice]
|
||||||
|
@ -57,33 +59,48 @@ class Device(Thing):
|
||||||
self.lots = ... # type: Set[Lot]
|
self.lots = ... # type: Set[Lot]
|
||||||
self.production_date = ... # type: datetime
|
self.production_date = ... # type: datetime
|
||||||
|
|
||||||
|
@property
|
||||||
|
def events(self) -> List[e.Event]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def problems(self) -> List[e.Event]:
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> urlutils.URL:
|
def url(self) -> urlutils.URL:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rate(self) -> Union[e.AggregateRate, None]:
|
def rate(self) -> Optional[e.AggregateRate]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def price(self) -> Union[e.Price, None]:
|
def price(self) -> Optional[e.Price]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def trading(self) -> Union[states.Trading, None]:
|
def trading(self) -> Optional[states.Trading]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def physical(self) -> Union[states.Physical, None]:
|
def physical(self) -> Optional[states.Physical]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def physical_possessor(self) -> Union[Agent, None]:
|
def physical_possessor(self) -> Optional[Agent]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def working(self) -> List[e.Test]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def last_event_of(self, *types: Type[e.Event]) -> e.Event:
|
def last_event_of(self, *types: Type[e.Event]) -> e.Event:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _warning_events(self, events: Iterable[e.Event]) -> Generator[e.Event]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DisplayMixin:
|
class DisplayMixin:
|
||||||
technology = ... # type: Column
|
technology = ... # type: Column
|
||||||
|
@ -139,6 +156,10 @@ class Computer(DisplayMixin, Device):
|
||||||
def network_speeds(self) -> List[int]:
|
def network_speeds(self) -> List[int]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def privacy(self) -> Set[e.EraseBasic]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Desktop(Computer):
|
class Desktop(Computer):
|
||||||
pass
|
pass
|
||||||
|
@ -219,7 +240,7 @@ class DataStorage(Component):
|
||||||
self.interface = ... # type: DataStorageInterface
|
self.interface = ... # type: DataStorageInterface
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def privacy(self) -> DataStoragePrivacyCompliance:
|
def privacy(self) -> Optional[e.EraseBasic]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -373,6 +394,14 @@ class Videoconference(Video):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Cooking(Device):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Mixer(Cooking):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Manufacturer(Model):
|
class Manufacturer(Model):
|
||||||
CUSTOM_MANUFACTURERS = ... # type: set
|
CUSTOM_MANUFACTURERS = ... # type: set
|
||||||
name = ... # type: Column
|
name = ... # type: Column
|
||||||
|
|
|
@ -8,9 +8,8 @@ from teal.marshmallow import EnumField, SanitizedStr, URL, ValidationError
|
||||||
from teal.resource import Schema
|
from teal.resource import Schema
|
||||||
|
|
||||||
from ereuse_devicehub.marshmallow import NestedOn
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
|
from ereuse_devicehub.resources import enums
|
||||||
from ereuse_devicehub.resources.device import models as m, states
|
from ereuse_devicehub.resources.device import models as m, states
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
|
|
||||||
DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface
|
|
||||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
||||||
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
||||||
|
|
||||||
|
@ -31,6 +30,7 @@ class Device(Thing):
|
||||||
depth = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.depth.comment)
|
depth = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.depth.comment)
|
||||||
events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__)
|
events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__)
|
||||||
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
|
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
|
||||||
|
problems = NestedOn('Event', many=True, dump_only=True, description=m.Device.problems.__doc__)
|
||||||
url = URL(dump_only=True, description=m.Device.url.__doc__)
|
url = URL(dump_only=True, description=m.Device.url.__doc__)
|
||||||
lots = NestedOn('Lot',
|
lots = NestedOn('Lot',
|
||||||
many=True,
|
many=True,
|
||||||
|
@ -44,6 +44,10 @@ class Device(Thing):
|
||||||
production_date = DateTime('iso',
|
production_date = DateTime('iso',
|
||||||
description=m.Device.updated.comment,
|
description=m.Device.updated.comment,
|
||||||
data_key='productionDate')
|
data_key='productionDate')
|
||||||
|
working = NestedOn('Event',
|
||||||
|
many=True,
|
||||||
|
dump_only=True,
|
||||||
|
description=m.Device.working.__doc__)
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def from_events_to_events_one(self, data: dict):
|
def from_events_to_events_one(self, data: dict):
|
||||||
|
@ -72,12 +76,13 @@ class Device(Thing):
|
||||||
|
|
||||||
class Computer(Device):
|
class Computer(Device):
|
||||||
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet)
|
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet)
|
||||||
chassis = EnumField(ComputerChassis, required=True)
|
chassis = EnumField(enums.ComputerChassis, required=True)
|
||||||
ram_size = Integer(dump_only=True, data_key='ramSize')
|
ram_size = Integer(dump_only=True, data_key='ramSize')
|
||||||
data_storage_size = Integer(dump_only=True, data_key='dataStorageSize')
|
data_storage_size = Integer(dump_only=True, data_key='dataStorageSize')
|
||||||
processor_model = Str(dump_only=True, data_key='processorModel')
|
processor_model = Str(dump_only=True, data_key='processorModel')
|
||||||
graphic_card_model = Str(dump_only=True, data_key='graphicCardModel')
|
graphic_card_model = Str(dump_only=True, data_key='graphicCardModel')
|
||||||
network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds')
|
network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds')
|
||||||
|
privacy = NestedOn('Event', many=True, dump_only=True, collection_class=set)
|
||||||
|
|
||||||
|
|
||||||
class Desktop(Computer):
|
class Desktop(Computer):
|
||||||
|
@ -94,7 +99,7 @@ class Server(Computer):
|
||||||
|
|
||||||
class DisplayMixin:
|
class DisplayMixin:
|
||||||
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150))
|
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150))
|
||||||
technology = EnumField(DisplayTech,
|
technology = EnumField(enums.DisplayTech,
|
||||||
description=m.DisplayMixin.technology.comment)
|
description=m.DisplayMixin.technology.comment)
|
||||||
resolution_width = Integer(data_key='resolutionWidth',
|
resolution_width = Integer(data_key='resolutionWidth',
|
||||||
validate=Range(10, 20000),
|
validate=Range(10, 20000),
|
||||||
|
@ -168,8 +173,8 @@ class DataStorage(Component):
|
||||||
size = Integer(validate=Range(0, 10 ** 8),
|
size = Integer(validate=Range(0, 10 ** 8),
|
||||||
unit=UnitCodes.mbyte,
|
unit=UnitCodes.mbyte,
|
||||||
description=m.DataStorage.size.comment)
|
description=m.DataStorage.size.comment)
|
||||||
interface = EnumField(DataStorageInterface)
|
interface = EnumField(enums.DataStorageInterface)
|
||||||
privacy = EnumField(DataStoragePrivacyCompliance, dump_only=True)
|
privacy = NestedOn('Event', dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
class HardDrive(DataStorage):
|
class HardDrive(DataStorage):
|
||||||
|
@ -203,8 +208,8 @@ class Processor(Component):
|
||||||
class RamModule(Component):
|
class RamModule(Component):
|
||||||
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
|
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
|
||||||
speed = Integer(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
|
speed = Integer(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
|
||||||
interface = EnumField(RamInterface)
|
interface = EnumField(enums.RamInterface)
|
||||||
format = EnumField(RamFormat)
|
format = EnumField(enums.RamFormat)
|
||||||
|
|
||||||
|
|
||||||
class SoundCard(Component):
|
class SoundCard(Component):
|
||||||
|
@ -264,7 +269,7 @@ class WirelessAccessPoint(Networking):
|
||||||
class Printer(Device):
|
class Printer(Device):
|
||||||
wireless = Boolean(required=True, missing=False)
|
wireless = Boolean(required=True, missing=False)
|
||||||
scanning = Boolean(required=True, missing=False)
|
scanning = Boolean(required=True, missing=False)
|
||||||
technology = EnumField(PrinterTechnology, required=True)
|
technology = EnumField(enums.PrinterTechnology, required=True)
|
||||||
monochrome = Boolean(required=True, missing=True)
|
monochrome = Boolean(required=True, missing=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -290,3 +295,11 @@ class VideoScaler(Video):
|
||||||
|
|
||||||
class Videoconference(Video):
|
class Videoconference(Video):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Cooking(Device):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Mixer(Cooking):
|
||||||
|
pass
|
||||||
|
|
|
@ -73,6 +73,12 @@ class DeviceSearch(db.Model):
|
||||||
it deletes unlogged tables as ours.
|
it deletes unlogged tables as ours.
|
||||||
"""
|
"""
|
||||||
if not DeviceSearch.query.first():
|
if not DeviceSearch.query.first():
|
||||||
|
cls.regenerate_search_table(session)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def regenerate_search_table(cls, session: db.Session):
|
||||||
|
"""Deletes and re-computes all the search table."""
|
||||||
|
DeviceSearch.query.delete()
|
||||||
for device in Device.query:
|
for device in Device.query:
|
||||||
if not isinstance(device, Component):
|
if not isinstance(device, Component):
|
||||||
cls.set_device_tokens(session, device)
|
cls.set_device_tokens(session, device)
|
||||||
|
@ -83,14 +89,29 @@ class DeviceSearch(db.Model):
|
||||||
assert not isinstance(device, Component)
|
assert not isinstance(device, Component)
|
||||||
|
|
||||||
tokens = [
|
tokens = [
|
||||||
|
(str(device.id), search.Weight.A),
|
||||||
(inflection.humanize(device.type), search.Weight.B),
|
(inflection.humanize(device.type), search.Weight.B),
|
||||||
(Device.model, search.Weight.B),
|
(Device.model, search.Weight.B),
|
||||||
(Device.manufacturer, search.Weight.C),
|
(Device.manufacturer, search.Weight.C),
|
||||||
(Device.serial_number, search.Weight.A)
|
(Device.serial_number, search.Weight.A)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if device.manufacturer:
|
||||||
|
# todo this has to be done using a dictionary
|
||||||
|
manufacturer = device.manufacturer.lower()
|
||||||
|
if 'asus' in manufacturer:
|
||||||
|
tokens.append(('asus', search.Weight.B))
|
||||||
|
if 'hewlett' in manufacturer or 'hp' in manufacturer or 'h.p' in manufacturer:
|
||||||
|
tokens.append(('hp', search.Weight.B))
|
||||||
|
tokens.append(('h.p', search.Weight.C))
|
||||||
|
tokens.append(('hewlett', search.Weight.C))
|
||||||
|
tokens.append(('packard', search.Weight.C))
|
||||||
|
|
||||||
if isinstance(device, Computer):
|
if isinstance(device, Computer):
|
||||||
|
# Aggregate the values of all the components of pc
|
||||||
Comp = aliased(Component)
|
Comp = aliased(Component)
|
||||||
tokens.extend((
|
tokens.extend((
|
||||||
|
(db.func.string_agg(db.cast(Comp.id, db.TEXT), ' '), search.Weight.D),
|
||||||
(db.func.string_agg(Comp.model, ' '), search.Weight.C),
|
(db.func.string_agg(Comp.model, ' '), search.Weight.C),
|
||||||
(db.func.string_agg(Comp.manufacturer, ' '), search.Weight.D),
|
(db.func.string_agg(Comp.manufacturer, ' '), search.Weight.D),
|
||||||
(db.func.string_agg(Comp.serial_number, ' '), search.Weight.B),
|
(db.func.string_agg(Comp.serial_number, ' '), search.Weight.B),
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import csv
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from io import StringIO
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import marshmallow
|
import marshmallow
|
||||||
from flask import current_app as app, render_template, request, make_response
|
from flask import current_app as app, render_template, request
|
||||||
from flask.json import jsonify
|
from flask.json import jsonify
|
||||||
from flask_sqlalchemy import Pagination
|
from flask_sqlalchemy import Pagination
|
||||||
from marshmallow import fields, fields as f, validate as v
|
from marshmallow import fields, fields as f, validate as v
|
||||||
|
@ -16,13 +12,12 @@ from teal.resource import View
|
||||||
|
|
||||||
from ereuse_devicehub import auth
|
from ereuse_devicehub import auth
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.query import SearchQueryParser
|
||||||
from ereuse_devicehub.resources import search
|
from ereuse_devicehub.resources import search
|
||||||
# from ereuse_devicehub.resources.device.definitions import ComponentDef
|
from ereuse_devicehub.resources.device.models import Component, Computer, Device, Manufacturer
|
||||||
from ereuse_devicehub.resources.device.models import Component, Computer, Device, Manufacturer, \
|
|
||||||
RamModule, Processor, DataStorage, GraphicCard, Motherboard, Display, NetworkAdapter, SoundCard
|
|
||||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||||
from ereuse_devicehub.resources.event.models import Rate, Event
|
from ereuse_devicehub.resources.event.models import Rate
|
||||||
from ereuse_devicehub.resources.lot.models import Lot, LotDevice
|
from ereuse_devicehub.resources.lot.models import LotDeviceDescendants
|
||||||
from ereuse_devicehub.resources.tag.model import Tag
|
from ereuse_devicehub.resources.tag.model import Tag
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,24 +43,20 @@ class TagQ(query.Query):
|
||||||
|
|
||||||
|
|
||||||
class LotQ(query.Query):
|
class LotQ(query.Query):
|
||||||
id = query.Or(query.QueryField(Lot.descendantsq, fields.UUID()))
|
id = query.Or(query.Equal(LotDeviceDescendants.ancestor_lot_id, fields.UUID()))
|
||||||
|
|
||||||
|
|
||||||
class Filters(query.Query):
|
class Filters(query.Query):
|
||||||
_parent = aliased(Computer)
|
|
||||||
_device_inside_lot = (Device.id == LotDevice.device_id) & (Lot.id == LotDevice.lot_id)
|
|
||||||
_component_inside_lot_through_parent = (Device.id == Component.id) \
|
|
||||||
& (Component.parent_id == _parent.id) \
|
|
||||||
& (_parent.id == LotDevice.device_id) \
|
|
||||||
& (Lot.id == LotDevice.lot_id)
|
|
||||||
|
|
||||||
type = query.Or(OfType(Device.type))
|
type = query.Or(OfType(Device.type))
|
||||||
model = query.ILike(Device.model)
|
model = query.ILike(Device.model)
|
||||||
manufacturer = query.ILike(Device.manufacturer)
|
manufacturer = query.ILike(Device.manufacturer)
|
||||||
serialNumber = query.ILike(Device.serial_number)
|
serialNumber = query.ILike(Device.serial_number)
|
||||||
rating = query.Join(Device.id == Rate.device_id, RateQ)
|
rating = query.Join(Device.id == Rate.device_id, RateQ)
|
||||||
tag = query.Join(Device.id == Tag.device_id, TagQ)
|
tag = query.Join(Device.id == Tag.device_id, TagQ)
|
||||||
lot = query.Join(_device_inside_lot | _component_inside_lot_through_parent, LotQ)
|
# todo This part of the query is really slow
|
||||||
|
# And forces usage of distinct, as it returns many rows
|
||||||
|
# due to having multiple paths to the same
|
||||||
|
lot = query.Join(Device.id == LotDeviceDescendants.device_id, LotQ)
|
||||||
|
|
||||||
|
|
||||||
class Sorting(query.Sort):
|
class Sorting(query.Sort):
|
||||||
|
@ -74,6 +65,8 @@ class Sorting(query.Sort):
|
||||||
|
|
||||||
|
|
||||||
class DeviceView(View):
|
class DeviceView(View):
|
||||||
|
QUERY_PARSER = SearchQueryParser()
|
||||||
|
|
||||||
class FindArgs(marshmallow.Schema):
|
class FindArgs(marshmallow.Schema):
|
||||||
search = f.Str()
|
search = f.Str()
|
||||||
filter = f.Nested(Filters, missing=[])
|
filter = f.Nested(Filters, missing=[])
|
||||||
|
@ -94,7 +87,15 @@ class DeviceView(View):
|
||||||
200:
|
200:
|
||||||
description: The device or devices.
|
description: The device or devices.
|
||||||
"""
|
"""
|
||||||
return super().get(id)
|
# 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
|
||||||
|
|
||||||
def one(self, id: int):
|
def one(self, id: int):
|
||||||
"""Gets one device."""
|
"""Gets one device."""
|
||||||
|
@ -116,7 +117,7 @@ class DeviceView(View):
|
||||||
def find(self, args: dict):
|
def find(self, args: dict):
|
||||||
"""Gets many devices."""
|
"""Gets many devices."""
|
||||||
search_p = args.get('search', None)
|
search_p = args.get('search', None)
|
||||||
query = Device.query
|
query = Device.query.distinct() # todo we should not force to do this if the query is ok
|
||||||
if search_p:
|
if search_p:
|
||||||
properties = DeviceSearch.properties
|
properties = DeviceSearch.properties
|
||||||
tags = DeviceSearch.tags
|
tags = DeviceSearch.tags
|
||||||
|
|
|
@ -244,7 +244,7 @@ class ComputerChassis(Enum):
|
||||||
Tablet = 'Tablet'
|
Tablet = 'Tablet'
|
||||||
Virtual = 'Non-physical device'
|
Virtual = 'Non-physical device'
|
||||||
|
|
||||||
def __format__(self, format_spec):
|
def __str__(self):
|
||||||
return inflection.humanize(inflection.underscore(self.value))
|
return inflection.humanize(inflection.underscore(self.value))
|
||||||
|
|
||||||
|
|
||||||
|
@ -260,24 +260,6 @@ class ReceiverRole(Enum):
|
||||||
Transporter = 'An user that ships the devices to another one.'
|
Transporter = 'An user that ships the devices to another one.'
|
||||||
|
|
||||||
|
|
||||||
class DataStoragePrivacyCompliance(Enum):
|
|
||||||
EraseBasic = 'EraseBasic'
|
|
||||||
EraseBasicError = 'EraseBasicError'
|
|
||||||
EraseSectors = 'EraseSectors'
|
|
||||||
EraseSectorsError = 'EraseSectorsError'
|
|
||||||
Destruction = 'Destruction'
|
|
||||||
DestructionError = 'DestructionError'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_erase(cls, erasure) -> 'DataStoragePrivacyCompliance':
|
|
||||||
"""Returns the correct enum depending of the passed-in erasure."""
|
|
||||||
from ereuse_devicehub.resources.event.models import EraseSectors
|
|
||||||
if isinstance(erasure, EraseSectors):
|
|
||||||
return cls.EraseSectors if not erasure.error else cls.EraseSectorsError
|
|
||||||
else:
|
|
||||||
return cls.EraseBasic if not erasure.error else cls.EraseBasicError
|
|
||||||
|
|
||||||
|
|
||||||
class PrinterTechnology(Enum):
|
class PrinterTechnology(Enum):
|
||||||
"""Technology of the printer."""
|
"""Technology of the printer."""
|
||||||
Toner = 'Toner / Laser'
|
Toner = 'Toner / Laser'
|
||||||
|
@ -285,3 +267,38 @@ class PrinterTechnology(Enum):
|
||||||
SolidInk = 'Solid ink'
|
SolidInk = 'Solid ink'
|
||||||
Dye = 'Dye-sublimation'
|
Dye = 'Dye-sublimation'
|
||||||
Thermal = 'Thermal'
|
Thermal = 'Thermal'
|
||||||
|
|
||||||
|
|
||||||
|
class Severity(IntEnum):
|
||||||
|
"""A flag evaluating the event execution. Ex. failed events
|
||||||
|
have the value `Severity.Error`.
|
||||||
|
|
||||||
|
Devicehub uses 4 severity levels:
|
||||||
|
|
||||||
|
- 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
|
||||||
|
to check negatively affecting the event.
|
||||||
|
- Error: the event failed.
|
||||||
|
|
||||||
|
Devicehub specially raises user awareness when an event
|
||||||
|
has a Severity of ``Warning`` or greater.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Info = 0
|
||||||
|
Notice = 1
|
||||||
|
Warning = 2
|
||||||
|
Error = 3
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self == self.Info:
|
||||||
|
m = '✓'
|
||||||
|
elif self == self.Notice:
|
||||||
|
m = 'ℹ️'
|
||||||
|
elif self == self.Warning:
|
||||||
|
m = '⚠'
|
||||||
|
else:
|
||||||
|
m = '❌'
|
||||||
|
return m
|
||||||
|
|
|
@ -6,18 +6,19 @@ from typing import Set, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import inflection
|
import inflection
|
||||||
|
import teal.db
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from flask import current_app as app, g
|
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, DateTime, Enum as DBEnum, \
|
||||||
Float, ForeignKey, Interval, JSON, Numeric, SmallInteger, Unicode, event, orm
|
Float, ForeignKey, Integer, Interval, JSON, Numeric, SmallInteger, Unicode, event, orm
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.ext.orderinglist import ordering_list
|
from sqlalchemy.ext.orderinglist import ordering_list
|
||||||
from sqlalchemy.orm import backref, relationship, validates
|
from sqlalchemy.orm import backref, relationship, validates
|
||||||
from sqlalchemy.orm.events import AttributeEvents as Events
|
from sqlalchemy.orm.events import AttributeEvents as Events
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, IP, POLYMORPHIC_ID, \
|
from teal.db import ArrayOfEnum, CASCADE_OWN, INHERIT_COND, IP, POLYMORPHIC_ID, \
|
||||||
POLYMORPHIC_ON, StrictVersionType, URL, check_lower, check_range
|
POLYMORPHIC_ON, StrictVersionType, URL, check_lower, check_range
|
||||||
from teal.enums import Country, Currency, Subdivision
|
from teal.enums import Country, Currency, Subdivision
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
|
@ -27,9 +28,9 @@ from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.agent.models import Agent
|
from ereuse_devicehub.resources.agent.models import Agent
|
||||||
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
|
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
|
||||||
Device, Laptop, Server
|
Device, Laptop, Server
|
||||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||||
FunctionalityRange, PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
|
PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \
|
||||||
ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
||||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
@ -43,27 +44,19 @@ class JoinedTableMixin:
|
||||||
|
|
||||||
class Event(Thing):
|
class Event(Thing):
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||||
type = Column(Unicode, nullable=False)
|
type = Column(Unicode, nullable=False, index=True)
|
||||||
name = Column(CIText(), default='', nullable=False)
|
name = Column(CIText(), default='', nullable=False)
|
||||||
name.comment = """
|
name.comment = """
|
||||||
A name or title for the event. Used when searching for events.
|
A name or title for the event. Used when searching for events.
|
||||||
"""
|
"""
|
||||||
incidence = Column(Boolean, default=False, nullable=False)
|
severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False)
|
||||||
incidence.comment = """
|
severity.comment = Severity.__doc__
|
||||||
Should this event be reviewed due some anomaly?
|
|
||||||
"""
|
|
||||||
closed = Column(Boolean, default=True, nullable=False)
|
closed = Column(Boolean, default=True, nullable=False)
|
||||||
closed.comment = """
|
closed.comment = """
|
||||||
Whether the author has finished the event.
|
Whether the author has finished the event.
|
||||||
After this is set to True, no modifications are allowed.
|
After this is set to True, no modifications are allowed.
|
||||||
By default events are closed when performed.
|
By default events are closed when performed.
|
||||||
"""
|
"""
|
||||||
error = Column(Boolean, default=False, nullable=False)
|
|
||||||
error.comment = """
|
|
||||||
Did the event fail?
|
|
||||||
For example, a failure in ``Erase`` means that the data storage
|
|
||||||
unit did not erase correctly.
|
|
||||||
"""
|
|
||||||
description = Column(Unicode, default='', nullable=False)
|
description = Column(Unicode, default='', nullable=False)
|
||||||
description.comment = """
|
description.comment = """
|
||||||
A comment about the event.
|
A comment about the event.
|
||||||
|
@ -148,7 +141,7 @@ class Event(Thing):
|
||||||
For Add and Remove though, this has another meaning: the components
|
For Add and Remove though, this has another meaning: the components
|
||||||
that are added or removed.
|
that are added or removed.
|
||||||
"""
|
"""
|
||||||
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True)
|
||||||
parent = relationship(Computer,
|
parent = relationship(Computer,
|
||||||
backref=backref('events_parent',
|
backref=backref('events_parent',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
|
@ -181,6 +174,7 @@ class Event(Thing):
|
||||||
args = {POLYMORPHIC_ID: cls.t}
|
args = {POLYMORPHIC_ID: cls.t}
|
||||||
if cls.t == 'Event':
|
if cls.t == 'Event':
|
||||||
args[POLYMORPHIC_ON] = cls.type
|
args[POLYMORPHIC_ON] = cls.type
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
if JoinedTableMixin in cls.mro():
|
if JoinedTableMixin in cls.mro():
|
||||||
args[INHERIT_COND] = cls.id == Event.id
|
args[INHERIT_COND] = cls.id == Event.id
|
||||||
return args
|
return args
|
||||||
|
@ -197,16 +191,15 @@ class Event(Thing):
|
||||||
raise ValidationError('The event cannot start after it finished.')
|
raise ValidationError('The event cannot start after it finished.')
|
||||||
return start_time
|
return start_time
|
||||||
|
|
||||||
@property
|
|
||||||
def _err_str(self):
|
|
||||||
return '❌ Error.' if self.error else '✓'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _date_str(self):
|
def _date_str(self):
|
||||||
return '{:%c}'.format(self.end_time or self.created)
|
return '{:%c}'.format(self.end_time or self.created)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '{}'.format(self._err_str)
|
return '{}'.format(self.severity)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{0.t} {0.id} {0.severity}>'.format(self)
|
||||||
|
|
||||||
|
|
||||||
class EventComponent(db.Model):
|
class EventComponent(db.Model):
|
||||||
|
@ -222,17 +215,17 @@ class JoinedWithOneDeviceMixin:
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(JoinedTableMixin, Event):
|
class EventWithOneDevice(JoinedTableMixin, Event):
|
||||||
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False, index=True)
|
||||||
device = relationship(Device,
|
device = relationship(Device,
|
||||||
backref=backref('events_one',
|
backref=backref('events_one',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
cascade=CASCADE,
|
cascade=CASCADE_OWN,
|
||||||
order_by=lambda: EventWithOneDevice.created,
|
order_by=lambda: EventWithOneDevice.created,
|
||||||
collection_class=OrderedSet),
|
collection_class=OrderedSet),
|
||||||
primaryjoin=Device.id == device_id)
|
primaryjoin=Device.id == device_id)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<{0.t} {0.id!r} device={0.device!r}>'.format(self)
|
return '<{0.t} {0.id} {0.severity} device={0.device!r}>'.format(self)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def __mapper_args__(cls):
|
def __mapper_args__(cls):
|
||||||
|
@ -260,7 +253,7 @@ class EventWithMultipleDevices(Event):
|
||||||
collection_class=OrderedSet)
|
collection_class=OrderedSet)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<{0.t} {0.id!r} devices={0.devices!r}>'.format(self)
|
return '<{0.t} {0.id} {0.severity} devices={0.devices!r}>'.format(self)
|
||||||
|
|
||||||
|
|
||||||
class EventDevice(db.Model):
|
class EventDevice(db.Model):
|
||||||
|
@ -270,11 +263,19 @@ class EventDevice(db.Model):
|
||||||
|
|
||||||
|
|
||||||
class Add(EventWithOneDevice):
|
class Add(EventWithOneDevice):
|
||||||
pass
|
"""The act of adding components to a device.
|
||||||
|
|
||||||
|
It is usually used internally from a :class:`.Snapshot`, for
|
||||||
|
example, when adding a secondary data storage to a computer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Remove(EventWithOneDevice):
|
class Remove(EventWithOneDevice):
|
||||||
pass
|
"""The act of removing components from a device.
|
||||||
|
|
||||||
|
It is usually used internally from a :class:`.Snapshot`, for
|
||||||
|
example, when removing a component from a broken computer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Allocate(JoinedTableMixin, EventWithMultipleDevices):
|
class Allocate(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
@ -290,6 +291,30 @@ class Deallocate(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
|
||||||
|
|
||||||
class EraseBasic(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class EraseBasic(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
"""An erasure attempt to a ``DataStorage``. The event contains
|
||||||
|
information about success and nature of the erasure.
|
||||||
|
|
||||||
|
EraseBasic is a software-based fast non-100%-secured way of
|
||||||
|
erasing data storage, performed
|
||||||
|
by Workbench Computer when executing the open-source
|
||||||
|
`shred <https://en.wikipedia.org/wiki/Shred_(Unix)>`_.
|
||||||
|
|
||||||
|
Users can generate erasure certificates from successful erasures.
|
||||||
|
|
||||||
|
Erasures are an accumulation of **erasure steps**, that are performed
|
||||||
|
as separate actions, called ``StepRandom``, for an erasure step
|
||||||
|
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 = Column(Boolean, nullable=False)
|
||||||
zeros.comment = """
|
zeros.comment = """
|
||||||
Whether this erasure had a first erasure step consisting of
|
Whether this erasure had a first erasure step consisting of
|
||||||
|
@ -299,10 +324,19 @@ class EraseBasic(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
# todo return erasure properties like num steps, if it is british...
|
# todo return erasure properties like num steps, if it is british...
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '{} on {}.'.format(self._err_str, self.end_time)
|
return '{} on {}.'.format(self.severity, self.end_time)
|
||||||
|
|
||||||
|
|
||||||
class EraseSectors(EraseBasic):
|
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...
|
||||||
|
|
||||||
|
|
||||||
|
class ErasePhysical(EraseBasic):
|
||||||
|
"""The act of physically destroying a data storage unit."""
|
||||||
|
# todo add attributes
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -310,7 +344,7 @@ class Step(db.Model):
|
||||||
erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True)
|
erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True)
|
||||||
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
||||||
num = Column(SmallInteger, primary_key=True)
|
num = Column(SmallInteger, primary_key=True)
|
||||||
error = Column(Boolean, default=False, nullable=False)
|
severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False)
|
||||||
start_time = Column(DateTime, nullable=False)
|
start_time = Column(DateTime, nullable=False)
|
||||||
start_time.comment = Event.start_time.comment
|
start_time.comment = Event.start_time.comment
|
||||||
end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False)
|
end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False)
|
||||||
|
@ -347,6 +381,92 @@ class StepRandom(Step):
|
||||||
|
|
||||||
|
|
||||||
class Snapshot(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Snapshot(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
"""The Snapshot sets the physical information of the device (S/N, model...)
|
||||||
|
and updates it with erasures, benchmarks, ratings, and tests; updates the
|
||||||
|
composition of its components (adding / removing them), and links tags
|
||||||
|
to the device.
|
||||||
|
|
||||||
|
When receiving a Snapshot, the DeviceHub creates, adds and removes
|
||||||
|
components to match the Snapshot. For example, if a Snapshot of a computer
|
||||||
|
contains a new component, the system searches for the component in its
|
||||||
|
database and, if not found, its creates it; finally linking it to the
|
||||||
|
computer.
|
||||||
|
|
||||||
|
A Snapshot is used with Remove to represent changes in components for
|
||||||
|
a device:
|
||||||
|
|
||||||
|
1. ``Snapshot`` creates a device if it does not exist, and the same
|
||||||
|
for its components. This is all done in one ``Snapshot``.
|
||||||
|
2. If the device exists, it updates its component composition by
|
||||||
|
*adding* and *removing* them. If,
|
||||||
|
for example, this new Snasphot doesn't have a component, it means that
|
||||||
|
this component is not present anymore in the device, thus removing it
|
||||||
|
from it. Then we have that:
|
||||||
|
|
||||||
|
- Components that are added to the device: snapshot2.components -
|
||||||
|
snapshot1.components
|
||||||
|
- Components that are removed to the device: snapshot1.components -
|
||||||
|
snapshot2.components
|
||||||
|
|
||||||
|
When adding a component, there may be the case this component existed
|
||||||
|
before and it was inside another device. In such case, DeviceHub will
|
||||||
|
perform ``Remove`` on the old parent.
|
||||||
|
|
||||||
|
**Snapshots from Workbench**
|
||||||
|
|
||||||
|
When processing a device from the Workbench, this one performs a Snapshot
|
||||||
|
and then performs more events (like testings, benchmarking...).
|
||||||
|
|
||||||
|
There are two ways of sending this information. In an async way,
|
||||||
|
this is, submitting events as soon as Workbench performs then, or
|
||||||
|
submitting only one Snapshot event with all the other events embedded.
|
||||||
|
|
||||||
|
**Asynced**
|
||||||
|
|
||||||
|
The use case, which is represented in the ``test_workbench_phases``,
|
||||||
|
is as follows:
|
||||||
|
|
||||||
|
1. In **T1**, WorkbenchServer (as the middleware from Workbench and
|
||||||
|
Devicehub) submits:
|
||||||
|
|
||||||
|
- A ``Snapshot`` event with the required information to **synchronize**
|
||||||
|
and **rate** the device. This is:
|
||||||
|
|
||||||
|
- Identification information about the device and components
|
||||||
|
(S/N, model, physical characteristics...)
|
||||||
|
- ``Tags`` in a ``tags`` property in the ``device``.
|
||||||
|
- ``Rate`` in an ``events`` property in the ``device``.
|
||||||
|
- ``Benchmarks`` in an ``events`` property in each ``component``
|
||||||
|
or ``device``.
|
||||||
|
- ``TestDataStorage`` as in ``Benchmarks``.
|
||||||
|
- An ordered set of **expected events**, defining which are the next
|
||||||
|
events that Workbench will perform to the device in ideal
|
||||||
|
conditions (device doesn't fail, no Internet drop...).
|
||||||
|
|
||||||
|
Devicehub **syncs** the device with the database and perform the
|
||||||
|
``Benchmark``, the ``TestDataStorage``, and finally the ``Rate``.
|
||||||
|
This leaves the Snapshot **open** to wait for the next events
|
||||||
|
to come.
|
||||||
|
2. Assuming that we expect all events, in **T2**, WorkbenchServer
|
||||||
|
submits a ``StressTest`` with a ``snapshot`` field containing the
|
||||||
|
ID of the Snapshot in 1, and Devicehub links the event with such
|
||||||
|
``Snapshot``.
|
||||||
|
3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot``
|
||||||
|
and ``component`` IDs from 1, linking it to them. It repeats
|
||||||
|
this for all the erased data storage devices; **T3+Tn** being
|
||||||
|
*n* the erased data storage devices.
|
||||||
|
4. WorkbenchServer does like in 3. but for the event ``Install``,
|
||||||
|
finishing in **T3+Tn+Tx**, being *x* the number of data storage
|
||||||
|
devices with an OS installed into.
|
||||||
|
5. In **T3+Tn+Tx**, when all *expected events* have been performed,
|
||||||
|
Devicehub **closes** the ``Snapshot`` from 1.
|
||||||
|
|
||||||
|
**Synced**
|
||||||
|
|
||||||
|
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
||||||
|
the events in an ``events`` property inside each affected ``component``
|
||||||
|
or ``device``.
|
||||||
|
"""
|
||||||
uuid = Column(UUID(as_uuid=True), unique=True)
|
uuid = Column(UUID(as_uuid=True), unique=True)
|
||||||
version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
|
version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
|
||||||
software = Column(DBEnum(SnapshotSoftware), nullable=False)
|
software = Column(DBEnum(SnapshotSoftware), nullable=False)
|
||||||
|
@ -358,10 +478,13 @@ class Snapshot(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
|
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '{}. {} version {}.'.format(self._err_str, self.software, self.version)
|
return '{}. {} version {}.'.format(self.severity, self.software, self.version)
|
||||||
|
|
||||||
|
|
||||||
class Install(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Install(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
"""The action of installing an Operative System to a data
|
||||||
|
storage unit.
|
||||||
|
"""
|
||||||
elapsed = Column(Interval, nullable=False)
|
elapsed = Column(Interval, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -376,6 +499,48 @@ class SnapshotRequest(db.Model):
|
||||||
|
|
||||||
|
|
||||||
class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
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.
|
||||||
|
|
||||||
|
There are two base **types** of ``Rate``: ``WorkbenchRate``,
|
||||||
|
``ManualRate``. ``WorkbenchRate`` can have different
|
||||||
|
**software** algorithms, and each software algorithm can have several
|
||||||
|
**versions**. So, we have 3 dimensions for ``WorkbenchRate``:
|
||||||
|
type, software, version.
|
||||||
|
|
||||||
|
Devicehub generates a rate event for each software and version. So,
|
||||||
|
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),
|
||||||
|
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 technical Workflow in Devicehub is as follows:
|
||||||
|
|
||||||
|
1. In **T1**, the user 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
|
||||||
|
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.
|
||||||
|
2. In **T2**, the agent can optionally visually re-rate the device
|
||||||
|
using the mobile app, generating an ``AppRate``. This new
|
||||||
|
action generates a new ``AggregateRating`` with the ``AppRate``
|
||||||
|
plus the ``WorkbenchRate`` from 1.
|
||||||
|
"""
|
||||||
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
|
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."""
|
||||||
software = Column(DBEnum(RatingSoftware))
|
software = Column(DBEnum(RatingSoftware))
|
||||||
|
@ -573,6 +738,16 @@ class AggregateRate(Rate):
|
||||||
|
|
||||||
|
|
||||||
class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
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).
|
||||||
|
|
||||||
|
Devicehub automatically computes a price from ``AggregateRating``
|
||||||
|
events. As in a **Rate**, price can have **software** and **version**,
|
||||||
|
and there is an **official** price that is used to automatically
|
||||||
|
compute the price from an ``AggregateRating``. Only the official price
|
||||||
|
is computed from an ``AggregateRating``.
|
||||||
|
"""
|
||||||
SCALE = 4
|
SCALE = 4
|
||||||
ROUND = ROUND_HALF_EVEN
|
ROUND = ROUND_HALF_EVEN
|
||||||
currency = Column(DBEnum(Currency), nullable=False)
|
currency = Column(DBEnum(Currency), nullable=False)
|
||||||
|
@ -714,6 +889,12 @@ class EreusePrice(Price):
|
||||||
|
|
||||||
|
|
||||||
class Test(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Test(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
"""The act of testing the physical condition of a device and its
|
||||||
|
components.
|
||||||
|
|
||||||
|
Testing errors and warnings are easily taken in
|
||||||
|
:attr:`ereuse_devicehub.resources.device.models.Device.working`.
|
||||||
|
"""
|
||||||
elapsed = Column(Interval, nullable=False)
|
elapsed = Column(Interval, nullable=False)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
|
@ -732,6 +913,17 @@ class Test(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
|
||||||
|
|
||||||
class TestDataStorage(Test):
|
class TestDataStorage(Test):
|
||||||
|
"""
|
||||||
|
The act of testing the data storage.
|
||||||
|
|
||||||
|
Testing is done using the `S.M.A.R.T self test
|
||||||
|
<https://en.wikipedia.org/wiki/S.M.A.R.T.#Self-tests>`_. Note
|
||||||
|
that not all data storage units, specially some new PCIe ones, do not
|
||||||
|
support SMART testing.
|
||||||
|
|
||||||
|
The test takes to other SMART values indicators of the overall health
|
||||||
|
of the data storage.
|
||||||
|
"""
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True)
|
id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True)
|
||||||
length = Column(DBEnum(TestDataStorageLength), nullable=False) # todo from type
|
length = Column(DBEnum(TestDataStorageLength), nullable=False) # todo from type
|
||||||
status = Column(Unicode(), check_lower('status'), nullable=False)
|
status = Column(Unicode(), check_lower('status'), nullable=False)
|
||||||
|
@ -740,27 +932,25 @@ class TestDataStorage(Test):
|
||||||
reallocated_sector_count = Column(SmallInteger)
|
reallocated_sector_count = Column(SmallInteger)
|
||||||
power_cycle_count = Column(SmallInteger)
|
power_cycle_count = Column(SmallInteger)
|
||||||
reported_uncorrectable_errors = Column(SmallInteger)
|
reported_uncorrectable_errors = Column(SmallInteger)
|
||||||
command_timeout = Column(SmallInteger)
|
command_timeout = Column(Integer)
|
||||||
current_pending_sector_count = Column(SmallInteger)
|
current_pending_sector_count = Column(SmallInteger)
|
||||||
offline_uncorrectable = Column(SmallInteger)
|
offline_uncorrectable = Column(SmallInteger)
|
||||||
remaining_lifetime_percentage = Column(SmallInteger)
|
remaining_lifetime_percentage = Column(SmallInteger)
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
# Define severity
|
||||||
# As of https://www.backblaze.com/blog/hard-drive-smart-stats/ and
|
# As of https://www.backblaze.com/blog/hard-drive-smart-stats/ and
|
||||||
# https://www.backblaze.com/blog-smart-stats-2014-8.html
|
# https://www.backblaze.com/blog-smart-stats-2014-8.html
|
||||||
# We can guess some future disk failures by analyzing some
|
# We can guess some future disk failures by analyzing some SMART data.
|
||||||
# SMART data
|
if self.severity is None:
|
||||||
if (self.reallocated_sector_count or 0) > 10:
|
# Test finished successfully
|
||||||
self.incidence = True
|
|
||||||
self.description = 'Warning: Chance of disk failure within a year.'
|
|
||||||
if (self.current_pending_sector_count or 0) > 40 \
|
|
||||||
and (self.reported_uncorrectable_errors or 0) > 10:
|
|
||||||
self.incidence = True
|
|
||||||
self.description = 'Warning: Chance of disk failure within a year.'
|
|
||||||
if not self.assessment:
|
if not self.assessment:
|
||||||
self.incidence = True
|
self.severity = Severity.Error
|
||||||
self.description = 'Warning: Drive failure expected soon.'
|
elif self.current_pending_sector_count and self.current_pending_sector_count > 40 \
|
||||||
|
or self.reallocated_sector_count and self.reallocated_sector_count > 10:
|
||||||
|
self.severity = Severity.Warning
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
t = inflection.humanize(self.status)
|
t = inflection.humanize(self.status)
|
||||||
|
@ -771,6 +961,10 @@ class TestDataStorage(Test):
|
||||||
|
|
||||||
|
|
||||||
class StressTest(Test):
|
class StressTest(Test):
|
||||||
|
"""The act of stressing (putting to the maximum capacity)
|
||||||
|
a device for an amount of minutes. If the device is not in great
|
||||||
|
condition won't probably survive such test.
|
||||||
|
"""
|
||||||
|
|
||||||
@validates('elapsed')
|
@validates('elapsed')
|
||||||
def is_minute_and_bigger_than_1_minute(self, _, value: timedelta):
|
def is_minute_and_bigger_than_1_minute(self, _, value: timedelta):
|
||||||
|
@ -780,10 +974,11 @@ class StressTest(Test):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '{}. Computing for {}'.format(self._err_str, self.elapsed)
|
return '{}. Computing for {}'.format(self.severity, self.elapsed)
|
||||||
|
|
||||||
|
|
||||||
class Benchmark(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Benchmark(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
"""The act of gauging the performance of a device."""
|
||||||
elapsed = Column(Interval)
|
elapsed = Column(Interval)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
|
@ -802,6 +997,7 @@ class Benchmark(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkDataStorage(Benchmark):
|
class BenchmarkDataStorage(Benchmark):
|
||||||
|
"""Benchmarks the data storage unit reading and writing speeds."""
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
||||||
read_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
read_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
||||||
write_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
write_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
||||||
|
@ -811,19 +1007,27 @@ class BenchmarkDataStorage(Benchmark):
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkWithRate(Benchmark):
|
class BenchmarkWithRate(Benchmark):
|
||||||
|
"""The act of benchmarking a device with a single rate."""
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
||||||
rate = Column(SmallInteger, nullable=False)
|
rate = Column(Float, nullable=False)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '{} points'.format(self.rate)
|
return '{} points'.format(self.rate)
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkProcessor(BenchmarkWithRate):
|
class BenchmarkProcessor(BenchmarkWithRate):
|
||||||
|
"""Benchmarks a processor by executing `BogoMips
|
||||||
|
<https://en.wikipedia.org/wiki/BogoMips>`_. Note that this is not
|
||||||
|
a reliable way of rating processors and we keep it for compatibility
|
||||||
|
purposes.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkProcessorSysbench(BenchmarkProcessor):
|
class BenchmarkProcessorSysbench(BenchmarkProcessor):
|
||||||
pass
|
"""Benchmarks a processor by using the processor benchmarking
|
||||||
|
utility of `sysbench <https://github.com/akopytov/sysbench>`_.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkRamSysbench(BenchmarkWithRate):
|
class BenchmarkRamSysbench(BenchmarkWithRate):
|
||||||
|
@ -831,26 +1035,54 @@ class BenchmarkRamSysbench(BenchmarkWithRate):
|
||||||
|
|
||||||
|
|
||||||
class ToRepair(EventWithMultipleDevices):
|
class ToRepair(EventWithMultipleDevices):
|
||||||
pass
|
"""Select a device to be repaired."""
|
||||||
|
|
||||||
|
|
||||||
class Repair(EventWithMultipleDevices):
|
class Repair(EventWithMultipleDevices):
|
||||||
pass
|
"""Repair is the act of performing reparations.
|
||||||
|
|
||||||
|
If a repair without an error is performed,
|
||||||
|
it represents that the reparation has been successful.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ReadyToUse(EventWithMultipleDevices):
|
class ReadyToUse(EventWithMultipleDevices):
|
||||||
pass
|
"""The device is ready to be used.
|
||||||
|
|
||||||
|
This involves greater preparation from the ``Prepare`` event,
|
||||||
|
and users should only use a device after this event is performed.
|
||||||
|
|
||||||
|
Users usually require devices with this event before shipping them
|
||||||
|
to costumers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ToPrepare(EventWithMultipleDevices):
|
class ToPrepare(EventWithMultipleDevices):
|
||||||
|
"""The device has been selected for preparation.
|
||||||
|
|
||||||
|
See Prepare for more info.
|
||||||
|
|
||||||
|
Usually **ToPrepare** is the next event done after registering the
|
||||||
|
device.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Prepare(EventWithMultipleDevices):
|
class Prepare(EventWithMultipleDevices):
|
||||||
pass
|
"""Work has been performed to the device to a defined point of
|
||||||
|
acceptance.
|
||||||
|
|
||||||
|
Users using this event have to agree what is this point
|
||||||
|
of acceptance; for some is when the device just works, for others
|
||||||
|
when some testing has been performed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Live(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Live(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
"""A keep-alive from a device connected to the Internet with
|
||||||
|
information about its state (in the form of a ``Snapshot`` event)
|
||||||
|
and usage statistics.
|
||||||
|
"""
|
||||||
ip = Column(IP, nullable=False,
|
ip = Column(IP, nullable=False,
|
||||||
comment='The IP where the live was triggered.')
|
comment='The IP where the live was triggered.')
|
||||||
subdivision_confidence = Column(SmallInteger,
|
subdivision_confidence = Column(SmallInteger,
|
||||||
|
@ -873,18 +1105,34 @@ class Live(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
|
||||||
|
|
||||||
class Organize(JoinedTableMixin, EventWithMultipleDevices):
|
class Organize(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
pass
|
"""The act of manipulating/administering/supervising/controlling
|
||||||
|
one or more devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Reserve(Organize):
|
class Reserve(Organize):
|
||||||
pass
|
"""The act of reserving devices and cancelling them.
|
||||||
|
|
||||||
|
After this event is performed, the user is the **reservee** of the
|
||||||
|
devices. There can only be one non-cancelled reservation for
|
||||||
|
a device, and a reservation can only have one reservee.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class CancelReservation(Organize):
|
class CancelReservation(Organize):
|
||||||
pass
|
"""The act of cancelling a reservation."""
|
||||||
|
|
||||||
|
|
||||||
class Trade(JoinedTableMixin, EventWithMultipleDevices):
|
class Trade(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
"""Trade actions log the political exchange of devices between users.
|
||||||
|
Every time a trade event is performed, the old user looses its
|
||||||
|
political possession, for example ownership, in favor of another
|
||||||
|
user.
|
||||||
|
|
||||||
|
|
||||||
|
Performing trade events changes the *Trading* state of the
|
||||||
|
device —:class:`ereuse_devicehub.resources.device.states.Trading`.
|
||||||
|
"""
|
||||||
shipping_date = Column(DateTime)
|
shipping_date = Column(DateTime)
|
||||||
shipping_date.comment = """
|
shipping_date.comment = """
|
||||||
When are the devices going to be ready for shipping?
|
When are the devices going to be ready for shipping?
|
||||||
|
@ -927,36 +1175,67 @@ class Trade(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
|
||||||
|
|
||||||
class Sell(Trade):
|
class Sell(Trade):
|
||||||
pass
|
"""The act of taking money from a buyer in exchange of a device."""
|
||||||
|
|
||||||
|
|
||||||
class Donate(Trade):
|
class Donate(Trade):
|
||||||
pass
|
"""The act of giving devices without compensation."""
|
||||||
|
|
||||||
|
|
||||||
class Rent(Trade):
|
class Rent(Trade):
|
||||||
pass
|
"""The act of giving money in return for temporary use, but not
|
||||||
|
ownership, of a device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class CancelTrade(Trade):
|
class CancelTrade(Trade):
|
||||||
pass
|
"""The act of cancelling a `Sell`_, `Donate`_ or `Rent`_."""
|
||||||
|
# todo cancelTrade does not do anything
|
||||||
|
|
||||||
|
|
||||||
class ToDisposeProduct(Trade):
|
class ToDisposeProduct(Trade):
|
||||||
pass
|
"""The act of setting a device for being disposed.
|
||||||
|
|
||||||
|
See :class:`.DisposeProduct`.
|
||||||
|
"""
|
||||||
|
# todo test this
|
||||||
|
|
||||||
|
|
||||||
class DisposeProduct(Trade):
|
class DisposeProduct(Trade):
|
||||||
pass
|
"""The act of getting rid of devices by giving (selling, donating)
|
||||||
|
to another organization, like a waste manager.
|
||||||
|
|
||||||
|
|
||||||
|
See :class:`.ToDispose` and :class:`.DisposeProduct` for
|
||||||
|
disposing without trading the device. See :class:`.DisposeWaste`
|
||||||
|
and :class:`.Recover` for disposing in-house, this is,
|
||||||
|
without trading the device.
|
||||||
|
"""
|
||||||
|
# todo For usability purposes, users might not directly perform
|
||||||
|
# *DisposeProduct*, but this could automatically be done when
|
||||||
|
# performing :class:`.ToDispose` + :class:`.Receive` to a
|
||||||
|
# ``RecyclingCenter``.
|
||||||
|
|
||||||
|
|
||||||
class Receive(JoinedTableMixin, EventWithMultipleDevices):
|
class Receive(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
"""The act of physically taking delivery of a device.
|
||||||
|
|
||||||
|
The receiver confirms that the devices have arrived, and thus,
|
||||||
|
they are the
|
||||||
|
:attr:`ereuse_devicehub.resources.device.models.Device.physical_possessor`.
|
||||||
|
|
||||||
|
The receiver can optionally take a
|
||||||
|
:class:`ereuse_devicehub.resources.enums.ReceiverRole`.
|
||||||
|
"""
|
||||||
role = Column(DBEnum(ReceiverRole),
|
role = Column(DBEnum(ReceiverRole),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
default=ReceiverRole.Intermediary)
|
default=ReceiverRole.Intermediary)
|
||||||
|
|
||||||
|
|
||||||
class Migrate(JoinedTableMixin, EventWithMultipleDevices):
|
class Migrate(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
"""Moves the devices to a new database/inventory. Devices cannot be
|
||||||
|
modified anymore at the previous database.
|
||||||
|
"""
|
||||||
other = Column(URL(), nullable=False)
|
other = Column(URL(), nullable=False)
|
||||||
other.comment = """
|
other.comment = """
|
||||||
The URL of the Migrate in the other end.
|
The URL of the Migrate in the other end.
|
||||||
|
|
|
@ -17,8 +17,8 @@ from teal.enums import Country
|
||||||
from ereuse_devicehub.resources.agent.models import Agent
|
from ereuse_devicehub.resources.agent.models import Agent
|
||||||
from ereuse_devicehub.resources.device.models import Component, Computer, Device
|
from ereuse_devicehub.resources.device.models import Component, Computer, Device
|
||||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||||
PriceSoftware, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, \
|
PriceSoftware, RatingSoftware, ReceiverRole, Severity, SnapshotExpectedEvents, \
|
||||||
TestDataStorageLength
|
SnapshotSoftware, TestDataStorageLength
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
@ -27,8 +27,6 @@ class Event(Thing):
|
||||||
id = ... # type: Column
|
id = ... # type: Column
|
||||||
name = ... # type: Column
|
name = ... # type: Column
|
||||||
type = ... # type: Column
|
type = ... # type: Column
|
||||||
error = ... # type: Column
|
|
||||||
incidence = ... # type: Column
|
|
||||||
description = ... # type: Column
|
description = ... # type: Column
|
||||||
snapshot_id = ... # type: Column
|
snapshot_id = ... # type: Column
|
||||||
snapshot = ... # type: relationship
|
snapshot = ... # type: relationship
|
||||||
|
@ -41,17 +39,14 @@ class Event(Thing):
|
||||||
start_time = ... # type: Column
|
start_time = ... # type: Column
|
||||||
end_time = ... # type: Column
|
end_time = ... # type: Column
|
||||||
agent_id = ... # type: Column
|
agent_id = ... # type: Column
|
||||||
|
severity = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, id=None, name=None, incidence=None, closed=None, error=None,
|
def __init__(self, **kwargs) -> None:
|
||||||
description=None, start_time=None, end_time=None, snapshot=None, agent=None,
|
|
||||||
parent=None, created=None, updated=None, author=None) -> None:
|
|
||||||
super().__init__(created, updated)
|
super().__init__(created, updated)
|
||||||
self.id = ... # type: UUID
|
self.id = ... # type: UUID
|
||||||
self.name = ... # type: str
|
self.name = ... # type: str
|
||||||
self.type = ... # type: str
|
self.type = ... # type: str
|
||||||
self.incidence = ... # type: bool
|
|
||||||
self.closed = ... # type: bool
|
self.closed = ... # type: bool
|
||||||
self.error = ... # type: bool
|
|
||||||
self.description = ... # type: str
|
self.description = ... # type: str
|
||||||
self.start_time = ... # type: datetime
|
self.start_time = ... # type: datetime
|
||||||
self.end_time = ... # type: datetime
|
self.end_time = ... # type: datetime
|
||||||
|
@ -60,34 +55,25 @@ class Event(Thing):
|
||||||
self.parent = ... # type: Computer
|
self.parent = ... # type: Computer
|
||||||
self.agent = ... # type: Agent
|
self.agent = ... # type: Agent
|
||||||
self.author = ... # type: User
|
self.author = ... # type: User
|
||||||
|
self.severity = ... # type: Severity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> urlutils.URL:
|
def url(self) -> urlutils.URL:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
|
||||||
def _err_str(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(Event):
|
class EventWithOneDevice(Event):
|
||||||
|
|
||||||
def __init__(self, id=None, name=None, incidence=None, closed=None, error=None,
|
def __init__(self, **kwargs) -> None:
|
||||||
description=None, start_time=None, end_time=None, snapshot=None, agent=None,
|
super().__init__(**kwargs)
|
||||||
parent=None, created=None, updated=None, author=None, device=None) -> None:
|
|
||||||
super().__init__(id, name, incidence, closed, error, description, start_time, end_time,
|
|
||||||
snapshot, agent, parent, created, updated, author)
|
|
||||||
self.device = ... # type: Device
|
self.device = ... # type: Device
|
||||||
|
|
||||||
|
|
||||||
class EventWithMultipleDevices(Event):
|
class EventWithMultipleDevices(Event):
|
||||||
devices = ... # type: relationship
|
devices = ... # type: relationship
|
||||||
|
|
||||||
def __init__(self, id=None, name=None, incidence=None, closed=None, error=None,
|
def __init__(self, **kwargs) -> None:
|
||||||
description=None, start_time=None, end_time=None, snapshot=None, agent=None,
|
super().__init__(**kwargs)
|
||||||
parent=None, created=None, updated=None, author=None, devices=None) -> None:
|
|
||||||
super().__init__(id, name, incidence, closed, error, description, start_time, end_time,
|
|
||||||
snapshot, agent, parent, created, updated, author)
|
|
||||||
self.devices = ... # type: Set[Device]
|
self.devices = ... # type: Set[Device]
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,15 +86,21 @@ class Remove(EventWithOneDevice):
|
||||||
|
|
||||||
|
|
||||||
class Step(Model):
|
class Step(Model):
|
||||||
|
type = ... # type: Column
|
||||||
|
num = ... # type: Column
|
||||||
|
start_time = ... # type: Column
|
||||||
|
end_time = ... # type: Column
|
||||||
|
erasure = ... # type: relationship
|
||||||
|
severity = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, num=None, success=None, start_time=None, end_time=None,
|
def __init__(self, num=None, success=None, start_time=None, end_time=None,
|
||||||
erasure=None, error=None) -> None:
|
erasure=None, severity=None) -> None:
|
||||||
self.type = ... # type: str
|
self.type = ... # type: str
|
||||||
self.num = ... # type: int
|
self.num = ... # type: int
|
||||||
self.success = ... # type: bool
|
|
||||||
self.start_time = ... # type: datetime
|
self.start_time = ... # type: datetime
|
||||||
self.end_time = ... # type: datetime
|
self.end_time = ... # type: datetime
|
||||||
self.erasure = ... # type: EraseBasic
|
self.erasure = ... # type: EraseBasic
|
||||||
self.error = ... # type: bool
|
self.severity = ... # type: Severity
|
||||||
|
|
||||||
|
|
||||||
class StepZero(Step):
|
class StepZero(Step):
|
||||||
|
|
|
@ -12,7 +12,7 @@ from ereuse_devicehub.marshmallow import NestedOn
|
||||||
from ereuse_devicehub.resources.agent.schemas import Agent
|
from ereuse_devicehub.resources.agent.schemas import Agent
|
||||||
from ereuse_devicehub.resources.device.schemas import Component, Computer, Device
|
from ereuse_devicehub.resources.device.schemas import Component, Computer, Device
|
||||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||||
PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \
|
PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, Severity, \
|
||||||
SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
||||||
from ereuse_devicehub.resources.event import models as m
|
from ereuse_devicehub.resources.event import models as m
|
||||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
||||||
|
@ -25,9 +25,8 @@ class Event(Thing):
|
||||||
name = SanitizedStr(default='',
|
name = SanitizedStr(default='',
|
||||||
validate=Length(max=STR_BIG_SIZE),
|
validate=Length(max=STR_BIG_SIZE),
|
||||||
description=m.Event.name.comment)
|
description=m.Event.name.comment)
|
||||||
incidence = Boolean(default=False, description=m.Event.incidence.comment)
|
|
||||||
closed = Boolean(missing=True, description=m.Event.closed.comment)
|
closed = Boolean(missing=True, description=m.Event.closed.comment)
|
||||||
error = Boolean(default=False, description=m.Event.error.comment)
|
severity = EnumField(Severity, description=m.Event.severity.comment)
|
||||||
description = SanitizedStr(default='', description=m.Event.description.comment)
|
description = SanitizedStr(default='', description=m.Event.description.comment)
|
||||||
start_time = DateTime(data_key='startTime', description=m.Event.start_time.comment)
|
start_time = DateTime(data_key='startTime', description=m.Event.start_time.comment)
|
||||||
end_time = DateTime(data_key='endTime', description=m.Event.end_time.comment)
|
end_time = DateTime(data_key='endTime', description=m.Event.end_time.comment)
|
||||||
|
@ -85,7 +84,7 @@ class Step(Schema):
|
||||||
type = String(description='Only required when it is nested.')
|
type = String(description='Only required when it is nested.')
|
||||||
start_time = DateTime(required=True, data_key='startTime')
|
start_time = DateTime(required=True, data_key='startTime')
|
||||||
end_time = DateTime(required=True, data_key='endTime')
|
end_time = DateTime(required=True, data_key='endTime')
|
||||||
error = Boolean(default=False, description='Did the event fail?')
|
severity = EnumField(Severity, description=m.Event.severity.comment)
|
||||||
|
|
||||||
|
|
||||||
class StepZero(Step):
|
class StepZero(Step):
|
||||||
|
@ -299,7 +298,7 @@ class BenchmarkDataStorage(Benchmark):
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkWithRate(Benchmark):
|
class BenchmarkWithRate(Benchmark):
|
||||||
rate = Integer(required=True)
|
rate = Float(required=True)
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkProcessor(BenchmarkWithRate):
|
class BenchmarkProcessor(BenchmarkWithRate):
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from flask import g
|
from flask import g
|
||||||
from sqlalchemy import TEXT
|
from sqlalchemy import TEXT
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.sql import expression as exp
|
|
||||||
from sqlalchemy_utils import LtreeType
|
from sqlalchemy_utils import LtreeType
|
||||||
from sqlalchemy_utils.types.ltree import LQUERY
|
from sqlalchemy_utils.types.ltree import LQUERY
|
||||||
from teal.db import UUIDLtree
|
from teal.db import CASCADE_OWN, UUIDLtree
|
||||||
from teal.resource import url_for_resource
|
from teal.resource import url_for_resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import create_view, db, exp, f
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Component, Device
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ from ereuse_devicehub.resources.user.models import User
|
||||||
class Lot(Thing):
|
class Lot(Thing):
|
||||||
id = db.Column(UUID(as_uuid=True), primary_key=True) # uuid is generated on init by default
|
id = db.Column(UUID(as_uuid=True), primary_key=True) # uuid is generated on init by default
|
||||||
name = db.Column(CIText(), nullable=False)
|
name = db.Column(CIText(), nullable=False)
|
||||||
|
description = db.Column(CIText())
|
||||||
|
description.comment = """A comment about the lot."""
|
||||||
closed = db.Column(db.Boolean, default=False, nullable=False)
|
closed = db.Column(db.Boolean, default=False, nullable=False)
|
||||||
closed.comment = """
|
closed.comment = """
|
||||||
A closed lot cannot be modified anymore.
|
A closed lot cannot be modified anymore.
|
||||||
|
@ -28,6 +30,7 @@ class Lot(Thing):
|
||||||
devices = db.relationship(Device,
|
devices = db.relationship(Device,
|
||||||
backref=db.backref('lots', lazy=True, collection_class=set),
|
backref=db.backref('lots', lazy=True, collection_class=set),
|
||||||
secondary=lambda: LotDevice.__table__,
|
secondary=lambda: LotDevice.__table__,
|
||||||
|
lazy=True,
|
||||||
collection_class=set)
|
collection_class=set)
|
||||||
"""
|
"""
|
||||||
The **children** devices that the lot has.
|
The **children** devices that the lot has.
|
||||||
|
@ -35,48 +38,48 @@ class Lot(Thing):
|
||||||
Note that the lot can have more devices, if they are inside
|
Note that the lot can have more devices, if they are inside
|
||||||
descendant lots.
|
descendant lots.
|
||||||
"""
|
"""
|
||||||
|
parents = db.relationship(lambda: Lot,
|
||||||
|
viewonly=True,
|
||||||
|
lazy=True,
|
||||||
|
collection_class=set,
|
||||||
|
secondary=lambda: LotParent.__table__,
|
||||||
|
primaryjoin=lambda: Lot.id == LotParent.child_id,
|
||||||
|
secondaryjoin=lambda: LotParent.parent_id == Lot.id,
|
||||||
|
cascade='refresh-expire', # propagate changes outside ORM
|
||||||
|
backref=db.backref('children',
|
||||||
|
viewonly=True,
|
||||||
|
lazy=True,
|
||||||
|
cascade='refresh-expire',
|
||||||
|
collection_class=set)
|
||||||
|
)
|
||||||
|
"""The parent lots."""
|
||||||
|
|
||||||
def __init__(self, name: str, closed: bool = closed.default.arg) -> None:
|
all_devices = db.relationship(Device,
|
||||||
|
viewonly=True,
|
||||||
|
lazy=True,
|
||||||
|
collection_class=set,
|
||||||
|
secondary=lambda: LotDeviceDescendants.__table__,
|
||||||
|
primaryjoin=lambda: Lot.id == LotDeviceDescendants.ancestor_lot_id,
|
||||||
|
secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id)
|
||||||
|
"""All devices, including components, inside this lot and its
|
||||||
|
descendants.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, closed: bool = closed.default.arg,
|
||||||
|
description: str = None) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes a lot
|
Initializes a lot
|
||||||
:param name:
|
:param name:
|
||||||
:param closed:
|
:param closed:
|
||||||
"""
|
"""
|
||||||
super().__init__(id=uuid.uuid4(), name=name, closed=closed)
|
super().__init__(id=uuid.uuid4(), name=name, closed=closed, description=description)
|
||||||
Path(self) # Lots have always one edge per default.
|
Path(self) # Lots have always one edge per default.
|
||||||
|
|
||||||
def add_child(self, child):
|
|
||||||
"""Adds a child to this lot."""
|
|
||||||
if isinstance(child, Lot):
|
|
||||||
Path.add(self.id, child.id)
|
|
||||||
db.session.refresh(self) # todo is this useful?
|
|
||||||
db.session.refresh(child)
|
|
||||||
else:
|
|
||||||
assert isinstance(child, uuid.UUID)
|
|
||||||
Path.add(self.id, child)
|
|
||||||
db.session.refresh(self) # todo is this useful?
|
|
||||||
|
|
||||||
def remove_child(self, child):
|
|
||||||
if isinstance(child, Lot):
|
|
||||||
Path.delete(self.id, child.id)
|
|
||||||
else:
|
|
||||||
assert isinstance(child, uuid.UUID)
|
|
||||||
Path.delete(self.id, child)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> urlutils.URL:
|
def url(self) -> urlutils.URL:
|
||||||
"""The URL where to GET this event."""
|
"""The URL where to GET this event."""
|
||||||
return urlutils.URL(url_for_resource(Lot, item_id=self.id))
|
return urlutils.URL(url_for_resource(Lot, item_id=self.id))
|
||||||
|
|
||||||
@property
|
|
||||||
def children(self):
|
|
||||||
"""The children lots."""
|
|
||||||
# From https://stackoverflow.com/a/41158890
|
|
||||||
id = UUIDLtree.convert(self.id)
|
|
||||||
return self.query \
|
|
||||||
.join(self.__class__.paths) \
|
|
||||||
.filter(Path.path.lquery(exp.cast('*.{}.*{{1}}'.format(id), LQUERY)))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def descendants(self):
|
def descendants(self):
|
||||||
return self.descendantsq(self.id)
|
return self.descendantsq(self.id)
|
||||||
|
@ -86,29 +89,73 @@ class Lot(Thing):
|
||||||
_id = UUIDLtree.convert(id)
|
_id = UUIDLtree.convert(id)
|
||||||
return (cls.id == Path.lot_id) & Path.path.lquery(exp.cast('*.{}.*'.format(_id), LQUERY))
|
return (cls.id == Path.lot_id) & Path.path.lquery(exp.cast('*.{}.*'.format(_id), LQUERY))
|
||||||
|
|
||||||
@property
|
|
||||||
def parents(self):
|
|
||||||
return self.parentsq(self.id)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parentsq(cls, id: UUID):
|
|
||||||
"""The parent lots."""
|
|
||||||
id = UUIDLtree.convert(id)
|
|
||||||
i = db.func.index(Path.path, id)
|
|
||||||
parent_id = db.func.replace(exp.cast(db.func.subpath(Path.path, i - 1, i), TEXT), '_', '-')
|
|
||||||
join_clause = parent_id == exp.cast(Lot.id, TEXT)
|
|
||||||
return cls.query.join(Path, join_clause).filter(
|
|
||||||
Path.path.lquery(exp.cast('*{{1}}.{}.*'.format(id), LQUERY))
|
|
||||||
)
|
|
||||||
|
|
||||||
def __contains__(self, child: 'Lot'):
|
|
||||||
return Path.has_lot(self.id, child.id)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def roots(cls):
|
def roots(cls):
|
||||||
"""Gets the lots that are not under any other lot."""
|
"""Gets the lots that are not under any other lot."""
|
||||||
return cls.query.join(cls.paths).filter(db.func.nlevel(Path.path) == 1)
|
return cls.query.join(cls.paths).filter(db.func.nlevel(Path.path) == 1)
|
||||||
|
|
||||||
|
def add_children(self, *children):
|
||||||
|
"""Add children lots to this lot.
|
||||||
|
|
||||||
|
This operation is highly costly as it forces refreshing
|
||||||
|
many models in session.
|
||||||
|
"""
|
||||||
|
for child in children:
|
||||||
|
if isinstance(child, Lot):
|
||||||
|
Path.add(self.id, child.id)
|
||||||
|
db.session.refresh(child)
|
||||||
|
else:
|
||||||
|
assert isinstance(child, uuid.UUID)
|
||||||
|
Path.add(self.id, child)
|
||||||
|
# We need to refresh the models involved in this operation
|
||||||
|
# outside the session / ORM control so the models
|
||||||
|
# that have relationships to this model
|
||||||
|
# with the cascade 'refresh-expire' can welcome the changes
|
||||||
|
db.session.refresh(self)
|
||||||
|
|
||||||
|
def remove_children(self, *children):
|
||||||
|
"""Remove children lots from this lot.
|
||||||
|
|
||||||
|
This operation is highly costly as it forces refreshing
|
||||||
|
many models in session.
|
||||||
|
"""
|
||||||
|
for child in children:
|
||||||
|
if isinstance(child, Lot):
|
||||||
|
Path.delete(self.id, child.id)
|
||||||
|
db.session.refresh(child)
|
||||||
|
else:
|
||||||
|
assert isinstance(child, uuid.UUID)
|
||||||
|
Path.delete(self.id, child)
|
||||||
|
db.session.refresh(self)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""Deletes the lot.
|
||||||
|
|
||||||
|
This method removes the children lots and children
|
||||||
|
devices orphan from this lot and then marks this lot
|
||||||
|
for deletion.
|
||||||
|
"""
|
||||||
|
self.remove_children(*self.children)
|
||||||
|
db.session.delete(self)
|
||||||
|
|
||||||
|
def _refresh_models_with_relationships_to_lots(self):
|
||||||
|
session = db.Session.object_session(self)
|
||||||
|
for model in session:
|
||||||
|
if isinstance(model, (Device, Lot, Path)):
|
||||||
|
session.expire(model)
|
||||||
|
|
||||||
|
def __contains__(self, child: Union['Lot', Device]):
|
||||||
|
if isinstance(child, Lot):
|
||||||
|
return Path.has_lot(self.id, child.id)
|
||||||
|
elif isinstance(child, Device):
|
||||||
|
device = db.session.query(LotDeviceDescendants) \
|
||||||
|
.filter(LotDeviceDescendants.device_id == child.id) \
|
||||||
|
.filter(LotDeviceDescendants.ancestor_lot_id == self.id) \
|
||||||
|
.one_or_none()
|
||||||
|
return device
|
||||||
|
else:
|
||||||
|
raise TypeError('Lot only contains devices and lots, not {}'.format(child.__class__))
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<Lot {0.name} devices={0.devices!r}>'.format(self)
|
return '<Lot {0.name} devices={0.devices!r}>'.format(self)
|
||||||
|
|
||||||
|
@ -131,9 +178,12 @@ class Path(db.Model):
|
||||||
id = db.Column(db.UUID(as_uuid=True),
|
id = db.Column(db.UUID(as_uuid=True),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
server_default=db.text('gen_random_uuid()'))
|
server_default=db.text('gen_random_uuid()'))
|
||||||
lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False)
|
lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False, index=True)
|
||||||
lot = db.relationship(Lot,
|
lot = db.relationship(Lot,
|
||||||
backref=db.backref('paths', lazy=True, collection_class=set),
|
backref=db.backref('paths',
|
||||||
|
lazy=True,
|
||||||
|
collection_class=set,
|
||||||
|
cascade=CASCADE_OWN),
|
||||||
primaryjoin=Lot.id == lot_id)
|
primaryjoin=Lot.id == lot_id)
|
||||||
path = db.Column(LtreeType, nullable=False)
|
path = db.Column(LtreeType, nullable=False)
|
||||||
created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP'))
|
created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP'))
|
||||||
|
@ -171,3 +221,64 @@ class Path(db.Model):
|
||||||
"SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(parent_id, child_id)
|
"SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(parent_id, child_id)
|
||||||
).first()
|
).first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LotDeviceDescendants(db.Model):
|
||||||
|
"""A view facilitating querying inclusion between devices and lots,
|
||||||
|
including components.
|
||||||
|
|
||||||
|
The view has 4 columns:
|
||||||
|
1. The ID of the device.
|
||||||
|
2. The ID of a lot containing the device.
|
||||||
|
3. The ID of the lot that directly contains the device.
|
||||||
|
4. If 1. is a component, the ID of the device that is inside the lot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ancestor = Lot.__table__.alias(name='ancestor')
|
||||||
|
"""Ancestor lot table."""
|
||||||
|
_desc = Lot.__table__.alias()
|
||||||
|
"""Descendant lot table."""
|
||||||
|
lot_device = _desc \
|
||||||
|
.join(LotDevice, _desc.c.id == LotDevice.lot_id) \
|
||||||
|
.join(Path, _desc.c.id == Path.lot_id)
|
||||||
|
"""Join: Path -- Lot -- LotDevice"""
|
||||||
|
|
||||||
|
descendants = "path.path ~ (CAST('*.'|| replace(CAST({}.id as text), '-', '_') " \
|
||||||
|
"|| '.*' AS LQUERY))".format(_ancestor.name)
|
||||||
|
"""Query that gets the descendants of the ancestor lot."""
|
||||||
|
devices = db.select([
|
||||||
|
LotDevice.device_id,
|
||||||
|
_desc.c.id.label('parent_lot_id'),
|
||||||
|
_ancestor.c.id.label('ancestor_lot_id'),
|
||||||
|
None
|
||||||
|
]).select_from(_ancestor).select_from(lot_device).where(descendants)
|
||||||
|
|
||||||
|
# Components
|
||||||
|
_parent_device = Device.__table__.alias(name='parent_device')
|
||||||
|
"""The device that has the access to the lot."""
|
||||||
|
lot_device_component = lot_device \
|
||||||
|
.join(_parent_device, _parent_device.c.id == LotDevice.device_id) \
|
||||||
|
.join(Component, _parent_device.c.id == Component.parent_id)
|
||||||
|
"""Join: Path -- Lot -- LotDevice -- ParentDevice (Device) -- Component"""
|
||||||
|
|
||||||
|
components = db.select([
|
||||||
|
Component.id.label('device_id'),
|
||||||
|
_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)
|
||||||
|
|
||||||
|
__table__ = create_view('lot_device_descendants', devices.union(components))
|
||||||
|
|
||||||
|
|
||||||
|
class LotParent(db.Model):
|
||||||
|
i = f.index(Path.path, db.func.text2ltree(f.replace(exp.cast(Path.lot_id, TEXT), '-', '_')))
|
||||||
|
|
||||||
|
__table__ = create_view(
|
||||||
|
'lot_parent',
|
||||||
|
db.select([
|
||||||
|
Path.lot_id.label('child_id'),
|
||||||
|
exp.cast(f.replace(exp.cast(f.subltree(Path.path, i - 1, i), TEXT), '_', '-'),
|
||||||
|
UUID).label('parent_id')
|
||||||
|
]).select_from(Path).where(i > 0),
|
||||||
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Iterable, Set, Union
|
from typing import Iterable, Optional, Set, Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
|
@ -8,6 +8,7 @@ from sqlalchemy import Column
|
||||||
from sqlalchemy.orm import Query, relationship
|
from sqlalchemy.orm import Query, relationship
|
||||||
from sqlalchemy_utils import Ltree
|
from sqlalchemy_utils import Ltree
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
|
|
||||||
|
@ -20,6 +21,9 @@ class Lot(Thing):
|
||||||
closed = ... # type: Column
|
closed = ... # type: Column
|
||||||
devices = ... # type: relationship
|
devices = ... # type: relationship
|
||||||
paths = ... # type: relationship
|
paths = ... # type: relationship
|
||||||
|
description = ... # type: Column
|
||||||
|
all_devices = ... # type: relationship
|
||||||
|
parents = ... # type: relationship
|
||||||
|
|
||||||
def __init__(self, name: str, closed: bool = closed.default.arg) -> None:
|
def __init__(self, name: str, closed: bool = closed.default.arg) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -28,21 +32,21 @@ class Lot(Thing):
|
||||||
self.closed = ... # type: bool
|
self.closed = ... # type: bool
|
||||||
self.devices = ... # type: Set[Device]
|
self.devices = ... # type: Set[Device]
|
||||||
self.paths = ... # type: Set[Path]
|
self.paths = ... # type: Set[Path]
|
||||||
|
self.description = ... # type: str
|
||||||
|
self.all_devices = ... # type: Set[Device]
|
||||||
|
self.parents = ... # type: Set[Lot]
|
||||||
|
self.children = ... # type: Set[Lot]
|
||||||
|
|
||||||
def add_child(self, child: Union[Lot, uuid.UUID]):
|
def add_children(self, *children: Union[Lot, uuid.UUID]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove_child(self, child: Union[Lot, uuid.UUID]):
|
def remove_children(self, *children: Union[Lot, uuid.UUID]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def roots(cls) -> LotQuery:
|
def roots(cls) -> LotQuery:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
|
||||||
def children(self) -> LotQuery:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def descendants(self) -> LotQuery:
|
def descendants(self) -> LotQuery:
|
||||||
pass
|
pass
|
||||||
|
@ -51,18 +55,13 @@ class Lot(Thing):
|
||||||
def descendantsq(cls, id) -> LotQuery:
|
def descendantsq(cls, id) -> LotQuery:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
|
||||||
def parents(self) -> LotQuery:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parentsq(cls, id) -> LotQuery:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> urlutils.URL:
|
def url(self) -> urlutils.URL:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Path:
|
class Path:
|
||||||
id = ... # type: Column
|
id = ... # type: Column
|
||||||
|
@ -77,3 +76,17 @@ class Path:
|
||||||
self.lot = ... # type: Lot
|
self.lot = ... # type: Lot
|
||||||
self.path = ... # type: Ltree
|
self.path = ... # type: Ltree
|
||||||
self.created = ... # type: datetime
|
self.created = ... # type: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class LotDeviceDescendants(db.Model):
|
||||||
|
device_id = ... # type: Column
|
||||||
|
ancestor_lot_id = ... # type: Column
|
||||||
|
parent_lot_id = ... # type: Column
|
||||||
|
device_parent_id = ... # type: Column
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.device_id = ... # type: int
|
||||||
|
self.ancestor_lot_id = ... # type: UUID
|
||||||
|
self.parent_lot_id = ... # type: UUID
|
||||||
|
self.device_parent_id = ... # type: Optional[int]
|
||||||
|
|
|
@ -11,6 +11,7 @@ from ereuse_devicehub.resources.schemas import Thing
|
||||||
class Lot(Thing):
|
class Lot(Thing):
|
||||||
id = f.UUID(dump_only=True)
|
id = f.UUID(dump_only=True)
|
||||||
name = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), required=True)
|
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)
|
closed = f.Boolean(missing=False, description=m.Lot.closed.comment)
|
||||||
devices = NestedOn(Device, many=True, dump_only=True)
|
devices = NestedOn(Device, many=True, dump_only=True)
|
||||||
children = NestedOn('Lot', many=True, dump_only=True)
|
children = NestedOn('Lot', many=True, dump_only=True)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Set
|
from typing import Dict, List, Set, Union
|
||||||
|
|
||||||
import marshmallow as ma
|
import marshmallow as ma
|
||||||
from flask import Response, jsonify, request
|
from flask import Response, jsonify, request
|
||||||
|
@ -67,10 +67,12 @@ class LotView(View):
|
||||||
you can filter.
|
you can filter.
|
||||||
"""
|
"""
|
||||||
if args['format'] == LotFormat.UiTree:
|
if args['format'] == LotFormat.UiTree:
|
||||||
return jsonify({
|
lots = self.schema.dump(Lot.query, many=True, nested=1)
|
||||||
'items': self.ui_tree(),
|
ret = {
|
||||||
|
'items': {l['id']: l for l in lots},
|
||||||
|
'tree': self.ui_tree(),
|
||||||
'url': request.path
|
'url': request.path
|
||||||
})
|
}
|
||||||
else:
|
else:
|
||||||
query = Lot.query
|
query = Lot.query
|
||||||
if args['search']:
|
if args['search']:
|
||||||
|
@ -89,16 +91,22 @@ class LotView(View):
|
||||||
}
|
}
|
||||||
return jsonify(ret)
|
return jsonify(ret)
|
||||||
|
|
||||||
@classmethod
|
def delete(self, id):
|
||||||
def ui_tree(cls) -> List[dict]:
|
lot = Lot.query.filter_by(id=id).one()
|
||||||
nodes = []
|
lot.delete()
|
||||||
for model in Path.query: # type: Path
|
db.session.commit()
|
||||||
path = deque(model.path.path.split('.'))
|
return Response(status=204)
|
||||||
cls._p(nodes, path)
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _p(cls, nodes: List[dict], path: deque):
|
def ui_tree(cls) -> List[Dict]:
|
||||||
|
tree = []
|
||||||
|
for model in Path.query: # type: Path
|
||||||
|
path = deque(model.path.path.split('.'))
|
||||||
|
cls._p(tree, path)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _p(cls, nodes: List[Dict[str, Union[uuid.UUID, List]]], path: deque):
|
||||||
"""Recursively creates the nested lot structure.
|
"""Recursively creates the nested lot structure.
|
||||||
|
|
||||||
Every recursive step consumes path (a deque of lot_id),
|
Every recursive step consumes path (a deque of lot_id),
|
||||||
|
@ -110,14 +118,8 @@ class LotView(View):
|
||||||
# does lot_id exist already in node?
|
# does lot_id exist already in node?
|
||||||
node = next(part for part in nodes if lot_id == part['id'])
|
node = next(part for part in nodes if lot_id == part['id'])
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
lot = Lot.query.filter_by(id=lot_id).one()
|
|
||||||
node = {
|
node = {
|
||||||
'id': lot_id,
|
'id': lot_id,
|
||||||
'name': lot.name,
|
|
||||||
'url': lot.url.to_text(),
|
|
||||||
'closed': lot.closed,
|
|
||||||
'updated': lot.updated,
|
|
||||||
'created': lot.created,
|
|
||||||
'nodes': []
|
'nodes': []
|
||||||
}
|
}
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
@ -174,12 +176,10 @@ class LotChildrenView(LotBaseChildrenView):
|
||||||
id = ma.fields.List(ma.fields.UUID())
|
id = ma.fields.List(ma.fields.UUID())
|
||||||
|
|
||||||
def _post(self, lot: Lot, ids: Set[uuid.UUID]):
|
def _post(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||||
for id in ids:
|
lot.add_children(*ids)
|
||||||
lot.add_child(id) # todo what to do if child exists already?
|
|
||||||
|
|
||||||
def _delete(self, lot: Lot, ids: Set[uuid.UUID]):
|
def _delete(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||||
for id in ids:
|
lot.remove_children(*ids)
|
||||||
lot.remove_child(id)
|
|
||||||
|
|
||||||
|
|
||||||
class LotDeviceView(LotBaseChildrenView):
|
class LotDeviceView(LotBaseChildrenView):
|
||||||
|
|
|
@ -13,12 +13,14 @@ class Thing(db.Model):
|
||||||
# todo make updated to auto-update
|
# todo make updated to auto-update
|
||||||
updated = db.Column(db.TIMESTAMP(timezone=True),
|
updated = db.Column(db.TIMESTAMP(timezone=True),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
|
index=True,
|
||||||
server_default=db.text('CURRENT_TIMESTAMP'))
|
server_default=db.text('CURRENT_TIMESTAMP'))
|
||||||
updated.comment = """
|
updated.comment = """
|
||||||
When this was last changed.
|
When this was last changed.
|
||||||
"""
|
"""
|
||||||
created = db.Column(db.TIMESTAMP(timezone=True),
|
created = db.Column(db.TIMESTAMP(timezone=True),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
|
index=True,
|
||||||
server_default=db.text('CURRENT_TIMESTAMP'))
|
server_default=db.text('CURRENT_TIMESTAMP'))
|
||||||
created.comment = """
|
created.comment = """
|
||||||
When Devicehub created this.
|
When Devicehub created this.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Column
|
from sqlalchemy import Column, Table
|
||||||
from teal.db import Model
|
from teal.db import Model
|
||||||
|
|
||||||
STR_SIZE = 64
|
STR_SIZE = 64
|
||||||
|
@ -10,6 +10,7 @@ STR_XSM_SIZE = 16
|
||||||
|
|
||||||
|
|
||||||
class Thing(Model):
|
class Thing(Model):
|
||||||
|
__table__ = ... # type: Table
|
||||||
t = ... # type: str
|
t = ... # type: str
|
||||||
type = ... # type: str
|
type = ... # type: str
|
||||||
updated = ... # type: Column
|
updated = ... # type: Column
|
||||||
|
|
|
@ -32,12 +32,13 @@ class Tag(Thing):
|
||||||
"""
|
"""
|
||||||
device_id = Column(BigInteger,
|
device_id = Column(BigInteger,
|
||||||
# We don't want to delete the tag on device deletion, only set to null
|
# We don't want to delete the tag on device deletion, only set to null
|
||||||
ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL))
|
ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL),
|
||||||
|
index=True)
|
||||||
device = relationship(Device,
|
device = relationship(Device,
|
||||||
backref=backref('tags', lazy=True, collection_class=set),
|
backref=backref('tags', lazy=True, collection_class=set),
|
||||||
primaryjoin=Device.id == device_id)
|
primaryjoin=Device.id == device_id)
|
||||||
"""The device linked to this tag."""
|
"""The device linked to this tag."""
|
||||||
secondary = Column(Unicode(), check_lower('secondary'))
|
secondary = Column(Unicode(), check_lower('secondary'), index=True)
|
||||||
secondary.comment = """
|
secondary.comment = """
|
||||||
A secondary identifier for this tag. It has the same
|
A secondary identifier for this tag. It has the same
|
||||||
constraints as the main one. Only needed in special cases.
|
constraints as the main one. Only needed in special cases.
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
|
|
||||||
Define servername api.devicetag.io
|
Define servername api.devicetag.io
|
||||||
# The domain used to access the server
|
# The domain used to access the server
|
||||||
Define appdir /path/to/app/dir
|
Define appdir /home/devicetag/sites/${servername}/source/
|
||||||
# The path where the app directory is. Apache must have access to this folder.
|
# The path where the app directory is. Apache must have access to this folder.
|
||||||
Define wsgipath ${appdir}/wsgi.wsgi
|
Define wsgipath ${appdir}/wsgi.wsgi
|
||||||
# The location of the .wsgi file
|
# The location of the .wsgi file
|
||||||
Define pyvenv /path/to/venv
|
Define pyvenv ${appdir}/venv/
|
||||||
# The path where the virtual environment is (the folder containing bin/activate)
|
# The path where the virtual environment is (the folder containing bin/activate)
|
||||||
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Creates a database, user, and extensions to use Devicehub
|
# Creates a database, user, and extensions to use Devicehub
|
||||||
|
# $1 is the database to create
|
||||||
|
# $2 is the user to create and give full permissions on the database
|
||||||
|
# This script asks for the password of such user
|
||||||
|
|
||||||
|
read -s -p "Password for $2": pass
|
||||||
createdb $1 # Create main database
|
createdb $1 # Create main database
|
||||||
psql -d $1 -c "CREATE USER dhub WITH PASSWORD 'ereuse';" # Create user Devicehub uses to access db
|
psql -d $1 -c "CREATE USER $2 WITH PASSWORD '$pass';" # Create user Devicehub uses to access db
|
||||||
psql -d $1 -c "GRANT ALL PRIVILEGES ON DATABASE $1 TO dhub;" # Give access to the db
|
psql -d $1 -c "GRANT ALL PRIVILEGES ON DATABASE $1 TO $2;" # Give access to the db
|
||||||
psql -d $1 -c "CREATE EXTENSION pgcrypto SCHEMA public;" # Enable pgcrypto
|
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 ltree SCHEMA public;" # Enable ltree
|
||||||
psql -d $1 -c "CREATE EXTENSION citext SCHEMA public;" # Enable citext
|
psql -d $1 -c "CREATE EXTENSION citext SCHEMA public;" # Enable citext
|
||||||
|
|
|
@ -23,9 +23,9 @@ python-stdnum==1.9
|
||||||
PyYAML==3.13
|
PyYAML==3.13
|
||||||
requests==2.19.1
|
requests==2.19.1
|
||||||
requests-mock==1.5.2
|
requests-mock==1.5.2
|
||||||
SQLAlchemy==1.2.11
|
SQLAlchemy==1.2.14
|
||||||
SQLAlchemy-Utils==0.33.3
|
SQLAlchemy-Utils==0.33.6
|
||||||
teal==0.2.0a28
|
teal==0.2.0a30
|
||||||
webargs==4.0.0
|
webargs==4.0.0
|
||||||
Werkzeug==0.14.1
|
Werkzeug==0.14.1
|
||||||
sqlalchemy-citext==1.3.post0
|
sqlalchemy-citext==1.3.post0
|
||||||
|
|
12
setup.py
12
setup.py
|
@ -1,4 +1,3 @@
|
||||||
import re
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
@ -6,9 +5,6 @@ from setuptools import find_packages, setup
|
||||||
with open('README.md', encoding='utf8') as f:
|
with open('README.md', encoding='utf8') as f:
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
with open('ereuse_devicehub/__init__.py', 'rt', encoding='utf8') as f:
|
|
||||||
version = re.search(r'__version__ = \'(.*?)\'', f.read()).group(1)
|
|
||||||
|
|
||||||
test_requires = [
|
test_requires = [
|
||||||
'pytest',
|
'pytest',
|
||||||
'requests_mock'
|
'requests_mock'
|
||||||
|
@ -16,7 +12,7 @@ test_requires = [
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='ereuse-devicehub',
|
name='ereuse-devicehub',
|
||||||
version=version,
|
version='0.2.0b1',
|
||||||
url='https://github.com/ereuse/devicehub-teal',
|
url='https://github.com/ereuse/devicehub-teal',
|
||||||
project_urls=OrderedDict((
|
project_urls=OrderedDict((
|
||||||
('Documentation', 'http://devicheub.ereuse.org'),
|
('Documentation', 'http://devicheub.ereuse.org'),
|
||||||
|
@ -29,12 +25,11 @@ setup(
|
||||||
description='A system to manage devices focusing reuse.',
|
description='A system to manage devices focusing reuse.',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
platforms='any',
|
|
||||||
python_requires='>=3.5.3',
|
python_requires='>=3.5.3',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'teal>=0.2.0a28', # teal always first
|
'teal>=0.2.0a30', # teal always first
|
||||||
'click',
|
'click',
|
||||||
'click-spinner',
|
'click-spinner',
|
||||||
'ereuse-utils[Naming]>=0.4b10',
|
'ereuse-utils[Naming]>=0.4b10',
|
||||||
|
@ -55,6 +50,9 @@ setup(
|
||||||
'sphinxcontrib-plantuml >= 0.12',
|
'sphinxcontrib-plantuml >= 0.12',
|
||||||
'sphinxcontrib-websupport >= 1.0.1'
|
'sphinxcontrib-websupport >= 1.0.1'
|
||||||
],
|
],
|
||||||
|
'docs-auto': [
|
||||||
|
'sphinx-autobuild'
|
||||||
|
],
|
||||||
'test': test_requires
|
'test': test_requires
|
||||||
},
|
},
|
||||||
tests_require=test_requires,
|
tests_require=test_requires,
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
{
|
||||||
|
"type": "Snapshot",
|
||||||
|
"uuid": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"software": "Workbench",
|
||||||
|
"version": "11.0a1",
|
||||||
|
"expectedEvents": [],
|
||||||
|
"closed": true,
|
||||||
|
"endTime": "2018-07-19T15:48:40.635776",
|
||||||
|
"device": {
|
||||||
|
"manufacturer": "Dell Inc.",
|
||||||
|
"model": "Latitude E6440",
|
||||||
|
"serialNumber": "FJBQVZ1",
|
||||||
|
"events": [],
|
||||||
|
"type": "Laptop",
|
||||||
|
"chassis": "Laptop"
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"manufacturer": "Intel Corp.",
|
||||||
|
"model": "Intel Core i7-4600M CPU @ 2.90GHz",
|
||||||
|
"serialNumber": null,
|
||||||
|
"events": [],
|
||||||
|
"type": "Processor",
|
||||||
|
"speed": 1.259899,
|
||||||
|
"address": 64,
|
||||||
|
"cores": 2,
|
||||||
|
"threads": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Samsung",
|
||||||
|
"model": "M471B5173DB0-YK0",
|
||||||
|
"serialNumber": "732CD498",
|
||||||
|
"events": [],
|
||||||
|
"type": "RamModule",
|
||||||
|
"format": "SODIMM",
|
||||||
|
"size": 4096,
|
||||||
|
"interface": "DDR3",
|
||||||
|
"speed": 1600.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Samsung",
|
||||||
|
"model": "M471B5173DB0-YK0",
|
||||||
|
"serialNumber": "152DD498",
|
||||||
|
"events": [],
|
||||||
|
"type": "RamModule",
|
||||||
|
"format": "SODIMM",
|
||||||
|
"size": 4096,
|
||||||
|
"interface": "DDR3",
|
||||||
|
"speed": 1600.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": null,
|
||||||
|
"model": "Crucial_CT525MX3",
|
||||||
|
"serialNumber": "164014297BCC",
|
||||||
|
"events": [],
|
||||||
|
"type": "HardDrive",
|
||||||
|
"size": 500786,
|
||||||
|
"interface": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Intel Corporation",
|
||||||
|
"model": "4th Gen Core Processor Integrated Graphics Controller",
|
||||||
|
"serialNumber": null,
|
||||||
|
"events": [],
|
||||||
|
"type": "GraphicCard",
|
||||||
|
"memory": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Intel Corporation",
|
||||||
|
"model": "Ethernet Connection I217-LM",
|
||||||
|
"serialNumber": "ec:f4:bb:0b:18:90",
|
||||||
|
"events": [],
|
||||||
|
"type": "NetworkAdapter",
|
||||||
|
"speed": 1000,
|
||||||
|
"wireless": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Intel Corporation",
|
||||||
|
"model": "Centrino Advanced-N 6235",
|
||||||
|
"serialNumber": "c4:d9:87:47:90:e1",
|
||||||
|
"events": [],
|
||||||
|
"type": "NetworkAdapter",
|
||||||
|
"wireless": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": null,
|
||||||
|
"model": null,
|
||||||
|
"serialNumber": "da:b4:3a:25:88:6c",
|
||||||
|
"events": [],
|
||||||
|
"type": "NetworkAdapter",
|
||||||
|
"wireless": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Intel Corporation",
|
||||||
|
"model": "Xeon E3-1200 v3/4th Gen Core Processor HD Audio Controller",
|
||||||
|
"serialNumber": null,
|
||||||
|
"events": [],
|
||||||
|
"type": "SoundCard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "CNFCH52J48303+YF2",
|
||||||
|
"model": "Laptop_Integrated_Webcam_HD",
|
||||||
|
"serialNumber": null,
|
||||||
|
"events": [],
|
||||||
|
"type": "SoundCard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Intel Corporation",
|
||||||
|
"model": "8 Series/C220 Series Chipset High Definition Audio Controller",
|
||||||
|
"serialNumber": null,
|
||||||
|
"events": [],
|
||||||
|
"type": "SoundCard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer": "Dell Inc.",
|
||||||
|
"model": "0159N7",
|
||||||
|
"serialNumber": "/FJBQVZ1/CN1296342I009B/",
|
||||||
|
"events": [],
|
||||||
|
"type": "Motherboard",
|
||||||
|
"usb": 3,
|
||||||
|
"firewire": 0,
|
||||||
|
"serial": 1,
|
||||||
|
"pcmcia": 0,
|
||||||
|
"slots": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elapsed": 0
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"version": "11.0a3",
|
"version": "11.0a3",
|
||||||
"device": {
|
"device": {
|
||||||
"serialNumber": null,
|
"serialNumber": 'foo',
|
||||||
"manufacturer": null,
|
"manufacturer": 'bar',
|
||||||
"model": null,
|
"model": 'baz',
|
||||||
"type": "Desktop",
|
"type": "Desktop",
|
||||||
"events": [],
|
"events": [],
|
||||||
"chassis": "Tower"
|
"chassis": "Tower"
|
||||||
|
@ -53,7 +53,8 @@
|
||||||
"type": "NetworkAdapter",
|
"type": "NetworkAdapter",
|
||||||
"events": [],
|
"events": [],
|
||||||
"serialNumber": "f4:6d:04:12:9b:85",
|
"serialNumber": "f4:6d:04:12:9b:85",
|
||||||
"speed": 1000
|
"speed": 1000,
|
||||||
|
"wireless": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"serialNumber": "WD-WCAV29008961",
|
"serialNumber": "WD-WCAV29008961",
|
||||||
|
@ -68,12 +69,12 @@
|
||||||
{
|
{
|
||||||
"endTime": "2018-07-13T11:54:55.096491",
|
"endTime": "2018-07-13T11:54:55.096491",
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"startTime": "2018-07-13T10:52:45.092981"
|
"startTime": "2018-07-13T10:52:45.092981"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "EraseBasic",
|
"type": "EraseBasic",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"zeros": false,
|
"zeros": false,
|
||||||
"startTime": "2018-07-13T10:52:45.092612"
|
"startTime": "2018-07-13T10:52:45.092612"
|
||||||
},
|
},
|
||||||
|
@ -83,10 +84,10 @@
|
||||||
"elapsed": 131,
|
"elapsed": 131,
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"offlineUncorrectable": 1,
|
"offlineUncorrectable": 1,
|
||||||
"error": true,
|
"severity": "Error",
|
||||||
"currentPendingSectorCount": 1,
|
"currentPendingSectorCount": 1,
|
||||||
"powerCycleCount": 1253,
|
"powerCycleCount": 1253,
|
||||||
"reallocatedSectorCount": 6,
|
"reallocatedSectorCount": 15,
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"status": "Completed: read failure"
|
"status": "Completed: read failure"
|
||||||
}
|
}
|
||||||
|
@ -106,12 +107,12 @@
|
||||||
{
|
{
|
||||||
"endTime": "2018-07-13T12:55:47.326835",
|
"endTime": "2018-07-13T12:55:47.326835",
|
||||||
"type": "StepRandom",
|
"type": "StepRandom",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"startTime": "2018-07-13T11:54:55.100925"
|
"startTime": "2018-07-13T11:54:55.100925"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "EraseBasic",
|
"type": "EraseBasic",
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"zeros": false,
|
"zeros": false,
|
||||||
"startTime": "2018-07-13T11:54:55.100667"
|
"startTime": "2018-07-13T11:54:55.100667"
|
||||||
},
|
},
|
||||||
|
@ -121,7 +122,7 @@
|
||||||
"elapsed": 115,
|
"elapsed": 115,
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"offlineUncorrectable": 0,
|
"offlineUncorrectable": 0,
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"currentPendingSectorCount": 0,
|
"currentPendingSectorCount": 0,
|
||||||
"powerCycleCount": 1956,
|
"powerCycleCount": 1956,
|
||||||
"reallocatedSectorCount": 0,
|
"reallocatedSectorCount": 0,
|
||||||
|
|
|
@ -21,11 +21,11 @@ components:
|
||||||
endTime: 2018-06-01T09:12:06
|
endTime: 2018-06-01T09:12:06
|
||||||
steps:
|
steps:
|
||||||
- type: StepZero
|
- type: StepZero
|
||||||
error: False
|
severity: Info
|
||||||
startTime: 2018-06-01T08:15:00
|
startTime: 2018-06-01T08:15:00
|
||||||
endTime: 2018-06-01T09:16:00
|
endTime: 2018-06-01T09:16:00
|
||||||
- type: StepZero
|
- type: StepZero
|
||||||
error: False
|
severity: Info
|
||||||
startTime: 2018-06-01T08:16:00
|
startTime: 2018-06-01T08:16:00
|
||||||
endTime: 2018-06-01T09:17:00
|
endTime: 2018-06-01T09:17:00
|
||||||
- type: Processor
|
- type: Processor
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
"status": "Self-test routine in progress",
|
"status": "Self-test routine in progress",
|
||||||
"powerCycleCount": 648,
|
"powerCycleCount": 648,
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"error": false,
|
"severity": "Error",
|
||||||
"lifetime": 202
|
"lifetime": 202
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -110,7 +110,7 @@
|
||||||
"type": "BenchmarkRamSysbench"
|
"type": "BenchmarkRamSysbench"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"error": false,
|
"severity": "Info",
|
||||||
"elapsed": 60,
|
"elapsed": 60,
|
||||||
"type": "StressTest"
|
"type": "StressTest"
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ components:
|
||||||
serialNumber: 6VMB1A52
|
serialNumber: 6VMB1A52
|
||||||
size: 238475
|
size: 238475
|
||||||
test: {'@type': TestHardDrive, CommandTimeout: 1786733725708, CurrentPendingSectorCount: 0,
|
test: {'@type': TestHardDrive, CommandTimeout: 1786733725708, CurrentPendingSectorCount: 0,
|
||||||
OfflineUncorrectable: 0, assessment: true, error: false, firstError: null, lifetime: 16947,
|
OfflineUncorrectable: 0, assessment: true, severity: Info, firstError: null, lifetime: 16947,
|
||||||
passedLifetime: 16947, powerCycleCount: 1694, reallocatedSectorCount: 0, reportedUncorrectableErrors: 0,
|
passedLifetime: 16947, powerCycleCount: 1694, reallocatedSectorCount: 0, reportedUncorrectableErrors: 0,
|
||||||
status: Completed without error, type: Short offline}
|
status: Completed without error, type: Short offline}
|
||||||
type: HDD
|
type: HDD
|
||||||
|
|
|
@ -8,5 +8,5 @@
|
||||||
|
|
||||||
type: 'StressTest'
|
type: 'StressTest'
|
||||||
elapsed: 300
|
elapsed: 300
|
||||||
error: False
|
severity: Info
|
||||||
# snapshot: None fulfill!
|
# snapshot: None fulfill!
|
|
@ -7,7 +7,7 @@
|
||||||
# All numbers are invented
|
# All numbers are invented
|
||||||
|
|
||||||
type: 'EraseSectors'
|
type: 'EraseSectors'
|
||||||
error: False
|
severity: Info
|
||||||
# snapshot: None fulfill!
|
# snapshot: None fulfill!
|
||||||
# device: None fulfill!
|
# device: None fulfill!
|
||||||
zeros: False
|
zeros: False
|
||||||
|
@ -17,4 +17,4 @@ steps:
|
||||||
- type: 'StepRandom'
|
- type: 'StepRandom'
|
||||||
startTime: '2018-01-01T10:10:10'
|
startTime: '2018-01-01T10:10:10'
|
||||||
endTime: '2018-01-01T12:10:10'
|
endTime: '2018-01-01T12:10:10'
|
||||||
error: False
|
severity: Info
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
type: 'Install'
|
type: 'Install'
|
||||||
elapsed: 420
|
elapsed: 420
|
||||||
error: False
|
severity: Info
|
||||||
# snapshot: None fulfill!
|
# snapshot: None fulfill!
|
||||||
# device: None fulfill!
|
# device: None fulfill!
|
||||||
name: 'LinuxMint 18.01 32b'
|
name: 'LinuxMint 18.01 32b'
|
|
@ -40,4 +40,4 @@ def test_api_docs(client: Client):
|
||||||
'scheme': 'basic',
|
'scheme': 'basic',
|
||||||
'name': 'Authorization'
|
'name': 'Authorization'
|
||||||
}
|
}
|
||||||
assert 92 == len(docs['definitions'])
|
assert 94 == len(docs['definitions'])
|
||||||
|
|
|
@ -20,7 +20,8 @@ from ereuse_devicehub.resources.device.exceptions import NeedsId
|
||||||
from ereuse_devicehub.resources.device.schemas import Device as DeviceS
|
from ereuse_devicehub.resources.device.schemas import Device as DeviceS
|
||||||
from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \
|
from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \
|
||||||
Sync
|
Sync
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DisplayTech
|
from ereuse_devicehub.resources.enums import ComputerChassis, DisplayTech, Severity, \
|
||||||
|
SnapshotSoftware
|
||||||
from ereuse_devicehub.resources.event import models as m
|
from ereuse_devicehub.resources.event import models as m
|
||||||
from ereuse_devicehub.resources.event.models import Remove, Test
|
from ereuse_devicehub.resources.event.models import Remove, Test
|
||||||
from ereuse_devicehub.resources.tag.model import Tag
|
from ereuse_devicehub.resources.tag.model import Tag
|
||||||
|
@ -73,6 +74,11 @@ def test_device_model():
|
||||||
assert d.GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc'
|
assert d.GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason='Test not developed')
|
||||||
|
def test_device_problems():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
def test_device_schema():
|
def test_device_schema():
|
||||||
"""Ensures the user does not upload non-writable or extra fields."""
|
"""Ensures the user does not upload non-writable or extra fields."""
|
||||||
|
@ -393,7 +399,7 @@ def test_get_device(app: Devicehub, user: UserClient):
|
||||||
db.session.add(pc)
|
db.session.add(pc)
|
||||||
db.session.add(Test(device=pc,
|
db.session.add(Test(device=pc,
|
||||||
elapsed=timedelta(seconds=4),
|
elapsed=timedelta(seconds=4),
|
||||||
error=False,
|
severity=Severity.Info,
|
||||||
agent=Person(name='Timmy'),
|
agent=Person(name='Timmy'),
|
||||||
author=User(email='bar@bar.com')))
|
author=User(email='bar@bar.com')))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -402,7 +408,7 @@ def test_get_device(app: Devicehub, user: UserClient):
|
||||||
assert pc['events'][0]['type'] == 'Test'
|
assert pc['events'][0]['type'] == 'Test'
|
||||||
assert pc['events'][0]['device'] == 1
|
assert pc['events'][0]['device'] == 1
|
||||||
assert pc['events'][0]['elapsed'] == 4
|
assert pc['events'][0]['elapsed'] == 4
|
||||||
assert not pc['events'][0]['error']
|
assert pc['events'][0]['severity'] == 'Info'
|
||||||
assert UUID(pc['events'][0]['author'])
|
assert UUID(pc['events'][0]['author'])
|
||||||
assert 'events_components' not in pc, 'events_components are internal use only'
|
assert 'events_components' not in pc, 'events_components are internal use only'
|
||||||
assert 'events_one' not in pc, 'they are internal use only'
|
assert 'events_one' not in pc, 'they are internal use only'
|
||||||
|
@ -531,3 +537,30 @@ def test_networking_model():
|
||||||
switch = d.Switch(speed=1000, wireless=False)
|
switch = d.Switch(speed=1000, wireless=False)
|
||||||
db.session.add(switch)
|
db.session.add(switch)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_cooking_mixer():
|
||||||
|
mixer = d.Mixer(serial_number='foo', model='bar', manufacturer='foobar')
|
||||||
|
db.session.add(mixer)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_cooking_mixer_api(user: UserClient):
|
||||||
|
snapshot, _ = user.post(
|
||||||
|
{
|
||||||
|
'type': 'Snapshot',
|
||||||
|
'device': {
|
||||||
|
'serialNumber': 'foo',
|
||||||
|
'model': 'bar',
|
||||||
|
'manufacturer': 'foobar',
|
||||||
|
'type': 'Mixer'
|
||||||
|
},
|
||||||
|
'version': '11.0',
|
||||||
|
'software': SnapshotSoftware.Web.name
|
||||||
|
},
|
||||||
|
res=m.Snapshot
|
||||||
|
)
|
||||||
|
mixer, _ = user.get(res=d.Device, item=snapshot['device']['id'])
|
||||||
|
assert mixer['type'] == 'Mixer'
|
||||||
|
assert mixer['serialNumber'] == 'foo'
|
||||||
|
|
|
@ -4,7 +4,7 @@ from teal.utils import compiled
|
||||||
from ereuse_devicehub.client import UserClient
|
from ereuse_devicehub.client import UserClient
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.devicehub import Devicehub
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
from ereuse_devicehub.resources.device.models import Desktop, Device, Laptop, Processor, \
|
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, Laptop, Server, \
|
||||||
SolidStateDrive
|
SolidStateDrive
|
||||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||||
from ereuse_devicehub.resources.device.views import Filters, Sorting
|
from ereuse_devicehub.resources.device.views import Filters, Sorting
|
||||||
|
@ -56,51 +56,70 @@ def test_device_sort():
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def device_query_dummy(app: Devicehub):
|
def device_query_dummy(app: Devicehub):
|
||||||
|
"""
|
||||||
|
3 computers, where:
|
||||||
|
|
||||||
|
1. s1 Desktop with a Processor
|
||||||
|
2. s2 Desktop with an SSD
|
||||||
|
3. s3 Laptop
|
||||||
|
4. s4 Server with another SSD
|
||||||
|
|
||||||
|
:param app:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
devices = ( # The order matters ;-)
|
devices = ( # The order matters ;-)
|
||||||
Desktop(serial_number='s1',
|
Desktop(serial_number='1',
|
||||||
model='ml1',
|
model='ml1',
|
||||||
manufacturer='mr1',
|
manufacturer='mr1',
|
||||||
chassis=ComputerChassis.Tower),
|
chassis=ComputerChassis.Tower),
|
||||||
Laptop(serial_number='s3',
|
Desktop(serial_number='2',
|
||||||
model='ml3',
|
|
||||||
manufacturer='mr3',
|
|
||||||
chassis=ComputerChassis.Detachable),
|
|
||||||
Desktop(serial_number='s2',
|
|
||||||
model='ml2',
|
model='ml2',
|
||||||
manufacturer='mr2',
|
manufacturer='mr2',
|
||||||
chassis=ComputerChassis.Microtower),
|
chassis=ComputerChassis.Microtower),
|
||||||
SolidStateDrive(serial_number='s4', model='ml4', manufacturer='mr4')
|
Laptop(serial_number='3',
|
||||||
|
model='ml3',
|
||||||
|
manufacturer='mr3',
|
||||||
|
chassis=ComputerChassis.Detachable),
|
||||||
|
Server(serial_number='4',
|
||||||
|
model='ml4',
|
||||||
|
manufacturer='mr4',
|
||||||
|
chassis=ComputerChassis.Tower),
|
||||||
|
)
|
||||||
|
devices[0].components.add(
|
||||||
|
GraphicCard(serial_number='1-gc', model='s1ml', manufacturer='s1mr')
|
||||||
|
)
|
||||||
|
devices[1].components.add(
|
||||||
|
SolidStateDrive(serial_number='2-ssd', model='s2ml', manufacturer='s2mr')
|
||||||
|
)
|
||||||
|
devices[-1].components.add(
|
||||||
|
SolidStateDrive(serial_number='4-ssd', model='s4ml', manufacturer='s4mr')
|
||||||
)
|
)
|
||||||
devices[-1].parent = devices[0] # s4 in s1
|
|
||||||
db.session.add_all(devices)
|
db.session.add_all(devices)
|
||||||
|
|
||||||
devices[0].components.add(Processor(model='ml5', manufacturer='mr5'))
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(device_query_dummy.__name__)
|
@pytest.mark.usefixtures(device_query_dummy.__name__)
|
||||||
def test_device_query_no_filters(user: UserClient):
|
def test_device_query_no_filters(user: UserClient):
|
||||||
i, _ = user.get(res=Device)
|
i, _ = user.get(res=Device)
|
||||||
assert tuple(d['type'] for d in i['items']) == (
|
assert ('1', '2', '3', '4', '1-gc', '2-ssd', '4-ssd') == tuple(
|
||||||
'Desktop', 'Laptop', 'Desktop', 'SolidStateDrive', 'Processor'
|
d['serialNumber'] for d in i['items']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(device_query_dummy.__name__)
|
@pytest.mark.usefixtures(device_query_dummy.__name__)
|
||||||
def test_device_query_filter_type(user: UserClient):
|
def test_device_query_filter_type(user: UserClient):
|
||||||
i, _ = user.get(res=Device, query=[('filter', {'type': ['Desktop', 'Laptop']})])
|
i, _ = user.get(res=Device, query=[('filter', {'type': ['Desktop', 'Laptop']})])
|
||||||
assert tuple(d['type'] for d in i['items']) == ('Desktop', 'Laptop', 'Desktop')
|
assert ('1', '2', '3') == tuple(d['serialNumber'] for d in i['items'])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(device_query_dummy.__name__)
|
@pytest.mark.usefixtures(device_query_dummy.__name__)
|
||||||
def test_device_query_filter_sort(user: UserClient):
|
def test_device_query_filter_sort(user: UserClient):
|
||||||
i, _ = user.get(res=Device, query=[
|
i, _ = user.get(res=Device, query=[
|
||||||
('sort', {'created': Sorting.ASCENDING}),
|
('sort', {'created': Sorting.DESCENDING}),
|
||||||
('filter', {'type': ['Computer']})
|
('filter', {'type': ['Computer']})
|
||||||
])
|
])
|
||||||
assert tuple(d['type'] for d in i['items']) == ('Desktop', 'Laptop', 'Desktop')
|
assert ('4', '3', '2', '1') == tuple(d['serialNumber'] for d in i['items'])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(device_query_dummy.__name__)
|
@pytest.mark.usefixtures(device_query_dummy.__name__)
|
||||||
|
@ -111,7 +130,7 @@ def test_device_query_filter_lots(user: UserClient):
|
||||||
i, _ = user.get(res=Device, query=[
|
i, _ = user.get(res=Device, query=[
|
||||||
('filter', {'lot': {'id': [parent['id']]}})
|
('filter', {'lot': {'id': [parent['id']]}})
|
||||||
])
|
])
|
||||||
assert len(i['items']) == 0, 'No devices in lot'
|
assert not i['items'], 'No devices in lot'
|
||||||
|
|
||||||
parent, _ = user.post({},
|
parent, _ = user.post({},
|
||||||
res=Lot,
|
res=Lot,
|
||||||
|
@ -120,42 +139,37 @@ def test_device_query_filter_lots(user: UserClient):
|
||||||
i, _ = user.get(res=Device, query=[
|
i, _ = user.get(res=Device, query=[
|
||||||
('filter', {'type': ['Computer']})
|
('filter', {'type': ['Computer']})
|
||||||
])
|
])
|
||||||
lot, _ = user.post({},
|
assert ('1', '2', '3', '4') == tuple(d['serialNumber'] for d in i['items'])
|
||||||
|
parent, _ = user.post({},
|
||||||
res=Lot,
|
res=Lot,
|
||||||
item='{}/devices'.format(parent['id']),
|
item='{}/devices'.format(parent['id']),
|
||||||
query=[('id', d['id']) for d in i['items'][:-1]])
|
query=[('id', d['id']) for d in i['items'][:2]])
|
||||||
lot, _ = user.post({},
|
child, _ = user.post({},
|
||||||
res=Lot,
|
res=Lot,
|
||||||
item='{}/devices'.format(child['id']),
|
item='{}/devices'.format(child['id']),
|
||||||
query=[('id', i['items'][-1]['id'])])
|
query=[('id', d['id']) for d in i['items'][2:]])
|
||||||
i, _ = user.get(res=Device, query=[
|
i, _ = user.get(res=Device, query=[
|
||||||
('filter', {'lot': {'id': [parent['id']]}}),
|
('filter', {'lot': {'id': [parent['id']]}})
|
||||||
('sort', {'id': Sorting.ASCENDING})
|
|
||||||
])
|
])
|
||||||
assert tuple(x['id'] for x in i['items']) == (1, 2, 3, 4, 5), \
|
assert ('1', '2', '3', '4', '1-gc', '2-ssd', '4-ssd') == tuple(
|
||||||
'The parent lot contains 2 items plus indirectly the third one, and 1st device the HDD.'
|
x['serialNumber'] for x in i['items']
|
||||||
|
), 'The parent lot contains 2 items plus indirectly the other ' \
|
||||||
|
'2 from the child lot, with all their 2 components'
|
||||||
|
|
||||||
i, _ = user.get(res=Device, query=[
|
i, _ = user.get(res=Device, query=[
|
||||||
('filter', {'type': ['Computer'], 'lot': {'id': [parent['id']]}}),
|
('filter', {'type': ['Computer'], 'lot': {'id': [parent['id']]}}),
|
||||||
('sort', {'id': Sorting.ASCENDING})
|
|
||||||
])
|
])
|
||||||
assert tuple(x['id'] for x in i['items']) == (1, 2, 3)
|
assert ('1', '2', '3', '4') == tuple(x['serialNumber'] for x in i['items'])
|
||||||
|
|
||||||
s, _ = user.get(res=Device, query=[
|
s, _ = user.get(res=Device, query=[
|
||||||
('filter', {'lot': {'id': [child['id']]}})
|
('filter', {'lot': {'id': [child['id']]}})
|
||||||
])
|
])
|
||||||
assert len(s['items']) == 1
|
assert ('3', '4', '4-ssd') == tuple(x['serialNumber'] for x in s['items'])
|
||||||
assert s['items'][0]['chassis'] == 'Microtower', 'The child lot only contains the last device.'
|
|
||||||
s, _ = user.get(res=Device, query=[
|
s, _ = user.get(res=Device, query=[
|
||||||
('filter', {'lot': {'id': [child['id'], parent['id']]}})
|
('filter', {'lot': {'id': [child['id'], parent['id']]}})
|
||||||
])
|
])
|
||||||
assert all(x['id'] == id for x, id in zip(i['items'], (1, 2, 3, 4))), \
|
assert ('1', '2', '3', '4', '1-gc', '2-ssd', '4-ssd') == tuple(
|
||||||
'Adding both lots is redundant in this case and we have the 4 elements.'
|
x['serialNumber'] for x in s['items']
|
||||||
i, _ = user.get(res=Device, query=[
|
), 'Adding both lots is redundant in this case and we have the 4 elements.'
|
||||||
('filter', {'lot': {'id': [parent['id']]}, 'type': ['Computer']}),
|
|
||||||
('sort', {'id': Sorting.ASCENDING})
|
|
||||||
])
|
|
||||||
assert tuple(x['id'] for x in i['items']) == (1, 2, 3), 'Only computers now'
|
|
||||||
|
|
||||||
|
|
||||||
def test_device_query(user: UserClient):
|
def test_device_query(user: UserClient):
|
||||||
|
@ -190,6 +204,21 @@ def test_device_search_all_devices_token_if_empty(app: Devicehub, user: UserClie
|
||||||
assert i['items']
|
assert i['items']
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_search_regenerate_table(app: DeviceSearch, user: UserClient):
|
||||||
|
user.post(file('basic.snapshot'), res=Snapshot)
|
||||||
|
i, _ = user.get(res=Device, query=[('search', 'Desktop')])
|
||||||
|
assert i['items'], 'Normal search works'
|
||||||
|
with app.app_context():
|
||||||
|
app.db.session.execute('TRUNCATE TABLE {}'.format(DeviceSearch.__table__.name))
|
||||||
|
app.db.session.commit()
|
||||||
|
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)
|
||||||
|
i, _ = user.get(res=Device, query=[('search', 'Desktop')])
|
||||||
|
assert i['items'], 'Regenerated re-made the table'
|
||||||
|
|
||||||
|
|
||||||
def test_device_query_search(user: UserClient):
|
def test_device_query_search(user: UserClient):
|
||||||
# todo improve
|
# todo improve
|
||||||
user.post(file('basic.snapshot'), res=Snapshot)
|
user.post(file('basic.snapshot'), res=Snapshot)
|
||||||
|
@ -199,25 +228,27 @@ def test_device_query_search(user: UserClient):
|
||||||
assert i['items'][0]['id'] == 1
|
assert i['items'][0]['id'] == 1
|
||||||
i, _ = user.get(res=Device, query=[('search', 'intel')])
|
i, _ = user.get(res=Device, query=[('search', 'intel')])
|
||||||
assert len(i['items']) == 1
|
assert len(i['items']) == 1
|
||||||
|
i, _ = user.get(res=Device, query=[('search', '1')])
|
||||||
|
assert len(i['items']) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='No dictionary yet that knows asustek = asus')
|
|
||||||
def test_device_query_search_synonyms_asus(user: UserClient):
|
def test_device_query_search_synonyms_asus(user: UserClient):
|
||||||
user.post(file('real-eee-1001pxd.snapshot.11'), res=Snapshot)
|
user.post(file('real-eee-1001pxd.snapshot.11'), res=Snapshot)
|
||||||
i, _ = user.get(res=Device, query=[('search', 'asustek')])
|
i, _ = user.get(res=Device, query=[('search', 'asustek')])
|
||||||
assert len(i['items']) == 1
|
assert 1 == len(i['items'])
|
||||||
i, _ = user.get(res=Device, query=[('search', 'asus')])
|
i, _ = user.get(res=Device, query=[('search', 'asus')])
|
||||||
assert len(i['items']) == 1
|
assert 1 == len(i['items'])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='No dictionary yet that knows hp = hewlett packard')
|
|
||||||
def test_device_query_search_synonyms_intel(user: UserClient):
|
def test_device_query_search_synonyms_intel(user: UserClient):
|
||||||
s = file('real-hp.snapshot.11')
|
s = file('real-hp.snapshot.11')
|
||||||
s['device']['model'] = 'foo' # The model had the word 'HP' in it
|
s['device']['model'] = 'foo' # The model had the word 'HP' in it
|
||||||
user.post(s, res=Snapshot)
|
user.post(s, res=Snapshot)
|
||||||
i, _ = user.get(res=Device, query=[('search', 'hewlett packard')])
|
i, _ = user.get(res=Device, query=[('search', 'hewlett packard')])
|
||||||
assert len(i['items']) == 1
|
assert 1 == len(i['items'])
|
||||||
i, _ = user.get(res=Device, query=[('search', 'hewlett')])
|
i, _ = user.get(res=Device, query=[('search', 'hewlett')])
|
||||||
assert len(i['items']) == 1
|
assert 1 == len(i['items'])
|
||||||
i, _ = user.get(res=Device, query=[('search', 'hp')])
|
i, _ = user.get(res=Device, query=[('search', 'hp')])
|
||||||
assert len(i['items']) == 1
|
assert 1 == len(i['items'])
|
||||||
|
i, _ = user.get(res=Device, query=[('search', 'h.p')])
|
||||||
|
assert 1 == len(i['items'])
|
||||||
|
|
|
@ -13,7 +13,7 @@ from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \
|
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \
|
||||||
RamModule, SolidStateDrive
|
RamModule, SolidStateDrive
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, TestDataStorageLength
|
from ereuse_devicehub.resources.enums import ComputerChassis, Severity, TestDataStorageLength
|
||||||
from ereuse_devicehub.resources.event import models
|
from ereuse_devicehub.resources.event import models
|
||||||
from tests import conftest
|
from tests import conftest
|
||||||
from tests.conftest import create_user, file
|
from tests.conftest import create_user, file
|
||||||
|
@ -86,18 +86,35 @@ def test_erase_sectors_steps():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_test_data_storage():
|
def test_test_data_storage_working():
|
||||||
|
"""Tests TestDataStorage with the resulting properties in Device."""
|
||||||
|
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
||||||
test = models.TestDataStorage(
|
test = models.TestDataStorage(
|
||||||
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
device=hdd,
|
||||||
error=False,
|
severity=Severity.Error,
|
||||||
elapsed=timedelta(minutes=25),
|
elapsed=timedelta(minutes=25),
|
||||||
length=TestDataStorageLength.Short,
|
length=TestDataStorageLength.Short,
|
||||||
status='ok!',
|
status=':-(',
|
||||||
lifetime=timedelta(days=120)
|
lifetime=timedelta(days=120)
|
||||||
)
|
)
|
||||||
db.session.add(test)
|
db.session.add(test)
|
||||||
db.session.commit()
|
db.session.flush()
|
||||||
assert models.TestDataStorage.query.one()
|
assert hdd.working == [test]
|
||||||
|
assert not hdd.problems
|
||||||
|
# Add new test overriding the first test in the problems
|
||||||
|
# / working condition
|
||||||
|
test2 = models.TestDataStorage(
|
||||||
|
device=hdd,
|
||||||
|
severity=Severity.Warning,
|
||||||
|
elapsed=timedelta(minutes=25),
|
||||||
|
length=TestDataStorageLength.Short,
|
||||||
|
status=':-(',
|
||||||
|
lifetime=timedelta(days=120)
|
||||||
|
)
|
||||||
|
db.session.add(test2)
|
||||||
|
db.session.flush()
|
||||||
|
assert hdd.working == [test2]
|
||||||
|
assert hdd.problems == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
|
@ -192,7 +209,6 @@ def test_update_parent():
|
||||||
(models.Repair, states.Physical.Repaired),
|
(models.Repair, states.Physical.Repaired),
|
||||||
(models.ToPrepare, states.Physical.Preparing),
|
(models.ToPrepare, states.Physical.Preparing),
|
||||||
(models.ReadyToUse, states.Physical.ReadyToBeUsed),
|
(models.ReadyToUse, states.Physical.ReadyToBeUsed),
|
||||||
(models.ToPrepare, states.Physical.Preparing),
|
|
||||||
(models.Prepare, states.Physical.Prepared)
|
(models.Prepare, states.Physical.Prepared)
|
||||||
])
|
])
|
||||||
def test_generic_event(event_model_state: Tuple[models.Event, states.Trading], user: UserClient):
|
def test_generic_event(event_model_state: Tuple[models.Event, states.Trading], user: UserClient):
|
||||||
|
|
|
@ -23,39 +23,93 @@ In case of error, debug with:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def test_lot_modify_patch_endpoint(user: UserClient):
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
|
def test_lot_model_children():
|
||||||
|
"""Tests the property Lot.children
|
||||||
|
|
||||||
|
l1
|
||||||
|
|
|
||||||
|
l2
|
||||||
|
|
|
||||||
|
l3
|
||||||
|
"""
|
||||||
|
lots = Lot('1'), Lot('2'), Lot('3')
|
||||||
|
l1, l2, l3 = lots
|
||||||
|
db.session.add_all(lots)
|
||||||
|
db.session.flush()
|
||||||
|
assert not l1.children
|
||||||
|
assert not l1.parents
|
||||||
|
assert not l2.children
|
||||||
|
assert not l2.parents
|
||||||
|
assert not l3.parents
|
||||||
|
assert not l3.children
|
||||||
|
|
||||||
|
l1.add_children(l2)
|
||||||
|
assert l1.children == {l2}
|
||||||
|
assert l2.parents == {l1}
|
||||||
|
|
||||||
|
l2.add_children(l3)
|
||||||
|
assert l1.children == {l2}
|
||||||
|
assert l2.parents == {l1}
|
||||||
|
assert l2.children == {l3}
|
||||||
|
assert l3.parents == {l2}
|
||||||
|
|
||||||
|
l2.delete()
|
||||||
|
db.session.flush()
|
||||||
|
assert not l1.children
|
||||||
|
assert not l3.parents
|
||||||
|
|
||||||
|
l1.delete()
|
||||||
|
db.session.flush()
|
||||||
|
l3b = Lot.query.one()
|
||||||
|
assert l3 == l3b
|
||||||
|
assert not l3.parents
|
||||||
|
|
||||||
|
|
||||||
|
def test_lot_modify_patch_endpoint_and_delete(user: UserClient):
|
||||||
"""Creates and modifies lot properties through the endpoint"""
|
"""Creates and modifies lot properties through the endpoint"""
|
||||||
l, _ = user.post({'name': 'foo'}, res=Lot)
|
l, _ = user.post({'name': 'foo', 'description': 'baz'}, res=Lot)
|
||||||
assert l['name'] == 'foo'
|
assert l['name'] == 'foo'
|
||||||
user.patch({'name': 'bar'}, res=Lot, item=l['id'], status=204)
|
assert l['description'] == 'baz'
|
||||||
|
user.patch({'name': 'bar', 'description': 'bax'}, res=Lot, item=l['id'], status=204)
|
||||||
l_after, _ = user.get(res=Lot, item=l['id'])
|
l_after, _ = user.get(res=Lot, item=l['id'])
|
||||||
assert l_after['name'] == 'bar'
|
assert l_after['name'] == 'bar'
|
||||||
|
assert l_after['description'] == 'bax'
|
||||||
|
user.delete(res=Lot, item=l['id'], status=204)
|
||||||
|
user.get(res=Lot, item=l['id'], status=404)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='Components are not added to lots!')
|
|
||||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_lot_device_relationship():
|
def test_lot_device_relationship():
|
||||||
device = Desktop(serial_number='foo',
|
device = Desktop(serial_number='foo',
|
||||||
model='bar',
|
model='bar',
|
||||||
manufacturer='foobar',
|
manufacturer='foobar',
|
||||||
chassis=ComputerChassis.Lunchbox)
|
chassis=ComputerChassis.Lunchbox)
|
||||||
lot = Lot('lot1')
|
device.components.add(GraphicCard(serial_number='foo', model='bar1', manufacturer='baz'))
|
||||||
lot.devices.add(device)
|
child = Lot('child')
|
||||||
db.session.add(lot)
|
child.devices.add(device)
|
||||||
|
db.session.add(child)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
lot_device = LotDevice.query.one() # type: LotDevice
|
lot_device = LotDevice.query.one() # type: LotDevice
|
||||||
assert lot_device.device_id == device.id
|
assert lot_device.device_id == device.id
|
||||||
assert lot_device.lot_id == lot.id
|
assert lot_device.lot_id == child.id
|
||||||
assert lot_device.created
|
assert lot_device.created
|
||||||
assert lot_device.author_id == g.user.id
|
assert lot_device.author_id == g.user.id
|
||||||
assert device.lots == {lot}
|
assert device.lots == {child}
|
||||||
assert device in lot
|
assert device in child
|
||||||
|
assert device in child.all_devices
|
||||||
|
|
||||||
graphic = GraphicCard(serial_number='foo', model='bar')
|
graphic = GraphicCard(serial_number='foo', model='bar')
|
||||||
device.components.add(graphic)
|
device.components.add(graphic)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
assert graphic in lot
|
assert graphic in child
|
||||||
|
|
||||||
|
parent = Lot('parent')
|
||||||
|
db.session.add(parent)
|
||||||
|
db.session.flush()
|
||||||
|
parent.add_children(child)
|
||||||
|
assert child in parent
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
|
@ -67,13 +121,13 @@ def test_add_edge():
|
||||||
db.session.add(parent)
|
db.session.add(parent)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
parent.add_child(child)
|
parent.add_children(child)
|
||||||
|
|
||||||
assert child in parent
|
assert child in parent
|
||||||
assert len(child.paths) == 1
|
assert len(child.paths) == 1
|
||||||
assert len(parent.paths) == 1
|
assert len(parent.paths) == 1
|
||||||
|
|
||||||
parent.remove_child(child)
|
parent.remove_children(child)
|
||||||
assert child not in parent
|
assert child not in parent
|
||||||
assert len(child.paths) == 1
|
assert len(child.paths) == 1
|
||||||
assert len(parent.paths) == 1
|
assert len(parent.paths) == 1
|
||||||
|
@ -82,8 +136,8 @@ def test_add_edge():
|
||||||
db.session.add(grandparent)
|
db.session.add(grandparent)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
grandparent.add_child(parent)
|
grandparent.add_children(parent)
|
||||||
parent.add_child(child)
|
parent.add_children(child)
|
||||||
|
|
||||||
assert parent in grandparent
|
assert parent in grandparent
|
||||||
assert child in parent
|
assert child in parent
|
||||||
|
@ -104,31 +158,36 @@ def test_lot_multiple_parents(auth_app_context):
|
||||||
db.session.add_all(lots)
|
db.session.add_all(lots)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
grandparent1.add_child(parent)
|
grandparent1.add_children(parent)
|
||||||
assert parent in grandparent1
|
assert parent in grandparent1
|
||||||
parent.add_child(child)
|
parent.add_children(child)
|
||||||
assert child in parent
|
assert child in parent
|
||||||
assert child in grandparent1
|
assert child in grandparent1
|
||||||
grandparent2.add_child(parent)
|
grandparent2.add_children(parent)
|
||||||
assert parent in grandparent1
|
assert parent in grandparent1
|
||||||
assert parent in grandparent2
|
assert parent in grandparent2
|
||||||
assert child in parent
|
assert child in parent
|
||||||
assert child in grandparent1
|
assert child in grandparent1
|
||||||
assert child in grandparent2
|
assert child in grandparent2
|
||||||
|
|
||||||
|
p = parent.id
|
||||||
|
c = child.id
|
||||||
|
gp1 = grandparent1.id
|
||||||
|
gp2 = grandparent2.id
|
||||||
|
|
||||||
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
|
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
|
||||||
assert nodes[0]['name'] == 'grandparent1'
|
assert nodes[0]['id'] == gp1
|
||||||
assert nodes[0]['nodes'][0]['name'] == 'parent'
|
assert nodes[0]['nodes'][0]['id'] == p
|
||||||
assert nodes[0]['nodes'][0]['nodes'][0]['name'] == 'child'
|
assert nodes[0]['nodes'][0]['nodes'][0]['id'] == c
|
||||||
assert nodes[0]['nodes'][0]['nodes'][0]['nodes'] == []
|
assert nodes[0]['nodes'][0]['nodes'][0]['nodes'] == []
|
||||||
assert nodes[1]['name'] == 'grandparent2'
|
assert nodes[1]['id'] == gp2
|
||||||
assert nodes[1]['nodes'][0]['name'] == 'parent'
|
assert nodes[1]['nodes'][0]['id'] == p
|
||||||
assert nodes[1]['nodes'][0]['nodes'][0]['name'] == 'child'
|
assert nodes[1]['nodes'][0]['nodes'][0]['id'] == c
|
||||||
assert nodes[1]['nodes'][0]['nodes'][0]['nodes'] == []
|
assert nodes[1]['nodes'][0]['nodes'][0]['nodes'] == []
|
||||||
|
|
||||||
# Now remove all childs
|
# Now remove all childs
|
||||||
|
|
||||||
grandparent1.remove_child(parent)
|
grandparent1.remove_children(parent)
|
||||||
assert parent not in grandparent1
|
assert parent not in grandparent1
|
||||||
assert child in parent
|
assert child in parent
|
||||||
assert parent in grandparent2
|
assert parent in grandparent2
|
||||||
|
@ -136,14 +195,14 @@ def test_lot_multiple_parents(auth_app_context):
|
||||||
assert child in grandparent2
|
assert child in grandparent2
|
||||||
|
|
||||||
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
|
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
|
||||||
assert nodes[0]['name'] == 'grandparent1'
|
assert nodes[0]['id'] == gp1
|
||||||
assert nodes[0]['nodes'] == []
|
assert nodes[0]['nodes'] == []
|
||||||
assert nodes[1]['name'] == 'grandparent2'
|
assert nodes[1]['id'] == gp2
|
||||||
assert nodes[1]['nodes'][0]['name'] == 'parent'
|
assert nodes[1]['nodes'][0]['id'] == p
|
||||||
assert nodes[1]['nodes'][0]['nodes'][0]['name'] == 'child'
|
assert nodes[1]['nodes'][0]['nodes'][0]['id'] == c
|
||||||
assert nodes[1]['nodes'][0]['nodes'][0]['nodes'] == []
|
assert nodes[1]['nodes'][0]['nodes'][0]['nodes'] == []
|
||||||
|
|
||||||
grandparent2.remove_child(parent)
|
grandparent2.remove_children(parent)
|
||||||
assert parent not in grandparent2
|
assert parent not in grandparent2
|
||||||
assert parent not in grandparent1
|
assert parent not in grandparent1
|
||||||
assert child not in grandparent2
|
assert child not in grandparent2
|
||||||
|
@ -151,27 +210,27 @@ def test_lot_multiple_parents(auth_app_context):
|
||||||
assert child in parent
|
assert child in parent
|
||||||
|
|
||||||
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
|
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
|
||||||
assert nodes[0]['name'] == 'grandparent1'
|
assert nodes[0]['id'] == gp1
|
||||||
assert nodes[0]['nodes'] == []
|
assert nodes[0]['nodes'] == []
|
||||||
assert nodes[1]['name'] == 'grandparent2'
|
assert nodes[1]['id'] == gp2
|
||||||
assert nodes[1]['nodes'] == []
|
assert nodes[1]['nodes'] == []
|
||||||
assert nodes[2]['name'] == 'parent'
|
assert nodes[2]['id'] == p
|
||||||
assert nodes[2]['nodes'][0]['name'] == 'child'
|
assert nodes[2]['nodes'][0]['id'] == c
|
||||||
assert nodes[2]['nodes'][0]['nodes'] == []
|
assert nodes[2]['nodes'][0]['nodes'] == []
|
||||||
|
|
||||||
parent.remove_child(child)
|
parent.remove_children(child)
|
||||||
assert child not in parent
|
assert child not in parent
|
||||||
assert len(child.paths) == 1
|
assert len(child.paths) == 1
|
||||||
assert len(parent.paths) == 1
|
assert len(parent.paths) == 1
|
||||||
|
|
||||||
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
|
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
|
||||||
assert nodes[0]['name'] == 'grandparent1'
|
assert nodes[0]['id'] == gp1
|
||||||
assert nodes[0]['nodes'] == []
|
assert nodes[0]['nodes'] == []
|
||||||
assert nodes[1]['name'] == 'grandparent2'
|
assert nodes[1]['id'] == gp2
|
||||||
assert nodes[1]['nodes'] == []
|
assert nodes[1]['nodes'] == []
|
||||||
assert nodes[2]['name'] == 'parent'
|
assert nodes[2]['id'] == p
|
||||||
assert nodes[2]['nodes'] == []
|
assert nodes[2]['nodes'] == []
|
||||||
assert nodes[3]['name'] == 'child'
|
assert nodes[3]['id'] == c
|
||||||
assert nodes[3]['nodes'] == []
|
assert nodes[3]['nodes'] == []
|
||||||
|
|
||||||
|
|
||||||
|
@ -199,29 +258,29 @@ def test_lot_unite_graphs_and_find():
|
||||||
db.session.add_all(lots)
|
db.session.add_all(lots)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
l1.add_child(l2)
|
l1.add_children(l2)
|
||||||
assert l2 in l1
|
assert l2 in l1
|
||||||
l3.add_child(l2)
|
l3.add_children(l2)
|
||||||
assert l2 in l3
|
assert l2 in l3
|
||||||
l5.add_child(l7)
|
l5.add_children(l7)
|
||||||
assert l7 in l5
|
assert l7 in l5
|
||||||
l4.add_child(l5)
|
l4.add_children(l5)
|
||||||
assert l5 in l4
|
assert l5 in l4
|
||||||
assert l7 in l4
|
assert l7 in l4
|
||||||
l5.add_child(l8)
|
l5.add_children(l8)
|
||||||
assert l8 in l5
|
assert l8 in l5
|
||||||
l4.add_child(l6)
|
l4.add_children(l6)
|
||||||
assert l6 in l4
|
assert l6 in l4
|
||||||
l6.add_child(l5)
|
l6.add_children(l5)
|
||||||
assert l5 in l6 and l5 in l4
|
assert l5 in l6 and l5 in l4
|
||||||
|
|
||||||
# We unite the two graphs
|
# We unite the two graphs
|
||||||
l2.add_child(l4)
|
l2.add_children(l4)
|
||||||
assert l4 in l2 and l5 in l2 and l6 in l2 and l7 in l2 and l8 in l2
|
assert l4 in l2 and l5 in l2 and l6 in l2 and l7 in l2 and l8 in l2
|
||||||
assert l4 in l3 and l5 in l3 and l6 in l3 and l7 in l3 and l8 in l3
|
assert l4 in l3 and l5 in l3 and l6 in l3 and l7 in l3 and l8 in l3
|
||||||
|
|
||||||
# We remove the union
|
# We remove the union
|
||||||
l2.remove_child(l4)
|
l2.remove_children(l4)
|
||||||
assert l4 not in l2 and l5 not in l2 and l6 not in l2 and l7 not in l2 and l8 not in l2
|
assert l4 not in l2 and l5 not in l2 and l6 not in l2 and l7 not in l2 and l8 not in l2
|
||||||
assert l4 not in l3 and l5 not in l3 and l6 not in l3 and l7 not in l3 and l8 not in l3
|
assert l4 not in l3 and l5 not in l3 and l6 not in l3 and l7 not in l3 and l8 not in l3
|
||||||
|
|
||||||
|
@ -235,25 +294,10 @@ def test_lot_roots():
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
assert set(Lot.roots()) == {l1, l2, l3}
|
assert set(Lot.roots()) == {l1, l2, l3}
|
||||||
l1.add_child(l2)
|
l1.add_children(l2)
|
||||||
assert set(Lot.roots()) == {l1, l3}
|
assert set(Lot.roots()) == {l1, l3}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
|
||||||
def test_lot_model_children():
|
|
||||||
"""Tests the property Lot.children"""
|
|
||||||
lots = Lot('1'), Lot('2'), Lot('3')
|
|
||||||
l1, l2, l3 = lots
|
|
||||||
db.session.add_all(lots)
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
l1.add_child(l2)
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
children = l1.children
|
|
||||||
assert list(children) == [l2]
|
|
||||||
|
|
||||||
|
|
||||||
def test_post_get_lot(user: UserClient):
|
def test_post_get_lot(user: UserClient):
|
||||||
"""Tests submitting and retreiving a basic lot."""
|
"""Tests submitting and retreiving a basic lot."""
|
||||||
l, _ = user.post({'name': 'Foo'}, res=Lot)
|
l, _ = user.post({'name': 'Foo'}, res=Lot)
|
||||||
|
@ -277,21 +321,26 @@ def test_lot_post_add_children_view_ui_tree_normal(user: UserClient):
|
||||||
assert child['parents'][0]['id'] == parent['id']
|
assert child['parents'][0]['id'] == parent['id']
|
||||||
|
|
||||||
# Format UiTree
|
# Format UiTree
|
||||||
lots = user.get(res=Lot, query=[('format', 'UiTree')])[0]['items']
|
r = user.get(res=Lot, query=[('format', 'UiTree')])[0]
|
||||||
assert len(lots) == 1
|
lots, nodes = r['items'], r['tree']
|
||||||
assert lots[0]['name'] == 'Parent'
|
assert 1 == len(nodes)
|
||||||
assert len(lots[0]['nodes']) == 1
|
assert nodes[0]['id'] == parent['id']
|
||||||
assert lots[0]['nodes'][0]['name'] == 'Child'
|
assert len(nodes[0]['nodes']) == 1
|
||||||
|
assert nodes[0]['nodes'][0]['id'] == child['id']
|
||||||
|
assert 2 == len(lots)
|
||||||
|
assert 'Parent' == lots[parent['id']]['name']
|
||||||
|
assert 'Child' == lots[child['id']]['name']
|
||||||
|
assert lots[child['id']]['parents'][0]['name'] == 'Parent'
|
||||||
|
|
||||||
# Normal list format
|
# Normal list format
|
||||||
lots = user.get(res=Lot)[0]['items']
|
lots = user.get(res=Lot)[0]['items']
|
||||||
assert len(lots) == 2
|
assert 2 == len(lots)
|
||||||
assert lots[0]['name'] == 'Parent'
|
assert lots[0]['name'] == 'Parent'
|
||||||
assert lots[1]['name'] == 'Child'
|
assert lots[1]['name'] == 'Child'
|
||||||
|
|
||||||
# List format with a filter
|
# List format with a filter
|
||||||
lots = user.get(res=Lot, query=[('search', 'pa')])[0]['items']
|
lots = user.get(res=Lot, query=[('search', 'pa')])[0]['items']
|
||||||
assert len(lots) == 1
|
assert 1 == len(lots)
|
||||||
assert lots[0]['name'] == 'Parent'
|
assert lots[0]['name'] == 'Parent'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -289,8 +289,10 @@ def test_snapshot_component_containing_components(user: UserClient):
|
||||||
user.post(s, res=Snapshot, status=ValidationError)
|
user.post(s, res=Snapshot, status=ValidationError)
|
||||||
|
|
||||||
|
|
||||||
def test_erase(user: UserClient):
|
def test_erase_privacy(user: UserClient):
|
||||||
"""Tests a Snapshot with EraseSectors."""
|
"""Tests a Snapshot with EraseSectors and the resulting
|
||||||
|
privacy properties.
|
||||||
|
"""
|
||||||
s = file('erase-sectors.snapshot')
|
s = file('erase-sectors.snapshot')
|
||||||
snapshot = snapshot_and_check(user, s, (EraseSectors.t,), perform_second_snapshot=True)
|
snapshot = snapshot_and_check(user, s, (EraseSectors.t,), perform_second_snapshot=True)
|
||||||
storage, *_ = snapshot['components']
|
storage, *_ = snapshot['components']
|
||||||
|
@ -310,16 +312,32 @@ def test_erase(user: UserClient):
|
||||||
assert erasure['device']['id'] == storage['id']
|
assert erasure['device']['id'] == storage['id']
|
||||||
for step in erasure['steps']:
|
for step in erasure['steps']:
|
||||||
assert step['type'] == 'StepZero'
|
assert step['type'] == 'StepZero'
|
||||||
assert step['error'] is False
|
assert step['severity'] == 'Info'
|
||||||
assert 'num' not in step
|
assert 'num' not in step
|
||||||
assert storage['privacy'] == erasure['device']['privacy'] == 'EraseSectors'
|
assert storage['privacy']['type'] == 'EraseSectors'
|
||||||
|
pc, _ = user.get(res=m.Device, item=snapshot['device']['id'])
|
||||||
|
assert pc['privacy'] == [storage['privacy']]
|
||||||
|
|
||||||
# Let's try a second erasure with an error
|
# Let's try a second erasure with an error
|
||||||
s['uuid'] = uuid4()
|
s['uuid'] = uuid4()
|
||||||
s['components'][0]['events'][0]['error'] = True
|
s['components'][0]['events'][0]['severity'] = 'Error'
|
||||||
snapshot, _ = user.post(s, res=Snapshot)
|
snapshot, _ = user.post(s, res=Snapshot)
|
||||||
assert snapshot['components'][0]['hid'] == 'c1mr-c1s-c1ml'
|
storage, _ = user.get(res=m.Device, item=storage['id'])
|
||||||
assert snapshot['components'][0]['privacy'] == 'EraseSectorsError'
|
assert storage['hid'] == 'c1mr-c1s-c1ml'
|
||||||
|
assert storage['privacy']['type'] == 'EraseSectors'
|
||||||
|
pc, _ = user.get(res=m.Device, item=snapshot['device']['id'])
|
||||||
|
assert pc['privacy'] == [storage['privacy']]
|
||||||
|
|
||||||
|
|
||||||
|
def test_test_data_storage(user: UserClient):
|
||||||
|
"""Tests a Snapshot with EraseSectors."""
|
||||||
|
s = file('erase-sectors-2-hdd.snapshot')
|
||||||
|
snapshot, _ = user.post(res=Snapshot, data=s)
|
||||||
|
incidence_test = next(
|
||||||
|
ev for ev in snapshot['events']
|
||||||
|
if ev.get('reallocatedSectorCount', None) == 15
|
||||||
|
)
|
||||||
|
assert incidence_test['severity'] == 'Error'
|
||||||
|
|
||||||
|
|
||||||
def test_snapshot_computer_monitor(user: UserClient):
|
def test_snapshot_computer_monitor(user: UserClient):
|
||||||
|
|
|
@ -49,7 +49,7 @@ def test_workbench_server_condensed(user: UserClient):
|
||||||
('TestDataStorage', 6)
|
('TestDataStorage', 6)
|
||||||
}
|
}
|
||||||
assert snapshot['closed']
|
assert snapshot['closed']
|
||||||
assert not snapshot['error']
|
assert snapshot['severity'] == 'Info'
|
||||||
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
||||||
assert device['dataStorageSize'] == 1100
|
assert device['dataStorageSize'] == 1100
|
||||||
assert device['chassis'] == 'Tower'
|
assert device['chassis'] == 'Tower'
|
||||||
|
@ -59,7 +59,7 @@ def test_workbench_server_condensed(user: UserClient):
|
||||||
assert device['processorModel'] == device['components'][3]['model'] == 'p1-1ml'
|
assert device['processorModel'] == device['components'][3]['model'] == 'p1-1ml'
|
||||||
assert device['ramSize'] == 2048, 'There are 3 RAM: 2 x 1024 and 1 None sizes'
|
assert device['ramSize'] == 2048, 'There are 3 RAM: 2 x 1024 and 1 None sizes'
|
||||||
assert device['rate']['closed']
|
assert device['rate']['closed']
|
||||||
assert not device['rate']['error']
|
assert device['rate']['severity'] == 'Info'
|
||||||
assert device['rate']['rating'] == 0
|
assert device['rate']['rating'] == 0
|
||||||
assert device['rate']['workbench']
|
assert device['rate']['workbench']
|
||||||
assert device['rate']['appearanceRange'] == 'A'
|
assert device['rate']['appearanceRange'] == 'A'
|
||||||
|
@ -129,7 +129,7 @@ def test_workbench_server_phases(user: UserClient):
|
||||||
assert events[8]['type'] == 'Install'
|
assert events[8]['type'] == 'Install'
|
||||||
assert events[8]['device'] == 6
|
assert events[8]['device'] == 6
|
||||||
assert snapshot['closed']
|
assert snapshot['closed']
|
||||||
assert not snapshot['error']
|
assert snapshot['severity'] == 'Info'
|
||||||
|
|
||||||
pc, _ = user.get(res=Device, item=snapshot['id'])
|
pc, _ = user.get(res=Device, item=snapshot['id'])
|
||||||
assert len(pc['events']) == 10 # todo shall I add child events?
|
assert len(pc['events']) == 10 # todo shall I add child events?
|
||||||
|
@ -165,6 +165,7 @@ def test_real_toshiba_11(user: UserClient):
|
||||||
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason='Wrong rates values')
|
||||||
def test_snapshot_real_eee_1001pxd(user: UserClient):
|
def test_snapshot_real_eee_1001pxd(user: UserClient):
|
||||||
"""
|
"""
|
||||||
Checks the values of the device, components,
|
Checks the values of the device, components,
|
||||||
|
@ -264,7 +265,7 @@ def test_snapshot_real_eee_1001pxd(user: UserClient):
|
||||||
assert erase['endTime']
|
assert erase['endTime']
|
||||||
assert erase['startTime']
|
assert erase['startTime']
|
||||||
assert erase['zeros'] is False
|
assert erase['zeros'] is False
|
||||||
assert erase['error'] is False
|
assert erase['severity'] == 'Info'
|
||||||
assert hdd['privacy'] == 'EraseBasic'
|
assert hdd['privacy'] == 'EraseBasic'
|
||||||
mother = components[8]
|
mother = components[8]
|
||||||
assert mother['hid'] == 'asustek_computer_inc-eee0123456789-1001pxd'
|
assert mother['hid'] == 'asustek_computer_inc-eee0123456789-1001pxd'
|
||||||
|
@ -321,3 +322,8 @@ def test_workbench_asus_1001pxd_rate_low(user: UserClient):
|
||||||
"""Tests an Asus 1001pxd with a low rate."""
|
"""Tests an Asus 1001pxd with a low rate."""
|
||||||
s = file('asus-1001pxd.snapshot')
|
s = file('asus-1001pxd.snapshot')
|
||||||
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
||||||
|
|
||||||
|
|
||||||
|
def test_david(user: UserClient):
|
||||||
|
s = file('david.lshw.snapshot')
|
||||||
|
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
||||||
|
|
Reference in New Issue