Add public device view

This commit is contained in:
Xavier Bustamante Talavera 2018-10-03 14:51:22 +02:00
parent 38060d47ec
commit 77c96c5956
9 changed files with 283 additions and 14 deletions

View File

@ -1,3 +1,5 @@
from typing import Callable, Iterable, Tuple
from teal.resource import Converters, Resource from teal.resource import Converters, Resource
from ereuse_devicehub.resources.device import schemas from ereuse_devicehub.resources.device import schemas
@ -9,7 +11,19 @@ class DeviceDef(Resource):
SCHEMA = schemas.Device SCHEMA = schemas.Device
VIEW = DeviceView VIEW = DeviceView
ID_CONVERTER = Converters.int 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): class ComputerDef(DeviceDef):

View File

@ -3,7 +3,7 @@ import pathlib
from contextlib import suppress from contextlib import suppress
from itertools import chain from itertools import chain
from operator import attrgetter from operator import attrgetter
from typing import Dict, Set from typing import Dict, List, Set
from boltons import urlutils from boltons import urlutils
from citext import CIText from citext import CIText
@ -111,8 +111,18 @@ class Device(Thing):
def __lt__(self, other): def __lt__(self, other):
return self.id < other.id return self.id < other.id
def __repr__(self) -> str: def __str__(self) -> str:
return '<{0.t} {0.id!r} model={0.model!r} S/N={0.serial_number!r}>'.format(self) 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: class DisplayMixin:
@ -135,6 +145,14 @@ class DisplayMixin:
in pixels. 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): class Computer(Device):
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
@ -144,6 +162,46 @@ class Computer(Device):
def events(self) -> list: def events(self) -> list:
return sorted(chain(super().events, self.events_parent), key=attrgetter('created')) 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): class Desktop(Computer):
pass pass
@ -260,6 +318,12 @@ class DataStorage(JoinedComponentTableMixin, Component):
""" """
interface = Column(DBEnum(DataStorageInterface)) 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): class HardDrive(DataStorage):
pass pass
@ -290,6 +354,12 @@ class NetworkMixin:
Whether it is a wireless interface. 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): class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
pass pass

View File

@ -76,6 +76,29 @@ class Computer(DisplayMixin, Device):
self.events_parent = ... # type: Set[Event] self.events_parent = ... # type: Set[Event]
self.chassis = ... # type: ComputerChassis 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): class Desktop(Computer):
pass pass

View File

