Enhance Lot; use timezone aware datetime

This commit is contained in:
Xavier Bustamante Talavera 2018-08-09 21:46:54 +02:00
parent f5d69070e6
commit 2ed558ac2b
6 changed files with 167 additions and 40 deletions

View File

@ -67,13 +67,13 @@ class Event(Thing):
description.comment = """ description.comment = """
A comment about the event. A comment about the event.
""" """
start_time = Column(DateTime) start_time = Column(db.TIMESTAMP(timezone=True))
start_time.comment = """ start_time.comment = """
When the action starts. For some actions like reservations When the action starts. For some actions like reservations
the time when they are available, for others like renting the time when they are available, for others like renting
when the renting starts. when the renting starts.
""" """
end_time = Column(DateTime) end_time = Column(db.TIMESTAMP(timezone=True))
end_time.comment = """ end_time.comment = """
When the action ends. For some actions like reservations When the action ends. For some actions like reservations
the time when they expire, for others like renting the time when they expire, for others like renting

View File

@ -12,12 +12,11 @@ from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.models import STR_SIZE, Thing from ereuse_devicehub.resources.models import STR_SIZE, Thing
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from teal.db import UUIDLtree
class Lot(Thing): class Lot(Thing):
id = db.Column(UUID(as_uuid=True), id = db.Column(UUID(as_uuid=True), primary_key=True) # uuid is generated on init by default
primary_key=True,
server_default=db.text('gen_random_uuid()'))
name = db.Column(db.Unicode(STR_SIZE), nullable=False) name = db.Column(db.Unicode(STR_SIZE), nullable=False)
closed = db.Column(db.Boolean, default=False, nullable=False) closed = db.Column(db.Boolean, default=False, nullable=False)
closed.comment = """ closed.comment = """
@ -28,8 +27,14 @@ class Lot(Thing):
secondary=lambda: LotDevice.__table__, secondary=lambda: LotDevice.__table__,
collection_class=set) collection_class=set)
def __repr__(self) -> str: def __init__(self, name: str, closed: bool = closed.default.arg) -> None:
return '<Lot {0.name} devices={0.devices!r}>'.format(self) """
Initializes a lot
:param name:
:param closed:
"""
super().__init__(id=uuid.uuid4(), name=name, closed=closed)
Edge(self) # Lots have always one edge per default.
def add_child(self, child: 'Lot'): def add_child(self, child: 'Lot'):
"""Adds a child to this lot.""" """Adds a child to this lot."""
@ -43,6 +48,9 @@ class Lot(Thing):
def __contains__(self, child: 'Lot'): def __contains__(self, child: 'Lot'):
return Edge.has_lot(self.id, child.id) return Edge.has_lot(self.id, child.id)
def __repr__(self) -> str:
return '<Lot {0.name} devices={0.devices!r}>'.format(self)
class LotDevice(db.Model): class LotDevice(db.Model):
device_id = db.Column(db.BigInteger, db.ForeignKey(Device.id), primary_key=True) device_id = db.Column(db.BigInteger, db.ForeignKey(Device.id), primary_key=True)
@ -58,7 +66,7 @@ class LotDevice(db.Model):
""" """
class Edge(Thing): class Edge(db.Model):
id = db.Column(db.UUID(as_uuid=True), id = db.Column(db.UUID(as_uuid=True),
primary_key=True, primary_key=True,
server_default=db.text('gen_random_uuid()')) server_default=db.text('gen_random_uuid()'))
@ -66,7 +74,11 @@ class Edge(Thing):
lot = db.relationship(Lot, lot = db.relationship(Lot,
backref=db.backref('edges', lazy=True, collection_class=set), backref=db.backref('edges', lazy=True, collection_class=set),
primaryjoin=Lot.id == lot_id) primaryjoin=Lot.id == lot_id)
path = db.Column(LtreeType, unique=True, nullable=False) path = db.Column(LtreeType, nullable=False)
created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP'))
created.comment = """
When Devicehub created this.
"""
__table_args__ = ( __table_args__ = (
db.UniqueConstraint(path, name='edge_path_unique', deferrable=True, initially='immediate'), db.UniqueConstraint(path, name='edge_path_unique', deferrable=True, initially='immediate'),
@ -74,8 +86,13 @@ class Edge(Thing):
db.Index('path_btree', path, postgresql_using='btree') db.Index('path_btree', path, postgresql_using='btree')
) )
def __init__(self, lot: Lot) -> None:
super().__init__(lot=lot)
self.path = UUIDLtree(lot.id)
def children(self) -> Set['Edge']: def children(self) -> Set['Edge']:
"""Get the children edges.""" """Get the children edges."""
# todo is it useful? test it when first usage
# From https://stackoverflow.com/a/41158890 # From https://stackoverflow.com/a/41158890
exp = '*.{}.*{{1}}'.format(self.lot_id) exp = '*.{}.*{{1}}'.format(self.lot_id)
return set(self.query return set(self.query

View File

@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timezone
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
@ -10,11 +10,16 @@ STR_XSM_SIZE = 16
class Thing(db.Model): class Thing(db.Model):
__abstract__ = True __abstract__ = True
updated = db.Column(db.DateTime, onupdate=datetime.utcnow) # todo make updated to auto-update
updated = db.Column(db.TIMESTAMP(timezone=True),
nullable=False,
server_default=db.text('CURRENT_TIMESTAMP'))
updated.comment = """ updated.comment = """
When this was last changed. When this was last changed.
""" """
created = db.Column(db.DateTime, default=datetime.utcnow) created = db.Column(db.TIMESTAMP(timezone=True),
nullable=False,
server_default=db.text('CURRENT_TIMESTAMP'))
created.comment = """ created.comment = """
When Devicehub created this. When Devicehub created this.
""" """
@ -22,4 +27,4 @@ class Thing(db.Model):
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
if not self.created: if not self.created:
self.created = datetime.utcnow() self.created = datetime.now(timezone.utc)

View File

@ -1,5 +1,5 @@
import ipaddress import ipaddress
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import pytest import pytest
from flask import current_app as app, g from flask import current_app as app, g
@ -38,8 +38,8 @@ def test_erase_basic():
erasure = models.EraseBasic( erasure = models.EraseBasic(
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'), device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
zeros=True, zeros=True,
start_time=datetime.now(), start_time=datetime.now(timezone.utc),
end_time=datetime.now(), end_time=datetime.now(timezone.utc),
error=False error=False
) )
db.session.add(erasure) db.session.add(erasure)
@ -59,8 +59,8 @@ def test_validate_device_data_storage():
models.EraseBasic( models.EraseBasic(
device=GraphicCard(serial_number='foo', manufacturer='bar', model='foo-bar'), device=GraphicCard(serial_number='foo', manufacturer='bar', model='foo-bar'),
clean_with_zeros=True, clean_with_zeros=True,
start_time=datetime.now(), start_time=datetime.now(timezone.utc),
end_time=datetime.now(), end_time=datetime.now(timezone.utc),
error=False error=False
) )
@ -70,19 +70,19 @@ def test_erase_sectors_steps():
erasure = models.EraseSectors( erasure = models.EraseSectors(
device=SolidStateDrive(serial_number='foo', manufacturer='bar', model='foo-bar'), device=SolidStateDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
zeros=True, zeros=True,
start_time=datetime.now(), start_time=datetime.now(timezone.utc),
end_time=datetime.now(), end_time=datetime.now(timezone.utc),
error=False, error=False,
steps=[ steps=[
models.StepZero(error=False, models.StepZero(error=False,
start_time=datetime.now(), start_time=datetime.now(timezone.utc),
end_time=datetime.now()), end_time=datetime.now(timezone.utc)),
models.StepRandom(error=False, models.StepRandom(error=False,
start_time=datetime.now(), start_time=datetime.now(timezone.utc),
end_time=datetime.now()), end_time=datetime.now(timezone.utc)),
models.StepZero(error=False, models.StepZero(error=False,
start_time=datetime.now(), start_time=datetime.now(timezone.utc),
end_time=datetime.now()) end_time=datetime.now(timezone.utc))
] ]
) )
db.session.add(erasure) db.session.add(erasure)

View File

@ -1,13 +1,25 @@
import pytest import pytest
from flask import g from flask import g
from sqlalchemy_utils import Ltree
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Desktop from ereuse_devicehub.resources.device.models import Desktop
from ereuse_devicehub.resources.enums import ComputerChassis from ereuse_devicehub.resources.enums import ComputerChassis
from ereuse_devicehub.resources.lot.models import Edge, Lot, LotDevice from ereuse_devicehub.resources.lot.models import Lot, LotDevice
from tests import conftest from tests import conftest
"""
In case of error, debug with:
try:
with db.session.begin_nested():
except Exception as e:
db.session.commit()
print(e)
a=1
"""
@pytest.mark.usefixtures(conftest.auth_app_context.__name__) @pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_lot_device_relationship(): def test_lot_device_relationship():
@ -15,7 +27,7 @@ def test_lot_device_relationship():
model='bar', model='bar',
manufacturer='foobar', manufacturer='foobar',
chassis=ComputerChassis.Lunchbox) chassis=ComputerChassis.Lunchbox)
lot = Lot(name='lot1') lot = Lot('lot1')
lot.devices.add(device) lot.devices.add(device)
db.session.add(lot) db.session.add(lot)
db.session.flush() db.session.flush()
@ -30,15 +42,12 @@ def test_lot_device_relationship():
@pytest.mark.usefixtures(conftest.auth_app_context.__name__) @pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_add_edge(): def test_add_edge():
child = Lot(name='child') """Tests creating an edge between child - parent - grandparent."""
parent = Lot(name='parent') child = Lot('child')
parent = Lot('parent')
db.session.add(child) db.session.add(child)
db.session.add(parent) db.session.add(parent)
db.session.flush() db.session.flush()
# todo edges should automatically be created when the lot is created
child.edges.add(Edge(path=Ltree(str(child.id).replace('-', '_'))))
parent.edges.add(Edge(path=Ltree(str(parent.id).replace('-', '_'))))
db.session.flush()
parent.add_child(child) parent.add_child(child)
@ -51,11 +60,9 @@ def test_add_edge():
assert len(child.edges) == 1 assert len(child.edges) == 1
assert len(parent.edges) == 1 assert len(parent.edges) == 1
grandparent = Lot(name='grandparent') grandparent = Lot('grandparent')
db.session.add(grandparent) db.session.add(grandparent)
db.session.flush() db.session.flush()
grandparent.edges.add(Edge(path=Ltree(str(grandparent.id).replace('-', '_'))))
db.session.flush()
grandparent.add_child(parent) grandparent.add_child(parent)
parent.add_child(child) parent.add_child(child)
@ -63,3 +70,101 @@ def test_add_edge():
assert parent in grandparent assert parent in grandparent
assert child in parent assert child in parent
assert child in grandparent assert child in grandparent
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_lot_multiple_parents():
"""Tests creating a lot with two parent lots:
grandparent1 grandparent2
\ /
parent
|
child
"""
lots = Lot('child'), Lot('parent'), Lot('grandparent1'), Lot('grandparent2')
child, parent, grandparent1, grandparent2 = lots
db.session.add_all(lots)
db.session.flush()
grandparent1.add_child(parent)
assert parent in grandparent1
parent.add_child(child)
assert child in parent
assert child in grandparent1
grandparent2.add_child(parent)
assert parent in grandparent1
assert parent in grandparent2
assert child in parent
assert child in grandparent1
assert child in grandparent2
grandparent1.remove_child(parent)
assert parent not in grandparent1
assert child in parent
assert parent in grandparent2
assert child not in grandparent1
assert child in grandparent2
grandparent2.remove_child(parent)
assert parent not in grandparent2
assert parent not in grandparent1
assert child not in grandparent2
assert child not in grandparent1
assert child in parent
parent.remove_child(child)
assert child not in parent
assert len(child.edges) == 1
assert len(parent.edges) == 1
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_lot_unite_graphs():
"""Adds and removes children uniting already existing graphs.
1 3
\/
2
4
| \
| 6
\ /
5
| \
7 8
This builds the graph and then unites 2 - 4.
"""
lots = tuple(Lot(str(i)) for i in range(1, 9))
l1, l2, l3, l4, l5, l6, l7, l8 = lots
db.session.add_all(lots)
db.session.flush()
l1.add_child(l2)
assert l2 in l1
l3.add_child(l2)
assert l2 in l3
l5.add_child(l7)
assert l7 in l5
l4.add_child(l5)
assert l5 in l4
assert l7 in l4
l5.add_child(l8)
assert l8 in l5
l4.add_child(l6)
assert l6 in l4
l6.add_child(l5)
assert l5 in l6 and l5 in l4
# We unite the two graphs
l2.add_child(l4)
assert l4 in l2 and l5 in l2 and l6 in l2 and l7 in l2 and l8 in l2
assert l4 in l3 and l5 in l3 and l6 in l3 and l7 in l3 and l8 in l3
# We remove the union
l2.remove_child(l4)
assert l4 not in l2 and l5 not in l2 and l6 not in l2 and l7 not in l2 and l8 not in l2
assert l4 not in l3 and l5 not in l3 and l6 not in l3 and l7 not in l3 and l8 not in l3

View File

@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from distutils.version import StrictVersion from distutils.version import StrictVersion
from typing import List, Tuple from typing import List, Tuple
from uuid import uuid4 from uuid import uuid4
@ -29,7 +29,7 @@ def test_snapshot_model():
device = m.Desktop(serial_number='a1', chassis=ComputerChassis.Tower) device = m.Desktop(serial_number='a1', chassis=ComputerChassis.Tower)
# noinspection PyArgumentList # noinspection PyArgumentList
snapshot = Snapshot(uuid=uuid4(), snapshot = Snapshot(uuid=uuid4(),
end_time=datetime.now(), end_time=datetime.now(timezone.utc),
version='1.0', version='1.0',
software=SnapshotSoftware.DesktopApp, software=SnapshotSoftware.DesktopApp,
elapsed=timedelta(seconds=25)) elapsed=timedelta(seconds=25))