Get resources correctly with polymorphic
This commit is contained in:
parent
78b5a230d4
commit
ac26d8d610
|
@ -1,7 +1,11 @@
|
||||||
|
from typing import Type, Union
|
||||||
|
|
||||||
|
from boltons.typeutils import issubclass
|
||||||
from ereuse_utils.test import JSON
|
from ereuse_utils.test import JSON
|
||||||
from flask import Response
|
from flask import Response
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
|
from ereuse_devicehub.resources.models import Thing
|
||||||
from teal.client import Client as TealClient
|
from teal.client import Client as TealClient
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,6 +14,26 @@ class Client(TealClient):
|
||||||
allow_subdomain_redirects=False):
|
allow_subdomain_redirects=False):
|
||||||
super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects)
|
super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects)
|
||||||
|
|
||||||
|
def open(self, uri: str, res: str or Type[Thing] = None, status: int or HTTPException = 200,
|
||||||
|
query: dict = {}, accept=JSON, content_type=JSON, item=None, headers: dict = None,
|
||||||
|
token: str = None, **kw) -> (dict or str, Response):
|
||||||
|
if issubclass(res, Thing):
|
||||||
|
res = res.__name__
|
||||||
|
return super().open(uri, res, status, query, accept, content_type, item, headers, token,
|
||||||
|
**kw)
|
||||||
|
|
||||||
|
def get(self, uri: str = '', res: Union[Type[Thing], str] = None, query: dict = {},
|
||||||
|
status: int or HTTPException = 200, item: Union[int, str] = None, accept: str = JSON,
|
||||||
|
headers: dict = None, token: str = None, **kw) -> (dict or str, Response):
|
||||||
|
return super().get(uri, res, query, status, item, accept, headers, token, **kw)
|
||||||
|
|
||||||
|
def post(self, data: str or dict, uri: str = '', res: Union[Type[Thing], str] = None,
|
||||||
|
query: dict = {}, status: int or HTTPException = 201, content_type: str = JSON,
|
||||||
|
accept: str = JSON, headers: dict = None, token: str = None, **kw) -> (
|
||||||
|
dict or str, Response):
|
||||||
|
return super().post(data, uri, res, query, status, content_type, accept, headers, token,
|
||||||
|
**kw)
|
||||||
|
|
||||||
def login(self, email: str, password: str):
|
def login(self, email: str, password: str):
|
||||||
assert isinstance(email, str)
|
assert isinstance(email, str)
|
||||||
assert isinstance(password, str)
|
assert isinstance(password, str)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from distutils.version import StrictVersion
|
||||||
|
|
||||||
from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DesktopDef, DeviceDef, \
|
from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DesktopDef, DeviceDef, \
|
||||||
GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, MotherboardDef, NetbookDef, \
|
GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, MotherboardDef, NetbookDef, \
|
||||||
NetworkAdapterDef, RamModuleDef, ServerDef
|
NetworkAdapterDef, ProcessorDef, RamModuleDef, ServerDef
|
||||||
from ereuse_devicehub.resources.event import EventDef, SnapshotDef
|
from ereuse_devicehub.resources.event import EventDef, SnapshotDef
|
||||||
from ereuse_devicehub.resources.user import UserDef
|
from ereuse_devicehub.resources.user import UserDef
|
||||||
from teal.config import Config
|
from teal.config import Config
|
||||||
|
@ -12,8 +12,8 @@ class DevicehubConfig(Config):
|
||||||
RESOURCE_DEFINITIONS = (
|
RESOURCE_DEFINITIONS = (
|
||||||
DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, MicrotowerDef,
|
DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, MicrotowerDef,
|
||||||
ComponentDef, GraphicCardDef, HardDriveDef, MotherboardDef, NetworkAdapterDef,
|
ComponentDef, GraphicCardDef, HardDriveDef, MotherboardDef, NetworkAdapterDef,
|
||||||
RamModuleDef, UserDef, EventDef, SnapshotDef
|
RamModuleDef, ProcessorDef, UserDef, EventDef, SnapshotDef
|
||||||
)
|
)
|
||||||
PASSWORD_SCHEMES = {'pbkdf2_sha256'}
|
PASSWORD_SCHEMES = {'pbkdf2_sha256'}
|
||||||
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1'
|
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1'
|
||||||
MIN_WORKBENCH = StrictVersion('11.0')
|
MIN_WORKBENCH = StrictVersion('11.0')
|
||||||
|
|
|
@ -6,6 +6,7 @@ from teal.marshmallow import NestedOn as TealNestedOn
|
||||||
|
|
||||||
|
|
||||||
class NestedOn(TealNestedOn):
|
class NestedOn(TealNestedOn):
|
||||||
|
__doc__ = TealNestedOn.__doc__
|
||||||
|
|
||||||
def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, default=missing_,
|
def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, default=missing_,
|
||||||
exclude=tuple(), only=None, **kwargs):
|
exclude=tuple(), only=None, **kwargs):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from ereuse_devicehub.resources.device.schemas import Component, Computer, Desktop, Device, \
|
from ereuse_devicehub.resources.device.schemas import Component, Computer, Desktop, Device, \
|
||||||
GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, RamModule, \
|
GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, Processor, \
|
||||||
Server
|
RamModule, Server
|
||||||
from ereuse_devicehub.resources.device.views import DeviceView
|
from ereuse_devicehub.resources.device.views import DeviceView
|
||||||
from teal.resource import Converters, Resource
|
from teal.resource import Converters, Resource
|
||||||
|
|
||||||
|
@ -58,3 +58,7 @@ class NetworkAdapterDef(ComponentDef):
|
||||||
|
|
||||||
class RamModuleDef(ComponentDef):
|
class RamModuleDef(ComponentDef):
|
||||||
SCHEMA = RamModule
|
SCHEMA = RamModule
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessorDef(ComponentDef):
|
||||||
|
SCHEMA = Processor
|
||||||
|
|
|
@ -144,6 +144,13 @@ class NetworkAdapter(Component):
|
||||||
speed = Column(SmallInteger, check_range('speed', min=10, max=10000)) # type: int
|
speed = Column(SmallInteger, check_range('speed', min=10, max=10000)) # type: int
|
||||||
|
|
||||||
|
|
||||||
|
class Processor(Component):
|
||||||
|
id = Column(BigInteger, ForeignKey(Component.id), primary_key=True) # type: int
|
||||||
|
speed = Column(Float, check_range('speed', 0.1, 15))
|
||||||
|
cores = Column(SmallInteger, check_range('cores', 1, 10))
|
||||||
|
address = Column(SmallInteger, check_range('address', 8, 256))
|
||||||
|
|
||||||
|
|
||||||
class RamModule(Component):
|
class RamModule(Component):
|
||||||
id = Column(BigInteger, ForeignKey(Component.id), primary_key=True) # type: int
|
id = Column(BigInteger, ForeignKey(Component.id), primary_key=True) # type: int
|
||||||
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from marshmallow.fields import Float, Integer, Nested, Str
|
from marshmallow import post_dump
|
||||||
from marshmallow.validate import Length, Range
|
from marshmallow.fields import Float, Integer, Str
|
||||||
|
from marshmallow.validate import Length, OneOf, Range
|
||||||
|
|
||||||
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
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, UnitCodes
|
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
||||||
|
|
||||||
|
@ -29,11 +31,21 @@ class Device(Thing):
|
||||||
height = Float(validate=Range(0.1, 3),
|
height = Float(validate=Range(0.1, 3),
|
||||||
unit=UnitCodes.m,
|
unit=UnitCodes.m,
|
||||||
description='The height of the device in meters.')
|
description='The height of the device in meters.')
|
||||||
events = Nested('Event', many=True, dump_only=True, only='id')
|
events = NestedOn('Event', many=True, dump_only=True)
|
||||||
|
events_one = NestedOn('Event', many=True, dump_only=True, description='Not used.')
|
||||||
|
events_components = NestedOn('Event', many=True, dump_only=True, description='Not used.')
|
||||||
|
|
||||||
|
@post_dump
|
||||||
|
def merge_events(self, data: dict) -> dict:
|
||||||
|
if isinstance(data.get('events_one', None), list):
|
||||||
|
data.setdefault('events', []).extend(data.pop('events_one'))
|
||||||
|
if isinstance(data.get('events_components', None), list):
|
||||||
|
data.setdefault('events', []).extend(data.pop('events_components'))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class Computer(Device):
|
class Computer(Device):
|
||||||
components = Nested('Component', many=True, dump_only=True, only='id')
|
components = NestedOn('Component', many=True, dump_only=True)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,7 +70,7 @@ class Microtower(Computer):
|
||||||
|
|
||||||
|
|
||||||
class Component(Device):
|
class Component(Device):
|
||||||
parent = Nested(Device, dump_only=True, only='id')
|
parent = NestedOn(Device, dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
class GraphicCard(Component):
|
class GraphicCard(Component):
|
||||||
|
@ -71,9 +83,9 @@ class HardDrive(Component):
|
||||||
size = Integer(validate=Range(0, 10 ** 8),
|
size = Integer(validate=Range(0, 10 ** 8),
|
||||||
unit=UnitCodes.mbyte,
|
unit=UnitCodes.mbyte,
|
||||||
description='The size of the hard-drive in MB.')
|
description='The size of the hard-drive in MB.')
|
||||||
erasure = Nested('EraseBasic', load_only=True)
|
erasure = NestedOn('EraseBasic', load_only=True)
|
||||||
tests = Nested('TestHardDrive', many=True, load_only=True)
|
tests = NestedOn('TestHardDrive', many=True, load_only=True)
|
||||||
benchmarks = Nested('BenchmarkHardDrive', load_only=True, many=True)
|
benchmarks = NestedOn('BenchmarkHardDrive', load_only=True, many=True)
|
||||||
|
|
||||||
|
|
||||||
class Motherboard(Component):
|
class Motherboard(Component):
|
||||||
|
@ -90,6 +102,12 @@ class NetworkAdapter(Component):
|
||||||
description='The maximum speed this network adapter can handle, in mbps.')
|
description='The maximum speed this network adapter can handle, in mbps.')
|
||||||
|
|
||||||
|
|
||||||
|
class Processor(Component):
|
||||||
|
speed = Float(validate=Range(min=0.1, max=15), unit=UnitCodes.ghz)
|
||||||
|
cores = Integer(validate=Range(min=1, max=10)) # todo from numberOfCores
|
||||||
|
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}))
|
||||||
|
|
||||||
|
|
||||||
class RamModule(Component):
|
class RamModule(Component):
|
||||||
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
|
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
|
||||||
speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
|
speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
|
||||||
|
|
|
@ -160,7 +160,6 @@ class Sync:
|
||||||
"""
|
"""
|
||||||
events = []
|
events = []
|
||||||
old_components = set(device.components)
|
old_components = set(device.components)
|
||||||
|
|
||||||
adding = components - old_components
|
adding = components - old_components
|
||||||
if adding:
|
if adding:
|
||||||
add = Add(device=device, components=list(adding))
|
add = Add(device=device, components=list(adding))
|
||||||
|
@ -177,5 +176,4 @@ class Sync:
|
||||||
removing = old_components - components
|
removing = old_components - components
|
||||||
if removing:
|
if removing:
|
||||||
events.append(Remove(device=device, components=list(removing)))
|
events.append(Remove(device=device, components=list(removing)))
|
||||||
|
|
||||||
return events
|
return events
|
||||||
|
|
|
@ -5,4 +5,10 @@ from teal.resource import View
|
||||||
class DeviceView(View):
|
class DeviceView(View):
|
||||||
def one(self, id: int):
|
def one(self, id: int):
|
||||||
"""Gets one device."""
|
"""Gets one device."""
|
||||||
return Device.query.filter_by(id=id).one()
|
device = Device.query.filter_by(id=id).one()
|
||||||
|
return self.schema.jsonify_polymorphic(device)
|
||||||
|
|
||||||
|
def find(self, args: dict):
|
||||||
|
"""Gets many devices"""
|
||||||
|
devices = Device.query.all()
|
||||||
|
return self.schema.jsonify_polymorphic_many(devices)
|
||||||
|
|
|
@ -30,17 +30,17 @@ class Event(Thing):
|
||||||
'hardware without margin of doubt.')
|
'hardware without margin of doubt.')
|
||||||
incidence = Boolean(default=False,
|
incidence = Boolean(default=False,
|
||||||
description='Was something wrong in this event?')
|
description='Was something wrong in this event?')
|
||||||
snapshot = Nested('Snapshot', dump_only=True, only='id')
|
snapshot = NestedOn('Snapshot', dump_only=True, only='id')
|
||||||
description = String(default='', description='A comment about the event.')
|
description = String(default='', description='A comment about the event.')
|
||||||
components = Nested(Component, dump_only=True, only='id', many=True)
|
components = NestedOn(Component, dump_only=True, many=True)
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(Event):
|
class EventWithOneDevice(Event):
|
||||||
device = Nested(Device, only='id')
|
device = NestedOn(Device, only='id')
|
||||||
|
|
||||||
|
|
||||||
class EventWithMultipleDevices(Event):
|
class EventWithMultipleDevices(Event):
|
||||||
device = Nested(Device, many=True, only='id')
|
device = NestedOn(Device, many=True, only='id')
|
||||||
|
|
||||||
|
|
||||||
class Add(EventWithOneDevice):
|
class Add(EventWithOneDevice):
|
||||||
|
@ -52,14 +52,14 @@ class Remove(EventWithOneDevice):
|
||||||
|
|
||||||
|
|
||||||
class Allocate(EventWithMultipleDevices):
|
class Allocate(EventWithMultipleDevices):
|
||||||
to = Nested(User, only='id',
|
to = NestedOn(User,
|
||||||
description='The user the devices are allocated to.')
|
description='The user the devices are allocated to.')
|
||||||
organization = String(validate=Length(STR_SIZE),
|
organization = String(validate=Length(STR_SIZE),
|
||||||
description='The organization where the user was when this happened.')
|
description='The organization where the user was when this happened.')
|
||||||
|
|
||||||
|
|
||||||
class Deallocate(EventWithMultipleDevices):
|
class Deallocate(EventWithMultipleDevices):
|
||||||
from_rel = Nested(User, only='id',
|
from_rel = Nested(User,
|
||||||
data_key='from',
|
data_key='from',
|
||||||
description='The user where the devices are not allocated to anymore.')
|
description='The user where the devices are not allocated to anymore.')
|
||||||
organization = String(validate=Length(STR_SIZE),
|
organization = String(validate=Length(STR_SIZE),
|
||||||
|
@ -138,6 +138,7 @@ class Snapshot(EventWithOneDevice):
|
||||||
color = Color(description='Main color of the device.')
|
color = Color(description='Main color of the device.')
|
||||||
orientation = EnumField(Orientation, description='Is the device main stand wider or larger?')
|
orientation = EnumField(Orientation, description='Is the device main stand wider or larger?')
|
||||||
force_creation = Boolean(data_key='forceCreation')
|
force_creation = Boolean(data_key='forceCreation')
|
||||||
|
events = NestedOn(Event, many=True)
|
||||||
|
|
||||||
@validates_schema
|
@validates_schema
|
||||||
def validate_workbench_version(self, data: dict):
|
def validate_workbench_version(self, data: dict):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
|
||||||
from flask import request, Response
|
from flask import request
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.device.sync import Sync
|
from ereuse_devicehub.resources.device.sync import Sync
|
||||||
|
@ -28,12 +28,14 @@ class SnapshotView(View):
|
||||||
device = s.pop('device')
|
device = s.pop('device')
|
||||||
components = s.pop('components') if s['software'] == SoftwareType.Workbench else None
|
components = s.pop('components') if s['software'] == SoftwareType.Workbench else None
|
||||||
# noinspection PyArgumentList
|
# noinspection PyArgumentList
|
||||||
del s['type']
|
|
||||||
snapshot = Snapshot(**s)
|
snapshot = Snapshot(**s)
|
||||||
snapshot.device, snapshot.events = Sync.run(device, components, snapshot.force_creation)
|
snapshot.device, snapshot.events = Sync.run(device, components, snapshot.force_creation)
|
||||||
|
snapshot.components = snapshot.device.components
|
||||||
db.session.add(snapshot)
|
db.session.add(snapshot)
|
||||||
# transform it back
|
db.session.flush() # Take to DB so we get db-generated values
|
||||||
return Response(status=201)
|
ret = self.schema.jsonify(snapshot) # transform it back
|
||||||
|
ret.status_code = 201
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class TestHardDriveView(View):
|
class TestHardDriveView(View):
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from marshmallow.fields import DateTime, List, Nested, URL, String
|
from marshmallow import post_load
|
||||||
|
from marshmallow.fields import DateTime, List, String, URL
|
||||||
|
|
||||||
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
from teal.resource import Schema
|
from teal.resource import Schema
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,4 +24,8 @@ class Thing(Schema):
|
||||||
same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs')
|
same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs')
|
||||||
updated = DateTime('iso', dump_only=True)
|
updated = DateTime('iso', dump_only=True)
|
||||||
created = DateTime('iso', dump_only=True)
|
created = DateTime('iso', dump_only=True)
|
||||||
author = Nested('User', only='id', dump_only=True)
|
author = NestedOn('User', dump_only=True, exclude=('token',))
|
||||||
|
|
||||||
|
@post_load
|
||||||
|
def remove_type(self, data: dict):
|
||||||
|
data.pop('type', None)
|
||||||
|
|
|
@ -28,9 +28,10 @@ class UserDef(Resource):
|
||||||
"""
|
"""
|
||||||
Creates an user.
|
Creates an user.
|
||||||
"""
|
"""
|
||||||
with self.app.test_request_context():
|
with self.app.app_context():
|
||||||
self.schema.load({'email': email, 'password': password})
|
self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
|
||||||
|
.load({'email': email, 'password': password})
|
||||||
user = User(email=email, password=password)
|
user = User(email=email, password=password)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user.dump()
|
return self.schema.dump(user)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
from marshmallow import pre_dump
|
from marshmallow import post_dump
|
||||||
from marshmallow.fields import Email, String, UUID
|
from marshmallow.fields import Email, String, UUID
|
||||||
|
|
||||||
from ereuse_devicehub.resources.schemas import Thing
|
from ereuse_devicehub.resources.schemas import Thing
|
||||||
|
@ -10,13 +10,33 @@ class User(Thing):
|
||||||
id = UUID(dump_only=True)
|
id = UUID(dump_only=True)
|
||||||
email = Email(required=True)
|
email = Email(required=True)
|
||||||
password = String(load_only=True, required=True)
|
password = String(load_only=True, required=True)
|
||||||
|
name = String()
|
||||||
token = String(dump_only=True,
|
token = String(dump_only=True,
|
||||||
description='Use this token in an Authorization header to access the app.'
|
description='Use this token in an Authorization header to access the app.'
|
||||||
'The token can change overtime.')
|
'The token can change overtime.')
|
||||||
|
|
||||||
@pre_dump
|
def __init__(self,
|
||||||
|
only=None,
|
||||||
|
exclude=('token',),
|
||||||
|
prefix='',
|
||||||
|
many=False,
|
||||||
|
context=None,
|
||||||
|
load_only=(),
|
||||||
|
dump_only=(),
|
||||||
|
partial=False):
|
||||||
|
"""
|
||||||
|
Instantiates the User.
|
||||||
|
|
||||||
|
By default we exclude token from both load/dump
|
||||||
|
so they are not taken / set in normal usage by mistake.
|
||||||
|
"""
|
||||||
|
super().__init__(only, exclude, prefix, many, context, load_only, dump_only, partial)
|
||||||
|
|
||||||
|
@post_dump
|
||||||
def base64encode_token(self, data: dict):
|
def base64encode_token(self, data: dict):
|
||||||
"""Encodes the token to base64 so clients don't have to."""
|
"""Encodes the token to base64 so clients don't have to."""
|
||||||
# framework needs ':' at the end
|
if 'token' in data:
|
||||||
data['token'] = b64encode(str.encode(str(data['token']) + ':'))
|
# In many cases we don't dump the token (ex. relationships)
|
||||||
|
# Framework needs ':' at the end
|
||||||
|
data['token'] = b64encode(str.encode(str(data['token']) + ':')).decode()
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from flask import current_app as app, request
|
from flask import g, request
|
||||||
|
|
||||||
from ereuse_devicehub.resources.user.exceptions import WrongCredentials
|
from ereuse_devicehub.resources.user.exceptions import WrongCredentials
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
@ -14,10 +14,13 @@ class UserView(View):
|
||||||
|
|
||||||
|
|
||||||
def login():
|
def login():
|
||||||
user_s = app.resources['User'].schema # type: UserS
|
# We use custom schema as we only want to parse a subset of user
|
||||||
u = user_s.load(request.get_json(), partial=('email', 'password'))
|
user_s = g.resource_def.SCHEMA(only=('email', 'password')) # type: UserS
|
||||||
|
# noinspection PyArgumentList
|
||||||
|
u = request.get_json(schema=user_s)
|
||||||
user = User.query.filter_by(email=u['email']).one_or_none()
|
user = User.query.filter_by(email=u['email']).one_or_none()
|
||||||
if user and user.password == u['password']:
|
if user and user.password == u['password']:
|
||||||
return user_s.jsonify(user)
|
schema_with_token = g.resource_def.SCHEMA(exclude=set())
|
||||||
|
return schema_with_token.jsonify(user)
|
||||||
else:
|
else:
|
||||||
raise WrongCredentials()
|
raise WrongCredentials()
|
||||||
|
|
|
@ -28,7 +28,6 @@ def _app(config: TestConfig) -> Devicehub:
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def app(request, _app: Devicehub) -> Devicehub:
|
def app(request, _app: Devicehub) -> Devicehub:
|
||||||
db.drop_all(app=_app) # In case the test before was killed
|
|
||||||
db.create_all(app=_app)
|
db.create_all(app=_app)
|
||||||
# More robust than 'yield'
|
# More robust than 'yield'
|
||||||
request.addfinalizer(lambda *args, **kw: db.drop_all(app=_app))
|
request.addfinalizer(lambda *args, **kw: db.drop_all(app=_app))
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
device:
|
||||||
|
manufacturer: 'p1'
|
||||||
|
serialNumber: 'p1'
|
||||||
|
model: 'p1'
|
||||||
|
type: 'Desktop'
|
||||||
|
secured: False
|
||||||
|
components:
|
||||||
|
- manufacturer: 'p1c1m'
|
||||||
|
serialNumber: 'p1c1s'
|
||||||
|
type: 'Motherboard'
|
||||||
|
- manufacturer: 'p1c2m'
|
||||||
|
serialNumber: 'p1c2s'
|
||||||
|
model: 'p1c2'
|
||||||
|
speed: 1.23
|
||||||
|
cores: 2
|
||||||
|
type: 'Processor'
|
||||||
|
- manufacturer: 'p1c3m'
|
||||||
|
serialNumber: 'p1c3s'
|
||||||
|
type: 'GraphicCard'
|
||||||
|
memory: 1.5
|
||||||
|
condition:
|
||||||
|
appearance: 'A'
|
||||||
|
functionality: 'B'
|
||||||
|
elapsed: 25
|
||||||
|
software: 'Workbench'
|
||||||
|
uuid: '76860eca-c3fd-41f6-a801-6af7bd8cf832'
|
||||||
|
version: '11.0'
|
|
@ -0,0 +1,16 @@
|
||||||
|
device:
|
||||||
|
manufacturer: 'p2m'
|
||||||
|
serialNumber: 'p2s'
|
||||||
|
model: 'p2'
|
||||||
|
type: 'Microtower'
|
||||||
|
secured: False
|
||||||
|
components:
|
||||||
|
- manufacturer: 'p2c1m'
|
||||||
|
serialNumber: 'p2c1s'
|
||||||
|
type: 'Motherboard'
|
||||||
|
- manufacturer: 'p1c2m'
|
||||||
|
serialNumber: 'p1c2s'
|
||||||
|
model: 'p1'
|
||||||
|
speed: 1.23
|
||||||
|
cores: 2
|
||||||
|
type: 'Processor'
|
|
@ -0,0 +1,17 @@
|
||||||
|
device:
|
||||||
|
manufactuer: 'p1'
|
||||||
|
serialNumber: 'p1'
|
||||||
|
model: 'p1'
|
||||||
|
type: 'Desktop'
|
||||||
|
secured: False
|
||||||
|
components:
|
||||||
|
- manufacturer: 'p1c2m'
|
||||||
|
serialNumber: 'p1c2s'
|
||||||
|
model: 'p1'
|
||||||
|
type: 'Processor'
|
||||||
|
cores: 2
|
||||||
|
speed: 1.23
|
||||||
|
- manufacturer: 'p1c3m'
|
||||||
|
serialNumber: 'p1c3s'
|
||||||
|
type: 'GraphicCard'
|
||||||
|
memory: 1.5
|
|
@ -0,0 +1,15 @@
|
||||||
|
device:
|
||||||
|
manufactuer: 'p1'
|
||||||
|
serialNumber: 'p1'
|
||||||
|
model: 'p1'
|
||||||
|
type: 'Desktop'
|
||||||
|
secured: False
|
||||||
|
components:
|
||||||
|
- manufacturer: 'p1c4m'
|
||||||
|
serialNumber: 'p1c4s'
|
||||||
|
type: 'NetworkAdapter'
|
||||||
|
speed: 1000
|
||||||
|
- manufacturer: 'p1c3m'
|
||||||
|
serialNumber: 'p1c3s'
|
||||||
|
type: 'GraphicCard'
|
||||||
|
memory: 1.5
|
|
@ -1,10 +1,11 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from ereuse_devicehub.client import 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.device.exceptions import NeedsId
|
from ereuse_devicehub.resources.device.exceptions import NeedsId
|
||||||
from ereuse_devicehub.resources.device.models import Component, Computer, Desktop, Device, \
|
from ereuse_devicehub.resources.device.models import Component, Computer, Desktop, Device, \
|
||||||
GraphicCard, Motherboard, NetworkAdapter
|
GraphicCard, Laptop, Microtower, 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.sync import Sync
|
from ereuse_devicehub.resources.device.sync import Sync
|
||||||
from ereuse_devicehub.resources.event.models import Add, Remove
|
from ereuse_devicehub.resources.event.models import Add, Remove
|
||||||
|
@ -12,46 +13,47 @@ from teal.db import ResourceNotFound
|
||||||
from tests.conftest import file
|
from tests.conftest import file
|
||||||
|
|
||||||
|
|
||||||
def test_device_model(app: Devicehub):
|
@pytest.mark.usefixtures('app_context')
|
||||||
|
def test_device_model():
|
||||||
"""
|
"""
|
||||||
Tests that the correctness of the device model and its relationships.
|
Tests that the correctness of the device model and its relationships.
|
||||||
"""
|
"""
|
||||||
with app.test_request_context():
|
pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s')
|
||||||
pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s')
|
pc.components = components = [
|
||||||
pc.components = components = [
|
NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'),
|
||||||
NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'),
|
GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500)
|
||||||
GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500)
|
]
|
||||||
]
|
db.session.add(pc)
|
||||||
db.session.add(pc)
|
db.session.commit()
|
||||||
db.session.commit()
|
pc = Desktop.query.one()
|
||||||
pc = Desktop.query.one()
|
assert pc.serial_number == 'p1s'
|
||||||
assert pc.serial_number == 'p1s'
|
assert pc.components == components
|
||||||
assert pc.components == components
|
network_adapter = NetworkAdapter.query.one()
|
||||||
network_adapter = NetworkAdapter.query.one()
|
assert network_adapter.parent == pc
|
||||||
assert network_adapter.parent == pc
|
|
||||||
|
|
||||||
# Removing a component from pc doesn't delete the component
|
# Removing a component from pc doesn't delete the component
|
||||||
del pc.components[0]
|
del pc.components[0]
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
pc = Device.query.first() # this is the same as querying for Desktop directly
|
pc = Device.query.first() # this is the same as querying for Desktop directly
|
||||||
assert pc.components[0].type == GraphicCard.__name__
|
assert pc.components[0].type == GraphicCard.__name__
|
||||||
network_adapter = NetworkAdapter.query.one()
|
network_adapter = NetworkAdapter.query.one()
|
||||||
assert network_adapter not in pc.components
|
assert network_adapter not in pc.components
|
||||||
assert network_adapter.parent is None
|
assert network_adapter.parent is None
|
||||||
|
|
||||||
# Deleting the pc deletes everything
|
# Deleting the pc deletes everything
|
||||||
gcard = GraphicCard.query.one()
|
gcard = GraphicCard.query.one()
|
||||||
db.session.delete(pc)
|
db.session.delete(pc)
|
||||||
assert pc.id == 1
|
assert pc.id == 1
|
||||||
assert Desktop.query.first() is None
|
assert Desktop.query.first() is None
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
assert Desktop.query.first() is None
|
assert Desktop.query.first() is None
|
||||||
assert network_adapter.id == 2
|
assert network_adapter.id == 2
|
||||||
assert NetworkAdapter.query.first() is not None, 'We removed the network adaptor'
|
assert NetworkAdapter.query.first() is not None, 'We removed the network adaptor'
|
||||||
assert gcard.id == 3, 'We should still hold a reference to a zombie graphic card'
|
assert gcard.id == 3, 'We should still hold a reference to a zombie graphic card'
|
||||||
assert GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc'
|
assert GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('app_context')
|
||||||
def test_device_schema():
|
def test_device_schema():
|
||||||
"""Ensures the user does not upload non-writable or extra fields."""
|
"""Ensures the user does not upload non-writable or extra fields."""
|
||||||
device_s = DeviceS()
|
device_s = DeviceS()
|
||||||
|
@ -172,3 +174,44 @@ def test_execute_register_computer_no_hid():
|
||||||
# 2: device has no HID and we force it
|
# 2: device has no HID and we force it
|
||||||
db_pc, _ = Sync.execute_register(pc, set(), force_creation=True)
|
db_pc, _ = Sync.execute_register(pc, set(), force_creation=True)
|
||||||
assert pc.physical_properties == db_pc.physical_properties
|
assert pc.physical_properties == db_pc.physical_properties
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_device(app: Devicehub, user: UserClient):
|
||||||
|
"""Checks GETting a Desktop with its components."""
|
||||||
|
with app.app_context():
|
||||||
|
pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s')
|
||||||
|
pc.components = [
|
||||||
|
NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'),
|
||||||
|
GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500)
|
||||||
|
]
|
||||||
|
db.session.add(pc)
|
||||||
|
db.session.commit()
|
||||||
|
pc, _ = user.get(res=Device, item=1)
|
||||||
|
assert pc['events'] == []
|
||||||
|
assert 'events_components' not in pc, 'events_components are internal use only'
|
||||||
|
assert 'events_one' not in pc, 'they are internal use only'
|
||||||
|
assert 'author' not in pc
|
||||||
|
assert tuple(c['id'] for c in pc['components']) == (2, 3)
|
||||||
|
assert pc['hid'] == 'p1ma-p1s-p1mo'
|
||||||
|
assert pc['model'] == 'p1mo'
|
||||||
|
assert pc['manufacturer'] == 'p1ma'
|
||||||
|
assert pc['serialNumber'] == 'p1s'
|
||||||
|
assert pc['type'] == 'Desktop'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_devices(app: Devicehub, user: UserClient):
|
||||||
|
"""Checks GETting multiple devices."""
|
||||||
|
with app.app_context():
|
||||||
|
pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s')
|
||||||
|
pc.components = [
|
||||||
|
NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'),
|
||||||
|
GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500)
|
||||||
|
]
|
||||||
|
pc1 = Microtower(model='p2mo', manufacturer='p2ma', serial_number='p2s')
|
||||||
|
pc2 = Laptop(model='p3mo', manufacturer='p3ma', serial_number='p3s')
|
||||||
|
db.session.add_all((pc, pc1, pc2))
|
||||||
|
db.session.commit()
|
||||||
|
devices, _ = user.get(res=Device)
|
||||||
|
assert tuple(d['id'] for d in devices) == (1, 2, 3, 4, 5)
|
||||||
|
assert tuple(d['type'] for d in devices) == ('Desktop', 'Microtower',
|
||||||
|
'Laptop', 'NetworkAdapter', 'GraphicCard')
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -13,6 +14,44 @@ from ereuse_devicehub.resources.user.models import User
|
||||||
from tests.conftest import file
|
from tests.conftest import file
|
||||||
|
|
||||||
|
|
||||||
|
def assert_similar_device(device1: dict, device2: dict):
|
||||||
|
"""
|
||||||
|
Like Model.is_similar() but adapted for testing.
|
||||||
|
"""
|
||||||
|
assert isinstance(device1, dict) and device1
|
||||||
|
assert isinstance(device2, dict) and device2
|
||||||
|
for key in 'serialNumber', 'model', 'manufacturer', 'type':
|
||||||
|
assert device1.get(key, None) == device2.get(key, None)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_similar_components(components1: List[dict], components2: List[dict]):
|
||||||
|
"""
|
||||||
|
Asserts that the components in components1 are
|
||||||
|
similar than the components in components2.
|
||||||
|
"""
|
||||||
|
assert len(components1) == len(components2)
|
||||||
|
for c1, c2 in zip(components1, components2):
|
||||||
|
assert_similar_device(c1, c2)
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_and_check(user: UserClient,
|
||||||
|
input_snapshot: dict,
|
||||||
|
num_events: int = 0,
|
||||||
|
perform_second_snapshot=True) -> dict:
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
snapshot, _ = user.post(res=Snapshot, data=input_snapshot)
|
||||||
|
assert len(snapshot['events']) == num_events
|
||||||
|
assert input_snapshot['device']
|
||||||
|
assert_similar_device(input_snapshot['device'], snapshot['device'])
|
||||||
|
assert_similar_components(input_snapshot['components'], snapshot['components'])
|
||||||
|
if perform_second_snapshot:
|
||||||
|
return snapshot_and_check(user, input_snapshot, num_events, False)
|
||||||
|
else:
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('auth_app_context')
|
@pytest.mark.usefixtures('auth_app_context')
|
||||||
def test_snapshot_model():
|
def test_snapshot_model():
|
||||||
"""
|
"""
|
||||||
|
@ -56,6 +95,25 @@ def test_snapshot_schema(app: Devicehub):
|
||||||
|
|
||||||
|
|
||||||
def test_snapshot_post(user: UserClient):
|
def test_snapshot_post(user: UserClient):
|
||||||
"""Tests the post snapshot endpoint (validation, etc)."""
|
"""
|
||||||
s = file('basic.snapshot')
|
Tests the post snapshot endpoint (validation, etc)
|
||||||
snapshot, _ = user.post(s, res=Snapshot.__name__)
|
and data correctness.
|
||||||
|
"""
|
||||||
|
snapshot = snapshot_and_check(user, file('basic.snapshot'))
|
||||||
|
assert snapshot['software'] == 'Workbench'
|
||||||
|
assert snapshot['version'] == '11.0'
|
||||||
|
assert snapshot['uuid'] == 'f5efd26e-8754-46bc-87bf-fbccc39d60d9'
|
||||||
|
assert snapshot['events'] == []
|
||||||
|
assert snapshot['elapsed'] == 4
|
||||||
|
assert snapshot['author']['id'] == user.user['id']
|
||||||
|
assert 'events' not in snapshot['device']
|
||||||
|
assert 'author' not in snapshot['device']
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_add_remove(user: UserClient):
|
||||||
|
s1 = file('1-device-with-components.snapshot')
|
||||||
|
snapshot_and_check(user, s1)
|
||||||
|
|
||||||
|
s2 = file('2-second-device-with-components-of-first.snapshot')
|
||||||
|
s3 = file('3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot')
|
||||||
|
s4 = file('4-first-device-but-removing-processor.snapshot-and-adding-graphic-card')
|
||||||
|
|
Reference in New Issue