Add public device view
This commit is contained in:
parent
38060d47ec
commit
77c96c5956
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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 \
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
2
setup.py
2
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',
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue