Merge remote-tracking branch 'origin/master' into reports

This commit is contained in:
JNadeu 2018-10-17 12:31:31 +02:00
commit c4129a2a1a
22 changed files with 616 additions and 107 deletions

View file

@ -35,7 +35,9 @@ call the new file ``app.py``.
Create a PostgreSQL database called *devicehub* by running
[create-db](examples/create-db.sh):
- In Debian 9: `sudo su - postgres; examples/create-db.sh devicehub`
- In a Debian 9 terminal, execute the following two commands:
1. `sudo su - postgres`.
2. `bash examples/create-db.sh devicehub`.
- In MacOS: `examples/create-db.sh devicehub`.
Create the tables in the database by executing in the same directory

View file

@ -58,16 +58,28 @@ class Dummy:
'-o', org_id
],
catch_exceptions=False)
# create tag for pc-laudem
runner.invoke(args=[
'create-tag', 'tagA',
'-p', 'https://t.devicetag.io',
'-s', 'tagA-secondary'
],
catch_exceptions=False)
files = tuple(Path(__file__).parent.joinpath('files').iterdir())
print('done.')
sample_pc = None # We treat this one as a special sample for demonstrations
pcs = set() # type: Set[int]
with click.progressbar(files, label='Creating devices...'.ljust(28)) as bar:
for path in bar:
with path.open() as f:
snapshot = yaml.load(f)
s, _ = user.post(res=m.Snapshot, data=snapshot)
pcs.add(s['device']['id'])
if s.get('uuid', None) == 'ec23c11b-80b6-42cd-ac5c-73ba7acddbc4':
sample_pc = s['device']['id']
else:
pcs.add(s['device']['id'])
assert sample_pc
print('PC sample is', sample_pc)
# Link tags and eTags
for tag, pc in zip((self.TAGS[1], self.TAGS[2], self.ET[0][0], self.ET[1][1]), pcs):
user.put({}, res=Tag, item='{}/device/{}'.format(tag, pc), status=204)
@ -105,9 +117,29 @@ class Dummy:
assert len(inventory['items'])
i, _ = user.get(res=Device, query=[('search', 'intel')])
assert len(i['items']) == 10
i, _ = user.get(res=Device, query=[('search', 'pc')])
assert len(i['items']) == 11
i, _ = user.get(res=Device, query=[('search', 'pc')])
assert len(i['items']) == 12
# Let's create a set of events for the pc device
# Make device Ready
user.post({'type': m.ToPrepare.t, 'devices': [sample_pc]}, res=m.Event)
user.post({'type': m.Prepare.t, 'devices': [sample_pc]}, res=m.Event)
user.post({'type': m.ReadyToUse.t, 'devices': [sample_pc]}, res=m.Event)
user.post({'type': m.Price.t, 'device': sample_pc, 'currency': 'EUR', 'price': 85},
res=m.Event)
# todo test reserve
user.post( # Sell device
{
'type': m.Sell.t,
'to': user.user['individuals'][0]['id'],
'devices': [sample_pc]
},
res=m.Event)
# todo Receive
# For netbook: to preapre -> torepair -> to dispose -> disposed
print('⭐ Done.')
def user_client(self, email: str, password: str):

View file

