Merge remote-tracking branch 'origin/master' into reports
This commit is contained in:
commit
c4129a2a1a
|
@ -35,7 +35,9 @@ call the new file ``app.py``.
|
||||||
Create a PostgreSQL database called *devicehub* by running
|
Create a PostgreSQL database called *devicehub* by running
|
||||||
[create-db](examples/create-db.sh):
|
[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`.
|
- In MacOS: `examples/create-db.sh devicehub`.
|
||||||
|
|
||||||
Create the tables in the database by executing in the same directory
|
Create the tables in the database by executing in the same directory
|
||||||
|
|
|
@ -58,16 +58,28 @@ class Dummy:
|
||||||
'-o', org_id
|
'-o', org_id
|
||||||
],
|
],
|
||||||
catch_exceptions=False)
|
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())
|
files = tuple(Path(__file__).parent.joinpath('files').iterdir())
|
||||||
print('done.')
|
print('done.')
|
||||||
|
sample_pc = None # We treat this one as a special sample for demonstrations
|
||||||
pcs = set() # type: Set[int]
|
pcs = set() # type: Set[int]
|
||||||
with click.progressbar(files, label='Creating devices...'.ljust(28)) as bar:
|
with click.progressbar(files, label='Creating devices...'.ljust(28)) as bar:
|
||||||
for path in bar:
|
for path in bar:
|
||||||
with path.open() as f:
|
with path.open() as f:
|
||||||
snapshot = yaml.load(f)
|
snapshot = yaml.load(f)
|
||||||
s, _ = user.post(res=m.Snapshot, data=snapshot)
|
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
|
# Link tags and eTags
|
||||||
for tag, pc in zip((self.TAGS[1], self.TAGS[2], self.ET[0][0], self.ET[1][1]), pcs):
|
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)
|
user.put({}, res=Tag, item='{}/device/{}'.format(tag, pc), status=204)
|
||||||
|
@ -105,9 +117,29 @@ class Dummy:
|
||||||
assert len(inventory['items'])
|
assert len(inventory['items'])
|
||||||
|
|
||||||
i, _ = user.get(res=Device, query=[('search', 'intel')])
|
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
|
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.')
|
print('⭐ Done.')
|
||||||
|
|
||||||
def user_client(self, email: str, password: str):
|
def user_client(self, email: str, password: str):
|
||||||
|
|
146
ereuse_devicehub/dummy/files/pc-laudem.snapshot.11.yaml
Normal file
146
ereuse_devicehub/dummy/files/pc-laudem.snapshot.11.yaml
Normal 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"
|
||||||
|
}
|
|
@ -14,7 +14,8 @@ class DeviceDef(Resource):
|
||||||
AUTH = False # We manage this at each view
|
AUTH = False # We manage this at each view
|
||||||
|
|
||||||
def __init__(self, app,
|
def __init__(self, app,
|
||||||
import_name=__name__, static_folder=None,
|
import_name=__name__,
|
||||||
|
static_folder='static',
|
||||||
static_url_path=None,
|
static_url_path=None,
|
||||||
template_folder='templates',
|
template_folder='templates',
|
||||||
url_prefix=None,
|
url_prefix=None,
|
||||||
|
@ -30,6 +31,12 @@ class ComputerDef(DeviceDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.Computer
|
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):
|
class DesktopDef(ComputerDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
|
@ -50,6 +57,12 @@ class MonitorDef(DeviceDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.Monitor
|
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):
|
class ComputerMonitorDef(MonitorDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
|
@ -65,6 +78,12 @@ class MobileDef(DeviceDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.Mobile
|
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):
|
class SmartphoneDef(MobileDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
|
@ -85,6 +104,12 @@ class ComponentDef(DeviceDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.Component
|
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):
|
class GraphicCardDef(ComponentDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
|
|
|
@ -187,7 +187,9 @@ class Device(Thing):
|
||||||
if 't' in format_spec:
|
if 't' in format_spec:
|
||||||
v += '{0.t} {0.model}'.format(self)
|
v += '{0.t} {0.model}'.format(self)
|
||||||
if 's' in format_spec:
|
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
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
@ -272,7 +274,9 @@ class Computer(Device):
|
||||||
if 't' in format_spec:
|
if 't' in format_spec:
|
||||||
v += '{0.chassis} {0.model}'.format(self)
|
v += '{0.chassis} {0.model}'.format(self)
|
||||||
elif 's' in format_spec:
|
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
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
import inflection
|
||||||
|
|
||||||
from ereuse_devicehub.resources.event import models as e
|
from ereuse_devicehub.resources.event import models as e
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,6 +11,9 @@ class State(Enum):
|
||||||
"""Events participating in this state."""
|
"""Events participating in this state."""
|
||||||
return (s.value for s in cls)
|
return (s.value for s in cls)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return inflection.humanize(inflection.underscore(self.name))
|
||||||
|
|
||||||
|
|
||||||
class Trading(State):
|
class Trading(State):
|
||||||
Reserved = e.Reserve
|
Reserved = e.Reserve
|
||||||
|
|
3
ereuse_devicehub/resources/device/static/ereuse-logo.svg
Normal file
3
ereuse_devicehub/resources/device/static/ereuse-logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.4 KiB |
3
ereuse_devicehub/resources/device/static/magrama.svg
Normal file
3
ereuse_devicehub/resources/device/static/magrama.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
|
@ -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 |
|
@ -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 |
|
@ -10,87 +10,212 @@
|
||||||
crossorigin="anonymous">
|
crossorigin="anonymous">
|
||||||
<title>Devicehub | {{ device.__format__('t') }}</title>
|
<title>Devicehub | {{ device.__format__('t') }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="container">
|
<body>
|
||||||
|
|
||||||
<div class="page-header">
|
<nav class="navbar navbar-default" style="background-color: gainsboro; margin: 0 !important">
|
||||||
<h1>{{ device.__format__('t') }}
|
<div class="container-fluid">
|
||||||
<small>{{ device.__format__('s') }}</small>
|
<a href="https://www.ereuse.org/" target="_blank">
|
||||||
</h1>
|
<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>
|
||||||
|
<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">
|
<p class="text-center">
|
||||||
<article class="col-md-6">
|
{% if device.trading %}
|
||||||
<table class="table">
|
{{ device.trading }}
|
||||||
<thead>
|
{% endif %}
|
||||||
<tr>
|
{% if device.trading and device.physical %}
|
||||||
<th></th>
|
and
|
||||||
<th>Range</th>
|
{% endif %}
|
||||||
</tr>
|
{% if device.physical %}
|
||||||
</thead>
|
{{ device.physical }}
|
||||||
<tbody>
|
{% endif %}
|
||||||
<tr>
|
</p>
|
||||||
<td>
|
<div class="row">
|
||||||
<details>
|
<article class="col-md-6">
|
||||||
<summary>CPU – {{ device.processor_model }}</summary>
|
<h3>You can verify the originality of your device.</h3>
|
||||||
{{ macros.component_type(device.components, 'Processor') }}
|
<p>
|
||||||
</details>
|
If your device comes with the following tag
|
||||||
</td>
|
<img class="img-responsive center-block" style="width: 12em;"
|
||||||
<td></td>
|
src="{{ url_for('Device.static', filename='photochromic-alone.svg') }}">
|
||||||
</tr>
|
it means it has been refurbished by an eReuse.org
|
||||||
<tr>
|
certified organization.
|
||||||
<td>
|
</p>
|
||||||
<details>
|
<p>
|
||||||
<summary>RAM – {{ device.ram_size // 1000 }} GB</summary>
|
The tag is special –illuminate it with the torch of
|
||||||
{{ macros.component_type(device.components, 'RamModule') }}
|
your phone for 6 seconds and it will react like in
|
||||||
</details>
|
the following image:
|
||||||
</td>
|
<img class="img-responsive center-block" style="width: 30em;"
|
||||||
<td>//range//</td>
|
src="{{ url_for('Device.static', filename='photochromic-tag-web.svg') }}">
|
||||||
</tr>
|
This is proof that this device is genuine.
|
||||||
<tr>
|
</p>
|
||||||
<td>
|
</article>
|
||||||
<details>
|
<article class="col-md-6">
|
||||||
<summary>Data Storage – {{ device.data_storage_size // 1000 }} GB</summary>
|
<h3>These are the specifications</h3>
|
||||||
{{ macros.component_type(device.components, 'SolidStateDrive') }}
|
<div class="table-responsive">
|
||||||
{{ macros.component_type(device.components, 'HardDrive') }}
|
<table class="table table-striped">
|
||||||
</details>
|
<thead>
|
||||||
</td>
|
<tr>
|
||||||
<td>//range//</td>
|
<th></th>
|
||||||
</tr>
|
<th>Range</th>
|
||||||
<tr>
|
</tr>
|
||||||
<td>
|
</thead>
|
||||||
<details>
|
<tbody>
|
||||||
<summary>Graphics – {{ device.graphic_card_model }}</summary>
|
{% if device.processor_model %}
|
||||||
{{ macros.component_type(device.components, 'GraphicCard') }}
|
<tr>
|
||||||
</details>
|
<td>
|
||||||
</td>
|
CPU – {{ device.processor_model }}
|
||||||
<td>//range//</td>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
<tr>
|
{% if device.rate %}
|
||||||
<td>
|
{{ device.rate.processor_range }}
|
||||||
<details>
|
({{ device.rate.processor }})
|
||||||
<summary>Network –
|
{% endif %}
|
||||||
{% if device.network_speeds[0] %}
|
</td>
|
||||||
Ethernet of {{ device.network_speeds[0] }} Mbps
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if device.network_speeds[0] and device.network_speeds[1] %}
|
{% if device.ram_size %}
|
||||||
+
|
<tr>
|
||||||
{% endif %}
|
<td>
|
||||||
{% if device.network_speeds[1] %}
|
RAM – {{ device.ram_size // 1000 }} GB
|
||||||
WiFi of {{ device.network_speeds[1] }} Mbps
|
{{ macros.component_type(device.components, 'RamModule') }}
|
||||||
{% endif %}
|
</td>
|
||||||
</summary>
|
<td>
|
||||||
{{ macros.component_type(device.components, 'NetworkAdapter') }}
|
{% if device.rate %}
|
||||||
</details>
|
{{ device.rate.ram_range }}
|
||||||
</td>
|
({{ device.rate.ram }})
|
||||||
<td></td>
|
{% endif %}
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
{% endif %}
|
||||||
</article>
|
{% if device.data_storage_size %}
|
||||||
<aside class="col-md-6">
|
<tr>
|
||||||
<h2>Check the validity of the device</h2>
|
<td>
|
||||||
<p>Use the flashlight to scan...</p>
|
Data Storage – {{ device.data_storage_size // 1000 }} GB
|
||||||
</aside>
|
{{ 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>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{% macro component_type(components, type) %}
|
{% macro component_type(components, type) %}
|
||||||
<ul class="list-unstyled">
|
<ul>
|
||||||
{% for c in components if c.t == type %}
|
{% for c in components if c.t == type %}
|
||||||
<li>
|
<li>
|
||||||
<strong>{{ c.__format__('t') }}</strong>
|
{{ c.__format__('t') }}
|
||||||
<p>
|
<p>
|
||||||
<small>{{ c.__format__('s') }}</small>
|
<small class="text-muted">{{ c.__format__('s') }}</small>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -2,6 +2,8 @@ from distutils.version import StrictVersion
|
||||||
from enum import Enum, IntEnum, unique
|
from enum import Enum, IntEnum, unique
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
import inflection
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class SnapshotSoftware(Enum):
|
class SnapshotSoftware(Enum):
|
||||||
|
@ -11,6 +13,9 @@ class SnapshotSoftware(Enum):
|
||||||
Web = 'Web'
|
Web = 'Web'
|
||||||
DesktopApp = 'DesktopApp'
|
DesktopApp = 'DesktopApp'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class RatingSoftware(Enum):
|
class RatingSoftware(Enum):
|
||||||
|
@ -25,6 +30,9 @@ class RatingSoftware(Enum):
|
||||||
"""
|
"""
|
||||||
EMarket = 'EMarket'
|
EMarket = 'EMarket'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
RATE_POSITIVE = 0, 10
|
RATE_POSITIVE = 0, 10
|
||||||
RATE_NEGATIVE = -3, 5
|
RATE_NEGATIVE = -3, 5
|
||||||
|
@ -55,6 +63,12 @@ class RatingRange(IntEnum):
|
||||||
else:
|
else:
|
||||||
return cls.HIGH
|
return cls.HIGH
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return inflection.humanize(self.name)
|
||||||
|
|
||||||
|
def __format__(self, format_spec):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class PriceSoftware(Enum):
|
class PriceSoftware(Enum):
|
||||||
|
@ -81,6 +95,9 @@ class AppearanceRange(Enum):
|
||||||
D = 'D. Is acceptable (visual damage in visible parts, not screens)'
|
D = 'D. Is acceptable (visual damage in visible parts, not screens)'
|
||||||
E = 'E. Is unacceptable (considerable visual damage that can affect usage)'
|
E = 'E. Is unacceptable (considerable visual damage that can affect usage)'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class FunctionalityRange(Enum):
|
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'
|
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'
|
D = 'D. Multiple buttons don\'t work; screen has visual damage resulting in uncomfortable usage'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class Bios(Enum):
|
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)'
|
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.'
|
E = 'E. The device could not be booted through the network.'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class Orientation(Enum):
|
class Orientation(Enum):
|
||||||
|
@ -222,7 +245,7 @@ class ComputerChassis(Enum):
|
||||||
Virtual = 'Non-physical device'
|
Virtual = 'Non-physical device'
|
||||||
|
|
||||||
def __format__(self, format_spec):
|
def __format__(self, format_spec):
|
||||||
return self.value.lower()
|
return inflection.humanize(inflection.underscore(self.value))
|
||||||
|
|
||||||
|
|
||||||
class ReceiverRole(Enum):
|
class ReceiverRole(Enum):
|
||||||
|
|
|
@ -5,6 +5,7 @@ from distutils.version import StrictVersion
|
||||||
from typing import Set, Union
|
from typing import Set, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import inflection
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from flask import current_app as app, g
|
from flask import current_app as app, g
|
||||||
|
@ -196,6 +197,17 @@ class Event(Thing):
|
||||||
raise ValidationError('The event cannot start after it finished.')
|
raise ValidationError('The event cannot start after it finished.')
|
||||||
return start_time
|
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):
|
class EventComponent(db.Model):
|
||||||
device_id = Column(BigInteger, ForeignKey(Component.id), primary_key=True)
|
device_id = Column(BigInteger, ForeignKey(Component.id), primary_key=True)
|
||||||
|
@ -284,6 +296,11 @@ class EraseBasic(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
only writing zeros.
|
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):
|
class EraseSectors(EraseBasic):
|
||||||
pass
|
pass
|
||||||
|
@ -340,6 +357,9 @@ class Snapshot(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
"""
|
"""
|
||||||
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
|
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return '{}. {} version {}.'.format(self._err_str, self.software, self.version)
|
||||||
|
|
||||||
|
|
||||||
class Install(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Install(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
elapsed = Column(Interval, nullable=False)
|
elapsed = Column(Interval, nullable=False)
|
||||||
|
@ -368,7 +388,8 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rating_range(self) -> RatingRange:
|
def rating_range(self) -> RatingRange:
|
||||||
return RatingRange.from_score(self.rating)
|
if self.rating:
|
||||||
|
return RatingRange.from_score(self.rating)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def __mapper_args__(cls):
|
def __mapper_args__(cls):
|
||||||
|
@ -384,6 +405,9 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
args[POLYMORPHIC_ON] = cls.type
|
args[POLYMORPHIC_ON] = cls.type
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return '{} ({} v.{})'.format(self.rating_range, self.software, self.version)
|
||||||
|
|
||||||
|
|
||||||
class IndividualRate(Rate):
|
class IndividualRate(Rate):
|
||||||
pass
|
pass
|
||||||
|
@ -400,6 +424,12 @@ class ManualRate(IndividualRate):
|
||||||
functionality_range = Column(DBEnum(FunctionalityRange))
|
functionality_range = Column(DBEnum(FunctionalityRange))
|
||||||
functionality_range.comment = FunctionalityRange.__doc__
|
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):
|
class WorkbenchRate(ManualRate):
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id), primary_key=True)
|
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
|
from ereuse_devicehub.resources.event.rate.main import main
|
||||||
return main(self, **app.config.get_namespace('WORKBENCH_RATE_'))
|
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):
|
class AggregateRate(Rate):
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
||||||
|
@ -474,6 +524,22 @@ class AggregateRate(Rate):
|
||||||
def graphic_card(self):
|
def graphic_card(self):
|
||||||
return self.workbench.graphic_card
|
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
|
@property
|
||||||
def bios(self):
|
def bios(self):
|
||||||
return self.workbench.bios
|
return self.workbench.bios
|
||||||
|
@ -530,10 +596,11 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
uselist=False),
|
uselist=False),
|
||||||
primaryjoin=AggregateRate.id == rating_id)
|
primaryjoin=AggregateRate.id == rating_id)
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
if 'price' in kwargs:
|
if 'price' in kwargs:
|
||||||
assert isinstance(kwargs['price'], Decimal), 'Price must be a Decimal'
|
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
|
@classmethod
|
||||||
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
|
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
|
||||||
|
@ -543,6 +610,23 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
# equation from marshmallow.fields.Decimal
|
# equation from marshmallow.fields.Decimal
|
||||||
return value.quantize(Decimal((0, (1,), -cls.SCALE)), rounding=rounding)
|
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):
|
class EreusePrice(Price):
|
||||||
"""A Price class that auto-computes its amount by"""
|
"""A Price class that auto-computes its amount by"""
|
||||||
|
@ -661,11 +745,14 @@ class TestDataStorage(Test):
|
||||||
offline_uncorrectable = Column(SmallInteger)
|
offline_uncorrectable = Column(SmallInteger)
|
||||||
remaining_lifetime_percentage = 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
|
# todo remove lifetime / passed_lifetime as I think they are the same
|
||||||
|
|
||||||
|
|
||||||
class StressTest(Test):
|
class StressTest(Test):
|
||||||
pass
|
|
||||||
|
|
||||||
@validates('elapsed')
|
@validates('elapsed')
|
||||||
def is_minute_and_bigger_than_1_minute(self, _, value: timedelta):
|
def is_minute_and_bigger_than_1_minute(self, _, value: timedelta):
|
||||||
|
@ -674,6 +761,9 @@ class StressTest(Test):
|
||||||
assert seconds >= 60
|
assert seconds >= 60
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return '{}. Computing for {}'.format(self._err_str, self.elapsed)
|
||||||
|
|
||||||
|
|
||||||
class Benchmark(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Benchmark(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
elapsed = Column(Interval)
|
elapsed = Column(Interval)
|
||||||
|
@ -698,11 +788,17 @@ class BenchmarkDataStorage(Benchmark):
|
||||||
read_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
read_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
||||||
write_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):
|
class BenchmarkWithRate(Benchmark):
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
||||||
rate = Column(SmallInteger, nullable=False)
|
rate = Column(SmallInteger, nullable=False)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return '{} points'.format(self.rate)
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkProcessor(BenchmarkWithRate):
|
class BenchmarkProcessor(BenchmarkWithRate):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -65,6 +65,10 @@ class Event(Thing):
|
||||||
def url(self) -> urlutils.URL:
|
def url(self) -> urlutils.URL:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _err_str(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(Event):
|
class EventWithOneDevice(Event):
|
||||||
|
|
||||||
|
@ -256,6 +260,22 @@ class WorkbenchRate(ManualRate):
|
||||||
def ratings(self) -> Set[Rate]:
|
def ratings(self) -> Set[Rate]:
|
||||||
pass
|
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):
|
class Price(EventWithOneDevice):
|
||||||
SCALE = ...
|
SCALE = ...
|
||||||
|
|
|
@ -12,8 +12,8 @@ from ereuse_devicehub.marshmallow import NestedOn
|
||||||
from ereuse_devicehub.resources.agent.schemas import Agent
|
from ereuse_devicehub.resources.agent.schemas import Agent
|
||||||
from ereuse_devicehub.resources.device.schemas import Component, Computer, Device
|
from ereuse_devicehub.resources.device.schemas import Component, Computer, Device
|
||||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||||
PriceSoftware, RATE_POSITIVE, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, \
|
PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \
|
||||||
SnapshotSoftware, TestDataStorageLength
|
SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
||||||
from ereuse_devicehub.resources.event import models as m
|
from ereuse_devicehub.resources.event import models as m
|
||||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
||||||
from ereuse_devicehub.resources.schemas import Thing
|
from ereuse_devicehub.resources.schemas import Thing
|
||||||
|
@ -107,6 +107,7 @@ class Rate(EventWithOneDevice):
|
||||||
description=m.Rate.version.comment)
|
description=m.Rate.version.comment)
|
||||||
appearance = Integer(validate=Range(-3, 5), dump_only=True)
|
appearance = Integer(validate=Range(-3, 5), dump_only=True)
|
||||||
functionality = 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):
|
class IndividualRate(Rate):
|
||||||
|
@ -134,6 +135,10 @@ class WorkbenchRate(ManualRate):
|
||||||
bios_range = EnumField(Bios,
|
bios_range = EnumField(Bios,
|
||||||
description=m.WorkbenchRate.bios_range.comment,
|
description=m.WorkbenchRate.bios_range.comment,
|
||||||
data_key='biosRange')
|
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):
|
class AggregateRate(Rate):
|
||||||
|
@ -159,6 +164,10 @@ class AggregateRate(Rate):
|
||||||
data_key='functionalityRange',
|
data_key='functionalityRange',
|
||||||
description=m.ManualRate.functionality_range.comment)
|
description=m.ManualRate.functionality_range.comment)
|
||||||
labelling = Boolean(description=m.ManualRate.labelling.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):
|
class Price(EventWithOneDevice):
|
||||||
|
@ -265,7 +274,7 @@ class Test(EventWithOneDevice):
|
||||||
class TestDataStorage(Test):
|
class TestDataStorage(Test):
|
||||||
length = EnumField(TestDataStorageLength, required=True)
|
length = EnumField(TestDataStorageLength, required=True)
|
||||||
status = SanitizedStr(lower=True, validate=Length(max=STR_SIZE), 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()
|
assessment = Boolean()
|
||||||
reallocated_sector_count = Integer(data_key='reallocatedSectorCount')
|
reallocated_sector_count = Integer(data_key='reallocatedSectorCount')
|
||||||
power_cycle_count = Integer(data_key='powerCycleCount')
|
power_cycle_count = Integer(data_key='powerCycleCount')
|
||||||
|
|
|
@ -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.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.db import db
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
|
@ -27,6 +27,8 @@ class TagDeviceView(View):
|
||||||
tag = Tag.from_an_id(id).one() # type: Tag
|
tag = Tag.from_an_id(id).one() # type: Tag
|
||||||
if not tag.device:
|
if not tag.device:
|
||||||
raise TagNotLinked(tag.id)
|
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)
|
return app.resources[Device.t].schema.jsonify(tag.device)
|
||||||
|
|
||||||
# noinspection PyMethodOverriding
|
# 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
|
# todo this could be more efficient by Device.query... join with tag
|
||||||
device = Tag.query.filter_by(id=id).one().device
|
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:
|
if device is None:
|
||||||
raise TagNotLinked(id)
|
raise TagNotLinked(id)
|
||||||
return app.resources[Device.t].schema.jsonify(device)
|
return app.resources[Device.t].schema.jsonify(device)
|
||||||
|
|
|
@ -25,7 +25,7 @@ requests==2.19.1
|
||||||
requests-mock==1.5.2
|
requests-mock==1.5.2
|
||||||
SQLAlchemy==1.2.11
|
SQLAlchemy==1.2.11
|
||||||
SQLAlchemy-Utils==0.33.3
|
SQLAlchemy-Utils==0.33.3
|
||||||
teal==0.2.0a24
|
teal==0.2.0a25
|
||||||
webargs==4.0.0
|
webargs==4.0.0
|
||||||
Werkzeug==0.14.1
|
Werkzeug==0.14.1
|
||||||
sqlalchemy-citext==1.3.post0
|
sqlalchemy-citext==1.3.post0
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -34,7 +34,7 @@ setup(
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'teal>=0.2.0a24', # teal always first
|
'teal>=0.2.0a25', # teal always first
|
||||||
'click',
|
'click',
|
||||||
'click-spinner',
|
'click-spinner',
|
||||||
'ereuse-utils[Naming]>=0.4b9',
|
'ereuse-utils[Naming]>=0.4b9',
|
||||||
|
|
|
@ -28,7 +28,8 @@ def test_api_docs(client: Client):
|
||||||
'/manufacturers/',
|
'/manufacturers/',
|
||||||
'/lots/{id}/children',
|
'/lots/{id}/children',
|
||||||
'/lots/{id}/devices',
|
'/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['info'] == {'title': 'Devicehub', 'version': '0.2'}
|
||||||
assert docs['components']['securitySchemes']['bearerAuth'] == {
|
assert docs['components']['securitySchemes']['bearerAuth'] == {
|
||||||
|
|
|
@ -506,8 +506,8 @@ def test_device_properties_format(app: Devicehub, user: UserClient):
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
pc = Laptop.query.one() # type: Laptop
|
pc = Laptop.query.one() # type: Laptop
|
||||||
assert format(pc) == 'Laptop 1: model 1000h, S/N 94oaaq021116'
|
assert format(pc) == 'Laptop 1: model 1000h, S/N 94oaaq021116'
|
||||||
assert format(pc, 't') == 'netbook 1000h'
|
assert format(pc, 't') == 'Netbook 1000h'
|
||||||
assert format(pc, 's') == '(asustek computer inc.) S/N 94oaaq021116'
|
assert format(pc, 's') == '(asustek computer inc.) S/N 94OAAQ021116'
|
||||||
assert pc.ram_size == 1024
|
assert pc.ram_size == 1024
|
||||||
assert pc.data_storage_size == 152627
|
assert pc.data_storage_size == 152627
|
||||||
assert pc.graphic_card_model == 'mobile 945gse express integrated graphics controller'
|
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 ' \
|
assert format(net) == 'NetworkAdapter 2: model ar8121/ar8113/ar8114 ' \
|
||||||
'gigabit or fast ethernet, S/N 00:24:8c:7f:cf:2d'
|
'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, '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))
|
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) == 'HardDrive 7: model st9160310as, S/N 5sv4tqa6'
|
||||||
assert format(hdd, 't') == 'HardDrive st9160310as'
|
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):
|
def test_device_public(user: UserClient, client: Client):
|
||||||
s, _ = user.post(file('asus-eee-1000h.snapshot.11'), res=m.Snapshot)
|
s, _ = user.post(file('asus-eee-1000h.snapshot.11'), res=m.Snapshot)
|
||||||
html, _ = client.get(res=Device, item=s['device']['id'], accept=ANY)
|
html, _ = client.get(res=Device, item=s['device']['id'], accept=ANY)
|
||||||
assert 'intel atom cpu n270 @ 1.60ghz' in html
|
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.')
|
@pytest.mark.xfail(reason='Functionality not yet developed.')
|
||||||
|
|
|
@ -283,6 +283,11 @@ def test_price_custom():
|
||||||
assert c['price']['id'] == p['id']
|
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')
|
@pytest.mark.xfail(reson='Develop test')
|
||||||
def test_ereuse_price():
|
def test_ereuse_price():
|
||||||
"""Tests the several ways of creating eReuse Price, emulating
|
"""Tests the several ways of creating eReuse Price, emulating
|
||||||
|
|
Reference in a new issue