Enhance Lot; use timezone aware datetime
This commit is contained in:
parent
f5d69070e6
commit
2ed558ac2b
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Reference in a new issue