@ -0,0 +1,146 @@
{
"closed": true,
"components": [
{
"events": [],
"manufacturer": "Intel Corporation",
"model": "82567LM-3 Gigabit Network Connection",
"serialNumber": "00:23:7d:49:5e:31",
"speed": 1000,
"type": "NetworkAdapter",
"wireless": false
},
{
"events": [],
"manufacturer": "Intel Corporation",
"model": "82801JD/DO HD Audio Controller",
"serialNumber": null,
"type": "SoundCard"
},
{
"events": [],
"format": "DIMM",
"interface": "DDR2",
"manufacturer": null,
"model": "HYMP125U64CP8-S6",
"serialNumber": null,
"size": 2048,
"speed": 800.0,
"type": "RamModule"
},
{
"address": 64,
"cores": 2,
"events": [
{
"elapsed": 0,
"rate": 11970.54,
"type": "BenchmarkProcessor"
},
{
"elapsed": 20,
"rate": 19.6233,
"type": "BenchmarkProcessorSysbench"
}
],
"manufacturer": "Intel Corp.",
"model": "Intel Core2 Duo CPU E8400 @ 3.00GHz",
"serialNumber": null,
"speed": 3.0,
"threads": 2,
"type": "Processor"
},
{
"events": [
{
"elapsed": 16,
"readSpeed": 76.8,
"type": "BenchmarkDataStorage",
"writeSpeed": 21.1
},
{
"assessment": true,
"currentPendingSectorCount": 0,
"elapsed": 134,
"error": false,
"length": "Short",
"lifetime": 19549,
"offlineUncorrectable": 0,
"powerCycleCount": 3354,
"reallocatedSectorCount": 33,
"reportedUncorrectableErrors": 0,
"status": "Completed without error",
"type": "TestDataStorage"
}
],
"interface": "ATA",
"manufacturer": "Seagate",
"model": "ST3160815AS",
"serialNumber": "6RX7AWEZ",
"size": 152627,
"type": "HardDrive"
},
{
"events": [],
"manufacturer": "Intel Corporation",
"memory": 256.0,
"model": "4 Series Chipset Integrated Graphics Controller",
"serialNumber": null,
"type": "GraphicCard"
},
{
"events": [],
"firewire": 0,
"manufacturer": "Hewlett-Packard",
"model": "3031h",
"pcmcia": 0,
"serial": 0,
"serialNumber": "CZC901381R",
"slots": 0,
"type": "Motherboard",
"usb": 8
}
],
"device": {
"chassis": "Tower",
"events": [
{
"elapsed": 60,
"error": false,
"type": "StressTest"
},
{
"elapsed": 6,
"rate": 5.7674,
"type": "BenchmarkRamSysbench"
},
{
"appearanceRange": "A",
"biosRange": "A",
"functionalityRange": "A",
"type": "WorkbenchRate"
}
],
"manufacturer": "Hewlett-Packard",
"model": "HP Compaq dc7900 Small Form Factor",
"serialNumber": "CZC901381R",
"tags": [
{
"id": "tagA-secondary",
"type": "Tag"
}
],
"type": "Desktop"
},
"elapsed": 238,
"endTime": "2018-10-15T13:59:37.431309+00:00",
"expectedEvents": [
"Benchmark",
"TestDataStorage",
"StressTest"
],
"software": "Workbench",
"type": "Snapshot",
"uuid": "ec23c11b-80b6-42cd-ac5c-73ba7acddbc4",
"version": "11.0a6"
}

View file

