Add MakeAvailable and Repair; add device.image; add templating file, change layout for project LOT

This commit is contained in:
Xavier Bustamante Talavera 2019-07-07 21:36:09 +02:00
parent f8ec8fc882
commit ce961a1bed
19 changed files with 350 additions and 286 deletions

View File

@ -23,8 +23,8 @@ state Physical {
ToBeRepaired --> Repaired : Repair
Repaired -> Preparing : ToPrepare
Preparing --> Prepared : Prepare
Prepared --> ReadyToBeUsed : ReadyToUse
ReadyToBeUsed --> InUse : Live
Prepared --> Ready : ReadyToUse
Ready --> InUse : Live
InUse -> InUse : Live
state DisposeWaste
state Recover

View File

@ -44,5 +44,5 @@ Physical
:cvar Repaired: The device has been repaired.
:cvar Preparing: The device is going to be or being prepared.
:cvar Prepared: The device has been prepared.
:cvar ReadyToBeUsed: The device is in working conditions.
:cvar Ready: The device is in working conditions.
:cvar InUse: The device is being reported to be in active use.

View File

@ -18,11 +18,13 @@ from ereuse_devicehub.db import db
from ereuse_devicehub.dummy.dummy import Dummy
from ereuse_devicehub.resources.device.search import DeviceSearch
from ereuse_devicehub.resources.inventory import Inventory, InventoryDef
from ereuse_devicehub.templating import Environment
class Devicehub(Teal):
test_client_class = Client
Dummy = Dummy
jinja_environment = Environment
def __init__(self,
inventory: str,

View File

@ -104,7 +104,7 @@ class Dummy:
# Perform generic actions
for pc, model in zip(pcs,
{m.ToRepair, m.Repair, m.ToPrepare, m.Available, m.ToPrepare,
{m.ToRepair, m.Repair, m.ToPrepare, m.Ready, m.ToPrepare,
m.Prepare}):
user.post({'type': model.t, 'devices': [pc]}, res=m.Action)
@ -144,7 +144,7 @@ class Dummy:
user.post({'type': m.ToPrepare.t, 'devices': [sample_pc]}, res=m.Action)
user.post({'type': m.Prepare.t, 'devices': [sample_pc]}, res=m.Action)
user.post({'type': m.Available.t, 'devices': [sample_pc]}, res=m.Action)
user.post({'type': m.Ready.t, 'devices': [sample_pc]}, res=m.Action)
user.post({'type': m.Price.t, 'device': sample_pc, 'currency': 'EUR', 'price': 85},
res=m.Action)
# todo test reserve

View File

@ -188,9 +188,9 @@ class RepairDef(ActionDef):
SCHEMA = schemas.Repair
class Available(ActionDef):
class ReadyDef(ActionDef):
VIEW = None
SCHEMA = schemas.Available
SCHEMA = schemas.Ready
class ToPrepareDef(ActionDef):
@ -233,6 +233,11 @@ class RentDef(ActionDef):
SCHEMA = schemas.Rent
class MakeAvailable(ActionDef):
VIEW = None
SCHEMA = schemas.MakeAvailable
class CancelTradeDef(ActionDef):
VIEW = None
SCHEMA = schemas.CancelTrade

View File

@ -1256,7 +1256,7 @@ class Repair(ActionWithMultipleDevices):
"""
class Available(ActionWithMultipleDevices):
class Ready(ActionWithMultipleDevices):
"""The device is ready to be used.
This involves greater preparation from the ``Prepare`` action,
@ -1420,6 +1420,11 @@ class DisposeProduct(Trade):
# ``RecyclingCenter``.
class MakeAvailable(ActionWithMultipleDevices):
"""The act of setting willingness for trading."""
pass
class Receive(JoinedTableMixin, ActionWithMultipleDevices):
"""The act of physically taking delivery of a device.

View File

@ -434,7 +434,7 @@ class Repair(ActionWithMultipleDevices):
pass
class ReadyToUse(ActionWithMultipleDevices):
class Ready(ActionWithMultipleDevices):
pass
@ -505,6 +505,10 @@ class Rent(Trade):
pass
class MakeAvailable(ActionWithMultipleDevices):
pass
class CancelTrade(Trade):
pass

View File

@ -47,7 +47,11 @@ class ActionWithOneDevice(Action):
class ActionWithMultipleDevices(Action):
__doc__ = m.ActionWithMultipleDevices.__doc__
devices = NestedOn(s_device.Device, many=True, only_query='id', collection_class=OrderedSet)
devices = NestedOn(s_device.Device,
many=True,
required=True, # todo test ensuring len(devices) >= 1
only_query='id',
collection_class=OrderedSet)
class Add(ActionWithOneDevice):
@ -347,8 +351,8 @@ class Repair(ActionWithMultipleDevices):
__doc__ = m.Repair.__doc__
class Available(ActionWithMultipleDevices):
__doc__ = m.Available.__doc__
class Ready(ActionWithMultipleDevices):
__doc__ = m.Ready.__doc__
class ToPrepare(ActionWithMultipleDevices):
@ -405,6 +409,10 @@ class Rent(Trade):
__doc__ = m.Rent.__doc__
class MakeAvailable(ActionWithMultipleDevices):
__doc__ = m.MakeAvailable.__doc__
class CancelTrade(Trade):
__doc__ = m.CancelTrade.__doc__

View File

@ -302,9 +302,9 @@ class Mixer(CookingDef):
SCHEMA = schemas.Mixer
class DrillDef(DeviceDef):
class DIYAndGardeningDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Drill
SCHEMA = schemas.DIYAndGardening
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
@ -313,7 +313,12 @@ class DrillDef(DeviceDef):
url_prefix, subdomain, url_defaults, root_path, cli_commands)
class PackOfScrewdriversDef(DeviceDef):
class DrillDef(DIYAndGardeningDef):
VIEW = None
SCHEMA = schemas.Drill
class PackOfScrewdriversDef(DIYAndGardeningDef):
VIEW = None
SCHEMA = schemas.PackOfScrewdrivers
@ -324,21 +329,31 @@ class PackOfScrewdriversDef(DeviceDef):
url_prefix, subdomain, url_defaults, root_path, cli_commands)
class DehumidifierDef(DeviceDef):
class HomeDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Home
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
class DehumidifierDef(HomeDef):
VIEW = None
SCHEMA = schemas.Dehumidifier
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
class StairsDef(DeviceDef):
class StairsDef(HomeDef):
VIEW = None
SCHEMA = schemas.Stairs
class RecreationDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Recreation
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
@ -346,27 +361,15 @@ class StairsDef(DeviceDef):
url_prefix, subdomain, url_defaults, root_path, cli_commands)
class BikeDef(DeviceDef):
class BikeDef(RecreationDef):
VIEW = None
SCHEMA = schemas.Bike
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
class RacketDef(DeviceDef):
class RacketDef(RecreationDef):
VIEW = None
SCHEMA = schemas.Racket
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
class ManufacturerDef(Resource):
VIEW = ManufacturerView

View File

@ -79,14 +79,14 @@ class Device(Thing):
generation = db.Column(db.SmallInteger, check_range('generation', 0))
generation.comment = """The generation of the device."""
version = db.Column(db.CIText())
version.comment = """The version code this device, like v1 or A001."""
weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5))
weight.comment = """The weight of the device."""
width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5))
version.comment = """The version code of this device, like v1 or A001."""
weight = Column(Float(decimal_return_scale=4), check_range('weight', 0.1, 5))
weight.comment = """The weight of the device in Kg."""
width = Column(Float(decimal_return_scale=4), check_range('width', 0.1, 5))
width.comment = """The width of the device in meters."""
height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5))
height = Column(Float(decimal_return_scale=4), check_range('height', 0.1, 5))
height.comment = """The height of the device in meters."""
depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5))
depth = Column(Float(decimal_return_scale=4), check_range('depth', 0.1, 5))
depth.comment = """The depth of the device in meters."""
color = Column(ColorType)
color.comment = """The predominant color of the device."""
@ -101,6 +101,8 @@ class Device(Thing):
sku.comment = """The Stock Keeping Unit (SKU), i.e. a
merchant-specific identifier for a product or service.
"""
image = db.Column(db.URL)
image.comment = "An image of the device."
_NON_PHYSICAL_PROPS = {
'id',
@ -120,7 +122,8 @@ class Device(Thing):
'production_date',
'variant',
'version',
'sku'
'sku',
'image'
}
__table_args__ = (
@ -167,7 +170,7 @@ class Device(Thing):
def physical_properties(self) -> Dict[str, object or None]:
"""Fields that describe the physical properties of a device.
:return A generator where each value is a tuple with tho fields:
:return A dictionary:
- Column.
- Actual value of the column or None.
"""
@ -291,9 +294,13 @@ class Device(Thing):
if 't' in format_spec:
v += '{0.t} {0.model}'.format(self)
if 's' in format_spec:
v += '({0.manufacturer})'.format(self)
superclass = self.__class__.mro()[1]
if not isinstance(self, Device) and superclass != Device:
assert issubclass(superclass, Thing)
v += superclass.__name__ + ' '
v += '{0.manufacturer}'.format(self)
if self.serial_number:
v += ' S/N ' + self.serial_number.upper()
v += ' ' + self.serial_number.upper()
return v
@ -787,7 +794,11 @@ class Mixer(Cooking):
pass
class Drill(Device):
class DIYAndGardening(Device):
pass
class Drill(DIYAndGardening):
max_drill_bit_size = db.Column(db.SmallInteger)
@ -795,21 +806,29 @@ class PackOfScrewdrivers(Device):
pass
class Dehumidifier(Device):
class Home(Device):
pass
class Dehumidifier(Home):
size = db.Column(db.SmallInteger)
size.comment = """The capacity in Liters."""
class Stairs(Device):
class Stairs(Home):
max_allowed_weight = db.Column(db.Integer)
class Bike(Device):
class Recreation(Device):
pass
class Bike(Recreation):
wheel_size = db.Column(db.SmallInteger)
gears = db.Column(db.SmallInteger)
class Racket(Device):
class Racket(Recreation):
pass

View File

@ -45,6 +45,7 @@ class Device(Thing):
version = ... # type: Column
variant = ... # type: Column
sku = ... # type: Column
image = ... #type: Column
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
@ -70,6 +71,7 @@ class Device(Thing):
self.version = ... # type: Optional[str]
self.variant = ... # type: Optional[str]
self.sku = ... # type: Optional[str]
self.image = ... # type: Optional[urlutils.URL]
@property
def actions(self) -> List[e.Action]:

View File

@ -60,8 +60,9 @@ class Device(Thing):
many=True,
dump_only=True,
description=m.Device.working.__doc__)
variant = SanitizedStr(description=m.Device.variant)
sku = SanitizedStr(description=m.Device.sku)
variant = SanitizedStr(description=m.Device.variant.comment)
sku = SanitizedStr(description=m.Device.sku.comment)
image = URL(description=m.Device.image.comment)
@pre_load
def from_actions_to_actions_one(self, data: dict):
@ -413,26 +414,38 @@ class Mixer(Cooking):
__doc__ = m.Mixer.__doc__
class Drill(Device):
class DIYAndGardening(Device):
pass
class Drill(DIYAndGardening):
max_drill_bit_size = Integer(data_key='maxDrillBitSize')
class PackOfScrewdrivers(Device):
class PackOfScrewdrivers(DIYAndGardening):
size = Integer()
class Dehumidifier(Device):
class Home(Device):
pass
class Dehumidifier(Home):
size = Integer()
class Stairs(Device):
class Stairs(Home):
max_allowed_weight = Integer(data_key='maxAllowedWeight')
class Bike(Device):
class Recreation(Device):
pass
class Bike(Recreation):
wheel_size = Integer(data_key='wheelSize')
gears = Integer()
class Racket(Device):
class Racket(Recreation):
pass

View File

@ -40,6 +40,7 @@ class Trading(State):
# todo add Pay = e.Pay
ToBeDisposed = e.ToDisposeProduct
ProductDisposed = e.DisposeProduct
Available = e.MakeAvailable
class Physical(State):
@ -49,12 +50,12 @@ class Physical(State):
:cvar Repaired: The device has been repaired.
:cvar Preparing: The device is going to be or being prepared.
:cvar Prepared: The device has been prepared.
:cvar ReadyToBeUsed: The device is in working conditions.
:cvar Ready: The device is in working conditions.
:cvar InUse: The device is being reported to be in active use.
"""
ToBeRepaired = e.ToRepair
Repaired = e.Repair
Preparing = e.ToPrepare
Prepared = e.Prepare
ReadyToBeUsed = e.Available
Ready = e.Ready
InUse = e.Live

View File

@ -8,6 +8,7 @@
rel="stylesheet"
integrity="sha384-+ENW/yibaokMnme+vBLnHMphUYxHs34h9lpdbSLuAwGkOKFRl4C34WkjazBtb7eT"
crossorigin="anonymous">
<script src="https://use.fontawesome.com/7553aecc27.js"></script>
<title>Devicehub | {{ device.__format__('t') }}</title>
</head>
<body>
@ -22,55 +23,39 @@
</a>
</div>
</nav>
<div class="jumbotron">
<img class="center-block"
style="height: 13em; padding-bottom: 0.1em"
src="{{ url_for('Device.static', filename='magrama.svg') }}">
</div>
<div class="container">
<div class="page-header">
<div class="container-fluid">
<div class="row">
<div class="page-header col-md-6 col-md-offset-3">
<h1>{{ device.__format__('t') }}<br>
<small>{{ device.__format__('s') }}</small>
</h1>
</div>
</div>
<div class="container">
<h2 class='text-center'>
This is your {{ device.t }}.
</h2>
<p class="text-center">
{% if device.trading %}
{{ device.trading }}
{% endif %}
{% if device.trading and device.physical %}
and
{% endif %}
{% if device.physical %}
{{ device.physical }}
{% endif %}
</p>
</div>
<div class="row">
<article class="col-md-6">
<h3>You can verify the originality of your device.</h3>
<p>
If your device comes with the following tag
<img class="img-responsive center-block" style="width: 12em;"
src="{{ url_for('Device.static', filename='photochromic-alone.svg') }}">
it means it has been refurbished by an eReuse.org
certified organization.
</p>
<p>
The tag is special illuminate it with the torch of
your phone for 6 seconds and it will react like in
the following image:
<img class="img-responsive center-block" style="width: 30em;"
src="{{ url_for('Device.static', filename='photochromic-tag-web.svg') }}">
This is proof that this device is genuine.
</p>
</article>
<article class="col-md-6">
<h3>These are the specifications</h3>
<div class="col-md-3">
{% if device.image %}
<a href="{{ device.image.to_text() }}" class="thumbnail" target="_blank">
<img src="{{ device.image.to_text() }}"
alt="Cykel.JPG">
</a>
{% endif %}
</div>
<div class="col-md-6">
{% if device.trading == states.Trading.Available %}
<div class="alert alert-success">
<i class="fa fa-check-circle"></i> {{ device.trading }}
</div>
{% else %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-circle"></i> Not available.
</div>
{% endif %}
<ul>
{% for key, value in device.physical_properties.items() %}
<li>{{ key }}: {{ value }}
{% endfor %}
</ul>
{% if isinstance(device, d.Computer) %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
@ -189,7 +174,7 @@
</tbody>
</table>
</div>
<h3>This is the traceability log of your device</h3>
<h4>Public traceability log of the device</h4>
<div class="text-right">
<small>Latest one.</small>
</div>
@ -216,9 +201,9 @@
<div class="text-right">
<small>Oldest one.</small>
</div>
</article>
{% endif %}
</div>
</div>
</div>
</body>
</html>

View File

@ -14,6 +14,7 @@ from ereuse_devicehub.db import db
from ereuse_devicehub.query import SearchQueryParser, things_response
from ereuse_devicehub.resources import search
from ereuse_devicehub.resources.action import models as actions
from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.device.models import Device, Manufacturer
from ereuse_devicehub.resources.device.search import DeviceSearch
from ereuse_devicehub.resources.lot.models import LotDeviceDescendants
@ -101,7 +102,7 @@ class DeviceView(View):
def one_public(self, id: int):
device = Device.query.filter_by(id=id).one()
return render_template('devices/layout.html', device=device)
return render_template('devices/layout.html', device=device, states=states)
@auth.Auth.requires_auth
def one_private(self, id: int):

View File

@ -0,0 +1,13 @@
import flask.templating
import ereuse_devicehub.resources.device.models
class Environment(flask.templating.Environment):
"""As flask's environment but with some globals set"""
def __init__(self, app, **options):
super().__init__(app, **options)
self.globals[isinstance.__name__] = isinstance
self.globals[issubclass.__name__] = issubclass
self.globals['d'] = ereuse_devicehub.resources.device.models

View File

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

View File

@ -468,18 +468,18 @@ def test_device_properties_format(app: Devicehub, user: UserClient):
assert format(net) == 'NetworkAdapter 2: model ar8121/ar8113/ar8114 ' \
'gigabit or fast ethernet, S/N 00:24:8c:7f:cf:2d'
assert format(net, 't') == 'NetworkAdapter ar8121/ar8113/ar8114 gigabit or fast ethernet'
assert format(net, 's') == '(qualcomm atheros) S/N 00:24:8C:7F:CF:2D 100 Mbps'
assert format(net, 's') == 'qualcomm atheros 00:24:8C:7F:CF:2D 100 Mbps'
hdd = next(c for c in pc.components if isinstance(c, d.DataStorage))
assert format(hdd) == 'HardDrive 7: model st9160310as, S/N 5sv4tqa6'
assert format(hdd, 't') == 'HardDrive st9160310as'
assert format(hdd, 's') == '(seagate) S/N 5SV4TQA6 152 GB'
assert format(hdd, 's') == 'seagate 5SV4TQA6 152 GB'
def test_device_public(user: UserClient, client: Client):
s, _ = user.post(file('asus-eee-1000h.snapshot.11'), res=m.Snapshot)
html, _ = client.get(res=d.Device, item=s['device']['id'], accept=ANY)
assert 'intel atom cpu n270 @ 1.60ghz' in html
assert 'S/N 00:24:8C:7F:CF:2D 100 Mbps' in html
assert '00:24:8C:7F:CF:2D 100 Mbps' in html
@pytest.mark.usefixtures(conftest.app_context.__name__)

View File

@ -1,7 +1,7 @@
import ipaddress
from datetime import timedelta
from decimal import Decimal
from typing import Tuple
from typing import Tuple, Type
import pytest
from flask import current_app as app, g
@ -168,7 +168,7 @@ def test_update_components_action_multiple():
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
computer.components.add(hdd)
ready = models.Available()
ready = models.Ready()
assert not ready.devices
assert not ready.components
@ -214,7 +214,7 @@ def test_update_parent():
(models.ToRepair, states.Physical.ToBeRepaired),
(models.Repair, states.Physical.Repaired),
(models.ToPrepare, states.Physical.Preparing),
(models.Available, states.Physical.ReadyToBeUsed),
(models.Ready, states.Physical.Ready),
(models.Prepare, states.Physical.Prepared)
]))
def test_generic_action(action_model_state: Tuple[models.Action, states.Trading],
@ -268,24 +268,27 @@ def test_reserve_and_cancel(user: UserClient):
@pytest.mark.parametrize('action_model_state',
(pytest.param(ams, id=ams[0].__class__.__name__)
(pytest.param(ams, id=ams[0].__name__)
for ams in [
(models.MakeAvailable, states.Trading.Available),
(models.Sell, states.Trading.Sold),
(models.Donate, states.Trading.Donated),
(models.Rent, states.Trading.Renting),
(models.DisposeProduct, states.Trading.ProductDisposed)
]))
def test_trade(action_model_state: Tuple[models.Action, states.Trading], user: UserClient):
def test_trade(action_model_state: Tuple[Type[models.Action], states.Trading], user: UserClient):
"""Tests POSTing all Trade actions."""
# todo missing None states.Trading for after cancelling renting, for example
action_model, state = action_model_state
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
action = {
'type': action_model.t,
'devices': [snapshot['device']['id']],
'to': user.user['individuals'][0]['id'],
'shippingDate': '2018-06-29T12:28:54',
'invoiceNumber': 'ABC'
'devices': [snapshot['device']['id']]
}
if issubclass(action_model, models.Trade):
action['to'] = user.user['individuals'][0]['id']
action['shippingDate'] = '2018-06-29T12:28:54'
action['invoiceNumber'] = 'ABC'
action, _ = user.post(action, res=models.Action)
assert action['devices'][0]['id'] == snapshot['device']['id']
device, _ = user.get(res=Device, item=snapshot['device']['id'])