Complete events and agents. Bump to 0.2.0a11.
This commit is contained in:
parent
8efca0d589
commit
42b0b0ebbc
|
@ -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
|
|
@ -0,0 +1,443 @@
|
|||
Actions and states
|
||||
##################
|
||||
|
||||
Actions are events performed to devices, changing their **state**.
|
||||
Actions can have attributes defining
|
||||
**where** it happened, **who** performed them, **when**, etc.
|
||||
Actions are stored in a log for each device. An exemplifying action
|
||||
can be ``Repair``, which dictates that a device has been repaired,
|
||||
after this action, the device is in the ``repaired`` state.
|
||||
|
||||
Actions and states affect devices in different ways or **dimensions**.
|
||||
For example, ``Repair`` affects the **physical** dimension of a device,
|
||||
and ``Sell`` the **political** dimension of a device. A device
|
||||
can be in several states at the same time, one per dimension; ie. a
|
||||
device can be ``repaired`` (physical) and ``reserved`` (political),
|
||||
but not ``repaired`` and ``disposed`` at the same time.
|
||||
|
||||
Devicehub actions inherit from `schema actions
|
||||
<http://schema.org/Action>`_, are written in Pascal case and using
|
||||
a verb in infinitive. Some verbs represent the willingness or
|
||||
assignment to perform an action; ``ToRepair`` states that the device
|
||||
is going to be / must be repaired, whereas ``Repair`` states
|
||||
that the reparation happened. The former actions have the preposition
|
||||
*To* prefixing the verb.
|
||||
|
||||
In the following section we define the actions and states.
|
||||
To see how to perform actions to the Devicehub API head
|
||||
to the `Swagger docs
|
||||
<https://app.swaggerhub.com/apis/ereuse/devicehub/0.2>`_.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
actions
|
||||
|
||||
.. uml:: actions.puml
|
||||
|
||||
|
||||
Physical Actions
|
||||
****************
|
||||
The following actions describe and react on the physical condition
|
||||
of the devices.
|
||||
|
||||
ToPrepare, Prepare
|
||||
==================
|
||||
Work has been performed to the device to a defined point of
|
||||
acceptance. Users using this event have to agree what is this point
|
||||
of acceptance; for some is when the device just works, for others
|
||||
when some testing has been performed.
|
||||
|
||||
**Prepare** dictates that the device has been prepared, whereas
|
||||
**ToPrepare** that the device has been selected to be prepared.
|
||||
|
||||
Usually **ToPrepare** is the next event done after registering the
|
||||
device.
|
||||
|
||||
ToRepair, Repair
|
||||
================
|
||||
ToRepair is the act of selecting a device to be repaired, and
|
||||
Repair the act of performing the actual reparations. If a repair
|
||||
without an error is performed, it represents that the reparation
|
||||
has been successful.
|
||||
|
||||
ReadyToUse
|
||||
==========
|
||||
The device is ready to be used. This involves greater preparation
|
||||
from the ``Prepare`` event, and users should only use a device
|
||||
after this event is performed.
|
||||
|
||||
Users usually require devices with this event before shipping them
|
||||
to costumers.
|
||||
|
||||
Live
|
||||
====
|
||||
A keep-alive from a device connected to the Internet with information
|
||||
about its state (in the form of a ``Snapshot`` event) and usage
|
||||
statistics.
|
||||
|
||||
DisposeWaste, Recover
|
||||
=====================
|
||||
``RecyclingCenter`` users have two extra special events:
|
||||
- ``DisposeWaste``: The device has been disposed in an unspecified
|
||||
manner.
|
||||
- ``Recover``: The device has been scrapped and its materials have
|
||||
been recovered under a new product.
|
||||
|
||||
See `ToDisposeProduct, DisposeProduct`_.
|
||||
|
||||
Association actions
|
||||
*******************
|
||||
Actions that change the associations users have with devices;
|
||||
ie. the **owners**, **usufructuarees**, **reservees**,
|
||||
and **physical possessors**.
|
||||
|
||||
There are three sub-dimensions: **trade**, **transfer**,
|
||||
and **organize** actions.
|
||||
|
||||
.. uml:: association-events.puml
|
||||
|
||||
Trade actions
|
||||
=============
|
||||
Trade actions log the political exchange of devices between users,
|
||||
stating **owner** xor **usufructuaree**. Every time a trade event
|
||||
is performed, the old user looses its political possession in favor
|
||||
of another one.
|
||||
|
||||
Sell
|
||||
----
|
||||
The act of taking money from a buyer in exchange of a device.
|
||||
|
||||
Donate
|
||||
------
|
||||
The act of giving devices without compensation.
|
||||
|
||||
Rent
|
||||
----
|
||||
The act of giving money in return for temporary use, but not
|
||||
ownership, of a device.
|
||||
|
||||
CancelTrade
|
||||
-----------
|
||||
The act of cancelling a `Sell`_, `Donate`_ or `Rent`_.
|
||||
|
||||
ToDisposeProduct, DisposeProduct
|
||||
-------------------------
|
||||
``ToDispose`` and ``DisposeProduct`` manage the process of getting
|
||||
rid of devices by giving (selling, donating) to another organization
|
||||
like a waste manager.
|
||||
|
||||
``ToDispose`` marks a device for being disposed, and
|
||||
``DisposeProduct`` dictates that the device has been disposed.
|
||||
|
||||
See `DisposeWaste, Recover`_ events for disposing without trading
|
||||
the device.
|
||||
|
||||
.. note:: For usability purposes, users might not directly perform
|
||||
``Dispose``, but this could automatically be done when
|
||||
performing ``ToDispose`` + ``Receive`` to a ``RecyclingCenter``.
|
||||
|
||||
Transfer actions
|
||||
================
|
||||
The act of transferring/moving devices from one place to another.
|
||||
|
||||
Receive
|
||||
-------
|
||||
The act of physically taking delivery of a device. The receiver
|
||||
confirms that the devices have arrived, and thus, they
|
||||
**physically possess** them. Note that
|
||||
there can only be one **physical possessor** per device, and
|
||||
``Receive`` changes it.
|
||||
|
||||
The receiver can optionally take a role in the reception, giving
|
||||
it meaning; an user that takes the ``FinalUser`` role in the
|
||||
reception express that it will use the device, whereas a role
|
||||
``Transporter`` is used by intermediaries in shipping.
|
||||
|
||||
.. todo:: how do we ensure users specify type of reception?
|
||||
|
||||
Organize actions
|
||||
================
|
||||
The act of manipulating/administering/supervising/controlling one or
|
||||
more devices.
|
||||
|
||||
Reserve, CancelReservation
|
||||
--------------------------
|
||||
The act of reserving devices and cancelling them.
|
||||
|
||||
After this event is performed, the user is the **reservee** of the
|
||||
devices. There can only be one non-cancelled reservation for
|
||||
a device, and a reservation can only have one reservee.
|
||||
|
||||
Assign, Accept, Reject
|
||||
----------------------
|
||||
``Assign`` allocates devices to an user. The purpose or meaning
|
||||
of the association is defined by the users.
|
||||
|
||||
``Accept`` and ``Reject`` allow users to accept and reject the
|
||||
assignments.
|
||||
|
||||
.. todo:: shall we add ``Deassign`` or make ``Assign``
|
||||
always define all active users?
|
||||
|
||||
.. todo:: Assign won't be developed until further notice.
|
||||
|
||||
|
||||
Internal state actions
|
||||
**********************
|
||||
Actions providing metadata about devices that don't usually change
|
||||
their state.
|
||||
|
||||
Snapshot
|
||||
========
|
||||
The Snapshot sets the physical information of the device (S/N, model...)
|
||||
and updates it with erasures, benchmarks, ratings, and tests; updates the
|
||||
composition of its components (adding / removing them), and links tags
|
||||
to the device.
|
||||
|
||||
When receiving a Snapshot, the DeviceHub creates, adds and removes
|
||||
components to match the Snapshot. For example, if a Snapshot of a computer
|
||||
contains a new component, the system searches for the component in its
|
||||
database and, if not found, its creates it; finally linking it to the
|
||||
computer.
|
||||
|
||||
A Snapshot is used with Remove to represent changes in components for
|
||||
a device:
|
||||
|
||||
1. ``Snapshot`` creates a device if it does not exist, and the same
|
||||
for its components. This is all done in one ``Snapshot``.
|
||||
2. If the device exists, it updates its component composition by
|
||||
*adding* and *removing* them. If,
|
||||
for example, this new Snasphot doesn't have a component, it means that
|
||||
this component is not present anymore in the device, thus removing it
|
||||
from it. Then we have that:
|
||||
|
||||
- Components that are added to the device: snapshot2.components -
|
||||
snapshot1.components
|
||||
- Components that are removed to the device: snapshot1.components -
|
||||
snapshot2.components
|
||||
|
||||
When adding a component, there may be the case this component existed
|
||||
before and it was inside another device. In such case, DeviceHub will
|
||||
perform ``Remove`` on the old parent.
|
||||
|
||||
Snapshots from Workbench
|
||||
------------------------
|
||||
When processing a device from the Workbench, this one performs a Snapshot
|
||||
and then performs more events (like testings, benchmarking...).
|
||||
|
||||
There are two ways of sending this information. In an async way,
|
||||
this is, submitting events as soon as Workbench performs then, or
|
||||
submitting only one Snapshot event with all the other events embedded.
|
||||
|
||||
Asynced
|
||||
^^^^^^^
|
||||
The use case, which is represented in the ``test_workbench_phases``,
|
||||
is as follows:
|
||||
|
||||
1. In **T1**, WorkbenchServer (as the middleware from Workbench and
|
||||
Devicehub) submits:
|
||||
|
||||
- A ``Snapshot`` event with the required information to **synchronize**
|
||||
and **rate** the device. This is:
|
||||
|
||||
- Identification information about the device and components
|
||||
(S/N, model, physical characteristics...)
|
||||
- ``Tags`` in a ``tags`` property in the ``device``.
|
||||
- ``Rate`` in an ``events`` property in the ``device``.
|
||||
- ``Benchmarks`` in an ``events`` property in each ``component``
|
||||
or ``device``.
|
||||
- ``TestDataStorage`` as in ``Benchmarks``.
|
||||
- An ordered set of **expected events**, defining which are the next
|
||||
events that Workbench will perform to the device in ideal
|
||||
conditions (device doesn't fail, no Internet drop...).
|
||||
|
||||
Devicehub **syncs** the device with the database and perform the
|
||||
``Benchmark``, the ``TestDataStorage``, and finally the ``Rate``.
|
||||
This leaves the Snapshot **open** to wait for the next events
|
||||
to come.
|
||||
2. Assuming that we expect all events, in **T2**, WorkbenchServer
|
||||
submits a ``StressTest`` with a ``snapshot`` field containing the
|
||||
ID of the Snapshot in 1, and Devicehub links the event with such
|
||||
``Snapshot``.
|
||||
3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot``
|
||||
and ``component`` IDs from 1, linking it to them. It repeats
|
||||
this for all the erased data storage devices; **T3+Tn** being
|
||||
*n* the erased data storage devices.
|
||||
4. WorkbenchServer does like in 3. but for the event ``Install``,
|
||||
finishing in **T3+Tn+Tx**, being *x* the number of data storage
|
||||
devices with an OS installed into.
|
||||
5. In **T3+Tn+Tx**, when all *expected events* have been performed,
|
||||
Devicehub **closes** the ``Snapshot`` from 1.
|
||||
|
||||
Synced
|
||||
^^^^^^
|
||||
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
||||
the events in an ``events`` property inside each affected ``component``
|
||||
or ``device``.
|
||||
|
||||
Add, Remove
|
||||
===========
|
||||
The act of adding and removing components of and from a device.
|
||||
|
||||
These are usually used internally from `Snapshot`_, or manually, for
|
||||
example, when removing a component (like a ``DataStorage`` unit) from
|
||||
a broken computer.
|
||||
|
||||
EraseBasic, EraseSectors
|
||||
========================
|
||||
An erasure attempt to a ``DataStorage``. The event contains
|
||||
information about success and nature of the erasure.
|
||||
|
||||
``EraseBasic`` is a fast non-secured way of erasing data storage, and
|
||||
``EraseSectors`` is a slower secured, sector-by-sector, erasure
|
||||
method.
|
||||
|
||||
Users can generate erasure certificates from successful erasures.
|
||||
|
||||
Erasures are an accumulation of **erasure steps**, that are performed
|
||||
as separate actions, called ``StepRandom``, for an erasure step
|
||||
that has overwritten data with random bits, and ``StepZero``,
|
||||
for an erasure step that has overwritten data with zeros.
|
||||
|
||||
Install
|
||||
=======
|
||||
The action of install an Operative System to a data storage unit.
|
||||
|
||||
Test
|
||||
====
|
||||
The act of testing the physical condition of a device and its
|
||||
components.
|
||||
|
||||
TestDataStorage
|
||||
---------------
|
||||
The act of testing the data storage.
|
||||
|
||||
Testing is done using the `S.M.A.R.T self test
|
||||
<https://en.wikipedia.org/wiki/S.M.A.R.T.#Self-tests>`_. Note
|
||||
that not all data storage units, specially some new PCIe ones, do not
|
||||
support SMART testing.
|
||||
|
||||
The test takes to other SMART values indicators of the overall health
|
||||
of the data storage.
|
||||
|
||||
StressTest
|
||||
----------
|
||||
The act of stressing (putting to the maximum capacity)
|
||||
a device for an amount of minutes. If the device is not in great
|
||||
condition won't probably survive such test.
|
||||
|
||||
Benchmark
|
||||
=========
|
||||
The act of gauging the performance of a device.
|
||||
|
||||
BenchmarkDataStorage
|
||||
--------------------
|
||||
Benchmarks the data storage unit reading and writing speeds.
|
||||
|
||||
BenchmarkWithRate
|
||||
-----------------
|
||||
The act of benchmarking a device with a single rate.
|
||||
|
||||
BenchmarkProcessor
|
||||
------------------
|
||||
Benchmarks a processor by executing `BogoMips
|
||||
<https://en.wikipedia.org/wiki/BogoMips>`_. Note that this is not
|
||||
a reliable way of rating processors and we keep it for compatibility
|
||||
purposes.
|
||||
|
||||
BenchmarkProcessorSysbench
|
||||
--------------------------
|
||||
Benchmarks a processor by using the processor benchmarking utility of
|
||||
`sysbench <https://github.com/akopytov/sysbench>`_.
|
||||
|
||||
|
||||
Rate
|
||||
====
|
||||
Devicehub generates an rating for a device taking into consideration the
|
||||
visual, functional, and performance.
|
||||
|
||||
A Workflow is as follows:
|
||||
|
||||
1. An agent generates feedback from the device in the form of benchmark,
|
||||
visual, and functional information; which is filled in a ``Rate``
|
||||
event. This is done through a **software**, defining the type
|
||||
of ``Rate`` event. At the moment we have two rates: ``WorkbenchRate``
|
||||
and ``PhotoboxRate``.
|
||||
2. Devicehub gathers this information and computes a score that updates
|
||||
the ``Rate`` event.
|
||||
3. Devicehub aggregates different rates and computes a final score for
|
||||
the device by performing a new ``AggregateRating`` event.
|
||||
|
||||
There are three **types** of ``Rate``: ``WorkbenchRate``,
|
||||
``AppRate``, and ``PhotoboxRate``. ``WorkbenchRate`` can have different
|
||||
**software** algorithms, and each software algorithm can have several
|
||||
**versions**. So, we have 3 dimensions for ``WorkbenchRate``:
|
||||
type, software, version.
|
||||
|
||||
Devicehub generates a rate event for each software and version. So,
|
||||
if an agent fulfills a ``WorkbenchRate`` and there are 2 software
|
||||
algorithms and each has two versions, Devicehub will generate 4 rates.
|
||||
Devicehub understands that only one software and version are the
|
||||
**oficial** (set in the settings of each inventory),
|
||||
and it will generate an ``AggregateRating`` for only the official
|
||||
versions. At the same time, ``Price`` only computes the price of
|
||||
the **oficial** version.
|
||||
|
||||
The technical Workflow in Devicehub is as follows:
|
||||
|
||||
1. In **T1**, the user performs a ``Snapshot`` by processing the device
|
||||
through the Workbench. From the benchmarks and the visual and
|
||||
functional ratings the user does in the device, the system generates
|
||||
many ``WorkbenchRate`` (as many as software and versions defined).
|
||||
With only this information, the system generates an ``AggregateRating``,
|
||||
which is the event that the user will see in the web.
|
||||
2. In **T2**, the user takes pictures from the device through the
|
||||
Photobox, and DeviceHub crates an ``ImageSet`` with multiple
|
||||
``Image`` with information from the photobox.
|
||||
3. In **T3**, an agent (user or AI) rates the pictures, creating a
|
||||
``PhotoboxRate`` **for each** picture. When Devicehub receives the
|
||||
first ``PhotoboxRate`` it creates an ``AggregateRating`` linked
|
||||
to such ``PhotoboxRate``. So, the agent will perform as many
|
||||
``PhotoboxRate`` as pictures are in the ``ImageSet``, and Devicehub
|
||||
will link each ``PhotoboxRate`` to the same ``AggregateRating``.
|
||||
This will end in **T3+Tn**, being *n* the number of photos to rate.
|
||||
4. In **T3+Tn**, after the last photo is rated, Devicehub will generate
|
||||
a new rate for the device: it takes the ``AggregateRating`` from 3.
|
||||
and computes a rate from all the linked ``PhotoboxRate`` plus the
|
||||
last available ``WorkbenchRate`` for that device.
|
||||
|
||||
If the agent in 3. is an user, Devicehub creates ``PhotoboxUserRate``
|
||||
and if it is an AI it creates ``PhotoboxAIRate``.
|
||||
|
||||
The same ``ImageSet`` can be rated multiple times, generating a new
|
||||
``AggregateRating`` each time.
|
||||
|
||||
Price
|
||||
=====
|
||||
Price states a selling price for the device, but not necessariliy the
|
||||
final price this was sold (which is set in the Sell event).
|
||||
|
||||
Devicehub automatically computes a price from ``AggregateRating``
|
||||
events. As in a **Rate**, price can have **software** and **version**,
|
||||
and there is an **official** price that is used to automatically
|
||||
compute the price from an ``AggregateRating``. Only the official price
|
||||
is computed from an ``AggregateRating``.
|
||||
|
||||
Migrate
|
||||
=======
|
||||
Moves the devices to a new database/inventory. Devices cannot be
|
||||
modified anymore at the previous database.
|
||||
|
||||
Donation
|
||||
========
|
||||
.. todo:: nextcloud/eReuse/99. Tasks/224. Definir datos necesarios
|
||||
configuración licencia
|
||||
|
||||
|
||||
States
|
||||
******
|
||||
.. todo:: work on september.
|
||||
|
||||
.. uml:: states.puml
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
@startuml
|
||||
|
||||
abstract class User <<Common schema>>
|
||||
abstract class Individual
|
||||
abstract class Agent
|
||||
|
||||
Event "*" --> "1" User : Author >
|
||||
Event "*" -- "0..1" Agent : agent >
|
||||
Trade "*" -- "0..1" Agent : to >
|
||||
|
||||
User "0..1" - "0..1" Agent : user <
|
||||
|
||||
Agent <|-- Individual
|
||||
Individual <|-- Person
|
||||
Individual <|-- System
|
||||
Agent <|-- Organization
|
||||
Individual "*" -o "0..1" Organization
|
||||
(Individual, Organization) .. Membership
|
||||
class Membership {
|
||||
member_id
|
||||
}
|
||||
|
||||
|
||||
Individual "*" -o "0..1" Organization : activeOrg >
|
||||
@enduml
|
|
@ -0,0 +1,4 @@
|
|||
Agents
|
||||
######
|
||||
|
||||
.. uml:: agents.puml
|
|
@ -1,72 +1,25 @@
|
|||
@startuml
|
||||
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
|
|
@ -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'
|
||||
|
|
|
@ -15,7 +15,6 @@ Actors
|
|||
- Photochromic tag manufacturer.
|
||||
- User: organization that uses the tags.
|
||||
|
||||
|
||||
Requirements
|
||||
************
|
||||
|
||||
|
|
197
docs/events.rst
197
docs/events.rst
|
@ -1,197 +0,0 @@
|
|||
Events
|
||||
######
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
event-diagram
|
||||
|
||||
|
||||
Rate
|
||||
****
|
||||
Devicehub generates an rating for a device taking into consideration the
|
||||
visual, functional, and performance.
|
||||
|
||||
.. todo:: add performance as a result of component fusion + general
|
||||
tests in `here <https://github.com/eReuse/Rdevicescore/blob/master/
|
||||
img/input_process_output.png>`_.
|
||||
|
||||
A Workflow is as follows:
|
||||
|
||||
1. An agent generates feedback from the device in the form of benchmark,
|
||||
visual, and functional information; which is filled in a ``Rate``
|
||||
event. This is done through a **software**, defining the type
|
||||
of ``Rate`` event. At the moment we have two rates: ``WorkbenchRate``
|
||||
and ``PhotoboxRate``.
|
||||
2. Devicehub gathers this information and computes a score that updates
|
||||
the ``Rate`` event.
|
||||
3. Devicehub aggregates different rates and computes a final score for
|
||||
the device by performing a new ``AggregateRating`` event.
|
||||
|
||||
There are three **types** of ``Rate``: ``WorkbenchRate``,
|
||||
``AppRate``, and ``PhotoboxRate``. ``WorkbenchRate`` can have different
|
||||
**software** algorithms, and each software algorithm can have several
|
||||
**versions**. So, we have 3 dimensions for ``WorkbenchRate``:
|
||||
type, software, version.
|
||||
|
||||
Devicehub generates a rate event for each software and version. So,
|
||||
if an agent fulfills a ``WorkbenchRate`` and there are 2 software
|
||||
algorithms and each has two versions, Devicehub will generate 4 rates.
|
||||
Devicehub understands that only one software and version are the
|
||||
**oficial** (set in the settings of each inventory),
|
||||
and it will generate an ``AggregateRating`` for only the official
|
||||
versions. At the same time, ``Price`` only computes the price of
|
||||
the **oficial** version.
|
||||
|
||||
The technical Workflow in Devicehub is as follows:
|
||||
|
||||
1. In **T1**, the user performs a ``Snapshot`` by processing the device
|
||||
through the Workbench. From the benchmarks and the visual and
|
||||
functional ratings the user does in the device, the system generates
|
||||
many ``WorkbenchRate`` (as many as software and versions defined).
|
||||
With only this information, the system generates an ``AggregateRating``,
|
||||
which is the event that the user will see in the web.
|
||||
2. In **T2**, the user takes pictures from the device through the
|
||||
Photobox, and DeviceHub crates an ``ImageSet`` with multiple
|
||||
``Image`` with information from the photobox.
|
||||
3. In **T3**, an agent (user or AI) rates the pictures, creating a
|
||||
``PhotoboxRate`` **for each** picture. When Devicehub receives the
|
||||
first ``PhotoboxRate`` it creates an ``AggregateRating`` linked
|
||||
to such ``PhotoboxRate``. So, the agent will perform as many
|
||||
``PhotoboxRate`` as pictures are in the ``ImageSet``, and Devicehub
|
||||
will link each ``PhotoboxRate`` to the same ``AggregateRating``.
|
||||
This will end in **T3+Tn**, being *n* the number of photos to rate.
|
||||
4. In **T3+Tn**, after the last photo is rated, Devicehub will generate
|
||||
a new rate for the device: it takes the ``AggregateRating`` from 3.
|
||||
and computes a rate from all the linked ``PhotoboxRate`` plus the
|
||||
last available ``WorkbenchRate`` for that device.
|
||||
|
||||
If the agent in 3. is an user, Devicehub creates ``PhotoboxUserRate``
|
||||
and if it is an AI it creates ``PhotoboxAIRate``.
|
||||
|
||||
The same ``ImageSet`` can be rated multiple times, generating a new
|
||||
``AggregateRating`` each time.
|
||||
|
||||
.. todo:: which info does photobox provide for each picture?
|
||||
|
||||
Price
|
||||
*****
|
||||
Price states a selling price for the device, but not necessariliy the
|
||||
final price this was sold (which is set in the Sell event).
|
||||
|
||||
Devicehub automatically computes a price from ``AggregateRating``
|
||||
events. As in a **Rate**, price can have **software** and **version**,
|
||||
and there is an **official** price that is used to automatically
|
||||
compute the price from an ``AggregateRating``. Only the official price
|
||||
is computed from an ``AggregateRating``.
|
||||
|
||||
Snapshot
|
||||
********
|
||||
The Snapshot sets the physical information of the device (S/N, model...)
|
||||
and updates it with erasures, benchmarks, ratings, and tests; updates the
|
||||
composition of its components (adding / removing them), and links tags
|
||||
to the device.
|
||||
|
||||
When receiving a Snapshot, the DeviceHub creates, adds and removes
|
||||
components to match the Snapshot. For example, if a Snapshot of a computer
|
||||
contains a new component, the system searches for the component in its
|
||||
database and, if not found, its creates it; finally linking it to the
|
||||
computer.
|
||||
|
||||
A Snapshot is used with Remove to represent changes in components for
|
||||
a device:
|
||||
|
||||
1. ``Snapshot`` creates a device if it does not exist, and the same
|
||||
for its components. This is all done in one ``Snapshot``.
|
||||
2. If the device exists, it updates its component composition by
|
||||
*adding* and *removing* them. If,
|
||||
for example, this new Snasphot doesn't have a component, it means that
|
||||
this component is not present anymore in the device, thus removing it
|
||||
from it. Then we have that:
|
||||
|
||||
- Components that are added to the device: snapshot2.components -
|
||||
snapshot1.components
|
||||
- Components that are removed to the device: snapshot1.components -
|
||||
snapshot2.components
|
||||
|
||||
When adding a component, there may be the case this component existed
|
||||
before and it was inside another device. In such case, DeviceHub will
|
||||
perform ``Remove`` on the old parent.
|
||||
|
||||
Snapshots from Workbench
|
||||
========================
|
||||
When processing a device from the Workbench, this one performs a Snapshot
|
||||
and then performs more events (like testings, benchmarking...).
|
||||
|
||||
There are two ways of sending this information. In an async way,
|
||||
this is, submitting events as soon as Workbench performs then, or
|
||||
submitting only one Snapshot event with all the other events embedded.
|
||||
|
||||
Asynced
|
||||
-------
|
||||
The use case, which is represented in the ``test_workbench_phases``,
|
||||
is as follows:
|
||||
|
||||
1. In **T1**, WorkbenchServer (as the middleware from Workbench and
|
||||
Devicehub) submits:
|
||||
|
||||
- A ``Snapshot`` event with the required information to **synchronize**
|
||||
and **rate** the device. This is:
|
||||
|
||||
- Identification information about the device and components
|
||||
(S/N, model, physical characteristics...)
|
||||
- ``Tags`` in a ``tags`` property in the ``device``.
|
||||
- ``Rate`` in an ``events`` property in the ``device``.
|
||||
- ``Benchmarks`` in an ``events`` property in each ``component``
|
||||
or ``device``.
|
||||
- ``TestDataStorage`` as in ``Benchmarks``.
|
||||
- An ordered set of **expected events**, defining which are the next
|
||||
events that Workbench will perform to the device in ideal
|
||||
conditions (device doesn't fail, no Internet drop...).
|
||||
|
||||
Devicehub **syncs** the device with the database and perform the
|
||||
``Benchmark``, the ``TestDataStorage``, and finally the ``Rate``.
|
||||
This leaves the Snapshot **open** to wait for the next events
|
||||
to come.
|
||||
2. Assuming that we expect all events, in **T2**, WorkbenchServer
|
||||
submits a ``StressTest`` with a ``snapshot`` field containing the
|
||||
ID of the Snapshot in 1, and Devicehub links the event with such
|
||||
``Snapshot``.
|
||||
3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot``
|
||||
and ``component`` IDs from 1, linking it to them. It repeats
|
||||
this for all the erased data storage devices; **T3+Tn** being
|
||||
*n* the erased data storage devices.
|
||||
4. WorkbenchServer does like in 3. but for the event ``Install``,
|
||||
finishing in **T3+Tn+Tx**, being *x* the number of data storage
|
||||
devices with an OS installed into.
|
||||
5. In **T3+Tn+Tx**, when all *expected events* have been performed,
|
||||
Devicehub **closes** the ``Snapshot`` from 1.
|
||||
|
||||
Synced
|
||||
------
|
||||
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
||||
the events in an ``events`` property inside each affected ``component``
|
||||
or ``device``.
|
||||
|
||||
ToDispose and DisposeProduct
|
||||
****************************
|
||||
There are four events for getting rid of devices:
|
||||
|
||||
- ``ToDispose``: The device is marked to be disposed.
|
||||
- ``DisposeProduct``: The device has been disposed. This is a ``Trade``
|
||||
event, which means that you can optionally ``DisposeProduct``
|
||||
to someone.
|
||||
- ``RecyclingCenter`` have two extra special events:
|
||||
- ``DisposeWaste``: The device has been disposed in an unspecified
|
||||
manner.
|
||||
- ``Recover``: The device has been scrapped and its materials have
|
||||
been recovered under a new product.
|
||||
|
||||
.. note:: For usability purposes, users might not directly perform
|
||||
``Dispose``, but this could automatically be done when
|
||||
performing ``ToDispose`` + ``Receive`` to a ``RecyclingCenter``.
|
||||
|
||||
.. todo:: Ensure that ``Dispose`` is a ``Trade`` event. An Org could
|
||||
``Sell`` or ``Donate`` a device with the objective of disposing them.
|
||||
Is ``Dispose`` ok, or do we want to keep that extra ``Sell`` or
|
||||
``Donate`` event? Could dispose be a synonym of any of those?
|
|
@ -10,11 +10,12 @@ This is the documentation and API of the `eReuse.org DeviceHub
|
|||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
:maxdepth: 2
|
||||
|
||||
events
|
||||
tags
|
||||
actions
|
||||
agents
|
||||
inventory
|
||||
tags
|
||||
etag
|
||||
|
||||
* :ref:`genindex`
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
@startuml
|
||||
skinparam nodesep 10
|
||||
skinparam ranksep 1
|
||||
|
||||
|
||||
[*] -> Registered
|
||||
|
||||
state Attributes {
|
||||
|
||||
state Broken : cannot turn on
|
||||
state Owners
|
||||
state Usufructuarees
|
||||
state Reservees
|
||||
state "Physical\nPossessor"
|
||||
}
|
||||
|
||||
state Physical {
|
||||
Registered --> Preparing : ToPrepare
|
||||
Registered --> ToBeRepaired : ToRepair
|
||||
ToBeRepaired --> Repaired : Repair
|
||||
Repaired -> Preparing : ToPrepare
|
||||
Preparing --> Prepared : Prepare
|
||||
Prepared --> ReadyToBeUsed : ReadyToUse
|
||||
ReadyToBeUsed --> InUse : Live
|
||||
InUse -> InUse : Live
|
||||
state DisposeWaste
|
||||
state Recover
|
||||
|
||||
}
|
||||
|
||||
state Trading {
|
||||
Registered --> Reserved : Reserve
|
||||
Registered --> Sold : Sell
|
||||
Reserved -> Sold : Sell
|
||||
Reserved --> Cancelled : Cancel
|
||||
Sold --> Cancelled : Cancel
|
||||
Sold --> Payed : Pay
|
||||
Registered --> ToBeDisposed
|
||||
ToBeDisposed --> Disposed : DisposeProduct
|
||||
}
|
||||
|
||||
|
||||
@enduml
|
|
@ -1,4 +1,4 @@
|
|||
from distutils.version import StrictVersion
|
||||
|
||||
__version__ = '0.2.0a11'
|
||||
__version__ = '0.2.0a12'
|
||||
version = StrictVersion(__version__)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -175,6 +175,6 @@
|
|||
"EraseBasic"
|
||||
],
|
||||
"software": "Workbench",
|
||||
"date": "2018-07-11T10:30:22.395958",
|
||||
"endTime": "2018-07-11T10:30:22.395958",
|
||||
"elapsed": 2766
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import click
|
||||
from flask import current_app as app
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.agent import models, schemas
|
||||
from teal.db import SQLAlchemy
|
||||
from teal.resource import Converters, Resource
|
||||
|
||||
|
||||
class AgentDef(Resource):
|
||||
SCHEMA = schemas.Agent
|
||||
VIEW = None
|
||||
AUTH = True
|
||||
ID_CONVERTER = Converters.uuid
|
||||
|
||||
|
||||
class OrganizationDef(AgentDef):
|
||||
SCHEMA = schemas.Organization
|
||||
VIEW = None
|
||||
|
||||
def __init__(self, app, import_name=__package__, static_folder=None, static_url_path=None,
|
||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||
root_path=None):
|
||||
cli_commands = ((self.create_org, 'create-org'),)
|
||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||
|
||||
@click.argument('name')
|
||||
@click.argument('tax_id')
|
||||
@click.argument('country')
|
||||
def create_org(self, name: str, tax_id: str = None, country: str = None) -> dict:
|
||||
"""Creates an organization."""
|
||||
org = models.Organization(**self.schema.load(
|
||||
{
|
||||
'name': name,
|
||||
'taxId': tax_id,
|
||||
'country': country
|
||||
}
|
||||
))
|
||||
db.session.add(org)
|
||||
db.session.commit()
|
||||
return self.schema.dump(org)
|
||||
|
||||
def init_db(self, db: SQLAlchemy):
|
||||
"""Creates the default organization."""
|
||||
org = models.Organization(**app.config.get_namespace('ORGANIZATION_'))
|
||||
db.session.add(org)
|
||||
|
||||
|
||||
class Membership(Resource):
|
||||
SCHEMA = schemas.Membership
|
||||
VIEW = None
|
||||
ID_CONVERTER = Converters.string
|
||||
|
||||
|
||||
class IndividualDef(AgentDef):
|
||||
SCHEMA = schemas.Individual
|
||||
VIEW = None
|
||||
|
||||
|
||||
class PersonDef(IndividualDef):
|
||||
SCHEMA = schemas.Person
|
||||
VIEW = None
|
||||
|
||||
|
||||
class SystemDef(IndividualDef):
|
||||
SCHEMA = schemas.System
|
||||
VIEW = None
|
|
@ -0,0 +1,131 @@
|
|||
from itertools import chain
|
||||
from operator import attrgetter
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import current_app as app, g
|
||||
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.orm import backref, relationship
|
||||
from sqlalchemy_utils import EmailType, PhoneNumberType
|
||||
|
||||
from ereuse_devicehub.resources.models import STR_SIZE, STR_SM_SIZE, Thing
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
from teal import enums
|
||||
from teal.db import INHERIT_COND, POLYMORPHIC_ID, \
|
||||
POLYMORPHIC_ON
|
||||
|
||||
|
||||
class JoinedTableMixin:
|
||||
# noinspection PyMethodParameters
|
||||
@declared_attr
|
||||
def id(cls):
|
||||
return Column(UUID(as_uuid=True), ForeignKey(Agent.id), primary_key=True)
|
||||
|
||||
|
||||
class Agent(Thing):
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||
type = Column(Unicode, nullable=False)
|
||||
name = Column(Unicode(length=STR_SM_SIZE))
|
||||
name.comment = """
|
||||
The name of the organization or person.
|
||||
"""
|
||||
tax_id = Column(Unicode(length=STR_SM_SIZE))
|
||||
tax_id.comment = """
|
||||
The Tax / Fiscal ID of the organization,
|
||||
e.g. the TIN in the US or the CIF/NIF in Spain.
|
||||
"""
|
||||
country = Column(DBEnum(enums.Country))
|
||||
country.comment = """
|
||||
Country issuing the tax_id number.
|
||||
"""
|
||||
telephone = Column(PhoneNumberType())
|
||||
email = Column(EmailType, unique=True)
|
||||
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey(User.id), unique=True)
|
||||
user = relationship(User,
|
||||
backref=backref('individuals', lazy=True, collection_class=set),
|
||||
primaryjoin=user_id == User.id)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
||||
)
|
||||
|
||||
@declared_attr
|
||||
def __mapper_args__(cls):
|
||||
"""
|
||||
Defines inheritance.
|
||||
|
||||
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
||||
extensions/declarative/api.html
|
||||
#sqlalchemy.ext.declarative.declared_attr>`_
|
||||
"""
|
||||
args = {POLYMORPHIC_ID: cls.t}
|
||||
if cls.t == 'Agent':
|
||||
args[POLYMORPHIC_ON] = cls.type
|
||||
if JoinedTableMixin in cls.mro():
|
||||
args[INHERIT_COND] = cls.id == Agent.id
|
||||
return args
|
||||
|
||||
@property
|
||||
def events(self) -> list:
|
||||
# todo test
|
||||
return sorted(chain(self.events_agent, self.events_to), key=attrgetter('created'))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<{0.t} {0.name}>'.format(self)
|
||||
|
||||
|
||||
class Organization(JoinedTableMixin, Agent):
|
||||
def __init__(self, name: str, **kwargs) -> None:
|
||||
super().__init__(**kwargs, name=name)
|
||||
|
||||
@classmethod
|
||||
def get_default_org_id(cls) -> UUID:
|
||||
"""Retrieves the default organization."""
|
||||
return g.setdefault('org_id',
|
||||
Organization.query.filter_by(
|
||||
**app.config.get_namespace('ORGANIZATION_')
|
||||
).one().id)
|
||||
|
||||
|
||||
class Individual(JoinedTableMixin, Agent):
|
||||
active_org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id))
|
||||
active_org = relationship(Organization, primaryjoin=active_org_id == Organization.id)
|
||||
|
||||
|
||||
class Membership(Thing):
|
||||
"""Organizations that are related to the Individual.
|
||||
|
||||
For example, because the individual works in or because is a member of.
|
||||
"""
|
||||
id = Column(Unicode(length=STR_SIZE))
|
||||
organization_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True)
|
||||
organization = relationship(Organization,
|
||||
backref=backref('members', collection_class=set, lazy=True),
|
||||
primaryjoin=organization_id == Organization.id)
|
||||
individual_id = Column(UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True)
|
||||
individual = relationship(Individual,
|
||||
backref=backref('member_of', collection_class=set, lazy=True),
|
||||
primaryjoin=individual_id == Individual.id)
|
||||
|
||||
def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None:
|
||||
super().__init__(organization=organization,
|
||||
individual=individual,
|
||||
id=id)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(id, organization_id, name='One member id per organization.'),
|
||||
)
|
||||
|
||||
|
||||
class Person(Individual):
|
||||
"""
|
||||
A person in the system. There can be several persons pointing to
|
||||
a real.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class System(Individual):
|
||||
pass
|
|
@ -0,0 +1,79 @@
|
|||
import uuid
|
||||
from typing import List, Set
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy_utils import PhoneNumber
|
||||
|
||||
from ereuse_devicehub.resources.event.models import Event, Trade
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
from ereuse_devicehub.resources.user import User
|
||||
from teal import enums
|
||||
|
||||
|
||||
class Agent(Thing):
|
||||
id = ... # type: Column
|
||||
name = ... # type: Column
|
||||
tax_id = ... # type: Column
|
||||
country = ... # type: Column
|
||||
telephone = ... # type: Column
|
||||
email = ... # type: Column
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.id = ... # type: uuid.UUID
|
||||
self.name = ... # type: str
|
||||
self.tax_id = ... # type: str
|
||||
self.country = ... # type: enums.Country
|
||||
self.telephone = ... # type: PhoneNumber
|
||||
self.email = ... # type: str
|
||||
self.events_agent = ... # type: Set[Event] # Ordered
|
||||
self.events_to = ... # type: Set[Trade] # Ordered
|
||||
|
||||
@property
|
||||
def events(self) -> List[Event]:
|
||||
pass
|
||||
|
||||
|
||||
class Organization(Agent):
|
||||
def __init__(self, name: str, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.members = ... # type: Set[Membership]
|
||||
|
||||
@classmethod
|
||||
def get_default_org_id(cls) -> uuid.UUID:
|
||||
pass
|
||||
|
||||
|
||||
class Individual(Agent):
|
||||
member_of = ... # type:relationship
|
||||
active_org_id = ... # type:Column
|
||||
active_org = ... # type:relationship
|
||||
user_id = ... # type:Column
|
||||
user = ... # type:relationship
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.member_of = ... # type: Set[Membership]
|
||||
self.active_org = ... # type: Organization
|
||||
self.user = ... # type: User
|
||||
|
||||
|
||||
class Membership(Thing):
|
||||
organization = ... # type: Column
|
||||
individual = ... # type: Column
|
||||
id = ... # type: Column
|
||||
|
||||
def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None:
|
||||
super().__init__()
|
||||
self.organization = ... # type: Organization
|
||||
self.individual = ... # type: Individual
|
||||
self.id = ... # type: str
|
||||
|
||||
|
||||
class Person(Individual):
|
||||
pass
|
||||
|
||||
|
||||
class System(Individual):
|
||||
pass
|
|
@ -0,0 +1,40 @@
|
|||
from marshmallow import fields as ma_fields, validate as ma_validate
|
||||
from marshmallow.fields import Email
|
||||
|
||||
from ereuse_devicehub.marshmallow import NestedOn
|
||||
from ereuse_devicehub.resources.models import STR_SIZE, STR_SM_SIZE
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
from teal import enums
|
||||
from teal.marshmallow import EnumField, Phone
|
||||
|
||||
|
||||
class Agent(Thing):
|
||||
id = ma_fields.UUID(dump_only=True)
|
||||
name = ma_fields.String(validate=ma_validate.Length(max=STR_SM_SIZE))
|
||||
tax_id = ma_fields.String(validate=ma_validate.Length(max=STR_SM_SIZE),
|
||||
data_key='taxId')
|
||||
country = EnumField(enums.Country)
|
||||
telephone = Phone()
|
||||
email = Email()
|
||||
|
||||
|
||||
class Organization(Agent):
|
||||
members = NestedOn('Membership')
|
||||
|
||||
|
||||
class Membership(Thing):
|
||||
organization = NestedOn(Organization)
|
||||
individual = NestedOn('Individual')
|
||||
id = ma_fields.String(validate=ma_validate.Length(max=STR_SIZE))
|
||||
|
||||
|
||||
class Individual(Agent):
|
||||
member_of = NestedOn(Membership, many=True)
|
||||
|
||||
|
||||
class Person(Individual):
|
||||
pass
|
||||
|
||||
|
||||
class System(Individual):
|
||||
pass
|
|
@ -1,23 +1,23 @@
|
|||
from contextlib import suppress
|
||||
from 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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
from typing import Set, Union
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy_utils import Password
|
||||
|
||||
from ereuse_devicehub.resources.agent.models import Individual
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
|
||||
|
||||
class User(Thing):
|
||||
id = ... # type: Column
|
||||
email = ... # type: Column
|
||||
password = ... # type: Column
|
||||
token = ... # type: Column
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.id = ... # type: UUID
|
||||
self.email = ... # type: str
|
||||
self.password = ... # type: Password
|
||||
self.individuals = ... # type: Set[Individual]
|
||||
self.token = ... # type: UUID
|
||||
|
||||
@property
|
||||
def individual(self) -> Union[Individual, None]:
|
||||
pass
|
|
@ -3,6 +3,8 @@ from base64 import b64encode
|
|||
from marshmallow import post_dump
|
||||
from marshmallow.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.'
|
||||
|
|
6
setup.py
6
setup.py
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -152,5 +152,5 @@
|
|||
"serialNumber": "109192430003459"
|
||||
}
|
||||
],
|
||||
"date": "2018-07-13T10:48:36.738398"
|
||||
"endTime": "2018-07-13T10:48:36.738398"
|
||||
}
|
|
@ -123,6 +123,6 @@
|
|||
},
|
||||
"type": "Snapshot",
|
||||
"software": "Workbench",
|
||||
"date": "2018-07-19T15:48:40.635776",
|
||||
"endTime": "2018-07-19T15:48:40.635776",
|
||||
"closed": false
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy_utils import PhoneNumber
|
||||
|
||||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.resources.agent import OrganizationDef, models, schemas
|
||||
from ereuse_devicehub.resources.agent.models import Membership, Organization, Person, System
|
||||
from teal.enums import Country
|
||||
from tests.conftest import app_context, create_user
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_agent():
|
||||
"""Tests creating an person."""
|
||||
person = Person(name='Timmy',
|
||||
tax_id='XYZ',
|
||||
country=Country.ES,
|
||||
telephone=PhoneNumber('+34666666666'),
|
||||
email='foo@bar.com')
|
||||
db.session.add(person)
|
||||
db.session.commit()
|
||||
|
||||
p = schemas.Person().dump(person)
|
||||
assert p['name'] == person.name == 'Timmy'
|
||||
assert p['taxId'] == person.tax_id == 'XYZ'
|
||||
assert p['country'] == person.country.name == 'ES'
|
||||
assert p['telephone'] == person.telephone.international == '+34 666 66 66 66'
|
||||
assert p['email'] == person.email == 'foo@bar.com'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_system():
|
||||
"""Tests creating a system."""
|
||||
system = System(name='Workbench',
|
||||
email='hello@ereuse.org')
|
||||
db.session.add(system)
|
||||
db.session.commit()
|
||||
|
||||
s = schemas.System().dump(system)
|
||||
assert s['name'] == system.name == 'Workbench'
|
||||
assert s['email'] == system.email == 'hello@ereuse.org'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_organization():
|
||||
"""Tests creating an organization."""
|
||||
org = Organization(name='ACME',
|
||||
tax_id='XYZ',
|
||||
country=Country.ES,
|
||||
email='contact@acme.com')
|
||||
db.session.add(org)
|
||||
db.session.commit()
|
||||
|
||||
o = schemas.Organization().dump(org)
|
||||
assert o['name'] == org.name == 'ACME'
|
||||
assert o['taxId'] == org.tax_id == 'XYZ'
|
||||
assert org.country.name == o['country'] == 'ES'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_membership():
|
||||
"""Tests assigning an Individual to an Organization."""
|
||||
person = Person(name='Timmy')
|
||||
org = Organization(name='ACME')
|
||||
person.member_of.add(Membership(org, person, id='acme-1'))
|
||||
db.session.add(person)
|
||||
db.session.flush()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_membership_repeated():
|
||||
person = Person(name='Timmy')
|
||||
org = Organization(name='ACME')
|
||||
person.member_of.add(Membership(org, person, id='acme-1'))
|
||||
db.session.add(person)
|
||||
|
||||
person.member_of.add(Membership(org, person))
|
||||
with pytest.raises(IntegrityError):
|
||||
db.session.flush()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_membership_repeating_id():
|
||||
person = Person(name='Timmy')
|
||||
org = Organization(name='ACME')
|
||||
person.member_of.add(Membership(org, person, id='acme-1'))
|
||||
db.session.add(person)
|
||||
db.session.flush()
|
||||
|
||||
person2 = Person(name='Tommy')
|
||||
person2.member_of.add(Membership(org, person2, id='acme-1'))
|
||||
db.session.add(person2)
|
||||
with pytest.raises(IntegrityError) as e:
|
||||
db.session.flush()
|
||||
assert 'One member id per organization' in str(e)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_default_org_exists(config: DevicehubConfig):
|
||||
"""
|
||||
Ensures that the default organization is created on app
|
||||
initialization and that is accessible for the method
|
||||
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
|
||||
"""
|
||||
assert models.Organization.query.filter_by(name=config.ORGANIZATION_NAME,
|
||||
tax_id=config.ORGANIZATION_TAX_ID).one()
|
||||
assert isinstance(models.Organization.get_default_org_id(), UUID)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_assign_individual_user():
|
||||
"""Tests assigning an individual to an user."""
|
||||
user = create_user()
|
||||
assert len(user.individuals) == 1
|
||||
assert next(iter(user.individuals)).name == 'Timmy'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(app_context.__name__)
|
||||
def test_create_organization_main_method(app: Devicehub):
|
||||
org_def = app.resources[models.Organization.t] # type: OrganizationDef
|
||||
o = org_def.create_org('ACME', tax_id='FOO', country='ES')
|
||||
org = models.Agent.query.filter_by(id=o['id']).one() # type: Organization
|
||||
assert org.name == o['name'] == 'ACME'
|
||||
assert org.tax_id == o['taxId'] == 'FOO'
|
||||
assert org.country.name == o['country'] == 'ES'
|
|
@ -35,4 +35,4 @@ def test_api_docs(client: Client):
|
|||
'scheme': 'basic',
|
||||
'name': 'Authorization'
|
||||
}
|
||||
assert 60 == len(docs['definitions'])
|
||||
assert 76 == len(docs['definitions'])
|
||||
|
|
|
@ -9,6 +9,7 @@ from sqlalchemy.util import OrderedSet
|
|||
from ereuse_devicehub.client import UserClient
|
||||
from ereuse_devicehub.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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.resources.user import Organization
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('app_context')
|
||||
def test_default_org_exists(config: DevicehubConfig):
|
||||
"""
|
||||
Ensures that the default organization is created on app
|
||||
initialization and that is accessible for the method
|
||||
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
|
||||
"""
|
||||
assert Organization.query.filter_by(name=config.ORGANIZATION_NAME,
|
||||
tax_id=config.ORGANIZATION_TAX_ID).one()
|
||||
assert isinstance(Organization.get_default_org_id(), UUID)
|
|
@ -1,6 +0,0 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Just needs to do the test')
|
||||
def test_price_no_data_storage():
|
||||
pass
|
|
@ -4,6 +4,7 @@ from typing import List, Tuple
|
|||
from uuid import uuid4
|
||||
|
||||
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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Reference in New Issue