Complete events and agents. Bump to 0.2.0a11.

This commit is contained in:
Xavier Bustamante Talavera 2018-08-03 18:15:08 +02:00
parent 8efca0d589
commit 42b0b0ebbc
75 changed files with 4016 additions and 2832 deletions

View File

@ -1,4 +1,8 @@
@startuml
left to right direction
skinparam nodesep 20
skinparam ranksep 1
abstract class Rate
abstract class Event
abstract class Test
@ -38,13 +42,13 @@ EventWithOneDevice <|--- EraseBasic
EraseBasic <|- EraseSectors
Step <|-- StepZero
Step <|-- StepRandom
Snapshot "1" -- "1" SnapshotRequest
Step <|-- "Step\nRandom"
Snapshot "1" -- "1" "Snapshot\nRequest"
Event "*" -> "0..1" Snapshot : InSnapshot >
Event "*" -> "0..1" Component : affectedComponents >
Device "1" *-- "*" EventWithOneDevice : EventOn <
Device "1..*" *-- "1" EventWithMultipleDevices : EventOn <
EraseBasic "1" *-- "1..*" Step
Device "1" *- "*" EventWithOneDevice : EventOn <
Device "1..*" *- "1" EventWithMultipleDevices : EventOn <
EraseBasic "1" *- "1..*" Step
PhotoboxRate <|-- PhotoboxSystemRate
PhotoboxRate <|-- PhotoboxPersonRate
@ -84,18 +88,14 @@ Plan <|-- CancelReservation
package Agents {
abstract class User
abstract class User <<Common schema>>
abstract class Agent
Event "*" -> "1" User : Author >
Event "*" - "0..1" Agent : agent >
Trade "*" - "0..1" Agent : to >
Agent <|-- User
User <|-- Person
User <|-- System
Agent <|-- Organization
User "*" -o "0..1" Organization : WorksIn >
User "*" -o "0..1" Organization : activeOrganization >
User - Agent
}
@enduml

443
docs/actions.rst Normal file
View File

@ -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

25
docs/agents.puml Normal file
View File

@ -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

4
docs/agents.rst Normal file
View File

@ -0,0 +1,4 @@
Agents
######
.. uml:: agents.puml

View File

@ -1,72 +1,25 @@
@startuml
ChangeAssociation <|-- Organize
ChangeAssociation <|-- Transfer
Organize <|-- Plan
Organize <|-- Allocate
Allocate <|-- Accept
Allocate <|-- Reject
Allocate <|-- Assign
Allocate <|-- Authorize
Plan <|-- Reserve
Plan <|-- Cancel
skinparam nodesep 10
skinparam ranksep 30
abstract class Trade {
to: Agent
}
abstract class Transfer
abstract class Organize
"Associate" <|-- Organize
"Associate" <|-- Transfer
Organize <|-- Reserve
Organize <|--- "Cancel\nReservation"
Transfer <|-- Receive
ChangeAssociation <|-- Trade
"Associate" <|-- Trade
Trade <|-- Sell
Trade <|-- Donate
Trade <|-- Pay
Trade <|-- Rent
Trade <|-- DisposeProduct
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
}
Trade <|-- "Dispose\nProduct"
Association <|-- PhysicalPossessor
Association <|-- TradeAssociation
TradeAssociation <|-- Usufructuary
TradeAssociation <|-- Ownership
Sell - TradeAssociation
Donate - TradeAssociation
Rent -- Usufructuary : Sure?
Receive - PhysicalPossessor
@enduml

View File

@ -167,7 +167,7 @@ intersphinx_mapping = {'https://docs.python.org/': None}
todo_include_todos = True
# Plantuml
plantuml_output_format = 'svg'
plantuml_output_format = 'svg_img'
# favicon
html_favicon = 'img/favicon.ico'

View File

@ -15,7 +15,6 @@ Actors
- Photochromic tag manufacturer.
- User: organization that uses the tags.
Requirements
************

View File

@ -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?

View File