@ -14,7 +14,8 @@ class DeviceDef(Resource):
AUTH = False # We manage this at each view
def __init__(self, app,
import_name=__name__, static_folder=None,
import_name=__name__,
static_folder='static',
static_url_path=None,
template_folder='templates',
url_prefix=None,
@ -30,6 +31,12 @@ class ComputerDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Computer
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 DesktopDef(ComputerDef):
VIEW = None
@ -50,6 +57,12 @@ class MonitorDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Monitor
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 ComputerMonitorDef(MonitorDef):
VIEW = None
@ -65,6 +78,12 @@ class MobileDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Mobile
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 SmartphoneDef(MobileDef):
VIEW = None
@ -85,6 +104,12 @@ class ComponentDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Component
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 GraphicCardDef(ComponentDef):
VIEW = None

View file

@ -187,7 +187,9 @@ class Device(Thing):
if 't' in format_spec:
v += '{0.t} {0.model}'.format(self)
if 's' in format_spec:
v += '({0.manufacturer}) S/N {0.serial_number}'.format(self)
v += '({0.manufacturer})'.format(self)
if self.serial_number:
v += ' S/N ' + self.serial_number.upper()
return v
@ -272,7 +274,9 @@ class Computer(Device):
if 't' in format_spec:
v += '{0.chassis} {0.model}'.format(self)
elif 's' in format_spec:
v += '({0.manufacturer}) S/N {0.serial_number}'.format(self)
v += '({0.manufacturer})'.format(self)
if self.serial_number:
v += ' S/N ' + self.serial_number.upper()
return v

View file

@ -1,5 +1,7 @@
from enum import Enum
import inflection
from ereuse_devicehub.resources.event import models as e
@ -9,6 +11,9 @@ class State(Enum):
"""Events participating in this state."""
return (s.value for s in cls)
def __str__(self):
return inflection.humanize(inflection.underscore(self.name))
class Trading(State):
Reserved = e.Reserve

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC
"-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:serif="http://www.serif.com/" width="100%" height="100%" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;"><rect id="Photochromic-alone" serif:id="Photochromic alone" x="0" y="0" width="45.84" height="45.84" style="fill:none;"/><path d="M43.96,23.198c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z" style="fill:#fff;"/><clipPath id="_clip1"><path d="M43.96,23.198c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z"/></clipPath><g clip-path="url(#_clip1)"><clipPath id="_clip2"><rect x="4.275" y="4.975" width="36.85" height="36.85"/></clipPath><g clip-path="url(#_clip2)"><g id="Artboard1"><rect x="4.275" y="4.975" width="36.71" height="36.701" style="fill:none;"/><g id="Logo-01"><path d="M6.749,38.053c4.02,-13.193 12.218,-7.047 23.011,-10.325c17.626,-5.353 -0.052,-29.186 -15.117,-16.725c-15.241,12.606 -3.74,38.876 19.597,23.634" style="fill:none;stroke:#bfff04;stroke-width:4.6px;"/><path d="M36.496,29.706l2.816,5.333l-6.526,4.015c0,0 -1.033,0.738 -1.598,-0.147c-0.565,-0.887 -1.553,-2.671 -2.028,-3.741c-0.364,-0.817 0.661,-1.324 0.661,-1.324l6.675,-4.136Z" style="fill:#bfff04;fill-rule:nonzero;stroke:#bfff04;stroke-width:0.17px;stroke-linecap:butt;stroke-miterlimit:1.41421;"/><path d="M39.031,33.89l-2.012,-3.698l1.953,-1.153l2.012,3.699l-1.953,1.152Z" style="fill:#bfff04;fill-rule:nonzero;stroke:#bfff04;stroke-width:0.17px;stroke-linecap:butt;stroke-miterlimit:1.41421;"/></g></g></g></g><path d="M43.96,23.198c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z" style="fill:none;stroke:#333;stroke-width:0.1px;stroke-linejoin:miter;stroke-miterlimit:11;"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC
"-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:serif="http://www.serif.com/" width="100%" height="100%" viewBox="0 0 124 44" version="1.1" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:11;"><rect id="Photochromic-Tag-web" serif:id="Photochromic Tag web" x="0" y="0" width="123.413" height="43.733" style="fill:none;"/><clipPath id="_clip1"><rect x="0" y="0" width="123.413" height="43.733"/></clipPath><g clip-path="url(#_clip1)"><g><path d="M44.175,28.681l0,-4.925l24.55,0l0,-2.496l10.513,4.959l-10.513,4.958l0,-2.496l-24.55,0Z" style="fill:#333;stroke:#333;stroke-width:0.85px;"/><text x="49.508px" y="19.727px" style="font-family:'Lato-Regular', 'Lato', sans-serif;font-size:24.107px;fill:#231f20;">6s</text></g><path d="M42.52,21.058c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z" style="fill:#fff;"/><clipPath id="_clip2"><path d="M42.52,21.058c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z"/></clipPath><g clip-path="url(#_clip2)"><clipPath id="_clip3"><rect x="2.835" y="2.835" width="36.85" height="36.85"/></clipPath><g clip-path="url(#_clip3)"><g id="Artboard1"><rect x="2.835" y="2.835" width="36.71" height="36.701" style="fill:none;"/><g id="Logo-01"><path d="M5.309,35.913c4.02,-13.194 12.218,-7.047 23.011,-10.325c17.626,-5.353 -0.052,-29.186 -15.117,-16.725c-15.241,12.605 -3.74,38.876 19.597,23.634" style="fill:none;stroke:#bfff04;stroke-width:4.6px;stroke-linejoin:round;stroke-miterlimit:10;"/><path d="M35.056,27.566l2.816,5.333l-6.526,4.014c0,0 -1.033,0.739 -1.598,-0.147c-0.565,-0.886 -1.553,-2.67 -2.028,-3.74c-0.364,-0.817 0.661,-1.324 0.661,-1.324l6.675,-4.136Z" style="fill:#bfff04;fill-rule:nonzero;stroke:#bfff04;stroke-width:0.17px;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:1.41421;"/><path d="M37.591,31.75l-2.012,-3.699l1.953,-1.152l2.012,3.699l-1.953,1.152Z" style="fill:#bfff04;fill-rule:nonzero;stroke:#bfff04;stroke-width:0.17px;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:1.41421;"/></g></g></g></g><path d="M42.52,21.058c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z" style="fill:none;stroke:#333;stroke-width:0.1px;"/><path d="M123.005,21.058c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z" style="fill:#fff;"/><clipPath id="_clip4"><path d="M123.005,21.058c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z"/></clipPath><g clip-path="url(#_clip4)"><clipPath id="_clip5"><rect x="83.32" y="2.835" width="36.85" height="36.85"/></clipPath><g clip-path="url(#_clip5)"><g id="Artboard11" serif:id="Artboard1"><rect x="83.32" y="2.835" width="36.71" height="36.701" style="fill:none;"/><path d="M114.217,23.7c0.896,-2.192 1.142,-4.517 0.713,-6.743c-1.347,-6.998 -8.906,-11.33 -16.87,-9.668c-7.965,1.662 -13.337,8.693 -11.99,15.691c0.428,2.225 1.516,4.273 3.153,5.936l24.994,-5.216Z" style="fill:#e5f20d;"/><g id="Logo-011" serif:id="Logo-01"><path d="M85.795,35.92c4.019,-13.193 12.217,-7.047 23.01,-10.325c17.626,-5.353 -0.052,-29.186 -15.117,-16.725c-15.241,12.606 -3.74,38.876 19.597,23.634" style="fill:none;stroke:#bfff04;stroke-width:4.6px;stroke-linejoin:round;stroke-miterlimit:10;"/><path d="M115.541,27.573l2.816,5.333l-6.526,4.015c0,0 -1.033,0.738 -1.597,-0.147c-0.565,-0.887 -1.553,-2.671 -2.029,-3.741c-0.363,-0.817 0.661,-1.324 0.661,-1.324l6.675,-4.136Z" style="fill:#bfff04;fill-rule:nonzero;stroke:#bfff04;stroke-width:0.17px;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:1.41421;"/><path d="M118.077,31.757l-2.013,-3.698l1.953,-1.152l2.013,3.698l-1.953,1.152Z" style="fill:#bfff04;fill-rule:nonzero;stroke:#bfff04;stroke-width:0.17px;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:1.41421;"/></g></g></g></g><path d="M123.005,21.058c0,-11.622 -9.436,-21.058 -21.058,-21.058l-0.404,0c-11.622,0 -21.058,9.436 -21.058,21.058l0,0.404c0,11.622 9.436,21.058 21.058,21.058l0.404,0c11.622,0 21.058,-9.436 21.058,-21.058l0,-0.404Z" style="fill:none;stroke:#333;stroke-width:0.1px;"/></g></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -10,87 +10,212 @@
crossorigin="anonymous">
<title>Devicehub | {{ device.__format__('t') }}</title>
</head>
<body class="container">
<body>
<div class="page-header">
<h1>{{ device.__format__('t') }}
<small>{{ device.__format__('s') }}</small>
</h1>
<nav class="navbar navbar-default" style="background-color: gainsboro; margin: 0 !important">
<div class="container-fluid">
<a href="https://www.ereuse.org/" target="_blank">
<img alt="Brand"
class="center-block"
style="height: 4em; padding-bottom: 0.1em"
src="{{ url_for('Device.static', filename='ereuse-logo.svg') }}">
</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">
<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>
<div class="row">
<article class="col-md-6">
<table class="table">
<thead>
<tr>
<th></th>
<th>Range</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<details>
<summary>CPU {{ device.processor_model }}</summary>
{{ macros.component_type(device.components, 'Processor') }}
</details>
</td>
<td></td>
</tr>
<tr>
<td>
<details>
<summary>RAM {{ device.ram_size // 1000 }} GB</summary>
{{ macros.component_type(device.components, 'RamModule') }}
</details>
</td>
<td>//range//</td>
</tr>
<tr>
<td>
<details>
<summary>Data Storage {{ device.data_storage_size // 1000 }} GB</summary>
{{ macros.component_type(device.components, 'SolidStateDrive') }}
{{ macros.component_type(device.components, 'HardDrive') }}
</details>
</td>
<td>//range//</td>
</tr>
<tr>
<td>
<details>
<summary>Graphics {{ device.graphic_card_model }}</summary>
{{ macros.component_type(device.components, 'GraphicCard') }}
</details>
</td>
<td>//range//</td>
</tr>
<tr>
<td>
<details>
<summary>Network
{% if device.network_speeds[0] %}
Ethernet of {{ device.network_speeds[0] }} Mbps
{% endif %}
{% if device.network_speeds[0] and device.network_speeds[1] %}
+
{% endif %}
{% if device.network_speeds[1] %}
WiFi of {{ device.network_speeds[1] }} Mbps
{% endif %}
</summary>
{{ macros.component_type(device.components, 'NetworkAdapter') }}
</details>
</td>
<td></td>
</tr>
</tbody>
</table>
</article>
<aside class="col-md-6">
<h2>Check the validity of the device</h2>
<p>Use the flashlight to scan...</p>
</aside>
<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 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="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th>Range</th>
</tr>
</thead>
<tbody>
{% if device.processor_model %}
<tr>
<td>
CPU {{ device.processor_model }}
</td>
<td>
{% if device.rate %}
{{ device.rate.processor_range }}
({{ device.rate.processor }})
{% endif %}
</td>
</tr>
{% endif %}
{% if device.ram_size %}
<tr>
<td>
RAM {{ device.ram_size // 1000 }} GB
{{ macros.component_type(device.components, 'RamModule') }}
</td>
<td>
{% if device.rate %}
{{ device.rate.ram_range }}
({{ device.rate.ram }})
{% endif %}
</td>
</tr>
{% endif %}
{% if device.data_storage_size %}
<tr>
<td>
Data Storage {{ device.data_storage_size // 1000 }} GB
{{ macros.component_type(device.components, 'SolidStateDrive') }}
{{ macros.component_type(device.components, 'HardDrive') }}
</td>
<td>
{% if device.rate %}
{{ device.rate.data_storage_range }}
({{ device.rate.data_storage }})
{% endif %}
</td>
</tr>
{% endif %}
{% if device.graphic_card_model %}
<tr>
<td>
Graphics {{ device.graphic_card_model }}
{{ macros.component_type(device.components, 'GraphicCard') }}
</td>
<td></td>
</tr>
{% endif %}
{% if device.network_speeds %}
<tr>
<td>
Network
{% if device.network_speeds[0] %}
Ethernet
{% if device.network_speeds[0] != None %}
max. {{ device.network_speeds[0] }} Mbps
{% endif %}
{% endif %}
{% if device.network_speeds[0] and device.network_speeds[1] %}
+
{% endif %}
{% if device.network_speeds[1] %}
WiFi
{% if device.network_speeds[1] != None %}
max. {{ device.network_speeds[1] }} Mbps
{% endif %}
{% endif %}
{{ macros.component_type(device.components, 'NetworkAdapter') }}
</td>
<td></td>
</tr>
{% endif %}
{% if device.rate %}
<tr class="active">
<td class="text-right">
Total rate
</td>
<td>
{{ device.rate.rating_range }}
({{ device.rate.rating }})
</td>
</tr>
{% endif %}
{% if device.rate and device.rate.price %}
<tr class="active">
<td class="text-right">
Algorithm price
</td>
<td>
{{ device.rate.price }}
</td>
</tr>
{% endif %}
{% if device.price %}
<tr class="active">
<td class="text-right">
Actual price
</td>
<td>
{{ device.price }}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<h3>This is the traceability log of your device</h3>
<div class="text-right">
<small>Latest one.</small>
</div>
<ol>
{% for event in device.events|reverse %}
<li>
<strong>
{{ event.type }}
</strong>
{{ event }}
<br>
<div class="text-muted">
<small>
{{ event._date_str }}
</small>
</div>
</li>
{% endfor %}
</ol>
<div class="text-right">
<small>Oldest one.</small>
</div>
</article>
</div>
</div>
</body>

View file

@ -1,10 +1,10 @@
{% macro component_type(components, type) %}
<ul class="list-unstyled">
<ul>
{% for c in components if c.t == type %}
<li>
<strong>{{ c.__format__('t') }}</strong>
{{ c.__format__('t') }}
<p>
<small>{{ c.__format__('s') }}</small>
<small class="text-muted">{{ c.__format__('s') }}</small>
</p>
</li>
{% endfor %}

View file

@ -2,6 +2,8 @@ from distutils.version import StrictVersion
from enum import Enum, IntEnum, unique
from typing import Union
import inflection
@unique
class SnapshotSoftware(Enum):
@ -11,6 +13,9 @@ class SnapshotSoftware(Enum):
Web = 'Web'
DesktopApp = 'DesktopApp'
def __str__(self):
return self.name
@unique
class RatingSoftware(Enum):
@ -25,6 +30,9 @@ class RatingSoftware(Enum):
"""
EMarket = 'EMarket'
def __str__(self):
return self.name
RATE_POSITIVE = 0, 10
RATE_NEGATIVE = -3, 5
@ -55,6 +63,12 @@ class RatingRange(IntEnum):
else:
return cls.HIGH
def __str__(self):
return inflection.humanize(self.name)
def __format__(self, format_spec):
return str(self)
@unique
class PriceSoftware(Enum):
@ -81,6 +95,9 @@ class AppearanceRange(Enum):
D = 'D. Is acceptable (visual damage in visible parts, not screens)'
E = 'E. Is unacceptable (considerable visual damage that can affect usage)'
def __str__(self):
return self.name
@unique
class FunctionalityRange(Enum):
@ -91,6 +108,9 @@ class FunctionalityRange(Enum):
C = 'C. A non-important button (or similar) doesn\'t work; screen has multiple scratches in edges'
D = 'D. Multiple buttons don\'t work; screen has visual damage resulting in uncomfortable usage'
def __str__(self):
return self.name
@unique
class Bios(Enum):
@ -101,6 +121,9 @@ class Bios(Enum):
D = 'D. Like B or C, but you had to unlock the BIOS (i.e. by removing the battery)'
E = 'E. The device could not be booted through the network.'
def __str__(self):
return self.name
@unique
class Orientation(Enum):
@ -222,7 +245,7 @@ class ComputerChassis(Enum):
Virtual = 'Non-physical device'
def __format__(self, format_spec):
return self.value.lower()
return inflection.humanize(inflection.underscore(self.value))
class ReceiverRole(Enum):

View file

@ -5,6 +5,7 @@ from distutils.version import StrictVersion
from typing import Set, Union
from uuid import uuid4
import inflection
from boltons import urlutils
from citext import CIText
from flask import current_app as app, g
@ -196,6 +197,17 @@ class Event(Thing):
raise ValidationError('The event cannot start after it finished.')
return start_time
@property
def _err_str(self):
return '❌ Error.' if self.error else ''
@property
def _date_str(self):
return '{:%c}'.format(self.end_time or self.created)
def __str__(self) -> str:
return '{}'.format(self._err_str)
class EventComponent(db.Model):
device_id = Column(BigInteger, ForeignKey(Component.id), primary_key=True)
@ -284,6 +296,11 @@ class EraseBasic(JoinedWithOneDeviceMixin, EventWithOneDevice):
only writing zeros.
"""
# todo return erasure properties like num steps, if it is british...
def __str__(self) -> str:
return '{} on {}.'.format(self._err_str, self.end_time)
class EraseSectors(EraseBasic):
pass
@ -340,6 +357,9 @@ class Snapshot(JoinedWithOneDeviceMixin, EventWithOneDevice):
"""
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
def __str__(self) -> str:
return '{}. {} version {}.'.format(self._err_str, self.software, self.version)
class Install(JoinedWithOneDeviceMixin, EventWithOneDevice):
elapsed = Column(Interval, nullable=False)
@ -368,7 +388,8 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
@property
def rating_range(self) -> RatingRange:
return RatingRange.from_score(self.rating)
if self.rating:
return RatingRange.from_score(self.rating)
@declared_attr
def __mapper_args__(cls):
@ -384,6 +405,9 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
args[POLYMORPHIC_ON] = cls.type
return args
def __str__(self) -> str:
return '{} ({} v.{})'.format(self.rating_range, self.software, self.version)
class IndividualRate(Rate):
pass
@ -400,6 +424,12 @@ class ManualRate(IndividualRate):
functionality_range = Column(DBEnum(FunctionalityRange))
functionality_range.comment = FunctionalityRange.__doc__
def __str__(self) -> str:
return super().__str__() + '. Appearance {} and functionality {}'.format(
self.appearance_range,
self.functionality_range
)
class WorkbenchRate(ManualRate):
id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id), primary_key=True)
@ -425,6 +455,26 @@ class WorkbenchRate(ManualRate):
from ereuse_devicehub.resources.event.rate.main import main
return main(self, **app.config.get_namespace('WORKBENCH_RATE_'))
@property
def data_storage_range(self):
if self.data_storage:
return RatingRange.from_score(self.data_storage)
@property
def ram_range(self):
if self.ram:
return RatingRange.from_score(self.ram)
@property
def processor_range(self):
if self.processor:
return RatingRange.from_score(self.processor)
@property
def graphic_card_range(self):
if self.graphic_card:
return RatingRange.from_score(self.graphic_card)
class AggregateRate(Rate):
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
@ -474,6 +524,22 @@ class AggregateRate(Rate):
def graphic_card(self):
return self.workbench.graphic_card
@property
def data_storage_range(self):
return self.workbench.data_storage_range
@property
def ram_range(self):
return self.workbench.ram_range
@property
def processor_range(self):
return self.workbench.processor_range
@property
def graphic_card_range(self):
return self.workbench.graphic_card_range
@property
def bios(self):
return self.workbench.bios
@ -530,10 +596,11 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
uselist=False),
primaryjoin=AggregateRate.id == rating_id)
def __init__(self, **kwargs) -> None:
def __init__(self, *args, **kwargs) -> None:
if 'price' in kwargs:
assert isinstance(kwargs['price'], Decimal), 'Price must be a Decimal'
super().__init__(currency=kwargs.pop('currency', app.config['PRICE_CURRENCY']), **kwargs)
super().__init__(currency=kwargs.pop('currency', app.config['PRICE_CURRENCY']), *args,
**kwargs)
@classmethod
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
@ -543,6 +610,23 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
# equation from marshmallow.fields.Decimal
return value.quantize(Decimal((0, (1,), -cls.SCALE)), rounding=rounding)
@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 == 'Price':
args[POLYMORPHIC_ON] = cls.type
return args
def __str__(self) -> str:
return '{0:0.2f} {1}'.format(self.price, self.currency)
class EreusePrice(Price):
"""A Price class that auto-computes its amount by"""
@ -661,11 +745,14 @@ class TestDataStorage(Test):
offline_uncorrectable = Column(SmallInteger)
remaining_lifetime_percentage = Column(SmallInteger)
def __str__(self) -> str:
return '{}. Lifetime of {:.1f} years'.format(inflection.humanize(self.status),
self.lifetime.days / 365)
# todo remove lifetime / passed_lifetime as I think they are the same
class StressTest(Test):
pass
@validates('elapsed')
def is_minute_and_bigger_than_1_minute(self, _, value: timedelta):
@ -674,6 +761,9 @@ class StressTest(Test):
assert seconds >= 60
return value
def __str__(self) -> str:
return '{}. Computing for {}'.format(self._err_str, self.elapsed)
class Benchmark(JoinedWithOneDeviceMixin, EventWithOneDevice):
elapsed = Column(Interval)
@ -698,11 +788,17 @@ class BenchmarkDataStorage(Benchmark):
read_speed = Column(Float(decimal_return_scale=2), nullable=False)
write_speed = Column(Float(decimal_return_scale=2), nullable=False)
def __str__(self) -> str:
return 'Read: {} MB/s, write: {} MB/s'.format(self.read_speed, self.write_speed)
class BenchmarkWithRate(Benchmark):
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
rate = Column(SmallInteger, nullable=False)
def __str__(self) -> str:
return '{} points'.format(self.rate)
class BenchmarkProcessor(BenchmarkWithRate):
pass

View file

@ -65,6 +65,10 @@ class Event(Thing):
def url(self) -> urlutils.URL:
pass
@property
def _err_str(self):
pass
class EventWithOneDevice(Event):
@ -256,6 +260,22 @@ class WorkbenchRate(ManualRate):
def ratings(self) -> Set[Rate]:
pass
@property
def data_storage_range(self):
pass
@property
def ram_range(self):
pass
@property
def processor_range(self):
pass
@property
def graphic_card_range(self):
pass
class Price(EventWithOneDevice):
SCALE = ...

View file

@ -12,8 +12,8 @@ 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, ReceiverRole, SnapshotExpectedEvents, \
SnapshotSoftware, TestDataStorageLength
PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \
SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
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
@ -107,6 +107,7 @@ class Rate(EventWithOneDevice):
description=m.Rate.version.comment)
appearance = Integer(validate=Range(-3, 5), dump_only=True)
functionality = Integer(validate=Range(-3, 5), dump_only=True)
rating_range = EnumField(RatingRange, dump_only=True, data_key='ratingRange')
class IndividualRate(Rate):
@ -134,6 +135,10 @@ class WorkbenchRate(ManualRate):
bios_range = EnumField(Bios,
description=m.WorkbenchRate.bios_range.comment,
data_key='biosRange')
data_storage_range = EnumField(RatingRange, dump_only=True, data_key='dataStorageRange')
ram_range = EnumField(RatingRange, dump_only=True, data_key='ramRange')
processor_range = EnumField(RatingRange, dump_only=True, data_key='processorRange')
graphic_card_range = EnumField(RatingRange, dump_only=True, data_key='graphicCardRange')
class AggregateRate(Rate):
@ -159,6 +164,10 @@ class AggregateRate(Rate):
data_key='functionalityRange',
description=m.ManualRate.functionality_range.comment)
labelling = Boolean(description=m.ManualRate.labelling.comment)
data_storage_range = EnumField(RatingRange, dump_only=True, data_key='dataStorageRange')
ram_range = EnumField(RatingRange, dump_only=True, data_key='ramRange')
processor_range = EnumField(RatingRange, dump_only=True, data_key='processorRange')
graphic_card_range = EnumField(RatingRange, dump_only=True, data_key='graphicCardRange')
class Price(EventWithOneDevice):
@ -265,7 +274,7 @@ class Test(EventWithOneDevice):
class TestDataStorage(Test):
length = EnumField(TestDataStorageLength, required=True)
status = SanitizedStr(lower=True, validate=Length(max=STR_SIZE), required=True)
lifetime = TimeDelta(precision=TimeDelta.DAYS)
lifetime = TimeDelta(precision=TimeDelta.HOURS)
assessment = Boolean()
reallocated_sector_count = Integer(data_key='reallocatedSectorCount')
power_cycle_count = Integer(data_key='powerCycleCount')

View file

@ -1,6 +1,6 @@
from flask import Response, current_app as app, request
from flask import Response, current_app as app, redirect, request
from teal.marshmallow import ValidationError
from teal.resource import View
from teal.resource import View, url_for_resource
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Device
@ -27,6 +27,8 @@ class TagDeviceView(View):
tag = Tag.from_an_id(id).one() # type: Tag
if not tag.device:
raise TagNotLinked(tag.id)
if not request.authorization:
return redirect(location=url_for_resource(Device, tag.device.id))
return app.resources[Device.t].schema.jsonify(tag.device)
# noinspection PyMethodOverriding
@ -55,6 +57,8 @@ def get_device_from_tag(id: str):
"""
# todo this could be more efficient by Device.query... join with tag
device = Tag.query.filter_by(id=id).one().device
if not request.authorization:
return redirect(location=url_for_resource(Device, device.id))
if device is None:
raise TagNotLinked(id)
return app.resources[Device.t].schema.jsonify(device)

View file

@ -25,7 +25,7 @@ requests==2.19.1
requests-mock==1.5.2
SQLAlchemy==1.2.11
SQLAlchemy-Utils==0.33.3
teal==0.2.0a24
teal==0.2.0a25
webargs==4.0.0
Werkzeug==0.14.1
sqlalchemy-citext==1.3.post0

View file

@ -34,7 +34,7 @@ setup(
long_description=long_description,
long_description_content_type='text/markdown',
install_requires=[
'teal>=0.2.0a24', # teal always first
'teal>=0.2.0a25', # teal always first
'click',
'click-spinner',
'ereuse-utils[Naming]>=0.4b9',

View file

@ -28,7 +28,8 @@ def test_api_docs(client: Client):
'/manufacturers/',
'/lots/{id}/children',
'/lots/{id}/devices',
'/tags/{tag_id}/device/{device_id}'
'/tags/{tag_id}/device/{device_id}',
'/devices/static/{filename}'
}
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
assert docs['components']['securitySchemes']['bearerAuth'] == {

View file

@ -506,8 +506,8 @@ def test_device_properties_format(app: Devicehub, user: UserClient):
with app.app_context():
pc = Laptop.query.one() # type: Laptop
assert format(pc) == 'Laptop 1: model 1000h, S/N 94oaaq021116'
assert format(pc, 't') == 'netbook 1000h'
assert format(pc, 's') == '(asustek computer inc.) S/N 94oaaq021116'
assert format(pc, 't') == 'Netbook 1000h'
assert format(pc, 's') == '(asustek computer inc.) S/N 94OAAQ021116'
assert pc.ram_size == 1024
assert pc.data_storage_size == 152627
assert pc.graphic_card_model == 'mobile 945gse express integrated graphics controller'
@ -516,18 +516,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) S/N 00:24:8C:7F:CF:2D 100 Mbps'
hdd = next(c for c in pc.components if isinstance(c, 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) S/N 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=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 'S/N 00:24:8C:7F:CF:2D 100 Mbps' in html
@pytest.mark.xfail(reason='Functionality not yet developed.')

View file

@ -283,6 +283,11 @@ def test_price_custom():
assert c['price']['id'] == p['id']
@pytest.mark.xfail(reson='Develop test')
def test_price_custom_client():
"""As test_price_custom but creating the price through the API."""
@pytest.mark.xfail(reson='Develop test')
def test_ereuse_price():
"""Tests the several ways of creating eReuse Price, emulating