diff --git a/ereuse_devicehub/resources/device/__init__.py b/ereuse_devicehub/resources/device/__init__.py
index b8dda2ce..e888c988 100644
--- a/ereuse_devicehub/resources/device/__init__.py
+++ b/ereuse_devicehub/resources/device/__init__.py
@@ -1,3 +1,5 @@
+from typing import Callable, Iterable, Tuple
+
from teal.resource import Converters, Resource
from ereuse_devicehub.resources.device import schemas
@@ -9,7 +11,19 @@ class DeviceDef(Resource):
SCHEMA = schemas.Device
VIEW = DeviceView
ID_CONVERTER = Converters.int
- AUTH = True
+ AUTH = False # We manage this at each view
+
+ def __init__(self, app,
+ import_name=__name__, static_folder=None,
+ static_url_path=None,
+ template_folder='templates',
+ 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 ComputerDef(DeviceDef):
diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py
index 6292c908..c8fd186b 100644
--- a/ereuse_devicehub/resources/device/models.py
+++ b/ereuse_devicehub/resources/device/models.py
@@ -3,7 +3,7 @@ import pathlib
from contextlib import suppress
from itertools import chain
from operator import attrgetter
-from typing import Dict, Set
+from typing import Dict, List, Set
from boltons import urlutils
from citext import CIText
@@ -111,8 +111,18 @@ class Device(Thing):
def __lt__(self, other):
return self.id < other.id
- def __repr__(self) -> str:
- return '<{0.t} {0.id!r} model={0.model!r} S/N={0.serial_number!r}>'.format(self)
+ def __str__(self) -> str:
+ return '{0.t} {0.id}: model {0.model}, S/N {0.serial_number}'.format(self)
+
+ def __format__(self, format_spec):
+ if not format_spec:
+ return super().__format__(format_spec)
+ v = ''
+ 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)
+ return v
class DisplayMixin:
@@ -135,6 +145,14 @@ class DisplayMixin:
in pixels.
"""
+ def __format__(self, format_spec: str) -> str:
+ v = ''
+ 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} – {0.size}in {0.technology}'
+ return v
+
class Computer(Device):
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
@@ -144,6 +162,46 @@ class Computer(Device):
def events(self) -> list:
return sorted(chain(super().events, self.events_parent), key=attrgetter('created'))
+ @property
+ def ram_size(self) -> int:
+ """The total of RAM memory the computer has."""
+ return sum(ram.size for ram in self.components if isinstance(ram, RamModule))
+
+ @property
+ def data_storage_size(self) -> int:
+ """The total of data storage the computer has."""
+ return sum(ds.size for ds in self.components if isinstance(ds, DataStorage))
+
+ @property
+ def processor_model(self) -> str:
+ """The model of one of the processors of the computer."""
+ return next(p.model for p in self.components if isinstance(p, Processor))
+
+ @property
+ def graphic_card_model(self) -> str:
+ """The model of one of the graphic cards of the computer."""
+ return next(p.model for p in self.components if isinstance(p, GraphicCard))
+
+ @property
+ def network_speeds(self) -> List[int]:
+ """Returns two speeds: the first for the eth and the
+ second for the wifi networks, or 0 respectively if not found.
+ """
+ speeds = [0, 0]
+ for net in (c for c in self.components if isinstance(c, NetworkAdapter)):
+ speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless])
+ return speeds
+
+ def __format__(self, format_spec):
+ if not format_spec:
+ return super().__format__(format_spec)
+ v = ''
+ 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)
+ return v
+
class Desktop(Computer):
pass
@@ -260,6 +318,12 @@ class DataStorage(JoinedComponentTableMixin, Component):
"""
interface = Column(DBEnum(DataStorageInterface))
+ def __format__(self, format_spec):
+ v = super().__format__(format_spec)
+ if 's' in format_spec:
+ v += ' – {} GB'.format(self.size // 1000)
+ return v
+
class HardDrive(DataStorage):
pass
@@ -290,6 +354,12 @@ class NetworkMixin:
Whether it is a wireless interface.
"""
+ def __format__(self, format_spec):
+ v = super().__format__(format_spec)
+ if 's' in format_spec:
+ v += ' – {} Mbps'.format(self.speed)
+ return v
+
class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
pass
diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi
index 5dbf9782..a5bcb688 100644
--- a/ereuse_devicehub/resources/device/models.pyi
+++ b/ereuse_devicehub/resources/device/models.pyi
@@ -76,6 +76,29 @@ class Computer(DisplayMixin, Device):
self.events_parent = ... # type: Set[Event]
self.chassis = ... # type: ComputerChassis
+ @property
+ def events(self) -> List:
+ pass
+
+ @property
+ def ram_size(self) -> int:
+ pass
+
+ @property
+ def data_storage_size(self) -> int:
+ pass
+
+ @property
+ def processor_model(self) -> str:
+ pass
+
+ @property
+ def graphic_card_model(self) -> str:
+ pass
+
+ @property
+ def network_speeds(self) -> List[int]:
+ pass
class Desktop(Computer):
pass
diff --git a/ereuse_devicehub/resources/device/templates/devices/layout.html b/ereuse_devicehub/resources/device/templates/devices/layout.html
new file mode 100644
index 00000000..ef1fa6fb
--- /dev/null
+++ b/ereuse_devicehub/resources/device/templates/devices/layout.html
@@ -0,0 +1,97 @@
+{% import 'devices/macros.html' as macros %}
+
+
+
+
+
+
+ Devicehub | {{ device.__format__('t') }}
+
+
+
+
+
+
+
+
+
+
+ |
+ Range |
+
+
+
+
+
+
+ CPU – {{ device.processor_model }}
+ {{ macros.component_type(device.components, 'Processor') }}
+
+ |
+ |
+
+
+
+
+ RAM – {{ device.ram_size // 1000 }} GB
+ {{ macros.component_type(device.components, 'RamModule') }}
+
+ |
+ //range// |
+
+
+
+
+ Data Storage – {{ device.data_storage_size // 1000 }} GB
+ {{ macros.component_type(device.components, 'SolidStateDrive') }}
+ {{ macros.component_type(device.components, 'HardDrive') }}
+
+ |
+ //range// |
+
+
+
+
+ Graphics – {{ device.graphic_card_model }}
+ {{ macros.component_type(device.components, 'GraphicCard') }}
+
+ |
+ //range// |
+
+
+
+
+ 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 %}
+
+ {{ macros.component_type(device.components, 'NetworkAdapter') }}
+
+ |
+ |
+
+
+
+
+
+
+
+
+
diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py
index 2226cdc6..e6ae99ab 100644
--- a/ereuse_devicehub/resources/device/views.py
+++ b/ereuse_devicehub/resources/device/views.py
@@ -1,9 +1,13 @@
+import datetime
+
import marshmallow
-from flask import current_app as app
+from flask import current_app as app, render_template, request
from flask.json import jsonify
from flask_sqlalchemy import Pagination
+from teal.cache import cache
from teal.resource import View
+from ereuse_devicehub import auth
from ereuse_devicehub.resources.device.models import Device, Manufacturer
@@ -27,9 +31,21 @@ class DeviceView(View):
def one(self, id: int):
"""Gets one device."""
+ if not request.authorization:
+ return self.one_public(id)
+ else:
+ return self.one_private(id)
+
+ def one_public(self, id: int):
+ device = Device.query.filter_by(id=id).one()
+ return render_template('devices/layout.html', device=device)
+
+ @auth.Auth.requires_auth
+ def one_private(self, id: int):
device = Device.query.filter_by(id=id).one()
return self.schema.jsonify(device)
+ @auth.Auth.requires_auth
def find(self, args: dict):
"""Gets many devices."""
return self.schema.jsonify(Device.query, many=True)
@@ -41,6 +57,7 @@ class ManufacturerView(View):
# Disallow like operators
validate=lambda x: '%' not in x and '_' not in x)
+ @cache(datetime.timedelta(days=1))
def find(self, args: dict):
name = args['name']
manufacturers = Manufacturer.query \
diff --git a/ereuse_devicehub/resources/enums.py b/ereuse_devicehub/resources/enums.py
index 6340a6dc..3aed8e6e 100644
--- a/ereuse_devicehub/resources/enums.py
+++ b/ereuse_devicehub/resources/enums.py
@@ -166,12 +166,18 @@ class RamInterface(Enum):
DDR5 = 'DDR5 SDRAM'
DDR6 = 'DDR6 SDRAM'
+ def __str__(self):
+ return self.value
+
@unique
class RamFormat(Enum):
DIMM = 'DIMM'
SODIMM = 'SODIMM'
+ def __str__(self):
+ return self.value
+
@unique
class DataStorageInterface(Enum):
@@ -179,6 +185,9 @@ class DataStorageInterface(Enum):
USB = 'USB'
PCI = 'PCI'
+ def __str__(self):
+ return self.value
+
@unique
class DisplayTech(Enum):
@@ -190,15 +199,18 @@ class DisplayTech(Enum):
OLED = 'Organic light-emitting diode (OLED)'
AMOLED = 'Organic light-emitting diode (AMOLED)'
+ def __str__(self):
+ return self.name
+
@unique
class ComputerChassis(Enum):
"""The chassis of a computer."""
Tower = 'Tower'
Docking = 'Docking'
- AllInOne = 'AllInOne'
+ AllInOne = 'All in one'
Microtower = 'Microtower'
- PizzaBox = 'PizzaBox'
+ PizzaBox = 'Pizza box'
Lunchbox = 'Lunchbox'
Stick = 'Stick'
Netbook = 'Netbook'
@@ -207,7 +219,10 @@ class ComputerChassis(Enum):
Convertible = 'Convertible'
Detachable = 'Detachable'
Tablet = 'Tablet'
- Virtual = 'Virtual: A device with no chassis, probably non-physical.'
+ Virtual = 'Non-physical device'
+
+ def __format__(self, format_spec):
+ return self.value.lower()
class ReceiverRole(Enum):
diff --git a/requirements.txt b/requirements.txt
index b612d4e8..6ecbef7f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -26,7 +26,7 @@ requests==2.19.1
requests-mock==1.5.2
SQLAlchemy==1.2.11
SQLAlchemy-Utils==0.33.3
-teal==0.2.0a20
+teal==0.2.0a21
webargs==4.0.0
Werkzeug==0.14.1
sqlalchemy-citext==1.3.post0
diff --git a/setup.py b/setup.py
index 83ae609c..e39e665d 100644
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@ setup(
long_description=long_description,
long_description_content_type='text/markdown',
install_requires=[
- 'teal>=0.2.0a20', # teal always first
+ 'teal>=0.2.0a21', # teal always first
'click',
'click-spinner',
'ereuse-rate==0.0.2',
diff --git a/tests/test_device.py b/tests/test_device.py
index 693456a5..2ce2d6dd 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -1,20 +1,22 @@
+import datetime
from datetime import timedelta
from uuid import UUID
import pytest
from colour import Color
from ereuse_utils.naming import Naming
+from ereuse_utils.test import ANY
from pytest import raises
from sqlalchemy.util import OrderedSet
from teal.db import ResourceNotFound
-from ereuse_devicehub.client import UserClient
+from ereuse_devicehub.client import Client, UserClient
from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.agent.models import Person
from ereuse_devicehub.resources.device.exceptions import NeedsId
-from ereuse_devicehub.resources.device.models import Component, ComputerMonitor, Desktop, Device, \
- GraphicCard, Laptop, Motherboard, NetworkAdapter
+from ereuse_devicehub.resources.device.models import Component, ComputerMonitor, DataStorage, \
+ Desktop, Device, GraphicCard, Laptop, Motherboard, NetworkAdapter
from ereuse_devicehub.resources.device.schemas import Device as DeviceS
from ereuse_devicehub.resources.device.search import DeviceSearch
from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \
@@ -470,11 +472,42 @@ def test_device_search_all_devices_token_if_empty(app: Devicehub, user: UserClie
def test_manufacturer(user: UserClient):
- m, _ = user.get(res='Manufacturer', query=[('name', 'asus')])
+ m, r = user.get(res='Manufacturer', query=[('name', 'asus')])
assert m == {'items': [{'name': 'Asus', 'url': 'https://en.wikipedia.org/wiki/Asus'}]}
+ assert r.cache_control.public
+ assert r.expires > datetime.datetime.now()
@pytest.mark.xfail(reason='Develop functionality')
def test_manufacturer_enforced():
"""Ensures that non-computer devices can submit only
manufacturers from the Manufacturer table."""
+
+
+def test_device_properties_format(app: Devicehub, user: UserClient):
+ user.post(file('asus-eee-1000h.snapshot.11'), res=m.Snapshot)
+ 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 pc.ram_size == 1024
+ assert pc.data_storage_size == 152627
+ assert pc.graphic_card_model == 'mobile 945gse express integrated graphics controller'
+ assert pc.processor_model == 'intel atom cpu n270 @ 1.60ghz'
+ net = next(c for c in pc.components if isinstance(c, NetworkAdapter))
+ 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'
+ 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'
+
+
+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