From 2ed558ac2b5e4722960fbe9a2d9a12b933ab07a2 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Thu, 9 Aug 2018 21:46:54 +0200 Subject: [PATCH] Enhance Lot; use timezone aware datetime --- ereuse_devicehub/resources/event/models.py | 4 +- ereuse_devicehub/resources/lot/models.py | 31 +++-- ereuse_devicehub/resources/models.py | 13 ++- tests/test_event.py | 26 ++--- tests/test_lot.py | 129 +++++++++++++++++++-- tests/test_snapshot.py | 4 +- 6 files changed, 167 insertions(+), 40 deletions(-) diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index b532cb97..acf4894f 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -67,13 +67,13 @@ class Event(Thing): description.comment = """ A comment about the event. """ - start_time = Column(DateTime) + start_time = Column(db.TIMESTAMP(timezone=True)) start_time.comment = """ When the action starts. For some actions like reservations the time when they are available, for others like renting when the renting starts. """ - end_time = Column(DateTime) + end_time = Column(db.TIMESTAMP(timezone=True)) end_time.comment = """ When the action ends. For some actions like reservations the time when they expire, for others like renting diff --git a/ereuse_devicehub/resources/lot/models.py b/ereuse_devicehub/resources/lot/models.py index f8cd4d77..c442be42 100644 --- a/ereuse_devicehub/resources/lot/models.py +++ b/ereuse_devicehub/resources/lot/models.py @@ -12,12 +12,11 @@ from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.models import STR_SIZE, Thing from ereuse_devicehub.resources.user.models import User +from teal.db import UUIDLtree class Lot(Thing): - id = db.Column(UUID(as_uuid=True), - primary_key=True, - server_default=db.text('gen_random_uuid()')) + id = db.Column(UUID(as_uuid=True), primary_key=True) # uuid is generated on init by default name = db.Column(db.Unicode(STR_SIZE), nullable=False) closed = db.Column(db.Boolean, default=False, nullable=False) closed.comment = """ @@ -28,8 +27,14 @@ class Lot(Thing): secondary=lambda: LotDevice.__table__, collection_class=set) - def __repr__(self) -> str: - return ''.format(self) + def __init__(self, name: str, closed: bool = closed.default.arg) -> None: + """ + 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'): """Adds a child to this lot.""" @@ -43,6 +48,9 @@ class Lot(Thing): def __contains__(self, child: 'Lot'): return Edge.has_lot(self.id, child.id) + def __repr__(self) -> str: + return ''.format(self) + class LotDevice(db.Model): 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), primary_key=True, server_default=db.text('gen_random_uuid()')) @@ -66,7 +74,11 @@ class Edge(Thing): lot = db.relationship(Lot, backref=db.backref('edges', lazy=True, collection_class=set), 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__ = ( 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') ) + def __init__(self, lot: Lot) -> None: + super().__init__(lot=lot) + self.path = UUIDLtree(lot.id) + def children(self) -> Set['Edge']: """Get the children edges.""" + # todo is it useful? test it when first usage # From https://stackoverflow.com/a/41158890 exp = '*.{}.*{{1}}'.format(self.lot_id) return set(self.query diff --git a/ereuse_devicehub/resources/models.py b/ereuse_devicehub/resources/models.py index 22abc150..c8e9b7ec 100644 --- a/ereuse_devicehub/resources/models.py +++ b/ereuse_devicehub/resources/models.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from ereuse_devicehub.db import db @@ -10,11 +10,16 @@ STR_XSM_SIZE = 16 class Thing(db.Model): __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 = """ 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 = """ When Devicehub created this. """ @@ -22,4 +27,4 @@ class Thing(db.Model): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) if not self.created: - self.created = datetime.utcnow() + self.created = datetime.now(timezone.utc) diff --git a/tests/test_event.py b/tests/test_event.py index 888b6b4b..465aaf50 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -1,5 +1,5 @@ import ipaddress -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest from flask import current_app as app, g @@ -38,8 +38,8 @@ def test_erase_basic(): erasure = models.EraseBasic( device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'), zeros=True, - start_time=datetime.now(), - end_time=datetime.now(), + start_time=datetime.now(timezone.utc), + end_time=datetime.now(timezone.utc), error=False ) db.session.add(erasure) @@ -59,8 +59,8 @@ def test_validate_device_data_storage(): models.EraseBasic( device=GraphicCard(serial_number='foo', manufacturer='bar', model='foo-bar'), clean_with_zeros=True, - start_time=datetime.now(), - end_time=datetime.now(), + start_time=datetime.now(timezone.utc), + end_time=datetime.now(timezone.utc), error=False ) @@ -70,19 +70,19 @@ def test_erase_sectors_steps(): erasure = models.EraseSectors( device=SolidStateDrive(serial_number='foo', manufacturer='bar', model='foo-bar'), zeros=True, - start_time=datetime.now(), - end_time=datetime.now(), + start_time=datetime.now(timezone.utc), + end_time=datetime.now(timezone.utc), error=False, steps=[ models.StepZero(error=False, - start_time=datetime.now(), - end_time=datetime.now()), + start_time=datetime.now(timezone.utc), + end_time=datetime.now(timezone.utc)), models.StepRandom(error=False, - start_time=datetime.now(), - end_time=datetime.now()), + start_time=datetime.now(timezone.utc), + end_time=datetime.now(timezone.utc)), models.StepZero(error=False, - start_time=datetime.now(), - end_time=datetime.now()) + start_time=datetime.now(timezone.utc), + end_time=datetime.now(timezone.utc)) ] ) db.session.add(erasure) diff --git a/tests/test_lot.py b/tests/test_lot.py index 7c914600..e6b9a4d7 100644 --- a/tests/test_lot.py +++ b/tests/test_lot.py @@ -1,13 +1,25 @@ import pytest from flask import g -from sqlalchemy_utils import Ltree from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.models import Desktop 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 +""" +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__) def test_lot_device_relationship(): @@ -15,7 +27,7 @@ def test_lot_device_relationship(): model='bar', manufacturer='foobar', chassis=ComputerChassis.Lunchbox) - lot = Lot(name='lot1') + lot = Lot('lot1') lot.devices.add(device) db.session.add(lot) db.session.flush() @@ -30,15 +42,12 @@ def test_lot_device_relationship(): @pytest.mark.usefixtures(conftest.auth_app_context.__name__) def test_add_edge(): - child = Lot(name='child') - parent = Lot(name='parent') + """Tests creating an edge between child - parent - grandparent.""" + child = Lot('child') + parent = Lot('parent') db.session.add(child) db.session.add(parent) 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) @@ -51,11 +60,9 @@ def test_add_edge(): assert len(child.edges) == 1 assert len(parent.edges) == 1 - grandparent = Lot(name='grandparent') + grandparent = Lot('grandparent') db.session.add(grandparent) db.session.flush() - grandparent.edges.add(Edge(path=Ltree(str(grandparent.id).replace('-', '_')))) - db.session.flush() grandparent.add_child(parent) parent.add_child(child) @@ -63,3 +70,101 @@ def test_add_edge(): assert parent in grandparent assert child in parent 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 diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 290d1c0d..2bb2f755 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from distutils.version import StrictVersion from typing import List, Tuple from uuid import uuid4 @@ -29,7 +29,7 @@ def test_snapshot_model(): device = m.Desktop(serial_number='a1', chassis=ComputerChassis.Tower) # noinspection PyArgumentList snapshot = Snapshot(uuid=uuid4(), - end_time=datetime.now(), + end_time=datetime.now(timezone.utc), version='1.0', software=SnapshotSoftware.DesktopApp, elapsed=timedelta(seconds=25))