@ -0,0 +1,97 @@
{% import 'devices/macros.html' as macros %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="https://stackpath.bootstrapcdn.com/bootswatch/3.3.7/flatly/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-+ENW/yibaokMnme+vBLnHMphUYxHs34h9lpdbSLuAwGkOKFRl4C34WkjazBtb7eT"
crossorigin="anonymous">
<title>Devicehub | {{ device.__format__('t') }}</title>
</head>
<body class="container">
<div class="page-header">
<h1>{{ device.__format__('t') }}
<small>{{ device.__format__('s') }}</small>
</h1>
</div>
<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>
</div>
</body>
</html>

View File

@ -1,9 +1,13 @@
import datetime
import marshmallow 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.json import jsonify
from flask_sqlalchemy import Pagination from flask_sqlalchemy import Pagination
from teal.cache import cache
from teal.resource import View from teal.resource import View
from ereuse_devicehub import auth
from ereuse_devicehub.resources.device.models import Device, Manufacturer from ereuse_devicehub.resources.device.models import Device, Manufacturer
@ -27,9 +31,21 @@ class DeviceView(View):
def one(self, id: int): def one(self, id: int):
"""Gets one device.""" """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() device = Device.query.filter_by(id=id).one()
return self.schema.jsonify(device) return self.schema.jsonify(device)
@auth.Auth.requires_auth
def find(self, args: dict): def find(self, args: dict):
"""Gets many devices.""" """Gets many devices."""
return self.schema.jsonify(Device.query, many=True) return self.schema.jsonify(Device.query, many=True)
@ -41,6 +57,7 @@ class ManufacturerView(View):
# Disallow like operators # Disallow like operators
validate=lambda x: '%' not in x and '_' not in x) validate=lambda x: '%' not in x and '_' not in x)
@cache(datetime.timedelta(days=1))
def find(self, args: dict): def find(self, args: dict):
name = args['name'] name = args['name']
manufacturers = Manufacturer.query \ manufacturers = Manufacturer.query \

View File

@ -166,12 +166,18 @@ class RamInterface(Enum):
DDR5 = 'DDR5 SDRAM' DDR5 = 'DDR5 SDRAM'
DDR6 = 'DDR6 SDRAM' DDR6 = 'DDR6 SDRAM'
def __str__(self):
return self.value
@unique @unique
class RamFormat(Enum): class RamFormat(Enum):
DIMM = 'DIMM' DIMM = 'DIMM'
SODIMM = 'SODIMM' SODIMM = 'SODIMM'
def __str__(self):
return self.value
@unique @unique
class DataStorageInterface(Enum): class DataStorageInterface(Enum):
@ -179,6 +185,9 @@ class DataStorageInterface(Enum):
USB = 'USB' USB = 'USB'
PCI = 'PCI' PCI = 'PCI'
def __str__(self):
return self.value
@unique @unique
class DisplayTech(Enum): class DisplayTech(Enum):
@ -190,15 +199,18 @@ class DisplayTech(Enum):
OLED = 'Organic light-emitting diode (OLED)' OLED = 'Organic light-emitting diode (OLED)'
AMOLED = 'Organic light-emitting diode (AMOLED)' AMOLED = 'Organic light-emitting diode (AMOLED)'
def __str__(self):
return self.name
@unique @unique
class ComputerChassis(Enum): class ComputerChassis(Enum):
"""The chassis of a computer.""" """The chassis of a computer."""
Tower = 'Tower' Tower = 'Tower'
Docking = 'Docking' Docking = 'Docking'
AllInOne = 'AllInOne' AllInOne = 'All in one'
Microtower = 'Microtower' Microtower = 'Microtower'
PizzaBox = 'PizzaBox' PizzaBox = 'Pizza box'
Lunchbox = 'Lunchbox' Lunchbox = 'Lunchbox'
Stick = 'Stick' Stick = 'Stick'
Netbook = 'Netbook' Netbook = 'Netbook'
@ -207,7 +219,10 @@ class ComputerChassis(Enum):
Convertible = 'Convertible' Convertible = 'Convertible'
Detachable = 'Detachable' Detachable = 'Detachable'
Tablet = 'Tablet' 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): class ReceiverRole(Enum):

View File

@ -26,7 +26,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.0a20 teal==0.2.0a21
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

View File

@ -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.0a20', # teal always first 'teal>=0.2.0a21', # teal always first
'click', 'click',
'click-spinner', 'click-spinner',
'ereuse-rate==0.0.2', 'ereuse-rate==0.0.2',

View File

@ -1,20 +1,22 @@
import datetime
from datetime import timedelta from datetime import timedelta
from uuid import UUID from uuid import UUID
import pytest import pytest
from colour import Color from colour import Color
from ereuse_utils.naming import Naming from ereuse_utils.naming import Naming
from ereuse_utils.test import ANY
from pytest import raises from pytest import raises
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from teal.db import ResourceNotFound 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.db import db
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.agent.models import Person from ereuse_devicehub.resources.agent.models import Person
from ereuse_devicehub.resources.device.exceptions import NeedsId from ereuse_devicehub.resources.device.exceptions import NeedsId
from ereuse_devicehub.resources.device.models import Component, ComputerMonitor, Desktop, Device, \ from ereuse_devicehub.resources.device.models import Component, ComputerMonitor, DataStorage, \
GraphicCard, Laptop, Motherboard, NetworkAdapter Desktop, Device, GraphicCard, Laptop, Motherboard, NetworkAdapter
from ereuse_devicehub.resources.device.schemas import Device as DeviceS from ereuse_devicehub.resources.device.schemas import Device as DeviceS
from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.device.search import DeviceSearch
from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \ 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): 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 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') @pytest.mark.xfail(reason='Develop functionality')
def test_manufacturer_enforced(): def test_manufacturer_enforced():
"""Ensures that non-computer devices can submit only """Ensures that non-computer devices can submit only
manufacturers from the Manufacturer table.""" 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