@ -10,11 +10,12 @@ This is the documentation and API of the `eReuse.org DeviceHub
.. toctree::
:maxdepth: 4
:maxdepth: 2
events
tags
actions
agents
inventory
tags
etag
* :ref:`genindex`

43
docs/states.puml Normal file
View File

@ -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

View File

@ -1,4 +1,4 @@
from distutils.version import StrictVersion
__version__ = '0.2.0a11'
__version__ = '0.2.0a12'
version = StrictVersion(__version__)

View File

@ -109,3 +109,8 @@ class UserClient(Client):
**kw) -> Tuple[Union[Dict[str, object], str], Response]:
return super().open(uri, res, status, query, accept, content_type, item, headers,
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

View File

@ -2,11 +2,11 @@ from distutils.version import StrictVersion
from itertools import chain
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 teal.auth import TokenAuth
from teal.config import Config
from teal.currency import Currency
from teal.enums import Currency
from teal.utils import import_resource
@ -15,7 +15,8 @@ class DevicehubConfig(Config):
import_resource(event),
import_resource(user),
import_resource(tag),
import_resource(inventory)))
import_resource(inventory),
import_resource(agent)))
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/devicehub' # type: str
SCHEMA = 'dhub'

View File

@ -3,8 +3,10 @@ from pathlib import Path
import click
import click_spinner
import yaml
from ereuse_devicehub.client import UserClient
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.inventory import Inventory
from ereuse_devicehub.resources.tag.model import Tag
@ -49,11 +51,10 @@ class Dummy:
def user_client(self, email: str, password: str):
user = User(email=email, password=password)
user.individuals.add(Person(name='Timmy'))
db.session.add(user)
db.session.commit()
client = UserClient(application=self.app,
response_wrapper=self.app.response_class,
email=user.email,
password=password)
client.user, _ = client.login(client.email, client.password)
client = UserClient(self.app, user.email, password,
response_wrapper=self.app.response_class)
client.login()
return client

View File

@ -141,7 +141,7 @@
"version": "11.0a3",
"closed": false,
"elapsed": 1512,
"date": "2018-07-11T11:17:00.888231",
"endTime": "2018-07-11T11:17:00.888231",
"type": "Snapshot",
"expectedEvents": [
"Benchmark",

View File

@ -9,7 +9,7 @@
"closed": false,
"elapsed": -3058,
"uuid": "106930cd-e948-4cca-a8c8-1e39d6192ad6",
"date": "2018-07-11T10:47:50.822380",
"endTime": "2018-07-11T10:47:50.822380",
"components": [
{
"type": "Processor",

View File

@ -175,6 +175,6 @@
"EraseBasic"
],
"software": "Workbench",
"date": "2018-07-11T10:30:22.395958",
"endTime": "2018-07-11T10:30:22.395958",
"elapsed": 2766
}

View File

@ -2,7 +2,7 @@
"uuid": "9c3560a9-371c-4392-b586-37090b5f79c6",
"version": "11.0a3",
"closed": false,
"date": "2018-07-11T13:26:29.365504",
"endTime": "2018-07-11T13:26:29.365504",
"type": "Snapshot",
"device": {
"serialNumber": "PB357N0",

View File

@ -146,7 +146,7 @@
"StressTest",
"EraseBasic"
],
"date": "2018-07-11T10:28:55.879745",
"endTime": "2018-07-11T10:28:55.879745",
"type": "Snapshot",
"elapsed": 3886,
"closed": false

View File

@ -128,7 +128,7 @@
"version": "11.0a2",
"type": "Snapshot",
"software": "Workbench",
"date": "2018-07-03T09:10:57.034598",
"endTime": "2018-07-03T09:10:57.034598",
"device": {
"type": "Laptop",
"model": "1001PXD",

View File

@ -4,7 +4,7 @@
"uuid": "0c822fb7-6e51-4781-86cf-994bd306212e",
"software": "Workbench",
"closed": false,
"date": "2018-07-05T11:57:17.284891",
"endTime": "2018-07-05T11:57:17.284891",
"components": [
{
"type": "NetworkAdapter",

View File

@ -157,5 +157,5 @@
"model": "HP Compaq 8100 Elite SFF"
},
"type": "Snapshot",
"date": "2018-06-29T12:28:54.508266"
"endTime": "2018-06-29T12:28:54.508266"
}

View File

@ -4,7 +4,7 @@
"Benchmark",
"StressTest"
],
"date": "2018-06-29T15:29:29.322424",
"endTime": "2018-06-29T15:29:29.322424",
"elapsed": 391,
"software": "Workbench",
"components": [

View File

@ -158,7 +158,7 @@
"serialNumber": "CZC0408YJG"
}
],
"date": "2018-07-11T16:11:43.467824",
"endTime": "2018-07-11T16:11:43.467824",
"version": "11.0a3",
"software": "Workbench",
"type": "Snapshot",

View File

@ -29,19 +29,19 @@ device:
elapsed: 300
error: False
components:
- type: GraphicCard
- type: GraphicCard
serialNumber: gc1-1s
model: gc1-1ml
manufacturer: gc1-1mr
- type: RamModule
- type: RamModule
serialNumber: rm1-1s
model: rm1-1ml
manufacturer: rm1-1mr
- type: RamModule
- type: RamModule
serialNumber: rm2-1s
model: rm2-1ml
manufacturer: rm2-1mr
- type: Processor
- type: Processor
model: p1-1s
manufacturer: p1-1mr
events:
@ -49,7 +49,7 @@ components:
rate: 2410
- type: BenchmarkProcessorSysbench
rate: 4400
- type: SolidStateDrive
- type: SolidStateDrive
serialNumber: ssd1-1s
model: ssd1-1ml
manufacturer: ssd1-1mr
@ -83,7 +83,7 @@ components:
startTime: 2018-01-01T10:10:10
endTime: 2018-01-01T12:10:10
error: False
- type: HardDrive
- type: HardDrive
serialNumber: hdd1-1s
model: hdd1-1ml
manufacturer: hdd1-1mr
@ -105,7 +105,7 @@ components:
elapsed: 420
error: False
name: LinuxMint 18.01 32b
- type: Motherboard
- type: Motherboard
serialNumber: mb1-1s
model: mb1-1ml
manufacturer: mb1-1mr

View File

@ -9,6 +9,6 @@ class NestedOn(TealNestedOn):
__doc__ = TealNestedOn.__doc__
def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, collection_class=list,
default=missing_, exclude=tuple(), only=None, **kwargs):
super().__init__(nested, polymorphic_on, db, collection_class, default, exclude, only,
**kwargs)
default=missing_, exclude=tuple(), only_query: str = None, only=None, **kwargs):
super().__init__(nested, polymorphic_on, db, collection_class, default, exclude,
only_query, only, **kwargs)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,23 +1,23 @@
from contextlib import suppress
from itertools import chain
from operator import attrgetter
from typing import Dict, Set
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 itertools import chain
from sqlalchemy import BigInteger, Column, Enum as DBEnum, Float, ForeignKey, Integer, Sequence, \
SmallInteger, Unicode, inspect, Boolean
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
Sequence, SmallInteger, Unicode, inspect
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
from sqlalchemy.util import OrderedSet
from sqlalchemy_utils import ColorType
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.marshmallow import ValidationError
from ereuse_utils.naming import Naming
class Device(Thing):
"""

View File

@ -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.resources.device import models as m
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
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

View File

@ -195,3 +195,15 @@ class ComputerChassis(Enum):
Detachable = 'Detachable'
Tablet = 'Tablet'
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.'

View File

@ -94,9 +94,8 @@ class InstallDef(EventDef):
class SnapshotDef(EventDef):
VIEW = None
SCHEMA = schemas.Snapshot
VIEW = SnapshotView
SCHEMA = schemas.Snapshot
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,
@ -161,6 +160,11 @@ class RepairDef(EventDef):
SCHEMA = schemas.Repair
class ReadyToUse(EventDef):
VIEW = None
SCHEMA = schemas.ReadyToUse
class ToPrepareDef(EventDef):
VIEW = None
SCHEMA = schemas.ToPrepare
@ -171,11 +175,61 @@ class PrepareDef(EventDef):
SCHEMA = schemas.Prepare
class ToDisposeDef(EventDef):
class LiveDef(EventDef):
VIEW = None
SCHEMA = schemas.ToDispose
SCHEMA = schemas.Live
class DisposeDef(EventDef):
class ReserveDef(EventDef):
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

View File

@ -1,11 +1,11 @@
from collections import Iterable
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Set, Union
from uuid import uuid4
from flask import current_app as app, g
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.ext.declarative import declared_attr
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 ereuse_devicehub.db import db
from ereuse_devicehub.resources.agent.models import Agent
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
Device, Laptop, Server
from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \
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.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User
from teal.currency import Currency
from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \
POLYMORPHIC_ON, StrictVersionType, check_range
from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, IP, POLYMORPHIC_ID, \
POLYMORPHIC_ON, StrictVersionType, URL, check_range
from teal.enums import Country, Currency, Subdivision
from teal.marshmallow import ValidationError
"""
A quantity of money with a currency.
"""
class JoinedTableMixin:
@ -36,11 +42,11 @@ class JoinedTableMixin:
class Event(Thing):
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.comment = """
A name or title for the event. Used when searching for events.
"""
type = Column(Unicode)
incidence = Column(Boolean, default=False, nullable=False)
incidence.comment = """
Should this event be reviewed due some anomaly?
@ -61,13 +67,21 @@ class Event(Thing):
description.comment = """
A comment about the event.
"""
date = Column(DateTime)
date.comment = """
When this event happened.
Leave it blank if it is happening now
(the field ``created`` is used instead).
This is used for example when creating events retroactively.
start_time = Column(DateTime)
start_time.comment = """
When the action starts. For some actions like reservations
the time when they are available, for others like renting
when the renting starts.
"""
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',
use_alter=True,
name='snapshot_events'))
@ -82,9 +96,36 @@ class Event(Thing):
ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
# todo compute the org
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)
"""
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,
backref=backref('events_components',
lazy=True,
@ -93,7 +134,7 @@ class Event(Thing):
secondary=lambda: EventComponent.__table__,
order_by=lambda: Component.id,
collection_class=OrderedSet)
"""
components.comment = """
The components that are affected by the event.
When performing events to parent devices their components are
@ -138,13 +179,32 @@ class Event(Thing):
args[INHERIT_COND] = cls.id == Event.id
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):
device_id = Column(BigInteger, ForeignKey(Component.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 = relationship(Device,
backref=backref('events_one',
@ -198,9 +258,7 @@ class Deallocate(JoinedTableMixin, EventWithMultipleDevices):
organization = Column(Unicode(STR_SIZE))
class EraseBasic(JoinedTableMixin, EventWithOneDevice):
start_time = Column(DateTime, nullable=False)
end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False)
class EraseBasic(JoinedWithOneDeviceMixin, EventWithOneDevice):
zeros = Column(Boolean, nullable=False)
zeros.comment = """
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):
pass
@ -222,7 +276,9 @@ class Step(db.Model):
num = Column(SmallInteger, primary_key=True)
error = Column(Boolean, default=False, 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.comment = Event.end_time.comment
erasure = relationship(EraseBasic,
backref=backref('steps',
@ -254,7 +310,7 @@ class StepRandom(Step):
pass
class Snapshot(JoinedTableMixin, EventWithOneDevice):
class Snapshot(JoinedWithOneDeviceMixin, EventWithOneDevice):
uuid = Column(UUID(as_uuid=True), unique=True)
version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
software = Column(DBEnum(SnapshotSoftware), nullable=False)
@ -266,7 +322,7 @@ class Snapshot(JoinedTableMixin, EventWithOneDevice):
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
class Install(JoinedTableMixin, EventWithOneDevice):
class Install(JoinedWithOneDeviceMixin, EventWithOneDevice):
elapsed = Column(Interval, nullable=False)
@ -280,7 +336,7 @@ class SnapshotRequest(db.Model):
cascade=CASCADE_OWN))
class Rate(JoinedTableMixin, EventWithOneDevice):
class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
software = Column(DBEnum(RatingSoftware))
version = Column(StrictVersionType)
@ -401,9 +457,9 @@ class PhotoboxSystemRate(PhotoboxRate):
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)
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))
version = Column(StrictVersionType)
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)
class Test(JoinedTableMixin, EventWithOneDevice):
class Test(JoinedWithOneDeviceMixin, EventWithOneDevice):
elapsed = Column(Interval, nullable=False)
@declared_attr
@ -536,11 +592,14 @@ class StressTest(Test):
pass
@validates('elapsed')
def bigger_than_a_minute(self, _, value: timedelta):
assert value.total_seconds() >= 60
def is_minute_and_bigger_than_1_minute(self, _, value: timedelta):
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)
@declared_attr
@ -589,6 +648,10 @@ class Repair(EventWithMultipleDevices):
pass
class ReadyToUse(EventWithMultipleDevices):
pass
class ToPrepare(EventWithMultipleDevices):
pass
@ -597,11 +660,124 @@ class Prepare(EventWithMultipleDevices):
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
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

View File

@ -1,25 +1,31 @@
import ipaddress
from datetime import datetime, timedelta
from decimal import Decimal
from distutils.version import StrictVersion
from typing import Dict, List, Set
from typing import Dict, List, Set, Union
from uuid import UUID
from boltons.urlutils import URL
from sqlalchemy import Column
from sqlalchemy.orm import relationship
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.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.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.enums import Country
class Event(Thing):
id = ... # type: Column
name = ... # type: Column
date = ... # type: Column
type = ... # type: Column
error = ... # type: Column
incidence = ... # type: Column
@ -28,14 +34,19 @@ class Event(Thing):
snapshot_id = ... # type: Column
snapshot = ... # type: relationship
author_id = ... # type: Column
author = ... # type: relationship
agent = ... # type: relationship
components = ... # type: relationship
parent_id = ... # type: Column
parent = ... # type: relationship
closed = ... # type: Column
start_time = ... # type: Column
end_time = ... # type: Column
agent_id = ... # type: Column
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) -> None:
super().__init__(created, updated)
self.id = ... # type: UUID
self.name = ... # type: str
self.type = ... # type: str
@ -43,26 +54,32 @@ class Event(Thing):
self.closed = ... # type: bool
self.error = ... # type: bool
self.description = ... # type: str
self.date = ... # type: datetime
self.snapshot_id = ... # type: UUID
self.start_time = ... # type: datetime
self.end_time = ... # type: datetime
self.snapshot = ... # type: Snapshot
self.author_id = ... # type: UUID
self.author = ... # type: User
self.components = ... # type: Set[Component]
self.parent_id = ... # type: Computer
self.parent = ... # type: Computer
self.agent = ... # type: Agent
self.author = ... # type: User
class EventWithOneDevice(Event):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.device_id = ... # type: int
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, 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
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]
@ -75,14 +92,15 @@ class Remove(EventWithOneDevice):
class Step(Model):
def __init__(self, **kwargs) -> None:
self.erasure_id = ... # type: UUID
def __init__(self, num=None, success=None, start_time=None, end_time=None,
erasure=None, error=None) -> None:
self.type = ... # type: str
self.num = ... # type: int
self.success = ... # type: bool
self.start_time = ... # type: datetime
self.end_time = ... # type: datetime
self.erasure = ... # type: EraseBasic
self.error = ... # type: bool
class StepZero(Step):
@ -174,7 +192,6 @@ class PhotoboxRate(IndividualRate):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.num = ... # type: int
self.image_id = ... # type: UUID
self.image = ... # type: Image
@ -205,11 +222,10 @@ class Price(EventWithOneDevice):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.price = ... # type: Decimal
self.currency = ... # type: Currency
self.price = ... # type: float
self.software = ... # type: PriceSoftware
self.version = ... # type: StrictVersion
self.rating_id = ... # type: UUID
self.rating = ... # type: AggregateRate
@ -260,10 +276,6 @@ class EraseBasic(EventWithOneDevice):
self.success = ... # type: bool
class Ready(EventWithMultipleDevices):
pass
class EraseSectors(EraseBasic):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
@ -311,6 +323,10 @@ class Repair(EventWithMultipleDevices):
pass
class ReadyToUse(EventWithMultipleDevices):
pass
class ToPrepare(EventWithMultipleDevices):
pass
@ -319,9 +335,96 @@ class Prepare(EventWithMultipleDevices):
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
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

View File

@ -1,43 +1,49 @@
import decimal
from flask import current_app as app
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \
UUID
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \
TimeDelta, URL, UUID
from marshmallow.validate import Length, Range
from sqlalchemy.util import OrderedSet
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.enums import AppearanceRange, Bios, FunctionalityRange, \
PriceSoftware, RATE_POSITIVE, RatingSoftware, SnapshotExpectedEvents, SnapshotSoftware, \
TestHardDriveLength
PriceSoftware, RATE_POSITIVE, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, \
SnapshotSoftware, TestHardDriveLength
from ereuse_devicehub.resources.event import models as m
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.user.schemas import User
from teal.currency import Currency
from teal.marshmallow import EnumField, Version
from teal.enums import Country, Currency, Subdivision
from teal.marshmallow import EnumField, IP, Version
from teal.resource import Schema
class Event(Thing):
id = UUID(dump_only=True)
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)
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)
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)
class EventWithOneDevice(Event):
device = NestedOn(Device)
device = NestedOn(Device, only_query='id')
class EventWithMultipleDevices(Event):
devices = NestedOn(Device, many=True)
devices = NestedOn(Device, many=True, only_query='id', collection_class=OrderedSet)
class Add(EventWithOneDevice):
@ -51,7 +57,7 @@ class Remove(EventWithOneDevice):
class Allocate(EventWithMultipleDevices):
to = NestedOn(User,
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.')
@ -59,13 +65,11 @@ class Deallocate(EventWithMultipleDevices):
from_rel = Nested(User,
data_key='from',
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.')
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)
steps = NestedOn('Step', many=True, required=True)
@ -161,7 +165,7 @@ class WorkbenchRate(ManualRate):
class Price(EventWithOneDevice):
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)
version = Version(dump_only=True)
rating = NestedOn(AggregateRate, dump_only=True)
@ -208,7 +212,6 @@ class Snapshot(EventWithOneDevice):
'are performed. Setting this value will activate'
'the async Snapshot.')
device = NestedOn(Device)
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
components = NestedOn(Component,
many=True,
@ -309,6 +312,10 @@ class Repair(EventWithMultipleDevices):
pass
class ReadyToUse(EventWithMultipleDevices):
pass
class ToPrepare(EventWithMultipleDevices):
pass
@ -317,9 +324,73 @@ class Prepare(EventWithMultipleDevices):
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
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

View File

@ -3,7 +3,7 @@ from distutils.version import StrictVersion
from typing import List
from uuid import UUID
from flask import request
from flask import current_app as app, request
from sqlalchemy.util import OrderedSet
from ereuse_devicehub.db import db
@ -14,6 +14,18 @@ from teal.resource import 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):
"""Gets one event."""
event = Event.query.filter_by(id=id).one()

View File

@ -1,5 +1,3 @@
from datetime import datetime
from sqlalchemy import Column
from teal.db import Model
@ -15,8 +13,3 @@ class Thing(Model):
type = ... # type: str
updated = ... # type: Column
created = ... # type: Column
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.updated = ... # type: datetime
self.created = ... # type: datetime

View File

@ -2,9 +2,9 @@ from sqlalchemy import BigInteger, Column, ForeignKey, Unicode, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
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.models import Thing
from ereuse_devicehub.resources.user.models import Organization
from teal.db import DB_CASCADE_SET_NULL, URL
from teal.marshmallow import ValidationError

View File

@ -5,7 +5,7 @@ from webargs.flaskparser import parser
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.tag import Tag
from teal.marshmallow import ValidationError
from teal.resource import View, Schema
from teal.resource import Schema, View
class TagView(View):

View File

@ -2,9 +2,8 @@ from click import argument, option
from ereuse_devicehub.db import db
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 teal.db import SQLAlchemy
from teal.resource import Converters, Resource
@ -34,35 +33,3 @@ class UserDef(Resource):
db.session.add(user)
db.session.commit()
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)

View File

@ -1,11 +1,11 @@
from uuid import uuid4
from flask import current_app as app, g
from sqlalchemy import Column, Unicode, UniqueConstraint
from flask import current_app as app
from sqlalchemy import Column
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):
@ -22,32 +22,12 @@ class User(Thing):
From `here <https://sqlalchemy-utils.readthedocs.io/en/latest/
data_types.html#module-sqlalchemy_utils.types.password>`_
"""
name = Column(Unicode(length=STR_SIZE))
token = Column(UUID(as_uuid=True), default=uuid4, unique=True)
def __repr__(self) -> str:
return '<{0.t} {0.id} email={0.email}>'.format(self)
return '<User {0.email}>'.format(self)
class Organization(Thing):
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
name = Column(Unicode(length=STR_SM_SIZE), unique=True)
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)
@property
def individual(self):
"""The individual associated for this database, or None."""
return next(iter(self.individuals), None)

View File

@ -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

View File

@ -3,6 +3,8 @@ from base64 import b64encode
from marshmallow import post_dump
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
@ -10,6 +12,7 @@ class User(Thing):
id = UUID(dump_only=True)
email = Email(required=True)
password = String(load_only=True, required=True)
individuals = NestedOn(Individual, many=True, dump_only=True)
name = String()
token = String(dump_only=True,
description='Use this token in an Authorization header to access the app.'

View File

@ -35,7 +35,7 @@ setup(
long_description=long_description,
long_description_content_type='text/markdown',
install_requires=[
'teal>=0.2.0a9',
'teal>=0.2.0a11',
'marshmallow_enum',
'ereuse-utils[Naming]>=0.4b1',
'psycopg2-binary',
@ -44,7 +44,7 @@ setup(
'hashids',
'click',
'click-spinner',
'sqlalchemy-utils[password, color, babel]',
'sqlalchemy-utils[password, color, phone]',
'PyYAML',
'python-stdnum',
'ereuse-rate==0.0.2'
@ -53,7 +53,7 @@ setup(
'docs': [
'sphinx',
'sphinxcontrib-httpdomain >= 1.5.0',
'sphinxcontrib-plantuml >= 0.11',
'sphinxcontrib-plantuml >= 0.12',
'sphinxcontrib-websupport >= 1.0.1'
],
'test': test_requires

View File

@ -8,6 +8,7 @@ from ereuse_devicehub.client import Client, UserClient
from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.db import db
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.user.models import User
@ -66,16 +67,14 @@ def user(app: Devicehub) -> UserClient:
with app.app_context():
password = 'foo'
user = create_user(password=password)
client = UserClient(application=app,
response_wrapper=app.response_class,
email=user.email,
password=password)
client.user, _ = client.login(client.email, client.password)
client = UserClient(app, user.email, password, response_wrapper=app.response_class)
client.login()
return client
def create_user(email='foo@foo.com', password='foo') -> User:
user = User(email=email, password=password)
user.individuals.add(Person(name='Timmy'))
db.session.add(user)
db.session.commit()
return user

View File

@ -5,16 +5,16 @@ device:
type: Desktop
chassis: Tower
components:
- manufacturer: p1c1m
- manufacturer: p1c1m
serialNumber: p1c1s
type: Motherboard
- manufacturer: p1c2m
- manufacturer: p1c2m
serialNumber: p1c2s
model: p1c2
speed: 1.23
cores: 2
type: Processor
- manufacturer: p1c3m
- manufacturer: p1c3m
serialNumber: p1c3s
type: GraphicCard
memory: 1.5

View File

@ -5,10 +5,10 @@ device:
type: Desktop
chassis: Microtower
components:
- manufacturer: p2c1m
- manufacturer: p2c1m
serialNumber: p2c1s
type: Motherboard
- manufacturer: p1c2m
- manufacturer: p1c2m
serialNumber: p1c2s
model: p1c2
speed: 1.23

View File

@ -5,13 +5,13 @@ device:
type: Desktop
chassis: Microtower
components:
- manufacturer: p1c2m
- manufacturer: p1c2m
serialNumber: p1c2s
model: p1c2
type: Processor
cores: 2
speed: 1.23
- manufacturer: p1c3m
- manufacturer: p1c3m
serialNumber: p1c3s
type: GraphicCard
memory: 1.5

View File

@ -5,12 +5,12 @@ device:
type: Desktop
chassis: Tower
components:
- manufacturer: p1c4m
- manufacturer: p1c4m
serialNumber: p1c4s
type: NetworkAdapter
speed: 1000
wireless: False
- manufacturer: p1c3m
- manufacturer: p1c3m
serialNumber: p1c3s
type: GraphicCard
memory: 1.5

View File

@ -16,16 +16,16 @@ device:
labelling: True
bios: B
components:
- type: GraphicCard
- type: GraphicCard
serialNumber: gc1s
model: gc1ml
manufacturer: gc1mr
- type: RamModule
- type: RamModule
serialNumber: rm1s
model: rm1ml
manufacturer: rm1mr
speed: 1333
- type: Processor
- type: Processor
serialNumber: p1s
model: p1ml
manufacturer: p1mr

View File

@ -152,5 +152,5 @@
"serialNumber": "109192430003459"
}
],
"date": "2018-07-13T10:48:36.738398"
"endTime": "2018-07-13T10:48:36.738398"
}

View File

@ -10,7 +10,7 @@ device:
model: pc1ml
manufacturer: pc1mr
components:
- type: SolidStateDrive
- type: SolidStateDrive
serialNumber: c1s
model: c1ml
manufacturer: c1mr
@ -28,11 +28,11 @@ components:
error: False
startTime: 2018-06-01T08:16:00
endTime: 2018-06-01T09:17:00
- type: Processor
- type: Processor
serialNumber: p1s
model: p1ml
manufacturer: p1mr
- type: RamModule
- type: RamModule
serialNumber: rm1s
model: rm1ml
manufacturer: rm1mr

View File

@ -5,11 +5,11 @@ device:
model: d1ml
manufacturer: d1mr
components:
- type: GraphicCard
- type: GraphicCard
serial_number: gc1s
model: gc1ml
manufacturer: gc1mr
- type: RamModule
- type: RamModule
serial_number: rm1s
model: rm1ml
manufacturer: rm1mr

View File

@ -123,6 +123,6 @@
},
"type": "Snapshot",
"software": "Workbench",
"date": "2018-07-19T15:48:40.635776",
"endTime": "2018-07-19T15:48:40.635776",
"closed": false
}

View File

@ -128,7 +128,7 @@
"version": "11.0a2",
"type": "Snapshot",
"software": "Workbench",
"date": "2018-07-03T09:10:57.034598",
"endTime": "2018-07-03T09:10:57.034598",
"device": {
"type": "Laptop",
"model": "1001PXD",

View File

@ -4,7 +4,7 @@
"uuid": "0c822fb7-6e51-4781-86cf-994bd306212e",
"software": "Workbench",
"closed": false,
"date": "2018-07-05T11:57:17.284891",
"endTime": "2018-07-05T11:57:17.284891",
"components": [
{
"type": "NetworkAdapter",

View File

@ -157,5 +157,5 @@
"model": "HP Compaq 8100 Elite SFF"
},
"type": "Snapshot",
"date": "2018-06-29T12:28:54.508266"
"endTime": "2018-06-29T12:28:54.508266"
}

View File

@ -4,7 +4,7 @@
"Benchmark",
"StressTest"
],
"date": "2018-06-29T15:29:29.322424",
"endTime": "2018-06-29T15:29:29.322424",
"elapsed": 391,
"software": "Workbench",
"components": [

View File

@ -32,7 +32,7 @@ components:
passedLifetime: 16947, powerCycleCount: 1694, reallocatedSectorCount: 0, reportedUncorrectableErrors: 0,
status: Completed without error, type: Short offline}
type: HDD
- {'@type': GraphicCard, manufacturer: Intel Corporation, memory: 256.0, model: 4
- { '@type': GraphicCard, manufacturer: Intel Corporation, memory: 256.0, model: 4
Series Chipset Integrated Graphics Controller, serialNumber: null}
- '@type': Motherboard
connectors: {firewire: 0, pcmcia: 0, serial: 1, usb: 8}
@ -41,20 +41,20 @@ components:
serialNumber: null
totalSlots: 0
usedSlots: 2
- {'@type': NetworkAdapter, manufacturer: Intel Corporation, model: 82567LM-3 Gigabit
- { '@type': NetworkAdapter, manufacturer: Intel Corporation, model: 82567LM-3 Gigabit
Network Connection, serialNumber: '00:21:86:2c:5e:d6', speed: 1000}
- {'@type': SoundCard, manufacturer: Intel Corporation, model: 82801JD/DO HD Audio
- { '@type': SoundCard, manufacturer: Intel Corporation, model: 82801JD/DO HD Audio
Controller, serialNumber: null}
condition:
appearance: {general: B}
functionality: {general: A}
date: '2018-05-09T10:32:15'
debug:
capabilities: {dmi-2.5: DMI version 2.5, smbios-2.5: SMBIOS version 2.5, smp: Symmetric
capabilities: { dmi-2.5: DMI version 2.5, smbios-2.5: SMBIOS version 2.5, smp: Symmetric
Multi-Processing, smp-1.4: SMP specification v1.4}
children:
- children:
- capabilities: {acpi: ACPI, biosbootspecification: BIOS boot specification, cdboot: Booting
- capabilities: { acpi: ACPI, biosbootspecification: BIOS boot specification, cdboot: Booting
from CD-ROM/DVD, edd: Enhanced Disk Drive extensions, escd: ESCD, ls120boot: Booting
from LS-120, pci: PCI bus, pnp: Plug-and-Play, shadowing: BIOS shadowing,
smartbattery: Smart battery, upgrade: BIOS EEPROM can be upgraded, usb: USB
@ -71,7 +71,7 @@ debug:
vendor: LENOVO
version: 5CKT48AUS
- businfo: cpu@0
capabilities: {acpi: thermal control (ACPI), aperfmperf: true, apic: on-chip
capabilities: { acpi: thermal control (ACPI), aperfmperf: true, apic: on-chip
advanced programmable interrupt controller (APIC), arch_perfmon: true, boot: boot
processor, bts: true, clflush: true, cmov: conditional move instruction,
constant_tsc: true, cpufreq: CPU Frequency scaling, cx16: true, cx8: compare
@ -149,7 +149,7 @@ debug:
version: 6.7.10
width: 64
- children:
- {claimed: true, class: memory, clock: 1067000000, description: DIMM DDR2 Synchronous
- { claimed: true, class: memory, clock: 1067000000, description: DIMM DDR2 Synchronous
1067 MHz (0.9 ns), handle: 'DMI:001F', id: 'bank:0', physid: '0', product: '000000000000000000000000000000000000',
serial: '00000000', size: 2147483648, slot: J6G1, units: bytes, vendor: Unknown,
width: 40960}
@ -157,7 +157,7 @@ debug:
Synchronous 1067 MHz (0.9 ns) [empty]', handle: 'DMI:0020', id: 'bank:1',
physid: '1', product: 012345678901234567890123456789012345, serial: '01234567',
slot: J6G2, vendor: 48spaces}
- {claimed: true, class: memory, clock: 1067000000, description: DIMM DDR2 Synchronous
- { claimed: true, class: memory, clock: 1067000000, description: DIMM DDR2 Synchronous
1067 MHz (0.9 ns), handle: 'DMI:0021', id: 'bank:2', physid: '2', product: '000000000000000000000000000000000000',
serial: '00000000', size: 2147483648, slot: J6H1, units: bytes, vendor: Unknown,
width: 41984}
@ -205,7 +205,7 @@ debug:
- businfo: pci@0000:00:00.0
children:
- businfo: pci@0000:00:02.0
capabilities: {bus_master: bus mastering, cap_list: PCI capabilities listing,
capabilities: { bus_master: bus mastering, cap_list: PCI capabilities listing,
msi: Message Signalled Interrupts, pm: Power Management, rom: extension
ROM, vga_controller: true}
claimed: true
@ -265,7 +265,7 @@ debug:
version: '03'
width: 32
- businfo: pci@0000:00:03.3
capabilities: {'16550': true, bus_master: bus mastering, cap_list: PCI capabilities
capabilities: { '16550': true, bus_master: bus mastering, cap_list: PCI capabilities
listing, msi: Message Signalled Interrupts, pm: Power Management}
claimed: true
class: communication
@ -280,7 +280,7 @@ debug:
version: '03'
width: 32
- businfo: pci@0000:00:19.0
capabilities: {1000bt-fd: 1Gbit/s (full duplex), 100bt: 100Mbit/s, 100bt-fd: 100Mbit/s
capabilities: { 1000bt-fd: 1Gbit/s (full duplex), 100bt: 100Mbit/s, 100bt-fd: 100Mbit/s
(full duplex), 10bt: 10Mbit/s, 10bt-fd: 10Mbit/s (full duplex), autonegotiation: Auto-negotiation,
bus_master: bus mastering, cap_list: PCI capabilities listing, ethernet: true,
msi: Message Signalled Interrupts, physical: Physical interface, pm: Power
@ -575,7 +575,7 @@ debug:
version: '02'
width: 32
- businfo: pci@0000:00:1f.2
capabilities: {ahci_1.0: true, bus_master: bus mastering, cap_list: PCI capabilities
capabilities: { ahci_1.0: true, bus_master: bus mastering, cap_list: PCI capabilities
listing, msi: Message Signalled Interrupts, pm: Power Management, storage: true}
claimed: true
class: storage
@ -620,7 +620,7 @@ debug:
table}
children:
- businfo: scsi@0:0.0.0,1
capabilities: {dir_nlink: directories with 65000+ subdirs, ext2: EXT2/EXT3,
capabilities: { dir_nlink: directories with 65000+ subdirs, ext2: EXT2/EXT3,
ext4: true, extended_attributes: Extended Attributes, extents: extent-based
allocation, huge_files: 16TB+ files, initialized: initialized volume,
journaled: true, large_files: 4GB+ files, primary: Primary partition}
@ -672,7 +672,7 @@ debug:
- capabilities: {emulated: Emulated device}
children:
- businfo: scsi@1:0.0.0
capabilities: {audio: Audio CD playback, cd-r: CD-R burning, cd-rw: CD-RW
capabilities: { audio: Audio CD playback, cd-r: CD-R burning, cd-rw: CD-RW
burning, dvd: DVD playback, dvd-r: DVD-R burning, dvd-ram: DVD-RAM burning,
removable: support is removable}
claimed: true

View File

@ -27,19 +27,19 @@ device:
- type: BenchmarkRamSysbench
rate: 2444
components:
- type: GraphicCard
- type: GraphicCard
serialNumber: gc1-1s
model: gc1-1ml
manufacturer: gc1-1mr
- type: RamModule
- type: RamModule
serialNumber: rm1-1s
model: rm1-1ml
manufacturer: rm1-1mr
- type: RamModule
- type: RamModule
serialNumber: rm2-1s
model: rm2-1ml
manufacturer: rm2-1mr
- type: Processor
- type: Processor
model: p1-1s
manufacturer: p1-1mr
events:
@ -47,7 +47,7 @@ components:
rate: 2410
- type: BenchmarkProcessorSysbench
rate: 4400
- type: SolidStateDrive
- type: SolidStateDrive
serialNumber: ssd1-1s
model: ssd1-1ml
manufacturer: ssd1-1mr
@ -71,7 +71,7 @@ components:
currentPendingSectorCount: 1
offlineUncorrectable: 33
remainingLifetimePercentage: 1
- type: HardDrive
- type: HardDrive
serialNumber: hdd1-1s
model: hdd1-1ml
manufacturer: hdd1-1mr
@ -79,7 +79,7 @@ components:
- type: BenchmarkDataStorage
readSpeed: 10
writeSpeed: 5
- type: Motherboard
- type: Motherboard
serialNumber: mb1-1s
model: mb1-1ml
manufacturer: mb1-1mr

View File

@ -14,7 +14,7 @@ zeros: False
startTime: 2018-01-01T10:10:10
endTime: 2018-01-01T12:10:10
steps:
- type: 'StepRandom'
- type: 'StepRandom'
startTime: '2018-01-01T10:10:10'
endTime: '2018-01-01T12:10:10'
error: False

129
tests/test_agent.py Normal file
View File

@ -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'

View File

@ -35,4 +35,4 @@ def test_api_docs(client: Client):
'scheme': 'basic',
'name': 'Authorization'
}
assert 60 == len(docs['definitions'])
assert 76 == len(docs['definitions'])

View File

@ -9,6 +9,7 @@ from sqlalchemy.util import OrderedSet
from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db
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.models import Component, ComputerMonitor, Desktop, Device, \
GraphicCard, Laptop, Motherboard, NetworkAdapter
@ -369,6 +370,7 @@ def test_get_device(app: Devicehub, user: UserClient):
db.session.add(Test(device=pc,
elapsed=timedelta(seconds=4),
error=False,
agent=Person(name='Timmy'),
author=User(email='bar@bar.com')))
db.session.commit()
pc, _ = user.get(res=Device, item=1)

View File

@ -1,19 +1,22 @@
import ipaddress
from datetime import datetime, timedelta
import pytest
from flask import g
from flask import current_app as app, g
from sqlalchemy.util import OrderedSet
from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \
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 teal.enums import Currency, Subdivision
from tests import conftest
from tests.conftest import create_user, file
@pytest.mark.usefixtures('app_context')
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_author():
"""
Checks the default created author.
@ -30,7 +33,7 @@ def test_author():
assert e.author == user
@pytest.mark.usefixtures('auth_app_context')
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_erase_basic():
erasure = models.EraseBasic(
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
@pytest.mark.usefixtures('auth_app_context')
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_validate_device_data_storage():
"""Checks the validation for data-storage-only events works."""
# 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():
erasure = models.EraseSectors(
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
@pytest.mark.usefixtures('auth_app_context')
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_test_data_storage():
test = models.TestDataStorage(
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
@ -106,7 +109,7 @@ def test_test_data_storage():
assert models.TestDataStorage.query.one()
@pytest.mark.usefixtures('auth_app_context')
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_install():
hdd = HardDrive(serial_number='sn')
install = models.Install(name='LinuxMint 18.04 es',
@ -116,7 +119,7 @@ def test_install():
db.session.commit()
@pytest.mark.usefixtures('auth_app_context')
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_update_components_event_one():
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
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
@pytest.mark.usefixtures('auth_app_context')
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_update_components_event_multiple():
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
computer.components.add(hdd)
ready = models.Ready()
ready = models.ReadyToUse()
assert not ready.devices
assert not ready.components
@ -167,7 +170,7 @@ def test_update_components_event_multiple():
assert ready.components
@pytest.mark.usefixtures('auth_app_context')
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_update_parent():
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1')
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
@ -184,21 +187,96 @@ def test_update_parent():
assert not benchmark.parent
@pytest.mark.xfail(reason='No POST view for generic tests')
@pytest.mark.parametrize('event_model', [
models.ToRepair,
models.Repair,
models.ToPrepare,
models.ReadyToUse,
models.ToPrepare,
models.Prepare,
models.ToDispose,
models.Dispose,
models.Ready
])
def test_generic_event(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']]}
event, _ = user.post(event, res=event_model)
assert event['device'][0]['id'] == snapshot['device']['id']
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'][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'

View File

@ -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)

View File

@ -1,6 +0,0 @@
import pytest
@pytest.mark.xfail(reason='Just needs to do the test')
def test_price_no_data_storage():
pass

View File

@ -4,6 +4,7 @@ from typing import List, Tuple
from uuid import uuid4
import pytest
from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub
@ -28,7 +29,7 @@ def test_snapshot_model():
device = m.Desktop(serial_number='a1', chassis=ComputerChassis.Tower)
# noinspection PyArgumentList
snapshot = Snapshot(uuid=uuid4(),
date=datetime.now(),
end_time=datetime.now(),
version='1.0',
software=SnapshotSoftware.DesktopApp,
elapsed=timedelta(seconds=25))

View File

@ -5,11 +5,11 @@ from sqlalchemy.exc import IntegrityError
from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db
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.enums import ComputerChassis
from ereuse_devicehub.resources.tag import Tag
from ereuse_devicehub.resources.tag.view import CannotCreateETag, TagNotLinked
from ereuse_devicehub.resources.user import Organization
from teal.db import MultipleResourcesFound, ResourceNotFound
from teal.marshmallow import ValidationError

View File

@ -1,6 +1,7 @@
from base64 import b64decode
from uuid import UUID
import pytest
from sqlalchemy_utils import Password
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.models import User
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):
"""
Tests creating an user through the main method.
This method checks that the token is correct, too.
"""
with app.app_context():
user_def = app.resources['User'] # type: UserDef
u = user_def.create_user(email='foo@foo.com', password='foo')
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
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."""
with app.app_context():
user = User(email='FOO@foo.com')
db.session.add(user)
db.session.commit()
@ -41,9 +42,9 @@ def test_create_user_email_insensitive(app: Devicehub):
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."""
with app.app_context():
user = create_user()
assert isinstance(user.password, Password)
assert user.password == 'foo'
@ -66,6 +67,9 @@ def test_login_success(client: Client, app: Devicehub):
assert user['email'] == 'foo@foo.com'
assert UUID(b64decode(user['token'].encode()).decode()[:-1])
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):

View File

@ -3,12 +3,12 @@ Tests that emulates the behaviour of a WorkbenchServer.
"""
import pytest
from ereuse_devicehub.client import UserClient
from ereuse_devicehub.resources.device.exceptions import NeedsId
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.event import models as em
from ereuse_devicehub.resources.tag.model import Tag
from tests.conftest import file