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 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):

View File

@ -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

View File

@ -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

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
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 \

View File

@ -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):

View File

@ -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

View File

@ -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',

View File

@ -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