Complete events and agents. Bump to 0.2.0a11.
This commit is contained in:
parent
8efca0d589
commit
42b0b0ebbc
|
@ -1,4 +1,8 @@
|
||||||
@startuml
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
skinparam nodesep 20
|
||||||
|
skinparam ranksep 1
|
||||||
|
|
||||||
abstract class Rate
|
abstract class Rate
|
||||||
abstract class Event
|
abstract class Event
|
||||||
abstract class Test
|
abstract class Test
|
||||||
|
@ -38,13 +42,13 @@ EventWithOneDevice <|--- EraseBasic
|
||||||
EraseBasic <|- EraseSectors
|
EraseBasic <|- EraseSectors
|
||||||
|
|
||||||
Step <|-- StepZero
|
Step <|-- StepZero
|
||||||
Step <|-- StepRandom
|
Step <|-- "Step\nRandom"
|
||||||
Snapshot "1" -- "1" SnapshotRequest
|
Snapshot "1" -- "1" "Snapshot\nRequest"
|
||||||
Event "*" -> "0..1" Snapshot : InSnapshot >
|
Event "*" -> "0..1" Snapshot : InSnapshot >
|
||||||
Event "*" -> "0..1" Component : affectedComponents >
|
Event "*" -> "0..1" Component : affectedComponents >
|
||||||
Device "1" *-- "*" EventWithOneDevice : EventOn <
|
Device "1" *- "*" EventWithOneDevice : EventOn <
|
||||||
Device "1..*" *-- "1" EventWithMultipleDevices : EventOn <
|
Device "1..*" *- "1" EventWithMultipleDevices : EventOn <
|
||||||
EraseBasic "1" *-- "1..*" Step
|
EraseBasic "1" *- "1..*" Step
|
||||||
PhotoboxRate <|-- PhotoboxSystemRate
|
PhotoboxRate <|-- PhotoboxSystemRate
|
||||||
PhotoboxRate <|-- PhotoboxPersonRate
|
PhotoboxRate <|-- PhotoboxPersonRate
|
||||||
|
|
||||||
|
@ -84,18 +88,14 @@ Plan <|-- CancelReservation
|
||||||
|
|
||||||
|
|
||||||
package Agents {
|
package Agents {
|
||||||
abstract class User
|
abstract class User <<Common schema>>
|
||||||
abstract class Agent
|
abstract class Agent
|
||||||
|
|
||||||
Event "*" -> "1" User : Author >
|
Event "*" -> "1" User : Author >
|
||||||
Event "*" - "0..1" Agent : agent >
|
Event "*" - "0..1" Agent : agent >
|
||||||
|
Trade "*" - "0..1" Agent : to >
|
||||||
|
|
||||||
Agent <|-- User
|
User - Agent
|
||||||
|
|
||||||
User <|-- Person
|
|
||||||
User <|-- System
|
|
||||||
Agent <|-- Organization
|
|
||||||
User "*" -o "0..1" Organization : WorksIn >
|
|
||||||
User "*" -o "0..1" Organization : activeOrganization >
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@enduml
|
@enduml
|
|
@ -0,0 +1,443 @@
|
||||||
|
Actions and states
|
||||||
|
##################
|
||||||
|
|
||||||
|
Actions are events performed to devices, changing their **state**.
|
||||||
|
Actions can have attributes defining
|
||||||
|
**where** it happened, **who** performed them, **when**, etc.
|
||||||
|
Actions are stored in a log for each device. An exemplifying action
|
||||||
|
can be ``Repair``, which dictates that a device has been repaired,
|
||||||
|
after this action, the device is in the ``repaired`` state.
|
||||||
|
|
||||||
|
Actions and states affect devices in different ways or **dimensions**.
|
||||||
|
For example, ``Repair`` affects the **physical** dimension of a device,
|
||||||
|
and ``Sell`` the **political** dimension of a device. A device
|
||||||
|
can be in several states at the same time, one per dimension; ie. a
|
||||||
|
device can be ``repaired`` (physical) and ``reserved`` (political),
|
||||||
|
but not ``repaired`` and ``disposed`` at the same time.
|
||||||
|
|
||||||
|
Devicehub actions inherit from `schema actions
|
||||||
|
<http://schema.org/Action>`_, are written in Pascal case and using
|
||||||
|
a verb in infinitive. Some verbs represent the willingness or
|
||||||
|
assignment to perform an action; ``ToRepair`` states that the device
|
||||||
|
is going to be / must be repaired, whereas ``Repair`` states
|
||||||
|
that the reparation happened. The former actions have the preposition
|
||||||
|
*To* prefixing the verb.
|
||||||
|
|
||||||
|
In the following section we define the actions and states.
|
||||||
|
To see how to perform actions to the Devicehub API head
|
||||||
|
to the `Swagger docs
|
||||||
|
<https://app.swaggerhub.com/apis/ereuse/devicehub/0.2>`_.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
actions
|
||||||
|
|
||||||
|
.. uml:: actions.puml
|
||||||
|
|
||||||
|
|
||||||
|
Physical Actions
|
||||||
|
****************
|
||||||
|
The following actions describe and react on the physical condition
|
||||||
|
of the devices.
|
||||||
|
|
||||||
|
ToPrepare, Prepare
|
||||||
|
==================
|
||||||
|
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.
|
||||||
|
|
||||||
|
**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 is the act of selecting a device to be repaired, and
|
||||||
|
Repair the act of performing the actual reparations. If a repair
|
||||||
|
without an error is performed, it represents that the reparation
|
||||||
|
has been successful.
|
||||||
|
|
||||||
|
ReadyToUse
|
||||||
|
==========
|
||||||
|
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.
|
||||||
|
|
||||||
|
Live
|
||||||
|
====
|
||||||
|
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.
|
||||||
|
|
||||||
|
DisposeWaste, Recover
|
||||||
|
=====================
|
||||||
|
``RecyclingCenter`` users have two extra special events:
|
||||||
|
- ``DisposeWaste``: The device has been disposed in an unspecified
|
||||||
|
manner.
|
||||||
|
- ``Recover``: The device has been scrapped and its materials have
|
||||||
|
been recovered under a new product.
|
||||||
|
|
||||||
|
See `ToDisposeProduct, DisposeProduct`_.
|
||||||
|
|
||||||
|
Association actions
|
||||||
|
*******************
|
||||||
|
Actions that change the associations users have with devices;
|
||||||
|
ie. the **owners**, **usufructuarees**, **reservees**,
|
||||||
|
and **physical possessors**.
|
||||||
|
|
||||||
|
There are three sub-dimensions: **trade**, **transfer**,
|
||||||
|
and **organize** actions.
|
||||||
|
|
||||||
|
.. uml:: association-events.puml
|
||||||
|
|
||||||
|
Trade actions
|
||||||
|
=============
|
||||||
|
Trade actions log the political exchange of devices between users,
|
||||||
|
stating **owner** xor **usufructuaree**. Every time a trade event
|
||||||
|
is performed, the old user looses its political possession in favor
|
||||||
|
of another one.
|
||||||
|
|
||||||
|
Sell
|
||||||
|
----
|
||||||
|
The act of taking money from a buyer in exchange of a device.
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
The act of giving devices without compensation.
|
||||||
|
|
||||||
|
Rent
|
||||||
|
----
|
||||||
|
The act of giving money in return for temporary use, but not
|
||||||
|
ownership, of a device.
|
||||||
|
|
||||||
|
CancelTrade
|
||||||
|
-----------
|
||||||
|
The act of cancelling a `Sell`_, `Donate`_ or `Rent`_.
|
||||||
|
|
||||||
|
ToDisposeProduct, DisposeProduct
|
||||||
|
-------------------------
|
||||||
|
``ToDispose`` and ``DisposeProduct`` manage the process of getting
|
||||||
|
rid of devices by giving (selling, donating) to another organization
|
||||||
|
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
|
||||||
|
================
|
||||||
|
The act of transferring/moving devices from one place to another.
|
||||||
|
|
||||||
|
Receive
|
||||||
|
-------
|
||||||
|
The act of physically taking delivery of a device. The receiver
|
||||||
|
confirms that the devices have arrived, and thus, they
|
||||||
|
**physically possess** them. Note that
|
||||||
|
there can only be one **physical possessor** per device, and
|
||||||
|
``Receive`` changes it.
|
||||||
|
|
||||||
|
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
|
||||||
|
================
|
||||||
|
The act of manipulating/administering/supervising/controlling one or
|
||||||
|
more devices.
|
||||||
|
|
||||||
|
Reserve, CancelReservation
|
||||||
|
--------------------------
|
||||||
|
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.
|
||||||
|
|
||||||
|
Assign, Accept, Reject
|
||||||
|
----------------------
|
||||||
|
``Assign`` allocates devices to an user. The purpose or meaning
|
||||||
|
of the association is defined by the users.
|
||||||
|
|
||||||
|
``Accept`` and ``Reject`` allow users to accept and reject the
|
||||||
|
assignments.
|
||||||
|
|
||||||
|
.. todo:: shall we add ``Deassign`` or make ``Assign``
|
||||||
|
always define all active users?
|
||||||
|
|
||||||
|
.. todo:: Assign won't be developed until further notice.
|
||||||
|
|
||||||
|
|
||||||
|
Internal state actions
|
||||||
|
**********************
|
||||||
|
Actions providing metadata about devices that don't usually change
|
||||||
|
their state.
|
||||||
|
|
||||||
|
Snapshot
|
||||||
|
========
|
||||||
|
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``.
|
||||||
|
|
||||||
|
Add, Remove
|
||||||
|
===========
|
||||||
|
The act of adding and removing components of and from a device.
|
||||||
|
|
||||||
|
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
|
||||||
|
========================
|
||||||
|
An erasure attempt to a ``DataStorage``. The event contains
|
||||||
|
information about success and nature of the erasure.
|
||||||
|
|
||||||
|
``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
|
||||||
|
=======
|
||||||
|
The action of install an Operative System to a data storage unit.
|
||||||
|
|
||||||
|
Test
|
||||||
|
====
|
||||||
|
The act of testing the physical condition of a device and its
|
||||||
|
components.
|
||||||
|
|
||||||
|
TestDataStorage
|
||||||
|
---------------
|
||||||
|
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.
|
||||||
|
|
||||||
|
StressTest
|
||||||
|
----------
|
||||||
|
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.
|
||||||
|
|
||||||
|
Benchmark
|
||||||
|
=========
|
||||||
|
The act of gauging the performance of a device.
|
||||||
|
|
||||||
|
BenchmarkDataStorage
|
||||||
|
--------------------
|
||||||
|
Benchmarks the data storage unit reading and writing speeds.
|
||||||
|
|
||||||
|
BenchmarkWithRate
|
||||||
|
-----------------
|
||||||
|
The act of benchmarking a device with a single rate.
|
||||||
|
|
||||||
|
BenchmarkProcessor
|
||||||
|
------------------
|
||||||
|
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.
|
||||||
|
|
||||||
|
BenchmarkProcessorSysbench
|
||||||
|
--------------------------
|
||||||
|
Benchmarks a processor by using the processor benchmarking utility of
|
||||||
|
`sysbench <https://github.com/akopytov/sysbench>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Rate
|
||||||
|
====
|
||||||
|
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 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 states a selling price for the device, but not necessariliy the
|
||||||
|
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
|
||||||
|
=======
|
||||||
|
Moves the devices to a new database/inventory. Devices cannot be
|
||||||
|
modified anymore at the previous database.
|
||||||
|
|
||||||
|
Donation
|
||||||
|
========
|
||||||
|
.. todo:: nextcloud/eReuse/99. Tasks/224. Definir datos necesarios
|
||||||
|
configuración licencia
|
||||||
|
|
||||||
|
|
||||||
|
States
|
||||||
|
******
|
||||||
|
.. todo:: work on september.
|
||||||
|
|
||||||
|
.. uml:: states.puml
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
abstract class User <<Common schema>>
|
||||||
|
abstract class Individual
|
||||||
|
abstract class Agent
|
||||||
|
|
||||||
|
Event "*" --> "1" User : Author >
|
||||||
|
Event "*" -- "0..1" Agent : agent >
|
||||||
|
Trade "*" -- "0..1" Agent : to >
|
||||||
|
|
||||||
|
User "0..1" - "0..1" Agent : user <
|
||||||
|
|
||||||
|
Agent <|-- Individual
|
||||||
|
Individual <|-- Person
|
||||||
|
Individual <|-- System
|
||||||
|
Agent <|-- Organization
|
||||||
|
Individual "*" -o "0..1" Organization
|
||||||
|
(Individual, Organization) .. Membership
|
||||||
|
class Membership {
|
||||||
|
member_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Individual "*" -o "0..1" Organization : activeOrg >
|
||||||
|
@enduml
|
|
@ -0,0 +1,4 @@
|
||||||
|
Agents
|
||||||
|
######
|
||||||
|
|
||||||
|
.. uml:: agents.puml
|
|
@ -1,72 +1,25 @@
|
||||||
@startuml
|
@startuml
|
||||||
ChangeAssociation <|-- Organize
|
skinparam nodesep 10
|
||||||
ChangeAssociation <|-- Transfer
|
skinparam ranksep 30
|
||||||
Organize <|-- Plan
|
|
||||||
Organize <|-- Allocate
|
abstract class Trade {
|
||||||
Allocate <|-- Accept
|
to: Agent
|
||||||
Allocate <|-- Reject
|
}
|
||||||
Allocate <|-- Assign
|
abstract class Transfer
|
||||||
Allocate <|-- Authorize
|
abstract class Organize
|
||||||
Plan <|-- Reserve
|
|
||||||
Plan <|-- Cancel
|
|
||||||
|
"Associate" <|-- Organize
|
||||||
|
"Associate" <|-- Transfer
|
||||||
|
Organize <|-- Reserve
|
||||||
|
Organize <|--- "Cancel\nReservation"
|
||||||
Transfer <|-- Receive
|
Transfer <|-- Receive
|
||||||
ChangeAssociation <|-- Trade
|
"Associate" <|-- Trade
|
||||||
Trade <|-- Sell
|
Trade <|-- Sell
|
||||||
Trade <|-- Donate
|
Trade <|-- Donate
|
||||||
Trade <|-- Pay
|
Trade <|-- Pay
|
||||||
Trade <|-- Rent
|
Trade <|-- Rent
|
||||||
Trade <|-- DisposeProduct
|
Trade <|-- "Dispose\nProduct"
|
||||||
|
|
||||||
class ChangeAssociation {
|
|
||||||
agent: who did it
|
|
||||||
}
|
|
||||||
|
|
||||||
class Receive {
|
|
||||||
sender
|
|
||||||
recipient
|
|
||||||
}
|
|
||||||
|
|
||||||
class Reserve {
|
|
||||||
reservee
|
|
||||||
}
|
|
||||||
|
|
||||||
class Cancel {
|
|
||||||
reservee
|
|
||||||
}
|
|
||||||
|
|
||||||
class Trade {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Allocate {
|
|
||||||
purpose
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sell {
|
|
||||||
buyer
|
|
||||||
}
|
|
||||||
|
|
||||||
class Donate {
|
|
||||||
recipient
|
|
||||||
}
|
|
||||||
|
|
||||||
class Pay {
|
|
||||||
purpose
|
|
||||||
recipient
|
|
||||||
}
|
|
||||||
|
|
||||||
class Rent {
|
|
||||||
recipient
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Association <|-- PhysicalPossessor
|
|
||||||
Association <|-- TradeAssociation
|
|
||||||
TradeAssociation <|-- Usufructuary
|
|
||||||
TradeAssociation <|-- Ownership
|
|
||||||
|
|
||||||
Sell - TradeAssociation
|
|
||||||
Donate - TradeAssociation
|
|
||||||
Rent -- Usufructuary : Sure?
|
|
||||||
Receive - PhysicalPossessor
|
|
||||||
@enduml
|
@enduml
|
|
@ -167,7 +167,7 @@ intersphinx_mapping = {'https://docs.python.org/': None}
|
||||||
todo_include_todos = True
|
todo_include_todos = True
|
||||||
|
|
||||||
# Plantuml
|
# Plantuml
|
||||||
plantuml_output_format = 'svg'
|
plantuml_output_format = 'svg_img'
|
||||||
|
|
||||||
# favicon
|
# favicon
|
||||||
html_favicon = 'img/favicon.ico'
|
html_favicon = 'img/favicon.ico'
|
||||||
|
|
|
@ -15,7 +15,6 @@ Actors
|
||||||
- Photochromic tag manufacturer.
|
- Photochromic tag manufacturer.
|
||||||
- User: organization that uses the tags.
|
- User: organization that uses the tags.
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
************
|
************
|
||||||
|
|
||||||
|
|
197
docs/events.rst
197
docs/events.rst
|
@ -1,197 +0,0 @@
|
||||||
Events
|
|
||||||
######
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 4
|
|
||||||
|
|
||||||
event-diagram
|
|
||||||
|
|
||||||
|
|
||||||
Rate
|
|
||||||
****
|
|
||||||
Devicehub generates an rating for a device taking into consideration the
|
|
||||||
visual, functional, and performance.
|
|
||||||
|
|
||||||
.. todo:: add performance as a result of component fusion + general
|
|
||||||
tests in `here <https://github.com/eReuse/Rdevicescore/blob/master/
|
|
||||||
img/input_process_output.png>`_.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
.. todo:: which info does photobox provide for each picture?
|
|
||||||
|
|
||||||
Price
|
|
||||||
*****
|
|
||||||
Price states a selling price for the device, but not necessariliy the
|
|
||||||
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``.
|
|
||||||
|
|
||||||
Snapshot
|
|
||||||
********
|
|
||||||
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``.
|
|
||||||
|
|
||||||
ToDispose and DisposeProduct
|
|
||||||
****************************
|
|
||||||
There are four events for getting rid of devices:
|
|
||||||
|
|
||||||
- ``ToDispose``: The device is marked to be disposed.
|
|
||||||
- ``DisposeProduct``: The device has been disposed. This is a ``Trade``
|
|
||||||
event, which means that you can optionally ``DisposeProduct``
|
|
||||||
to someone.
|
|
||||||
- ``RecyclingCenter`` have two extra special events:
|
|
||||||
- ``DisposeWaste``: The device has been disposed in an unspecified
|
|
||||||
manner.
|
|
||||||
- ``Recover``: The device has been scrapped and its materials have
|
|
||||||
been recovered under a new product.
|
|
||||||
|
|
||||||
.. note:: For usability purposes, users might not directly perform
|
|
||||||
``Dispose``, but this could automatically be done when
|
|
||||||
performing ``ToDispose`` + ``Receive`` to a ``RecyclingCenter``.
|
|
||||||
|
|
||||||
.. todo:: Ensure that ``Dispose`` is a ``Trade`` event. An Org could
|
|
||||||
``Sell`` or ``Donate`` a device with the objective of disposing them.
|
|
||||||
Is ``Dispose`` ok, or do we want to keep that extra ``Sell`` or
|
|
||||||
``Donate`` event? Could dispose be a synonym of any of those?
|
|
|
@ -10,11 +10,12 @@ This is the documentation and API of the `eReuse.org DeviceHub
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 4
|
:maxdepth: 2
|
||||||
|
|
||||||
events
|
actions
|
||||||
tags
|
agents
|
||||||
inventory
|
inventory
|
||||||
|
tags
|
||||||
etag
|
etag
|
||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
@startuml
|
||||||
|
skinparam nodesep 10
|
||||||
|
skinparam ranksep 1
|
||||||
|
|
||||||
|
|
||||||
|
[*] -> Registered
|
||||||
|
|
||||||
|
state Attributes {
|
||||||
|
|
||||||
|
state Broken : cannot turn on
|
||||||
|
state Owners
|
||||||
|
state Usufructuarees
|
||||||
|
state Reservees
|
||||||
|
state "Physical\nPossessor"
|
||||||
|
}
|
||||||
|
|
||||||
|
state Physical {
|
||||||
|
Registered --> Preparing : ToPrepare
|
||||||
|
Registered --> ToBeRepaired : ToRepair
|
||||||
|
ToBeRepaired --> Repaired : Repair
|
||||||
|
Repaired -> Preparing : ToPrepare
|
||||||
|
Preparing --> Prepared : Prepare
|
||||||
|
Prepared --> ReadyToBeUsed : ReadyToUse
|
||||||
|
ReadyToBeUsed --> InUse : Live
|
||||||
|
InUse -> InUse : Live
|
||||||
|
state DisposeWaste
|
||||||
|
state Recover
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
state Trading {
|
||||||
|
Registered --> Reserved : Reserve
|
||||||
|
Registered --> Sold : Sell
|
||||||
|
Reserved -> Sold : Sell
|
||||||
|
Reserved --> Cancelled : Cancel
|
||||||
|
Sold --> Cancelled : Cancel
|
||||||
|
Sold --> Payed : Pay
|
||||||
|
Registered --> ToBeDisposed
|
||||||
|
ToBeDisposed --> Disposed : DisposeProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@enduml
|
|
@ -1,4 +1,4 @@
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
|
||||||
__version__ = '0.2.0a11'
|
__version__ = '0.2.0a12'
|
||||||
version = StrictVersion(__version__)
|
version = StrictVersion(__version__)
|
||||||
|
|
|
@ -109,3 +109,8 @@ class UserClient(Client):
|
||||||
**kw) -> Tuple[Union[Dict[str, object], str], Response]:
|
**kw) -> Tuple[Union[Dict[str, object], str], Response]:
|
||||||
return super().open(uri, res, status, query, accept, content_type, item, headers,
|
return super().open(uri, res, status, query, accept, content_type, item, headers,
|
||||||
self.user['token'] if self.user else token, **kw)
|
self.user['token'] if self.user else token, **kw)
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
response = super().login(self.email, self.password)
|
||||||
|
self.user = response[0]
|
||||||
|
return response
|
||||||
|
|
|
@ -2,11 +2,11 @@ from distutils.version import StrictVersion
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
from ereuse_devicehub.resources import device, event, inventory, tag, user
|
from ereuse_devicehub.resources import agent, device, event, inventory, tag, user
|
||||||
from ereuse_devicehub.resources.enums import PriceSoftware, RatingSoftware
|
from ereuse_devicehub.resources.enums import PriceSoftware, RatingSoftware
|
||||||
from teal.auth import TokenAuth
|
from teal.auth import TokenAuth
|
||||||
from teal.config import Config
|
from teal.config import Config
|
||||||
from teal.currency import Currency
|
from teal.enums import Currency
|
||||||
from teal.utils import import_resource
|
from teal.utils import import_resource
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,8 @@ class DevicehubConfig(Config):
|
||||||
import_resource(event),
|
import_resource(event),
|
||||||
import_resource(user),
|
import_resource(user),
|
||||||
import_resource(tag),
|
import_resource(tag),
|
||||||
import_resource(inventory)))
|
import_resource(inventory),
|
||||||
|
import_resource(agent)))
|
||||||
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
|
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
|
||||||
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/devicehub' # type: str
|
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/devicehub' # type: str
|
||||||
SCHEMA = 'dhub'
|
SCHEMA = 'dhub'
|
||||||
|
|
|
@ -3,8 +3,10 @@ from pathlib import Path
|
||||||
import click
|
import click
|
||||||
import click_spinner
|
import click_spinner
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
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.resources.agent.models import Person
|
||||||
from ereuse_devicehub.resources.event.models import Snapshot
|
from ereuse_devicehub.resources.event.models import Snapshot
|
||||||
from ereuse_devicehub.resources.inventory import Inventory
|
from ereuse_devicehub.resources.inventory import Inventory
|
||||||
from ereuse_devicehub.resources.tag.model import Tag
|
from ereuse_devicehub.resources.tag.model import Tag
|
||||||
|
@ -49,11 +51,10 @@ class Dummy:
|
||||||
|
|
||||||
def user_client(self, email: str, password: str):
|
def user_client(self, email: str, password: str):
|
||||||
user = User(email=email, password=password)
|
user = User(email=email, password=password)
|
||||||
|
user.individuals.add(Person(name='Timmy'))
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
client = UserClient(application=self.app,
|
client = UserClient(self.app, user.email, password,
|
||||||
response_wrapper=self.app.response_class,
|
response_wrapper=self.app.response_class)
|
||||||
email=user.email,
|
client.login()
|
||||||
password=password)
|
|
||||||
client.user, _ = client.login(client.email, client.password)
|
|
||||||
return client
|
return client
|
||||||
|
|
|
@ -141,7 +141,7 @@
|
||||||
"version": "11.0a3",
|
"version": "11.0a3",
|
||||||
"closed": false,
|
"closed": false,
|
||||||
"elapsed": 1512,
|
"elapsed": 1512,
|
||||||
"date": "2018-07-11T11:17:00.888231",
|
"endTime": "2018-07-11T11:17:00.888231",
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"expectedEvents": [
|
"expectedEvents": [
|
||||||
"Benchmark",
|
"Benchmark",
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"closed": false,
|
"closed": false,
|
||||||
"elapsed": -3058,
|
"elapsed": -3058,
|
||||||
"uuid": "106930cd-e948-4cca-a8c8-1e39d6192ad6",
|
"uuid": "106930cd-e948-4cca-a8c8-1e39d6192ad6",
|
||||||
"date": "2018-07-11T10:47:50.822380",
|
"endTime": "2018-07-11T10:47:50.822380",
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"type": "Processor",
|
"type": "Processor",
|
||||||
|
|
|
@ -175,6 +175,6 @@
|
||||||
"EraseBasic"
|
"EraseBasic"
|
||||||
],
|
],
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"date": "2018-07-11T10:30:22.395958",
|
"endTime": "2018-07-11T10:30:22.395958",
|
||||||
"elapsed": 2766
|
"elapsed": 2766
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
"uuid": "9c3560a9-371c-4392-b586-37090b5f79c6",
|
"uuid": "9c3560a9-371c-4392-b586-37090b5f79c6",
|
||||||
"version": "11.0a3",
|
"version": "11.0a3",
|
||||||
"closed": false,
|
"closed": false,
|
||||||
"date": "2018-07-11T13:26:29.365504",
|
"endTime": "2018-07-11T13:26:29.365504",
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"device": {
|
"device": {
|
||||||
"serialNumber": "PB357N0",
|
"serialNumber": "PB357N0",
|
||||||
|
|
|
@ -146,7 +146,7 @@
|
||||||
"StressTest",
|
"StressTest",
|
||||||
"EraseBasic"
|
"EraseBasic"
|
||||||
],
|
],
|
||||||
"date": "2018-07-11T10:28:55.879745",
|
"endTime": "2018-07-11T10:28:55.879745",
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"elapsed": 3886,
|
"elapsed": 3886,
|
||||||
"closed": false
|
"closed": false
|
||||||
|
|
|
@ -128,7 +128,7 @@
|
||||||
"version": "11.0a2",
|
"version": "11.0a2",
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"date": "2018-07-03T09:10:57.034598",
|
"endTime": "2018-07-03T09:10:57.034598",
|
||||||
"device": {
|
"device": {
|
||||||
"type": "Laptop",
|
"type": "Laptop",
|
||||||
"model": "1001PXD",
|
"model": "1001PXD",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"uuid": "0c822fb7-6e51-4781-86cf-994bd306212e",
|
"uuid": "0c822fb7-6e51-4781-86cf-994bd306212e",
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"closed": false,
|
"closed": false,
|
||||||
"date": "2018-07-05T11:57:17.284891",
|
"endTime": "2018-07-05T11:57:17.284891",
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"type": "NetworkAdapter",
|
"type": "NetworkAdapter",
|
||||||
|
|
|
@ -157,5 +157,5 @@
|
||||||
"model": "HP Compaq 8100 Elite SFF"
|
"model": "HP Compaq 8100 Elite SFF"
|
||||||
},
|
},
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"date": "2018-06-29T12:28:54.508266"
|
"endTime": "2018-06-29T12:28:54.508266"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"Benchmark",
|
"Benchmark",
|
||||||
"StressTest"
|
"StressTest"
|
||||||
],
|
],
|
||||||
"date": "2018-06-29T15:29:29.322424",
|
"endTime": "2018-06-29T15:29:29.322424",
|
||||||
"elapsed": 391,
|
"elapsed": 391,
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"components": [
|
"components": [
|
||||||
|
|
|
@ -158,7 +158,7 @@
|
||||||
"serialNumber": "CZC0408YJG"
|
"serialNumber": "CZC0408YJG"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"date": "2018-07-11T16:11:43.467824",
|
"endTime": "2018-07-11T16:11:43.467824",
|
||||||
"version": "11.0a3",
|
"version": "11.0a3",
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
|
|
|
@ -9,6 +9,6 @@ class NestedOn(TealNestedOn):
|
||||||
__doc__ = TealNestedOn.__doc__
|
__doc__ = TealNestedOn.__doc__
|
||||||
|
|
||||||
def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, collection_class=list,
|
def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, collection_class=list,
|
||||||
default=missing_, exclude=tuple(), only=None, **kwargs):
|
default=missing_, exclude=tuple(), only_query: str = None, only=None, **kwargs):
|
||||||
super().__init__(nested, polymorphic_on, db, collection_class, default, exclude, only,
|
super().__init__(nested, polymorphic_on, db, collection_class, default, exclude,
|
||||||
**kwargs)
|
only_query, only, **kwargs)
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import click
|
||||||
|
from flask import current_app as app
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.agent import models, schemas
|
||||||
|
from teal.db import SQLAlchemy
|
||||||
|
from teal.resource import Converters, Resource
|
||||||
|
|
||||||
|
|
||||||
|
class AgentDef(Resource):
|
||||||
|
SCHEMA = schemas.Agent
|
||||||
|
VIEW = None
|
||||||
|
AUTH = True
|
||||||
|
ID_CONVERTER = Converters.uuid
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationDef(AgentDef):
|
||||||
|
SCHEMA = schemas.Organization
|
||||||
|
VIEW = None
|
||||||
|
|
||||||
|
def __init__(self, app, import_name=__package__, static_folder=None, static_url_path=None,
|
||||||
|
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||||
|
root_path=None):
|
||||||
|
cli_commands = ((self.create_org, 'create-org'),)
|
||||||
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
|
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||||
|
|
||||||
|
@click.argument('name')
|
||||||
|
@click.argument('tax_id')
|
||||||
|
@click.argument('country')
|
||||||
|
def create_org(self, name: str, tax_id: str = None, country: str = None) -> dict:
|
||||||
|
"""Creates an organization."""
|
||||||
|
org = models.Organization(**self.schema.load(
|
||||||
|
{
|
||||||
|
'name': name,
|
||||||
|
'taxId': tax_id,
|
||||||
|
'country': country
|
||||||
|
}
|
||||||
|
))
|
||||||
|
db.session.add(org)
|
||||||
|
db.session.commit()
|
||||||
|
return self.schema.dump(org)
|
||||||
|
|
||||||
|
def init_db(self, db: SQLAlchemy):
|
||||||
|
"""Creates the default organization."""
|
||||||
|
org = models.Organization(**app.config.get_namespace('ORGANIZATION_'))
|
||||||
|
db.session.add(org)
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(Resource):
|
||||||
|
SCHEMA = schemas.Membership
|
||||||
|
VIEW = None
|
||||||
|
ID_CONVERTER = Converters.string
|
||||||
|
|
||||||
|
|
||||||
|
class IndividualDef(AgentDef):
|
||||||
|
SCHEMA = schemas.Individual
|
||||||
|
VIEW = None
|
||||||
|
|
||||||
|
|
||||||
|
class PersonDef(IndividualDef):
|
||||||
|
SCHEMA = schemas.Person
|
||||||
|
VIEW = None
|
||||||
|
|
||||||
|
|
||||||
|
class SystemDef(IndividualDef):
|
||||||
|
SCHEMA = schemas.System
|
||||||
|
VIEW = None
|
|
@ -0,0 +1,131 @@
|
||||||
|
from itertools import chain
|
||||||
|
from operator import attrgetter
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from flask import current_app as app, g
|
||||||
|
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
from sqlalchemy.orm import backref, relationship
|
||||||
|
from sqlalchemy_utils import EmailType, PhoneNumberType
|
||||||
|
|
||||||
|
from ereuse_devicehub.resources.models import STR_SIZE, STR_SM_SIZE, Thing
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
from teal import enums
|
||||||
|
from teal.db import INHERIT_COND, POLYMORPHIC_ID, \
|
||||||
|
POLYMORPHIC_ON
|
||||||
|
|
||||||
|
|
||||||
|
class JoinedTableMixin:
|
||||||
|
# noinspection PyMethodParameters
|
||||||
|
@declared_attr
|
||||||
|
def id(cls):
|
||||||
|
return Column(UUID(as_uuid=True), ForeignKey(Agent.id), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Agent(Thing):
|
||||||
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||||
|
type = Column(Unicode, nullable=False)
|
||||||
|
name = Column(Unicode(length=STR_SM_SIZE))
|
||||||
|
name.comment = """
|
||||||
|
The name of the organization or person.
|
||||||
|
"""
|
||||||
|
tax_id = Column(Unicode(length=STR_SM_SIZE))
|
||||||
|
tax_id.comment = """
|
||||||
|
The Tax / Fiscal ID of the organization,
|
||||||
|
e.g. the TIN in the US or the CIF/NIF in Spain.
|
||||||
|
"""
|
||||||
|
country = Column(DBEnum(enums.Country))
|
||||||
|
country.comment = """
|
||||||
|
Country issuing the tax_id number.
|
||||||
|
"""
|
||||||
|
telephone = Column(PhoneNumberType())
|
||||||
|
email = Column(EmailType, unique=True)
|
||||||
|
|
||||||
|
user_id = Column(UUID(as_uuid=True), ForeignKey(User.id), unique=True)
|
||||||
|
user = relationship(User,
|
||||||
|
backref=backref('individuals', lazy=True, collection_class=set),
|
||||||
|
primaryjoin=user_id == User.id)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
||||||
|
)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def __mapper_args__(cls):
|
||||||
|
"""
|
||||||
|
Defines inheritance.
|
||||||
|
|
||||||
|
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
||||||
|
extensions/declarative/api.html
|
||||||
|
#sqlalchemy.ext.declarative.declared_attr>`_
|
||||||
|
"""
|
||||||
|
args = {POLYMORPHIC_ID: cls.t}
|
||||||
|
if cls.t == 'Agent':
|
||||||
|
args[POLYMORPHIC_ON] = cls.type
|
||||||
|
if JoinedTableMixin in cls.mro():
|
||||||
|
args[INHERIT_COND] = cls.id == Agent.id
|
||||||
|
return args
|
||||||
|
|
||||||
|
@property
|
||||||
|
def events(self) -> list:
|
||||||
|
# todo test
|
||||||
|
return sorted(chain(self.events_agent, self.events_to), key=attrgetter('created'))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return '<{0.t} {0.name}>'.format(self)
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(JoinedTableMixin, Agent):
|
||||||
|
def __init__(self, name: str, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs, name=name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default_org_id(cls) -> UUID:
|
||||||
|
"""Retrieves the default organization."""
|
||||||
|
return g.setdefault('org_id',
|
||||||
|
Organization.query.filter_by(
|
||||||
|
**app.config.get_namespace('ORGANIZATION_')
|
||||||
|
).one().id)
|
||||||
|
|
||||||
|
|
||||||
|
class Individual(JoinedTableMixin, Agent):
|
||||||
|
active_org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id))
|
||||||
|
active_org = relationship(Organization, primaryjoin=active_org_id == Organization.id)
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(Thing):
|
||||||
|
"""Organizations that are related to the Individual.
|
||||||
|
|
||||||
|
For example, because the individual works in or because is a member of.
|
||||||
|
"""
|
||||||
|
id = Column(Unicode(length=STR_SIZE))
|
||||||
|
organization_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True)
|
||||||
|
organization = relationship(Organization,
|
||||||
|
backref=backref('members', collection_class=set, lazy=True),
|
||||||
|
primaryjoin=organization_id == Organization.id)
|
||||||
|
individual_id = Column(UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True)
|
||||||
|
individual = relationship(Individual,
|
||||||
|
backref=backref('member_of', collection_class=set, lazy=True),
|
||||||
|
primaryjoin=individual_id == Individual.id)
|
||||||
|
|
||||||
|
def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None:
|
||||||
|
super().__init__(organization=organization,
|
||||||
|
individual=individual,
|
||||||
|
id=id)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(id, organization_id, name='One member id per organization.'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Person(Individual):
|
||||||
|
"""
|
||||||
|
A person in the system. There can be several persons pointing to
|
||||||
|
a real.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class System(Individual):
|
||||||
|
pass
|
|
@ -0,0 +1,79 @@
|
||||||
|
import uuid
|
||||||
|
from typing import List, Set
|
||||||
|
|
||||||
|
from sqlalchemy import Column
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy_utils import PhoneNumber
|
||||||
|
|
||||||
|
from ereuse_devicehub.resources.event.models import Event, Trade
|
||||||
|
from ereuse_devicehub.resources.models import Thing
|
||||||
|
from ereuse_devicehub.resources.user import User
|
||||||
|
from teal import enums
|
||||||
|
|
||||||
|
|
||||||
|
class Agent(Thing):
|
||||||
|
id = ... # type: Column
|
||||||
|
name = ... # type: Column
|
||||||
|
tax_id = ... # type: Column
|
||||||
|
country = ... # type: Column
|
||||||
|
telephone = ... # type: Column
|
||||||
|
email = ... # type: Column
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.id = ... # type: uuid.UUID
|
||||||
|
self.name = ... # type: str
|
||||||
|
self.tax_id = ... # type: str
|
||||||
|
self.country = ... # type: enums.Country
|
||||||
|
self.telephone = ... # type: PhoneNumber
|
||||||
|
self.email = ... # type: str
|
||||||
|
self.events_agent = ... # type: Set[Event] # Ordered
|
||||||
|
self.events_to = ... # type: Set[Trade] # Ordered
|
||||||
|
|
||||||
|
@property
|
||||||
|
def events(self) -> List[Event]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(Agent):
|
||||||
|
def __init__(self, name: str, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.members = ... # type: Set[Membership]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default_org_id(cls) -> uuid.UUID:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Individual(Agent):
|
||||||
|
member_of = ... # type:relationship
|
||||||
|
active_org_id = ... # type:Column
|
||||||
|
active_org = ... # type:relationship
|
||||||
|
user_id = ... # type:Column
|
||||||
|
user = ... # type:relationship
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.member_of = ... # type: Set[Membership]
|
||||||
|
self.active_org = ... # type: Organization
|
||||||
|
self.user = ... # type: User
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(Thing):
|
||||||
|
organization = ... # type: Column
|
||||||
|
individual = ... # type: Column
|
||||||
|
id = ... # type: Column
|
||||||
|
|
||||||
|
def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.organization = ... # type: Organization
|
||||||
|
self.individual = ... # type: Individual
|
||||||
|
self.id = ... # type: str
|
||||||
|
|
||||||
|
|
||||||
|
class Person(Individual):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class System(Individual):
|
||||||
|
pass
|
|
@ -0,0 +1,40 @@
|
||||||
|
from marshmallow import fields as ma_fields, validate as ma_validate
|
||||||
|
from marshmallow.fields import Email
|
||||||
|
|
||||||
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
|
from ereuse_devicehub.resources.models import STR_SIZE, STR_SM_SIZE
|
||||||
|
from ereuse_devicehub.resources.schemas import Thing
|
||||||
|
from teal import enums
|
||||||
|
from teal.marshmallow import EnumField, Phone
|
||||||
|
|
||||||
|
|
||||||
|
class Agent(Thing):
|
||||||
|
id = ma_fields.UUID(dump_only=True)
|
||||||
|
name = ma_fields.String(validate=ma_validate.Length(max=STR_SM_SIZE))
|
||||||
|
tax_id = ma_fields.String(validate=ma_validate.Length(max=STR_SM_SIZE),
|
||||||
|
data_key='taxId')
|
||||||
|
country = EnumField(enums.Country)
|
||||||
|
telephone = Phone()
|
||||||
|
email = Email()
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(Agent):
|
||||||
|
members = NestedOn('Membership')
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(Thing):
|
||||||
|
organization = NestedOn(Organization)
|
||||||
|
individual = NestedOn('Individual')
|
||||||
|
id = ma_fields.String(validate=ma_validate.Length(max=STR_SIZE))
|
||||||
|
|
||||||
|
|
||||||
|
class Individual(Agent):
|
||||||
|
member_of = NestedOn(Membership, many=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Person(Individual):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class System(Individual):
|
||||||
|
pass
|
|
@ -1,23 +1,23 @@
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from itertools import chain
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import Dict, Set
|
from typing import Dict, Set
|
||||||
|
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
||||||
RamFormat, RamInterface
|
Sequence, SmallInteger, Unicode, inspect
|
||||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
|
|
||||||
from itertools import chain
|
|
||||||
from sqlalchemy import BigInteger, Column, Enum as DBEnum, Float, ForeignKey, Integer, Sequence, \
|
|
||||||
SmallInteger, Unicode, inspect, Boolean
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
|
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 ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
||||||
|
RamFormat, RamInterface
|
||||||
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
|
||||||
|
from ereuse_utils.naming import Naming
|
||||||
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range
|
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
|
|
||||||
from ereuse_utils.naming import Naming
|
|
||||||
|
|
||||||
|
|
||||||
class Device(Thing):
|
class Device(Thing):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
|
from marshmallow import post_load, pre_load
|
||||||
|
from marshmallow.fields import Boolean, Float, Integer, Str
|
||||||
|
from marshmallow.validate import Length, OneOf, Range
|
||||||
|
from sqlalchemy.util import OrderedSet
|
||||||
|
from stdnum import imei, meid
|
||||||
|
|
||||||
from ereuse_devicehub.marshmallow import NestedOn
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
from ereuse_devicehub.resources.device import models as m
|
from ereuse_devicehub.resources.device import models as m
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
||||||
RamFormat, RamInterface
|
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
|
||||||
from marshmallow import post_load, pre_load
|
|
||||||
from marshmallow.fields import Float, Integer, Str, Boolean
|
|
||||||
from marshmallow.validate import Length, OneOf, Range
|
|
||||||
from sqlalchemy.util import OrderedSet
|
|
||||||
from stdnum import imei, meid
|
|
||||||
from teal.marshmallow import EnumField, ValidationError
|
from teal.marshmallow import EnumField, ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -195,3 +195,15 @@ class ComputerChassis(Enum):
|
||||||
Detachable = 'Detachable'
|
Detachable = 'Detachable'
|
||||||
Tablet = 'Tablet'
|
Tablet = 'Tablet'
|
||||||
Virtual = 'Virtual: A device with no chassis, probably non-physical.'
|
Virtual = 'Virtual: A device with no chassis, probably non-physical.'
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiverRole(Enum):
|
||||||
|
"""
|
||||||
|
The role that the receiver takes in the reception;
|
||||||
|
the meaning of the reception.
|
||||||
|
"""
|
||||||
|
Intermediary = 'Generic user in the workflow of the device.'
|
||||||
|
FinalUser = 'The user that will use the device.'
|
||||||
|
CollectionPoint = 'A collection point.'
|
||||||
|
RecyclingPoint = 'A recycling point.'
|
||||||
|
Transporter = 'An user that ships the devices to another one.'
|
||||||
|
|
|
@ -94,9 +94,8 @@ class InstallDef(EventDef):
|
||||||
|
|
||||||
|
|
||||||
class SnapshotDef(EventDef):
|
class SnapshotDef(EventDef):
|
||||||
VIEW = None
|
|
||||||
SCHEMA = schemas.Snapshot
|
|
||||||
VIEW = SnapshotView
|
VIEW = SnapshotView
|
||||||
|
SCHEMA = schemas.Snapshot
|
||||||
|
|
||||||
def __init__(self, app, import_name=__package__, static_folder=None, static_url_path=None,
|
def __init__(self, app, import_name=__package__, static_folder=None, static_url_path=None,
|
||||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||||
|
@ -161,6 +160,11 @@ class RepairDef(EventDef):
|
||||||
SCHEMA = schemas.Repair
|
SCHEMA = schemas.Repair
|
||||||
|
|
||||||
|
|
||||||
|
class ReadyToUse(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.ReadyToUse
|
||||||
|
|
||||||
|
|
||||||
class ToPrepareDef(EventDef):
|
class ToPrepareDef(EventDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.ToPrepare
|
SCHEMA = schemas.ToPrepare
|
||||||
|
@ -171,11 +175,61 @@ class PrepareDef(EventDef):
|
||||||
SCHEMA = schemas.Prepare
|
SCHEMA = schemas.Prepare
|
||||||
|
|
||||||
|
|
||||||
class ToDisposeDef(EventDef):
|
class LiveDef(EventDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.ToDispose
|
SCHEMA = schemas.Live
|
||||||
|
|
||||||
|
|
||||||
class DisposeDef(EventDef):
|
class ReserveDef(EventDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.Dispose
|
SCHEMA = schemas.Reserve
|
||||||
|
|
||||||
|
|
||||||
|
class CancelReservationDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.CancelReservation
|
||||||
|
|
||||||
|
|
||||||
|
class SellDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.Sell
|
||||||
|
|
||||||
|
|
||||||
|
class DonateDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.Donate
|
||||||
|
|
||||||
|
|
||||||
|
class RentDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.Rent
|
||||||
|
|
||||||
|
|
||||||
|
class CancelTradeDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.CancelTrade
|
||||||
|
|
||||||
|
|
||||||
|
class ToDisposeProductDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.ToDisposeProduct
|
||||||
|
|
||||||
|
|
||||||
|
class DisposeProductDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.DisposeProduct
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiveDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.Receive
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateToDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.MigrateTo
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateFromDef(EventDef):
|
||||||
|
VIEW = None
|
||||||
|
SCHEMA = schemas.MigrateFrom
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from collections import Iterable
|
from collections import Iterable
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Set, Union
|
from typing import Set, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
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, SmallInteger, Unicode, event, orm
|
Float, ForeignKey, 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
|
||||||
|
@ -14,17 +14,23 @@ from sqlalchemy.orm.events import AttributeEvents as Events
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.agent.models import Agent
|
||||||
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
|
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
|
||||||
Device, Laptop, Server
|
Device, Laptop, Server
|
||||||
from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \
|
||||||
FunctionalityRange, PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
|
FunctionalityRange, PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
|
||||||
SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
|
ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
|
||||||
from ereuse_devicehub.resources.image.models import Image
|
from ereuse_devicehub.resources.image.models import Image
|
||||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
from teal.currency import Currency
|
from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, IP, POLYMORPHIC_ID, \
|
||||||
from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \
|
POLYMORPHIC_ON, StrictVersionType, URL, check_range
|
||||||
POLYMORPHIC_ON, StrictVersionType, check_range
|
from teal.enums import Country, Currency, Subdivision
|
||||||
|
from teal.marshmallow import ValidationError
|
||||||
|
|
||||||
|
"""
|
||||||
|
A quantity of money with a currency.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class JoinedTableMixin:
|
class JoinedTableMixin:
|
||||||
|
@ -36,11 +42,11 @@ 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)
|
||||||
name = Column(Unicode(STR_BIG_SIZE), default='', nullable=False)
|
name = Column(Unicode(STR_BIG_SIZE), 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.
|
||||||
"""
|
"""
|
||||||
type = Column(Unicode)
|
|
||||||
incidence = Column(Boolean, default=False, nullable=False)
|
incidence = Column(Boolean, default=False, nullable=False)
|
||||||
incidence.comment = """
|
incidence.comment = """
|
||||||
Should this event be reviewed due some anomaly?
|
Should this event be reviewed due some anomaly?
|
||||||
|
@ -61,13 +67,21 @@ class Event(Thing):
|
||||||
description.comment = """
|
description.comment = """
|
||||||
A comment about the event.
|
A comment about the event.
|
||||||
"""
|
"""
|
||||||
date = Column(DateTime)
|
start_time = Column(DateTime)
|
||||||
date.comment = """
|
start_time.comment = """
|
||||||
When this event happened.
|
When the action starts. For some actions like reservations
|
||||||
Leave it blank if it is happening now
|
the time when they are available, for others like renting
|
||||||
(the field ``created`` is used instead).
|
when the renting starts.
|
||||||
This is used for example when creating events retroactively.
|
|
||||||
"""
|
"""
|
||||||
|
end_time = Column(DateTime)
|
||||||
|
end_time.comment = """
|
||||||
|
When the action ends. For some actions like reservations
|
||||||
|
the time when they expire, for others like renting
|
||||||
|
the time the end rents. For punctual actions it is the time
|
||||||
|
they are performed; it differs with ``created`` in which
|
||||||
|
created is the where the system received the action.
|
||||||
|
"""
|
||||||
|
|
||||||
snapshot_id = Column(UUID(as_uuid=True), ForeignKey('snapshot.id',
|
snapshot_id = Column(UUID(as_uuid=True), ForeignKey('snapshot.id',
|
||||||
use_alter=True,
|
use_alter=True,
|
||||||
name='snapshot_events'))
|
name='snapshot_events'))
|
||||||
|
@ -82,9 +96,36 @@ class Event(Thing):
|
||||||
ForeignKey(User.id),
|
ForeignKey(User.id),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
default=lambda: g.user.id)
|
default=lambda: g.user.id)
|
||||||
|
# todo compute the org
|
||||||
author = relationship(User,
|
author = relationship(User,
|
||||||
backref=backref('events', lazy=True, collection_class=set),
|
backref=backref('authored_events', lazy=True, collection_class=set),
|
||||||
primaryjoin=author_id == User.id)
|
primaryjoin=author_id == User.id)
|
||||||
|
"""
|
||||||
|
The user that recorded this action in the system.
|
||||||
|
|
||||||
|
This does not necessarily has to be the person that produced
|
||||||
|
the action in the real world. For that purpose see
|
||||||
|
``agent``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
agent_id = Column(UUID(as_uuid=True),
|
||||||
|
ForeignKey(Agent.id),
|
||||||
|
nullable=False,
|
||||||
|
default=lambda: g.user.individual.id)
|
||||||
|
# todo compute the org
|
||||||
|
agent = relationship(Agent,
|
||||||
|
backref=backref('events_agent',
|
||||||
|
lazy=True,
|
||||||
|
collection_class=OrderedSet,
|
||||||
|
order_by=lambda: Event.created),
|
||||||
|
primaryjoin=agent_id == Agent.id, )
|
||||||
|
agent_id.comment = """
|
||||||
|
The direct performer or driver of the action. e.g. John wrote a book.
|
||||||
|
|
||||||
|
It can differ with the user that registered the action in the
|
||||||
|
system, which can be in their behalf.
|
||||||
|
"""
|
||||||
|
|
||||||
components = relationship(Component,
|
components = relationship(Component,
|
||||||
backref=backref('events_components',
|
backref=backref('events_components',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
|
@ -93,7 +134,7 @@ class Event(Thing):
|
||||||
secondary=lambda: EventComponent.__table__,
|
secondary=lambda: EventComponent.__table__,
|
||||||
order_by=lambda: Component.id,
|
order_by=lambda: Component.id,
|
||||||
collection_class=OrderedSet)
|
collection_class=OrderedSet)
|
||||||
"""
|
components.comment = """
|
||||||
The components that are affected by the event.
|
The components that are affected by the event.
|
||||||
|
|
||||||
When performing events to parent devices their components are
|
When performing events to parent devices their components are
|
||||||
|
@ -138,13 +179,32 @@ class Event(Thing):
|
||||||
args[INHERIT_COND] = cls.id == Event.id
|
args[INHERIT_COND] = cls.id == Event.id
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@validates('end_time')
|
||||||
|
def validate_end_time(self, _, end_time: datetime):
|
||||||
|
if self.start_time and end_time <= self.start_time:
|
||||||
|
raise ValidationError('The event cannot finish before it starts.')
|
||||||
|
return end_time
|
||||||
|
|
||||||
|
@validates('start_time')
|
||||||
|
def validate_start_time(self, _, start_time: datetime):
|
||||||
|
if self.end_time and start_time >= self.end_time:
|
||||||
|
raise ValidationError('The event cannot start after it finished.')
|
||||||
|
return start_time
|
||||||
|
|
||||||
|
|
||||||
class EventComponent(db.Model):
|
class EventComponent(db.Model):
|
||||||
device_id = Column(BigInteger, ForeignKey(Component.id), primary_key=True)
|
device_id = Column(BigInteger, ForeignKey(Component.id), primary_key=True)
|
||||||
event_id = Column(UUID(as_uuid=True), ForeignKey(Event.id), primary_key=True)
|
event_id = Column(UUID(as_uuid=True), ForeignKey(Event.id), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(Event):
|
class JoinedWithOneDeviceMixin:
|
||||||
|
# noinspection PyMethodParameters
|
||||||
|
@declared_attr
|
||||||
|
def id(cls):
|
||||||
|
return Column(UUID(as_uuid=True), ForeignKey(EventWithOneDevice.id), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class EventWithOneDevice(JoinedTableMixin, Event):
|
||||||
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
||||||
device = relationship(Device,
|
device = relationship(Device,
|
||||||
backref=backref('events_one',
|
backref=backref('events_one',
|
||||||
|
@ -198,9 +258,7 @@ class Deallocate(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
organization = Column(Unicode(STR_SIZE))
|
organization = Column(Unicode(STR_SIZE))
|
||||||
|
|
||||||
|
|
||||||
class EraseBasic(JoinedTableMixin, EventWithOneDevice):
|
class EraseBasic(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
start_time = Column(DateTime, nullable=False)
|
|
||||||
end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False)
|
|
||||||
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
|
||||||
|
@ -208,10 +266,6 @@ class EraseBasic(JoinedTableMixin, EventWithOneDevice):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Ready(EventWithMultipleDevices):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EraseSectors(EraseBasic):
|
class EraseSectors(EraseBasic):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -222,7 +276,9 @@ class Step(db.Model):
|
||||||
num = Column(SmallInteger, primary_key=True)
|
num = Column(SmallInteger, primary_key=True)
|
||||||
error = Column(Boolean, default=False, nullable=False)
|
error = Column(Boolean, default=False, nullable=False)
|
||||||
start_time = Column(DateTime, nullable=False)
|
start_time = Column(DateTime, nullable=False)
|
||||||
|
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)
|
||||||
|
end_time.comment = Event.end_time.comment
|
||||||
|
|
||||||
erasure = relationship(EraseBasic,
|
erasure = relationship(EraseBasic,
|
||||||
backref=backref('steps',
|
backref=backref('steps',
|
||||||
|
@ -254,7 +310,7 @@ class StepRandom(Step):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Snapshot(JoinedTableMixin, EventWithOneDevice):
|
class Snapshot(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
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)
|
||||||
|
@ -266,7 +322,7 @@ class Snapshot(JoinedTableMixin, EventWithOneDevice):
|
||||||
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
|
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
|
||||||
|
|
||||||
|
|
||||||
class Install(JoinedTableMixin, EventWithOneDevice):
|
class Install(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
elapsed = Column(Interval, nullable=False)
|
elapsed = Column(Interval, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -280,7 +336,7 @@ class SnapshotRequest(db.Model):
|
||||||
cascade=CASCADE_OWN))
|
cascade=CASCADE_OWN))
|
||||||
|
|
||||||
|
|
||||||
class Rate(JoinedTableMixin, EventWithOneDevice):
|
class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
|
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
|
||||||
software = Column(DBEnum(RatingSoftware))
|
software = Column(DBEnum(RatingSoftware))
|
||||||
version = Column(StrictVersionType)
|
version = Column(StrictVersionType)
|
||||||
|
@ -401,9 +457,9 @@ class PhotoboxSystemRate(PhotoboxRate):
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True)
|
id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
class Price(JoinedTableMixin, EventWithOneDevice):
|
class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
currency = Column(DBEnum(Currency), nullable=False)
|
currency = Column(DBEnum(Currency), nullable=False)
|
||||||
price = Column(Float(decimal_return_scale=2), check_range('price', 0), nullable=False)
|
price = Column(Numeric(precision=19, scale=4), check_range('price', 0), nullable=False)
|
||||||
software = Column(DBEnum(PriceSoftware))
|
software = Column(DBEnum(PriceSoftware))
|
||||||
version = Column(StrictVersionType)
|
version = Column(StrictVersionType)
|
||||||
rating_id = Column(UUID(as_uuid=True), ForeignKey(AggregateRate.id))
|
rating_id = Column(UUID(as_uuid=True), ForeignKey(AggregateRate.id))
|
||||||
|
@ -497,7 +553,7 @@ class EreusePrice(Price):
|
||||||
return self.Service(self.device, self.rating.rating_range, role, self.price)
|
return self.Service(self.device, self.rating.rating_range, role, self.price)
|
||||||
|
|
||||||
|
|
||||||
class Test(JoinedTableMixin, EventWithOneDevice):
|
class Test(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
elapsed = Column(Interval, nullable=False)
|
elapsed = Column(Interval, nullable=False)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
|
@ -536,11 +592,14 @@ class StressTest(Test):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@validates('elapsed')
|
@validates('elapsed')
|
||||||
def bigger_than_a_minute(self, _, value: timedelta):
|
def is_minute_and_bigger_than_1_minute(self, _, value: timedelta):
|
||||||
assert value.total_seconds() >= 60
|
seconds = value.total_seconds()
|
||||||
|
assert not bool(seconds % 60)
|
||||||
|
assert seconds >= 60
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class Benchmark(JoinedTableMixin, EventWithOneDevice):
|
class Benchmark(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
elapsed = Column(Interval)
|
elapsed = Column(Interval)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
|
@ -589,6 +648,10 @@ class Repair(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReadyToUse(EventWithMultipleDevices):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ToPrepare(EventWithMultipleDevices):
|
class ToPrepare(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -597,11 +660,124 @@ class Prepare(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ToDispose(EventWithMultipleDevices):
|
class Live(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
ip = Column(IP, nullable=False,
|
||||||
|
comment='The IP where the live was triggered.')
|
||||||
|
subdivision_confidence = Column(SmallInteger,
|
||||||
|
check_range('subdivision_confidence', 0, 100),
|
||||||
|
nullable=False)
|
||||||
|
subdivision = Column(DBEnum(Subdivision), nullable=False)
|
||||||
|
city = Column(Unicode(STR_SM_SIZE), nullable=False)
|
||||||
|
city_confidence = Column(SmallInteger,
|
||||||
|
check_range('city_confidence', 0, 100),
|
||||||
|
nullable=False)
|
||||||
|
isp = Column(Unicode(length=STR_SM_SIZE), nullable=False)
|
||||||
|
organization = Column(Unicode(length=STR_SIZE))
|
||||||
|
organization_type = Column(Unicode(length=STR_SM_SIZE))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def country(self) -> Country:
|
||||||
|
return self.subdivision.country
|
||||||
|
# todo relate to snapshot
|
||||||
|
# todo testing
|
||||||
|
|
||||||
|
|
||||||
|
class Organize(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Dispose(EventWithMultipleDevices):
|
class Reserve(Organize):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CancelReservation(Organize):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Trade(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
shipping_date = Column(DateTime)
|
||||||
|
shipping_date.comment = """
|
||||||
|
When are the devices going to be ready for shipping?
|
||||||
|
"""
|
||||||
|
invoice_number = Column(Unicode(length=STR_SIZE))
|
||||||
|
invoice_number.comment = """
|
||||||
|
The id of the invoice so they can be linked.
|
||||||
|
"""
|
||||||
|
price_id = Column(UUID(as_uuid=True), ForeignKey(Price.id))
|
||||||
|
price = relationship(Price,
|
||||||
|
backref=backref('trade', lazy=True, uselist=False),
|
||||||
|
primaryjoin=price_id == Price.id)
|
||||||
|
price_id.comment = """
|
||||||
|
The price set for this trade.
|
||||||
|
|
||||||
|
If no price is set it is supposed that the trade was
|
||||||
|
not payed, usual in donations.
|
||||||
|
"""
|
||||||
|
to_id = Column(UUID(as_uuid=True),
|
||||||
|
ForeignKey(Agent.id),
|
||||||
|
nullable=False,
|
||||||
|
default=lambda: g.user.id)
|
||||||
|
# todo compute the org
|
||||||
|
to = relationship(Agent,
|
||||||
|
backref=backref('events_to',
|
||||||
|
lazy=True,
|
||||||
|
collection_class=OrderedSet,
|
||||||
|
order_by=lambda: Event.created),
|
||||||
|
primaryjoin=to_id == Agent.id)
|
||||||
|
confirms_id = Column(UUID(as_uuid=True), ForeignKey(Organize.id))
|
||||||
|
confirms = relationship(Organize,
|
||||||
|
backref=backref('confirmation', lazy=True, uselist=False),
|
||||||
|
primaryjoin=confirms_id == Organize.id)
|
||||||
|
confirms_id.comment = """
|
||||||
|
An organize action that this association confirms.
|
||||||
|
|
||||||
|
For example, a ``Sell`` or ``Rent``
|
||||||
|
can confirm a ``Reserve`` action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Sell(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Donate(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Rent(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CancelTrade(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ToDisposeProduct(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DisposeProduct(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Receive(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
role = Column(DBEnum(ReceiverRole),
|
||||||
|
nullable=False,
|
||||||
|
default=ReceiverRole.Intermediary)
|
||||||
|
|
||||||
|
|
||||||
|
class Migrate(JoinedTableMixin, EventWithMultipleDevices):
|
||||||
|
other = Column(URL(), nullable=False)
|
||||||
|
other.comment = """
|
||||||
|
The URL of the Migrate in the other end.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateTo(Migrate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateFrom(Migrate):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,31 @@
|
||||||
|
import ipaddress
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from typing import Dict, List, Set
|
from typing import Dict, List, Set, Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from boltons.urlutils import URL
|
||||||
from sqlalchemy import Column
|
from sqlalchemy import Column
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy_utils import Currency
|
from sqlalchemy_utils import Currency
|
||||||
|
|
||||||
|
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, SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
|
PriceSoftware, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, \
|
||||||
|
TestHardDriveLength
|
||||||
from ereuse_devicehub.resources.image.models import Image
|
from ereuse_devicehub.resources.image.models import Image
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
from ereuse_devicehub.resources.user import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
from teal import enums
|
||||||
from teal.db import Model
|
from teal.db import Model
|
||||||
|
from teal.enums import Country
|
||||||
|
|
||||||
|
|
||||||
class Event(Thing):
|
class Event(Thing):
|
||||||
id = ... # type: Column
|
id = ... # type: Column
|
||||||
name = ... # type: Column
|
name = ... # type: Column
|
||||||
date = ... # type: Column
|
|
||||||
type = ... # type: Column
|
type = ... # type: Column
|
||||||
error = ... # type: Column
|
error = ... # type: Column
|
||||||
incidence = ... # type: Column
|
incidence = ... # type: Column
|
||||||
|
@ -28,14 +34,19 @@ class Event(Thing):
|
||||||
snapshot_id = ... # type: Column
|
snapshot_id = ... # type: Column
|
||||||
snapshot = ... # type: relationship
|
snapshot = ... # type: relationship
|
||||||
author_id = ... # type: Column
|
author_id = ... # type: Column
|
||||||
author = ... # type: relationship
|
agent = ... # type: relationship
|
||||||
components = ... # type: relationship
|
components = ... # type: relationship
|
||||||
parent_id = ... # type: Column
|
parent_id = ... # type: Column
|
||||||
parent = ... # type: relationship
|
parent = ... # type: relationship
|
||||||
closed = ... # type: Column
|
closed = ... # type: Column
|
||||||
|
start_time = ... # type: Column
|
||||||
|
end_time = ... # type: Column
|
||||||
|
agent_id = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, id=None, name=None, incidence=None, closed=None, error=None,
|
||||||
super().__init__(**kwargs)
|
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)
|
||||||
self.id = ... # type: UUID
|
self.id = ... # type: UUID
|
||||||
self.name = ... # type: str
|
self.name = ... # type: str
|
||||||
self.type = ... # type: str
|
self.type = ... # type: str
|
||||||
|
@ -43,26 +54,32 @@ class Event(Thing):
|
||||||
self.closed = ... # type: bool
|
self.closed = ... # type: bool
|
||||||
self.error = ... # type: bool
|
self.error = ... # type: bool
|
||||||
self.description = ... # type: str
|
self.description = ... # type: str
|
||||||
self.date = ... # type: datetime
|
self.start_time = ... # type: datetime
|
||||||
self.snapshot_id = ... # type: UUID
|
self.end_time = ... # type: datetime
|
||||||
self.snapshot = ... # type: Snapshot
|
self.snapshot = ... # type: Snapshot
|
||||||
self.author_id = ... # type: UUID
|
|
||||||
self.author = ... # type: User
|
|
||||||
self.components = ... # type: Set[Component]
|
self.components = ... # type: Set[Component]
|
||||||
self.parent_id = ... # type: Computer
|
|
||||||
self.parent = ... # type: Computer
|
self.parent = ... # type: Computer
|
||||||
|
self.agent = ... # type: Agent
|
||||||
|
self.author = ... # type: User
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(Event):
|
class EventWithOneDevice(Event):
|
||||||
def __init__(self, **kwargs) -> None:
|
|
||||||
super().__init__(**kwargs)
|
def __init__(self, id=None, name=None, incidence=None, closed=None, error=None,
|
||||||
self.device_id = ... # type: int
|
description=None, start_time=None, end_time=None, snapshot=None, agent=None,
|
||||||
|
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):
|
||||||
def __init__(self, **kwargs) -> None:
|
|
||||||
super().__init__(**kwargs)
|
def __init__(self, id=None, name=None, incidence=None, closed=None, error=None,
|
||||||
|
description=None, start_time=None, end_time=None, snapshot=None, agent=None,
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,14 +92,15 @@ class Remove(EventWithOneDevice):
|
||||||
|
|
||||||
|
|
||||||
class Step(Model):
|
class Step(Model):
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, num=None, success=None, start_time=None, end_time=None,
|
||||||
self.erasure_id = ... # type: UUID
|
erasure=None, error=None) -> None:
|
||||||
self.type = ... # type: str
|
self.type = ... # type: str
|
||||||
self.num = ... # type: int
|
self.num = ... # type: int
|
||||||
self.success = ... # type: bool
|
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
|
||||||
|
|
||||||
|
|
||||||
class StepZero(Step):
|
class StepZero(Step):
|
||||||
|
@ -174,7 +192,6 @@ class PhotoboxRate(IndividualRate):
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.num = ... # type: int
|
self.num = ... # type: int
|
||||||
self.image_id = ... # type: UUID
|
|
||||||
self.image = ... # type: Image
|
self.image = ... # type: Image
|
||||||
|
|
||||||
|
|
||||||
|
@ -205,11 +222,10 @@ class Price(EventWithOneDevice):
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
self.price = ... # type: Decimal
|
||||||
self.currency = ... # type: Currency
|
self.currency = ... # type: Currency
|
||||||
self.price = ... # type: float
|
|
||||||
self.software = ... # type: PriceSoftware
|
self.software = ... # type: PriceSoftware
|
||||||
self.version = ... # type: StrictVersion
|
self.version = ... # type: StrictVersion
|
||||||
self.rating_id = ... # type: UUID
|
|
||||||
self.rating = ... # type: AggregateRate
|
self.rating = ... # type: AggregateRate
|
||||||
|
|
||||||
|
|
||||||
|
@ -260,10 +276,6 @@ class EraseBasic(EventWithOneDevice):
|
||||||
self.success = ... # type: bool
|
self.success = ... # type: bool
|
||||||
|
|
||||||
|
|
||||||
class Ready(EventWithMultipleDevices):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EraseSectors(EraseBasic):
|
class EraseSectors(EraseBasic):
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
@ -311,6 +323,10 @@ class Repair(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReadyToUse(EventWithMultipleDevices):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ToPrepare(EventWithMultipleDevices):
|
class ToPrepare(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -319,9 +335,96 @@ class Prepare(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ToDispose(EventWithMultipleDevices):
|
class Live(EventWithOneDevice):
|
||||||
|
ip = ... # type: Column
|
||||||
|
subdivision_confidence = ... # type: Column
|
||||||
|
subdivision = ... # type: Column
|
||||||
|
city = ... # type: Column
|
||||||
|
city_confidence = ... # type: Column
|
||||||
|
isp = ... # type: Column
|
||||||
|
organization = ... # type: Column
|
||||||
|
organization_type = ... # type: Column
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.ip = ... # type: Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
|
||||||
|
self.subdivision_confidence = ... # type: int
|
||||||
|
self.subdivision = ... # type: enums.Subdivision
|
||||||
|
self.city = ... # type: str
|
||||||
|
self.city_confidence = ... # type: int
|
||||||
|
self.isp = ... # type: str
|
||||||
|
self.organization = ... # type: str
|
||||||
|
self.organization_type = ... # type: str
|
||||||
|
self.country = ... # type: Country
|
||||||
|
|
||||||
|
|
||||||
|
class Organize(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Dispose(EventWithMultipleDevices):
|
class Reserve(Organize):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Trade(EventWithMultipleDevices):
|
||||||
|
shipping_date = ... # type: Column
|
||||||
|
invoice_number = ... # type: Column
|
||||||
|
price = ... # type: relationship
|
||||||
|
to = ... # type: relationship
|
||||||
|
confirms = ... # type: relationship
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.shipping_date = ... # type: datetime
|
||||||
|
self.invoice_number = ... # type: str
|
||||||
|
self.price = ... # type: Price
|
||||||
|
self.to = ... # type: Agent
|
||||||
|
self.confirms = ... # type: Organize
|
||||||
|
|
||||||
|
|
||||||
|
class Sell(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Donate(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Rent(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CancelTrade(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ToDisposeProduct(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DisposeProduct(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Receive(EventWithMultipleDevices):
|
||||||
|
role = ... # type:Column
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.role = ... # type: ReceiverRole
|
||||||
|
|
||||||
|
|
||||||
|
class Migrate(EventWithMultipleDevices):
|
||||||
|
other = ... # type: Column
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.other = ... # type: URL
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateTo(Migrate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateFrom(Migrate):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,43 +1,49 @@
|
||||||
|
import decimal
|
||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
|
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
|
||||||
from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \
|
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \
|
||||||
UUID
|
TimeDelta, URL, UUID
|
||||||
from marshmallow.validate import Length, Range
|
from marshmallow.validate import Length, Range
|
||||||
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
from ereuse_devicehub.marshmallow import NestedOn
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
|
from ereuse_devicehub.resources.agent.schemas import Agent
|
||||||
from ereuse_devicehub.resources.device.schemas import Component, Computer, Device
|
from ereuse_devicehub.resources.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, RatingSoftware, SnapshotExpectedEvents, SnapshotSoftware, \
|
PriceSoftware, RATE_POSITIVE, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, \
|
||||||
TestHardDriveLength
|
SnapshotSoftware, TestHardDriveLength
|
||||||
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
|
||||||
from ereuse_devicehub.resources.schemas import Thing
|
from ereuse_devicehub.resources.schemas import Thing
|
||||||
from ereuse_devicehub.resources.user.schemas import User
|
from ereuse_devicehub.resources.user.schemas import User
|
||||||
from teal.currency import Currency
|
from teal.enums import Country, Currency, Subdivision
|
||||||
from teal.marshmallow import EnumField, Version
|
from teal.marshmallow import EnumField, IP, Version
|
||||||
from teal.resource import Schema
|
from teal.resource import Schema
|
||||||
|
|
||||||
|
|
||||||
class Event(Thing):
|
class Event(Thing):
|
||||||
id = UUID(dump_only=True)
|
id = UUID(dump_only=True)
|
||||||
name = String(default='', validate=Length(STR_BIG_SIZE), description=m.Event.name.comment)
|
name = String(default='', validate=Length(STR_BIG_SIZE), description=m.Event.name.comment)
|
||||||
date = DateTime('iso', description=m.Event.date.comment)
|
|
||||||
error = Boolean(default=False, description=m.Event.error.comment)
|
|
||||||
incidence = Boolean(default=False, description=m.Event.incidence.comment)
|
incidence = Boolean(default=False, description=m.Event.incidence.comment)
|
||||||
snapshot = NestedOn('Snapshot', dump_only=True)
|
|
||||||
components = NestedOn(Component, dump_only=True, many=True)
|
|
||||||
description = String(default='', description=m.Event.description.comment)
|
|
||||||
author = NestedOn(User, dump_only=True, exclude=('token',))
|
|
||||||
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)
|
||||||
|
description = String(default='', description=m.Event.description.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)
|
||||||
|
snapshot = NestedOn('Snapshot', dump_only=True)
|
||||||
|
agent = NestedOn(Agent, description=m.Event.agent_id.comment)
|
||||||
|
author = NestedOn(User, dump_only=True, exclude=('token',))
|
||||||
|
components = NestedOn(Component, dump_only=True, many=True)
|
||||||
parent = NestedOn(Computer, dump_only=True, description=m.Event.parent_id.comment)
|
parent = NestedOn(Computer, dump_only=True, description=m.Event.parent_id.comment)
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(Event):
|
class EventWithOneDevice(Event):
|
||||||
device = NestedOn(Device)
|
device = NestedOn(Device, only_query='id')
|
||||||
|
|
||||||
|
|
||||||
class EventWithMultipleDevices(Event):
|
class EventWithMultipleDevices(Event):
|
||||||
devices = NestedOn(Device, many=True)
|
devices = NestedOn(Device, many=True, only_query='id', collection_class=OrderedSet)
|
||||||
|
|
||||||
|
|
||||||
class Add(EventWithOneDevice):
|
class Add(EventWithOneDevice):
|
||||||
|
@ -51,7 +57,7 @@ class Remove(EventWithOneDevice):
|
||||||
class Allocate(EventWithMultipleDevices):
|
class Allocate(EventWithMultipleDevices):
|
||||||
to = NestedOn(User,
|
to = NestedOn(User,
|
||||||
description='The user the devices are allocated to.')
|
description='The user the devices are allocated to.')
|
||||||
organization = String(validate=Length(STR_SIZE),
|
organization = String(validate=Length(max=STR_SIZE),
|
||||||
description='The organization where the user was when this happened.')
|
description='The organization where the user was when this happened.')
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,13 +65,11 @@ class Deallocate(EventWithMultipleDevices):
|
||||||
from_rel = Nested(User,
|
from_rel = Nested(User,
|
||||||
data_key='from',
|
data_key='from',
|
||||||
description='The user where the devices are not allocated to anymore.')
|
description='The user where the devices are not allocated to anymore.')
|
||||||
organization = String(validate=Length(STR_SIZE),
|
organization = String(validate=Length(max=STR_SIZE),
|
||||||
description='The organization where the user was when this happened.')
|
description='The organization where the user was when this happened.')
|
||||||
|
|
||||||
|
|
||||||
class EraseBasic(EventWithOneDevice):
|
class EraseBasic(EventWithOneDevice):
|
||||||
start_time = DateTime(required=True, data_key='startTime')
|
|
||||||
end_time = DateTime(required=True, data_key='endTime')
|
|
||||||
zeros = Boolean(required=True, description=m.EraseBasic.zeros.comment)
|
zeros = Boolean(required=True, description=m.EraseBasic.zeros.comment)
|
||||||
steps = NestedOn('Step', many=True, required=True)
|
steps = NestedOn('Step', many=True, required=True)
|
||||||
|
|
||||||
|
@ -161,7 +165,7 @@ class WorkbenchRate(ManualRate):
|
||||||
|
|
||||||
class Price(EventWithOneDevice):
|
class Price(EventWithOneDevice):
|
||||||
currency = EnumField(Currency, required=True)
|
currency = EnumField(Currency, required=True)
|
||||||
price = Float(required=True)
|
price = Decimal(places=4, rounding=decimal.ROUND_HALF_EVEN, required=True)
|
||||||
software = EnumField(PriceSoftware, dump_only=True)
|
software = EnumField(PriceSoftware, dump_only=True)
|
||||||
version = Version(dump_only=True)
|
version = Version(dump_only=True)
|
||||||
rating = NestedOn(AggregateRate, dump_only=True)
|
rating = NestedOn(AggregateRate, dump_only=True)
|
||||||
|
@ -208,7 +212,6 @@ class Snapshot(EventWithOneDevice):
|
||||||
'are performed. Setting this value will activate'
|
'are performed. Setting this value will activate'
|
||||||
'the async Snapshot.')
|
'the async Snapshot.')
|
||||||
|
|
||||||
device = NestedOn(Device)
|
|
||||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
|
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
|
||||||
components = NestedOn(Component,
|
components = NestedOn(Component,
|
||||||
many=True,
|
many=True,
|
||||||
|
@ -309,6 +312,10 @@ class Repair(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReadyToUse(EventWithMultipleDevices):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ToPrepare(EventWithMultipleDevices):
|
class ToPrepare(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -317,9 +324,73 @@ class Prepare(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ToDispose(EventWithMultipleDevices):
|
class Live(EventWithOneDevice):
|
||||||
|
ip = IP(dump_only=True)
|
||||||
|
subdivision_confidence = Integer(dump_only=True, data_key='subdivisionConfidence')
|
||||||
|
subdivision = EnumField(Subdivision, dump_only=True)
|
||||||
|
country = EnumField(Country, dump_only=True)
|
||||||
|
city = String(dump_only=True)
|
||||||
|
city_confidence = Integer(dump_only=True, data_key='cityConfidence')
|
||||||
|
isp = String(dump_only=True)
|
||||||
|
organization = String(dump_only=True)
|
||||||
|
organization_type = String(dump_only=True, data_key='organizationType')
|
||||||
|
|
||||||
|
|
||||||
|
class Organize(EventWithMultipleDevices):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Dispose(EventWithMultipleDevices):
|
class Reserve(Organize):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CancelReservation(Organize):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Trade(EventWithMultipleDevices):
|
||||||
|
shipping_date = DateTime(data_key='shippingDate')
|
||||||
|
invoice_number = String(validate=Length(max=STR_SIZE), data_key='invoiceNumber')
|
||||||
|
price = NestedOn(Price)
|
||||||
|
to = NestedOn(Agent, only_query='id')
|
||||||
|
confirms = NestedOn(Organize)
|
||||||
|
|
||||||
|
|
||||||
|
class Sell(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Donate(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Rent(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CancelTrade(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ToDisposeProduct(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DisposeProduct(Trade):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Receive(EventWithMultipleDevices):
|
||||||
|
role = EnumField(ReceiverRole)
|
||||||
|
|
||||||
|
|
||||||
|
class Migrate(EventWithMultipleDevices):
|
||||||
|
other = URL()
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateTo(Migrate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateFrom(Migrate):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -3,7 +3,7 @@ from distutils.version import StrictVersion
|
||||||
from typing import List
|
from typing import List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from flask import request
|
from flask import current_app as app, request
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
@ -14,6 +14,18 @@ from teal.resource import View
|
||||||
|
|
||||||
|
|
||||||
class EventView(View):
|
class EventView(View):
|
||||||
|
def post(self):
|
||||||
|
"""Posts an event."""
|
||||||
|
json = request.get_json(validate=False)
|
||||||
|
e = app.resources[json['type']].schema.load(json)
|
||||||
|
Model = db.Model._decl_class_registry.data[json['type']]()
|
||||||
|
event = Model(**e)
|
||||||
|
db.session.add(event)
|
||||||
|
db.session.commit()
|
||||||
|
ret = self.schema.jsonify(event)
|
||||||
|
ret.status_code = 201
|
||||||
|
return ret
|
||||||
|
|
||||||
def one(self, id: UUID):
|
def one(self, id: UUID):
|
||||||
"""Gets one event."""
|
"""Gets one event."""
|
||||||
event = Event.query.filter_by(id=id).one()
|
event = Event.query.filter_by(id=id).one()
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import Column
|
from sqlalchemy import Column
|
||||||
|
|
||||||
from teal.db import Model
|
from teal.db import Model
|
||||||
|
@ -15,8 +13,3 @@ class Thing(Model):
|
||||||
type = ... # type: str
|
type = ... # type: str
|
||||||
updated = ... # type: Column
|
updated = ... # type: Column
|
||||||
created = ... # type: Column
|
created = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.updated = ... # type: datetime
|
|
||||||
self.created = ... # type: datetime
|
|
||||||
|
|
|
@ -2,9 +2,9 @@ from sqlalchemy import BigInteger, Column, ForeignKey, Unicode, UniqueConstraint
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import backref, relationship, validates
|
from sqlalchemy.orm import backref, relationship, validates
|
||||||
|
|
||||||
|
from ereuse_devicehub.resources.agent.models import Organization
|
||||||
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
|
||||||
from ereuse_devicehub.resources.user.models import Organization
|
|
||||||
from teal.db import DB_CASCADE_SET_NULL, URL
|
from teal.db import DB_CASCADE_SET_NULL, URL
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from webargs.flaskparser import parser
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
from ereuse_devicehub.resources.tag import Tag
|
from ereuse_devicehub.resources.tag import Tag
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
from teal.resource import View, Schema
|
from teal.resource import Schema, View
|
||||||
|
|
||||||
|
|
||||||
class TagView(View):
|
class TagView(View):
|
||||||
|
|
|
@ -2,9 +2,8 @@ from click import argument, option
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.user import schemas
|
from ereuse_devicehub.resources.user import schemas
|
||||||
from ereuse_devicehub.resources.user.models import Organization, User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
from ereuse_devicehub.resources.user.views import UserView, login
|
from ereuse_devicehub.resources.user.views import UserView, login
|
||||||
from teal.db import SQLAlchemy
|
|
||||||
from teal.resource import Converters, Resource
|
from teal.resource import Converters, Resource
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,35 +33,3 @@ class UserDef(Resource):
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return self.schema.dump(user)
|
return self.schema.dump(user)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationDef(Resource):
|
|
||||||
__type__ = 'Organization'
|
|
||||||
ID_CONVERTER = Converters.uuid
|
|
||||||
AUTH = True
|
|
||||||
|
|
||||||
def __init__(self, app, import_name=__package__, static_folder=None, static_url_path=None,
|
|
||||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
|
||||||
root_path=None):
|
|
||||||
cli_commands = ((self.create_org, 'create-org'),)
|
|
||||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
|
||||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
|
||||||
|
|
||||||
@argument('name')
|
|
||||||
@argument('tax_id')
|
|
||||||
@argument('country')
|
|
||||||
def create_org(self, **kw: dict) -> dict:
|
|
||||||
"""
|
|
||||||
Creates an organization.
|
|
||||||
COUNTRY has to be 2 characters as defined by
|
|
||||||
"""
|
|
||||||
org = Organization(**self.schema.load(kw))
|
|
||||||
db.session.add(org)
|
|
||||||
db.session.commit()
|
|
||||||
return self.schema.dump(org)
|
|
||||||
|
|
||||||
def init_db(self, db: SQLAlchemy):
|
|
||||||
"""Creates the default organization."""
|
|
||||||
from flask import current_app as app
|
|
||||||
org = Organization(**app.config.get_namespace('ORGANIZATION_'))
|
|
||||||
db.session.add(org)
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from flask import current_app as app, g
|
from flask import current_app as app
|
||||||
from sqlalchemy import Column, Unicode, UniqueConstraint
|
from sqlalchemy import Column
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy_utils import CountryType, EmailType, PasswordType
|
from sqlalchemy_utils import EmailType, PasswordType
|
||||||
|
|
||||||
from ereuse_devicehub.resources.models import STR_SIZE, STR_SM_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_SIZE, Thing
|
||||||
|
|
||||||
|
|
||||||
class User(Thing):
|
class User(Thing):
|
||||||
|
@ -22,32 +22,12 @@ class User(Thing):
|
||||||
From `here <https://sqlalchemy-utils.readthedocs.io/en/latest/
|
From `here <https://sqlalchemy-utils.readthedocs.io/en/latest/
|
||||||
data_types.html#module-sqlalchemy_utils.types.password>`_
|
data_types.html#module-sqlalchemy_utils.types.password>`_
|
||||||
"""
|
"""
|
||||||
name = Column(Unicode(length=STR_SIZE))
|
|
||||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True)
|
token = Column(UUID(as_uuid=True), default=uuid4, unique=True)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<{0.t} {0.id} email={0.email}>'.format(self)
|
return '<User {0.email}>'.format(self)
|
||||||
|
|
||||||
|
@property
|
||||||
class Organization(Thing):
|
def individual(self):
|
||||||
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
|
"""The individual associated for this database, or None."""
|
||||||
name = Column(Unicode(length=STR_SM_SIZE), unique=True)
|
return next(iter(self.individuals), None)
|
||||||
tax_id = Column(Unicode(length=STR_SM_SIZE),
|
|
||||||
comment='The Tax / Fiscal ID of the organization, '
|
|
||||||
'e.g. the TIN in the US or the CIF/NIF in Spain.')
|
|
||||||
country = Column(CountryType, comment='Country issuing the tax_id number.')
|
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_default_org_id(cls) -> UUID:
|
|
||||||
"""Retrieves the default organization."""
|
|
||||||
return g.setdefault('org_id',
|
|
||||||
Organization.query.filter_by(
|
|
||||||
**app.config.get_namespace('ORGANIZATION_')
|
|
||||||
).one().id)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return '<Org {0.id}: {0.name}>'.format(self)
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
from typing import Set, Union
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from sqlalchemy import Column
|
||||||
|
from sqlalchemy_utils import Password
|
||||||
|
|
||||||
|
from ereuse_devicehub.resources.agent.models import Individual
|
||||||
|
from ereuse_devicehub.resources.models import Thing
|
||||||
|
|
||||||
|
|
||||||
|
class User(Thing):
|
||||||
|
id = ... # type: Column
|
||||||
|
email = ... # type: Column
|
||||||
|
password = ... # type: Column
|
||||||
|
token = ... # type: Column
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.id = ... # type: UUID
|
||||||
|
self.email = ... # type: str
|
||||||
|
self.password = ... # type: Password
|
||||||
|
self.individuals = ... # type: Set[Individual]
|
||||||
|
self.token = ... # type: UUID
|
||||||
|
|
||||||
|
@property
|
||||||
|
def individual(self) -> Union[Individual, None]:
|
||||||
|
pass
|
|
@ -3,6 +3,8 @@ from base64 import b64encode
|
||||||
from marshmallow import post_dump
|
from marshmallow import post_dump
|
||||||
from marshmallow.fields import Email, String, UUID
|
from marshmallow.fields import Email, String, UUID
|
||||||
|
|
||||||
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
|
from ereuse_devicehub.resources.agent.schemas import Individual
|
||||||
from ereuse_devicehub.resources.schemas import Thing
|
from ereuse_devicehub.resources.schemas import Thing
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,6 +12,7 @@ class User(Thing):
|
||||||
id = UUID(dump_only=True)
|
id = UUID(dump_only=True)
|
||||||
email = Email(required=True)
|
email = Email(required=True)
|
||||||
password = String(load_only=True, required=True)
|
password = String(load_only=True, required=True)
|
||||||
|
individuals = NestedOn(Individual, many=True, dump_only=True)
|
||||||
name = String()
|
name = String()
|
||||||
token = String(dump_only=True,
|
token = String(dump_only=True,
|
||||||
description='Use this token in an Authorization header to access the app.'
|
description='Use this token in an Authorization header to access the app.'
|
||||||
|
|
6
setup.py
6
setup.py
|
@ -35,7 +35,7 @@ setup(
|
||||||
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.0a9',
|
'teal>=0.2.0a11',
|
||||||
'marshmallow_enum',
|
'marshmallow_enum',
|
||||||
'ereuse-utils[Naming]>=0.4b1',
|
'ereuse-utils[Naming]>=0.4b1',
|
||||||
'psycopg2-binary',
|
'psycopg2-binary',
|
||||||
|
@ -44,7 +44,7 @@ setup(
|
||||||
'hashids',
|
'hashids',
|
||||||
'click',
|
'click',
|
||||||
'click-spinner',
|
'click-spinner',
|
||||||
'sqlalchemy-utils[password, color, babel]',
|
'sqlalchemy-utils[password, color, phone]',
|
||||||
'PyYAML',
|
'PyYAML',
|
||||||
'python-stdnum',
|
'python-stdnum',
|
||||||
'ereuse-rate==0.0.2'
|
'ereuse-rate==0.0.2'
|
||||||
|
@ -53,7 +53,7 @@ setup(
|
||||||
'docs': [
|
'docs': [
|
||||||
'sphinx',
|
'sphinx',
|
||||||
'sphinxcontrib-httpdomain >= 1.5.0',
|
'sphinxcontrib-httpdomain >= 1.5.0',
|
||||||
'sphinxcontrib-plantuml >= 0.11',
|
'sphinxcontrib-plantuml >= 0.12',
|
||||||
'sphinxcontrib-websupport >= 1.0.1'
|
'sphinxcontrib-websupport >= 1.0.1'
|
||||||
],
|
],
|
||||||
'test': test_requires
|
'test': test_requires
|
||||||
|
|
|
@ -8,6 +8,7 @@ from ereuse_devicehub.client import Client, UserClient
|
||||||
from ereuse_devicehub.config import DevicehubConfig
|
from ereuse_devicehub.config import DevicehubConfig
|
||||||
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.agent.models import Person
|
||||||
from ereuse_devicehub.resources.tag import Tag
|
from ereuse_devicehub.resources.tag import Tag
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
@ -66,16 +67,14 @@ def user(app: Devicehub) -> UserClient:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
password = 'foo'
|
password = 'foo'
|
||||||
user = create_user(password=password)
|
user = create_user(password=password)
|
||||||
client = UserClient(application=app,
|
client = UserClient(app, user.email, password, response_wrapper=app.response_class)
|
||||||
response_wrapper=app.response_class,
|
client.login()
|
||||||
email=user.email,
|
|
||||||
password=password)
|
|
||||||
client.user, _ = client.login(client.email, client.password)
|
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
def create_user(email='foo@foo.com', password='foo') -> User:
|
def create_user(email='foo@foo.com', password='foo') -> User:
|
||||||
user = User(email=email, password=password)
|
user = User(email=email, password=password)
|
||||||
|
user.individuals.add(Person(name='Timmy'))
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
||||||
|
|
|
@ -152,5 +152,5 @@
|
||||||
"serialNumber": "109192430003459"
|
"serialNumber": "109192430003459"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"date": "2018-07-13T10:48:36.738398"
|
"endTime": "2018-07-13T10:48:36.738398"
|
||||||
}
|
}
|
|
@ -123,6 +123,6 @@
|
||||||
},
|
},
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"date": "2018-07-19T15:48:40.635776",
|
"endTime": "2018-07-19T15:48:40.635776",
|
||||||
"closed": false
|
"closed": false
|
||||||
}
|
}
|
|
@ -128,7 +128,7 @@
|
||||||
"version": "11.0a2",
|
"version": "11.0a2",
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"date": "2018-07-03T09:10:57.034598",
|
"endTime": "2018-07-03T09:10:57.034598",
|
||||||
"device": {
|
"device": {
|
||||||
"type": "Laptop",
|
"type": "Laptop",
|
||||||
"model": "1001PXD",
|
"model": "1001PXD",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"uuid": "0c822fb7-6e51-4781-86cf-994bd306212e",
|
"uuid": "0c822fb7-6e51-4781-86cf-994bd306212e",
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"closed": false,
|
"closed": false,
|
||||||
"date": "2018-07-05T11:57:17.284891",
|
"endTime": "2018-07-05T11:57:17.284891",
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"type": "NetworkAdapter",
|
"type": "NetworkAdapter",
|
||||||
|
|
|
@ -157,5 +157,5 @@
|
||||||
"model": "HP Compaq 8100 Elite SFF"
|
"model": "HP Compaq 8100 Elite SFF"
|
||||||
},
|
},
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"date": "2018-06-29T12:28:54.508266"
|
"endTime": "2018-06-29T12:28:54.508266"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"Benchmark",
|
"Benchmark",
|
||||||
"StressTest"
|
"StressTest"
|
||||||
],
|
],
|
||||||
"date": "2018-06-29T15:29:29.322424",
|
"endTime": "2018-06-29T15:29:29.322424",
|
||||||
"elapsed": 391,
|
"elapsed": 391,
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"components": [
|
"components": [
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy_utils import PhoneNumber
|
||||||
|
|
||||||
|
from ereuse_devicehub.config import DevicehubConfig
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
from ereuse_devicehub.resources.agent import OrganizationDef, models, schemas
|
||||||
|
from ereuse_devicehub.resources.agent.models import Membership, Organization, Person, System
|
||||||
|
from teal.enums import Country
|
||||||
|
from tests.conftest import app_context, create_user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_agent():
|
||||||
|
"""Tests creating an person."""
|
||||||
|
person = Person(name='Timmy',
|
||||||
|
tax_id='XYZ',
|
||||||
|
country=Country.ES,
|
||||||
|
telephone=PhoneNumber('+34666666666'),
|
||||||
|
email='foo@bar.com')
|
||||||
|
db.session.add(person)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
p = schemas.Person().dump(person)
|
||||||
|
assert p['name'] == person.name == 'Timmy'
|
||||||
|
assert p['taxId'] == person.tax_id == 'XYZ'
|
||||||
|
assert p['country'] == person.country.name == 'ES'
|
||||||
|
assert p['telephone'] == person.telephone.international == '+34 666 66 66 66'
|
||||||
|
assert p['email'] == person.email == 'foo@bar.com'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_system():
|
||||||
|
"""Tests creating a system."""
|
||||||
|
system = System(name='Workbench',
|
||||||
|
email='hello@ereuse.org')
|
||||||
|
db.session.add(system)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
s = schemas.System().dump(system)
|
||||||
|
assert s['name'] == system.name == 'Workbench'
|
||||||
|
assert s['email'] == system.email == 'hello@ereuse.org'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_organization():
|
||||||
|
"""Tests creating an organization."""
|
||||||
|
org = Organization(name='ACME',
|
||||||
|
tax_id='XYZ',
|
||||||
|
country=Country.ES,
|
||||||
|
email='contact@acme.com')
|
||||||
|
db.session.add(org)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
o = schemas.Organization().dump(org)
|
||||||
|
assert o['name'] == org.name == 'ACME'
|
||||||
|
assert o['taxId'] == org.tax_id == 'XYZ'
|
||||||
|
assert org.country.name == o['country'] == 'ES'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_membership():
|
||||||
|
"""Tests assigning an Individual to an Organization."""
|
||||||
|
person = Person(name='Timmy')
|
||||||
|
org = Organization(name='ACME')
|
||||||
|
person.member_of.add(Membership(org, person, id='acme-1'))
|
||||||
|
db.session.add(person)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_membership_repeated():
|
||||||
|
person = Person(name='Timmy')
|
||||||
|
org = Organization(name='ACME')
|
||||||
|
person.member_of.add(Membership(org, person, id='acme-1'))
|
||||||
|
db.session.add(person)
|
||||||
|
|
||||||
|
person.member_of.add(Membership(org, person))
|
||||||
|
with pytest.raises(IntegrityError):
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_membership_repeating_id():
|
||||||
|
person = Person(name='Timmy')
|
||||||
|
org = Organization(name='ACME')
|
||||||
|
person.member_of.add(Membership(org, person, id='acme-1'))
|
||||||
|
db.session.add(person)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
person2 = Person(name='Tommy')
|
||||||
|
person2.member_of.add(Membership(org, person2, id='acme-1'))
|
||||||
|
db.session.add(person2)
|
||||||
|
with pytest.raises(IntegrityError) as e:
|
||||||
|
db.session.flush()
|
||||||
|
assert 'One member id per organization' in str(e)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_default_org_exists(config: DevicehubConfig):
|
||||||
|
"""
|
||||||
|
Ensures that the default organization is created on app
|
||||||
|
initialization and that is accessible for the method
|
||||||
|
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
|
||||||
|
"""
|
||||||
|
assert models.Organization.query.filter_by(name=config.ORGANIZATION_NAME,
|
||||||
|
tax_id=config.ORGANIZATION_TAX_ID).one()
|
||||||
|
assert isinstance(models.Organization.get_default_org_id(), UUID)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_assign_individual_user():
|
||||||
|
"""Tests assigning an individual to an user."""
|
||||||
|
user = create_user()
|
||||||
|
assert len(user.individuals) == 1
|
||||||
|
assert next(iter(user.individuals)).name == 'Timmy'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_create_organization_main_method(app: Devicehub):
|
||||||
|
org_def = app.resources[models.Organization.t] # type: OrganizationDef
|
||||||
|
o = org_def.create_org('ACME', tax_id='FOO', country='ES')
|
||||||
|
org = models.Agent.query.filter_by(id=o['id']).one() # type: Organization
|
||||||
|
assert org.name == o['name'] == 'ACME'
|
||||||
|
assert org.tax_id == o['taxId'] == 'FOO'
|
||||||
|
assert org.country.name == o['country'] == 'ES'
|
|
@ -35,4 +35,4 @@ def test_api_docs(client: Client):
|
||||||
'scheme': 'basic',
|
'scheme': 'basic',
|
||||||
'name': 'Authorization'
|
'name': 'Authorization'
|
||||||
}
|
}
|
||||||
assert 60 == len(docs['definitions'])
|
assert 76 == len(docs['definitions'])
|
||||||
|
|
|
@ -9,6 +9,7 @@ from sqlalchemy.util import OrderedSet
|
||||||
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.agent.models import Person
|
||||||
from ereuse_devicehub.resources.device.exceptions import NeedsId
|
from ereuse_devicehub.resources.device.exceptions import NeedsId
|
||||||
from ereuse_devicehub.resources.device.models import Component, ComputerMonitor, Desktop, Device, \
|
from ereuse_devicehub.resources.device.models import Component, ComputerMonitor, Desktop, Device, \
|
||||||
GraphicCard, Laptop, Motherboard, NetworkAdapter
|
GraphicCard, Laptop, Motherboard, NetworkAdapter
|
||||||
|
@ -369,6 +370,7 @@ def test_get_device(app: Devicehub, user: UserClient):
|
||||||
db.session.add(Test(device=pc,
|
db.session.add(Test(device=pc,
|
||||||
elapsed=timedelta(seconds=4),
|
elapsed=timedelta(seconds=4),
|
||||||
error=False,
|
error=False,
|
||||||
|
agent=Person(name='Timmy'),
|
||||||
author=User(email='bar@bar.com')))
|
author=User(email='bar@bar.com')))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
pc, _ = user.get(res=Device, item=1)
|
pc, _ = user.get(res=Device, item=1)
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
|
import ipaddress
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import g
|
from flask import current_app as app, g
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
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.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 TestHardDriveLength
|
from ereuse_devicehub.resources.enums import ComputerChassis, TestHardDriveLength
|
||||||
from ereuse_devicehub.resources.event import models
|
from ereuse_devicehub.resources.event import models
|
||||||
|
from teal.enums import Currency, Subdivision
|
||||||
|
from tests import conftest
|
||||||
from tests.conftest import create_user, file
|
from tests.conftest import create_user, file
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('app_context')
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
def test_author():
|
def test_author():
|
||||||
"""
|
"""
|
||||||
Checks the default created author.
|
Checks the default created author.
|
||||||
|
@ -30,7 +33,7 @@ def test_author():
|
||||||
assert e.author == user
|
assert e.author == user
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_erase_basic():
|
def test_erase_basic():
|
||||||
erasure = models.EraseBasic(
|
erasure = models.EraseBasic(
|
||||||
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
||||||
|
@ -46,7 +49,7 @@ def test_erase_basic():
|
||||||
assert next(iter(db_erasure.device.events)) == erasure
|
assert next(iter(db_erasure.device.events)) == erasure
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_validate_device_data_storage():
|
def test_validate_device_data_storage():
|
||||||
"""Checks the validation for data-storage-only events works."""
|
"""Checks the validation for data-storage-only events works."""
|
||||||
# We can't set a GraphicCard
|
# We can't set a GraphicCard
|
||||||
|
@ -62,7 +65,7 @@ def test_validate_device_data_storage():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_erase_sectors_steps():
|
def test_erase_sectors_steps():
|
||||||
erasure = models.EraseSectors(
|
erasure = models.EraseSectors(
|
||||||
device=SolidStateDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
device=SolidStateDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
||||||
|
@ -91,7 +94,7 @@ def test_erase_sectors_steps():
|
||||||
assert db_erasure.steps[2].num == 2
|
assert db_erasure.steps[2].num == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_test_data_storage():
|
def test_test_data_storage():
|
||||||
test = models.TestDataStorage(
|
test = models.TestDataStorage(
|
||||||
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
||||||
|
@ -106,7 +109,7 @@ def test_test_data_storage():
|
||||||
assert models.TestDataStorage.query.one()
|
assert models.TestDataStorage.query.one()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_install():
|
def test_install():
|
||||||
hdd = HardDrive(serial_number='sn')
|
hdd = HardDrive(serial_number='sn')
|
||||||
install = models.Install(name='LinuxMint 18.04 es',
|
install = models.Install(name='LinuxMint 18.04 es',
|
||||||
|
@ -116,7 +119,7 @@ def test_install():
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_update_components_event_one():
|
def test_update_components_event_one():
|
||||||
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
|
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
|
||||||
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
||||||
|
@ -141,13 +144,13 @@ def test_update_components_event_one():
|
||||||
assert len(test.components) == 1
|
assert len(test.components) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_update_components_event_multiple():
|
def test_update_components_event_multiple():
|
||||||
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
|
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
|
||||||
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
||||||
computer.components.add(hdd)
|
computer.components.add(hdd)
|
||||||
|
|
||||||
ready = models.Ready()
|
ready = models.ReadyToUse()
|
||||||
assert not ready.devices
|
assert not ready.devices
|
||||||
assert not ready.components
|
assert not ready.components
|
||||||
|
|
||||||
|
@ -167,7 +170,7 @@ def test_update_components_event_multiple():
|
||||||
assert ready.components
|
assert ready.components
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
def test_update_parent():
|
def test_update_parent():
|
||||||
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
|
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
|
||||||
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
||||||
|
@ -184,21 +187,96 @@ def test_update_parent():
|
||||||
assert not benchmark.parent
|
assert not benchmark.parent
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='No POST view for generic tests')
|
|
||||||
@pytest.mark.parametrize('event_model', [
|
@pytest.mark.parametrize('event_model', [
|
||||||
models.ToRepair,
|
models.ToRepair,
|
||||||
models.Repair,
|
models.Repair,
|
||||||
models.ToPrepare,
|
models.ToPrepare,
|
||||||
|
models.ReadyToUse,
|
||||||
|
models.ToPrepare,
|
||||||
models.Prepare,
|
models.Prepare,
|
||||||
models.ToDispose,
|
|
||||||
models.Dispose,
|
|
||||||
models.Ready
|
|
||||||
])
|
])
|
||||||
def test_generic_event(event_model: models.Event, user: UserClient):
|
def test_generic_event(event_model: models.Event, user: UserClient):
|
||||||
"""Tests POSTing all generic events."""
|
"""Tests POSTing all generic events."""
|
||||||
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
|
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
|
||||||
event = {'type': event_model.t, 'devices': [snapshot['device']['id']]}
|
event = {'type': event_model.t, 'devices': [snapshot['device']['id']]}
|
||||||
event, _ = user.post(event, res=event_model)
|
event, _ = user.post(event, res=models.Event)
|
||||||
assert event['device'][0]['id'] == snapshot['device']['id']
|
assert event['devices'][0]['id'] == snapshot['device']['id']
|
||||||
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
||||||
assert device['events'][0]['id'] == event['id']
|
assert device['events'][-1]['id'] == event['id']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
|
def test_live():
|
||||||
|
"""Tests inserting a Live into the database and GETting it."""
|
||||||
|
db_live = models.Live(ip=ipaddress.ip_address('79.147.10.10'),
|
||||||
|
subdivision_confidence=84,
|
||||||
|
subdivision=Subdivision['ES-CA'],
|
||||||
|
city='Barcelona',
|
||||||
|
city_confidence=20,
|
||||||
|
isp='ACME',
|
||||||
|
device=Desktop(serial_number='sn1', model='ml1', manufacturer='mr1',
|
||||||
|
chassis=ComputerChassis.Docking),
|
||||||
|
organization='ACME1',
|
||||||
|
organization_type='ACME1bis')
|
||||||
|
db.session.add(db_live)
|
||||||
|
db.session.commit()
|
||||||
|
client = UserClient(app, 'foo@foo.com', 'foo', response_wrapper=app.response_class)
|
||||||
|
client.login()
|
||||||
|
live, _ = client.get(res=models.Event, item=str(db_live.id))
|
||||||
|
assert live['ip'] == '79.147.10.10'
|
||||||
|
assert live['subdivision'] == 'ES-CA'
|
||||||
|
assert live['country'] == 'ES'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reson='Functionality not developed.')
|
||||||
|
def test_live_geoip():
|
||||||
|
"""Tests performing a Live action using the GEOIP library."""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reson='Develop reserve')
|
||||||
|
def test_reserve(user: UserClient):
|
||||||
|
"""Performs a reservation and then cancels it."""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('event_model', [
|
||||||
|
models.Sell,
|
||||||
|
models.Donate,
|
||||||
|
models.Rent,
|
||||||
|
models.DisposeProduct
|
||||||
|
])
|
||||||
|
def test_trade(event_model: models.Event, user: UserClient):
|
||||||
|
"""Tests POSTing all generic events."""
|
||||||
|
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
|
||||||
|
event = {
|
||||||
|
'type': event_model.t,
|
||||||
|
'devices': [snapshot['device']['id']],
|
||||||
|
'to': user.user['individuals'][0]['id'],
|
||||||
|
'shippingDate': '2018-06-29T12:28:54',
|
||||||
|
'invoiceNumber': 'ABC'
|
||||||
|
}
|
||||||
|
event, _ = user.post(event, res=models.Event)
|
||||||
|
assert event['devices'][0]['id'] == snapshot['device']['id']
|
||||||
|
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
||||||
|
assert device['events'][-1]['id'] == event['id']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reson='Develop migrate')
|
||||||
|
def test_migrate():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
|
def test_price_custom():
|
||||||
|
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1',
|
||||||
|
chassis=ComputerChassis.Docking)
|
||||||
|
price = models.Price(price=25.25, currency=Currency.EUR)
|
||||||
|
price.device = computer
|
||||||
|
db.session.add(computer)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
client = UserClient(app, 'foo@foo.com', 'foo', response_wrapper=app.response_class)
|
||||||
|
client.login()
|
||||||
|
p, _ = client.get(res=models.Event, item=str(price.id))
|
||||||
|
assert p['device']['id'] == price.device.id == computer.id
|
||||||
|
assert p['price'] == 25.25
|
||||||
|
assert p['currency'] == Currency.EUR.name == 'EUR'
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ereuse_devicehub.config import DevicehubConfig
|
|
||||||
from ereuse_devicehub.resources.user import Organization
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('app_context')
|
|
||||||
def test_default_org_exists(config: DevicehubConfig):
|
|
||||||
"""
|
|
||||||
Ensures that the default organization is created on app
|
|
||||||
initialization and that is accessible for the method
|
|
||||||
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
|
|
||||||
"""
|
|
||||||
assert Organization.query.filter_by(name=config.ORGANIZATION_NAME,
|
|
||||||
tax_id=config.ORGANIZATION_TAX_ID).one()
|
|
||||||
assert isinstance(Organization.get_default_org_id(), UUID)
|
|
|
@ -1,6 +0,0 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='Just needs to do the test')
|
|
||||||
def test_price_no_data_storage():
|
|
||||||
pass
|
|
|
@ -4,6 +4,7 @@ from typing import List, Tuple
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
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
|
||||||
|
@ -28,7 +29,7 @@ def test_snapshot_model():
|
||||||
device = m.Desktop(serial_number='a1', chassis=ComputerChassis.Tower)
|
device = m.Desktop(serial_number='a1', chassis=ComputerChassis.Tower)
|
||||||
# noinspection PyArgumentList
|
# noinspection PyArgumentList
|
||||||
snapshot = Snapshot(uuid=uuid4(),
|
snapshot = Snapshot(uuid=uuid4(),
|
||||||
date=datetime.now(),
|
end_time=datetime.now(),
|
||||||
version='1.0',
|
version='1.0',
|
||||||
software=SnapshotSoftware.DesktopApp,
|
software=SnapshotSoftware.DesktopApp,
|
||||||
elapsed=timedelta(seconds=25))
|
elapsed=timedelta(seconds=25))
|
||||||
|
|
|
@ -5,11 +5,11 @@ from sqlalchemy.exc import IntegrityError
|
||||||
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.agent.models import Organization
|
||||||
from ereuse_devicehub.resources.device.models import Desktop
|
from ereuse_devicehub.resources.device.models import Desktop
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis
|
from ereuse_devicehub.resources.enums import ComputerChassis
|
||||||
from ereuse_devicehub.resources.tag import Tag
|
from ereuse_devicehub.resources.tag import Tag
|
||||||
from ereuse_devicehub.resources.tag.view import CannotCreateETag, TagNotLinked
|
from ereuse_devicehub.resources.tag.view import CannotCreateETag, TagNotLinked
|
||||||
from ereuse_devicehub.resources.user import Organization
|
|
||||||
from teal.db import MultipleResourcesFound, ResourceNotFound
|
from teal.db import MultipleResourcesFound, ResourceNotFound
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
import pytest
|
||||||
from sqlalchemy_utils import Password
|
from sqlalchemy_utils import Password
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
@ -11,16 +12,16 @@ from ereuse_devicehub.resources.user import UserDef
|
||||||
from ereuse_devicehub.resources.user.exceptions import WrongCredentials
|
from ereuse_devicehub.resources.user.exceptions import WrongCredentials
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
from tests.conftest import create_user
|
from tests.conftest import app_context, create_user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
def test_create_user_method(app: Devicehub):
|
def test_create_user_method(app: Devicehub):
|
||||||
"""
|
"""
|
||||||
Tests creating an user through the main method.
|
Tests creating an user through the main method.
|
||||||
|
|
||||||
This method checks that the token is correct, too.
|
This method checks that the token is correct, too.
|
||||||
"""
|
"""
|
||||||
with app.app_context():
|
|
||||||
user_def = app.resources['User'] # type: UserDef
|
user_def = app.resources['User'] # type: UserDef
|
||||||
u = user_def.create_user(email='foo@foo.com', password='foo')
|
u = user_def.create_user(email='foo@foo.com', password='foo')
|
||||||
user = User.query.filter_by(id=u['id']).one() # type: User
|
user = User.query.filter_by(id=u['id']).one() # type: User
|
||||||
|
@ -29,9 +30,9 @@ def test_create_user_method(app: Devicehub):
|
||||||
assert User.query.filter_by(email='foo@foo.com').one() == user
|
assert User.query.filter_by(email='foo@foo.com').one() == user
|
||||||
|
|
||||||
|
|
||||||
def test_create_user_email_insensitive(app: Devicehub):
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_create_user_email_insensitive():
|
||||||
"""Ensures email is case insensitive."""
|
"""Ensures email is case insensitive."""
|
||||||
with app.app_context():
|
|
||||||
user = User(email='FOO@foo.com')
|
user = User(email='FOO@foo.com')
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -41,9 +42,9 @@ def test_create_user_email_insensitive(app: Devicehub):
|
||||||
assert u1.email == 'foo@foo.com'
|
assert u1.email == 'foo@foo.com'
|
||||||
|
|
||||||
|
|
||||||
def test_hash_password(app: Devicehub):
|
@pytest.mark.usefixtures(app_context.__name__)
|
||||||
|
def test_hash_password():
|
||||||
"""Tests correct password hashing and equaling."""
|
"""Tests correct password hashing and equaling."""
|
||||||
with app.app_context():
|
|
||||||
user = create_user()
|
user = create_user()
|
||||||
assert isinstance(user.password, Password)
|
assert isinstance(user.password, Password)
|
||||||
assert user.password == 'foo'
|
assert user.password == 'foo'
|
||||||
|
@ -66,6 +67,9 @@ def test_login_success(client: Client, app: Devicehub):
|
||||||
assert user['email'] == 'foo@foo.com'
|
assert user['email'] == 'foo@foo.com'
|
||||||
assert UUID(b64decode(user['token'].encode()).decode()[:-1])
|
assert UUID(b64decode(user['token'].encode()).decode()[:-1])
|
||||||
assert 'password' not in user
|
assert 'password' not in user
|
||||||
|
assert user['individuals'][0]['name'] == 'Timmy'
|
||||||
|
assert user['individuals'][0]['type'] == 'Person'
|
||||||
|
assert len(user['individuals']) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_login_failure(client: Client, app: Devicehub):
|
def test_login_failure(client: Client, app: Devicehub):
|
||||||
|
|
|
@ -3,12 +3,12 @@ Tests that emulates the behaviour of a WorkbenchServer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ereuse_devicehub.client import UserClient
|
from ereuse_devicehub.client import UserClient
|
||||||
from ereuse_devicehub.resources.device.exceptions import NeedsId
|
from ereuse_devicehub.resources.device.exceptions import NeedsId
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
from ereuse_devicehub.resources.event import models as em
|
from ereuse_devicehub.resources.event import models as em
|
||||||
from ereuse_devicehub.resources.tag.model import Tag
|
from ereuse_devicehub.resources.tag.model import Tag
|
||||||
|
|
||||||
from tests.conftest import file
|
from tests.conftest import file
|
||||||
|
|
||||||
|
|
||||||
|
|
Reference in New Issue