
11 changed files with 638 additions and 142 deletions
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
""" sqlalchemy metadata configuration """ |
||||
|
||||
from datetime import datetime |
||||
|
||||
from sqlalchemy import ( |
||||
Enum, |
||||
Text, |
||||
Float, |
||||
Table, |
||||
Column, |
||||
Integer, |
||||
DateTime, |
||||
ForeignKey, |
||||
) |
||||
from sqlalchemy.orm import mapper, relationship |
||||
from sqlalchemy.schema import MetaData |
||||
|
||||
from . import models |
||||
|
||||
# Recommended naming convention used by Alembic, as various different database |
||||
# providers will autogenerate vastly different names making migrations more |
||||
# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html |
||||
NAMING_CONVENTION = { |
||||
"ix": "ix_%(column_0_label)s", |
||||
"uq": "uq_%(table_name)s_%(column_0_name)s", |
||||
"ck": "ck_%(table_name)s_%(constraint_name)s", |
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", |
||||
"pk": "pk_%(table_name)s", |
||||
} |
||||
|
||||
metadata = MetaData(naming_convention=NAMING_CONVENTION) |
||||
|
||||
order_table = Table( |
||||
"orders", |
||||
metadata, |
||||
Column("id", Integer, primary_key=True), |
||||
Column("cas_description", Text, nullable=False), |
||||
Column("catalog_nr", Text, nullable=False), |
||||
Column("vendor", Text, nullable=False), |
||||
Column("category", Enum(models.OrderCategory), nullable=False), |
||||
Column("package_size", Text, nullable=False), |
||||
Column("unit_price", Float, nullable=False), |
||||
Column("currency", Text, nullable=False, default="EUR"), |
||||
Column("amount", Integer, nullable=False), |
||||
Column("account", Text, nullable=False, default=""), |
||||
Column("comment", Text, nullable=False, default=""), |
||||
# redundant properties, could be determined from orders log |
||||
Column("created_on", DateTime, nullable=False, default=datetime.utcnow), |
||||
Column("created_by", Text, nullable=True, index=True), |
||||
Column("status", Enum(models.OrderStatus), nullable=True), |
||||
) |
||||
|
||||
|
||||
log_table = Table( |
||||
"logs", |
||||
metadata, |
||||
Column("order_id", Integer, ForeignKey("orders.id"), primary_key=True), |
||||
Column("status", Enum(models.OrderStatus), primary_key=True), |
||||
Column("date", DateTime, primary_key=True, default=datetime.utcnow), |
||||
Column("by", Text, nullable=False), |
||||
Column("user_id", Integer, nullable=False, index=True), |
||||
) |
||||
|
||||
|
||||
user_table = Table( |
||||
"users", |
||||
metadata, |
||||
Column("id", Integer, primary_key=True), |
||||
Column("username", Text, nullable=False, index=True), |
||||
Column("first_name", Text, nullable=False), |
||||
Column("last_name", Text, nullable=False), |
||||
Column("email", Text, nullable=False, index=True), |
||||
Column("password", Text, nullable=False), |
||||
Column("role", Enum(models.UserRole)), |
||||
) |
||||
|
||||
|
||||
def start_mappers(): |
||||
mapper( |
||||
models.OrderItem, |
||||
order_table, |
||||
properties={ |
||||
"log": relationship( |
||||
models.LogItem, backref="order", order_by=log_table.c.date |
||||
) |
||||
}, |
||||
) |
||||
mapper(models.LogItem, log_table) |
||||
mapper(models.User, user_table) |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
from datetime import datetime |
||||
|
||||
from . import models |
||||
|
||||
CONSUMABLE_STATI = { |
||||
models.OrderStatus.ORDERED, |
||||
models.OrderStatus.COMPLETED, |
||||
} |
||||
|
||||
|
||||
def find_consumables(repo, repeat=3, days=365 * 2): |
||||
""" search for orders that are requested often """ |
||||
unsorted = _find_consumables(repo, repeat, days) |
||||
return sorted(unsorted, key=lambda x: x.cas_description) |
||||
|
||||
|
||||
def _find_consumables(repo, repeat=3, days=365 * 2): |
||||
""" helper function for find_consumables() implementation """ |
||||
now = datetime.now() |
||||
by_date = (o for o in repo.list() if (now - o.created_on).days < days) |
||||
relevant = (o for o in by_date if o.status in CONSUMABLE_STATI) |
||||
counter = {} |
||||
for order in relevant: |
||||
item = counter.setdefault( |
||||
order.catalog_nr, models.ProposedConsumable(order) |
||||
) |
||||
item.times = item.times + 1 |
||||
if item.times == repeat: |
||||
yield item.order |
||||
|
||||
|
||||
def create_log_entry(order, status, user): |
||||
log_entry = models.LogItem(order.id, status, user.username, user.id) |
||||
order.add_to_log(log_entry) |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
import pytest |
||||
from sqlalchemy import create_engine |
||||
from sqlalchemy.orm import sessionmaker, clear_mappers |
||||
|
||||
from ordr3.adapters import metadata, start_mappers |
||||
|
||||
|
||||
@pytest.fixture |
||||
def in_memory_db(): |
||||
engine = create_engine("sqlite:///:memory:") |
||||
metadata.create_all(engine) |
||||
return engine |
||||
|
||||
|
||||
@pytest.fixture |
||||
def session(in_memory_db): |
||||
start_mappers() |
||||
yield sessionmaker(bind=in_memory_db)() |
||||
clear_mappers() |
@ -0,0 +1,151 @@
@@ -0,0 +1,151 @@
|
||||
from datetime import datetime |
||||
|
||||
import pytest |
||||
|
||||
|
||||
@pytest.fixture |
||||
def order_sql(): |
||||
return """ |
||||
INSERT INTO orders ( |
||||
id, cas_description, catalog_nr, vendor, category, package_size, |
||||
unit_price, currency, amount, account, comment, created_on, |
||||
created_by, status |
||||
) VALUES ( |
||||
1, "Ethanol", "1-23", "VWR", "SOLVENT", "5 l", |
||||
20, "EUR", 2, "DFG", "urgent", "2020-02-04 15:14:13.000000", |
||||
"me", "OPEN" |
||||
), ( |
||||
2, "Gloves", "12-3", "Carl Roth", "DISPOSABLE", "100 St.", |
||||
40, "USD", 5, "BMBF", "no comment", "2020-02-03 15:14:13.000000", |
||||
"you", "APPROVAL" |
||||
) |
||||
""" |
||||
|
||||
|
||||
def test_order_mapper(session, order_sql): |
||||
from ordr3.models import OrderItem, OrderCategory, OrderStatus |
||||
|
||||
session.execute(order_sql) |
||||
expected = [ |
||||
OrderItem( |
||||
1, |
||||
"Ethanol", |
||||
"1-23", |
||||
"VWR", |
||||
OrderCategory.SOLVENT, |
||||
"5 l", |
||||
20.0, |
||||
2, |
||||
"EUR", |
||||
"DFG", |
||||
"urgent", |
||||
datetime(2020, 2, 4, 15, 14, 13), |
||||
"me", |
||||
OrderStatus.OPEN, |
||||
), |
||||
OrderItem( |
||||
2, |
||||
"Gloves", |
||||
"12-3", |
||||
"Carl Roth", |
||||
OrderCategory.DISPOSABLE, |
||||
"100 St.", |
||||
40.0, |
||||
5, |
||||
"USD", |
||||
"BMBF", |
||||
"no comment", |
||||
datetime(2020, 2, 3, 15, 14, 13), |
||||
"you", |
||||
OrderStatus.APPROVAL, |
||||
), |
||||
] |
||||
|
||||
assert session.query(OrderItem).all() == expected |
||||
|
||||
|
||||
def test_log_mapper(session, order_sql): |
||||
from ordr3.models import LogItem, OrderItem, OrderStatus |
||||
|
||||
session.execute(order_sql) |
||||
session.execute( |
||||
""" |
||||
INSERT INTO logs |
||||
(order_id, status, by, user_id, date) |
||||
VALUES |
||||
(1, "OPEN", "ME", 1, "2020-02-05 15:14:13.000000"), |
||||
(1, "COMPLETED", "YOU", 2, "2020-02-07 15:14:13.000000"), |
||||
(1, "ORDERED", "ME", 1, "2020-02-06 15:14:13.000000") |
||||
""" |
||||
) |
||||
expected = [ |
||||
LogItem( |
||||
1, OrderStatus.OPEN, "ME", 1, datetime(2020, 2, 5, 15, 14, 13), |
||||
), |
||||
LogItem( |
||||
1, |
||||
OrderStatus.COMPLETED, |
||||
"YOU", |
||||
2, |
||||
datetime(2020, 2, 7, 15, 14, 13), |
||||
), |
||||
LogItem( |
||||
1, OrderStatus.ORDERED, "ME", 1, datetime(2020, 2, 6, 15, 14, 13) |
||||
), |
||||
] |
||||
|
||||
assert session.query(LogItem).all() == expected |
||||
|
||||
order = session.query(OrderItem).first() |
||||
assert order.log == sorted(expected, key=lambda x: x.date) |
||||
|
||||
logitem = session.query(LogItem).first() |
||||
assert logitem.order.id == order.id |
||||
|
||||
|
||||
def test_user_mapper(session): |
||||
from ordr3.models import User, UserRole |
||||
|
||||
session.execute( |
||||
""" |
||||
INSERT INTO users |
||||
(id, username, first_name, last_name, email, password, role) |
||||
VALUES |
||||
(1, "Me", "Jane", "Doe", "jane.doe", "1234", "USER"), |
||||
(2, "You", "Jim", "Smith", "jim.smith", "abcd", "ADMIN") |
||||
""" |
||||
) |
||||
|
||||
expected = [ |
||||
User(1, "Me", "Jane", "Doe", "jane.doe", "1234", UserRole.USER), |
||||
User(2, "You", "Jim", "Smith", "jim.smith", "abcd", UserRole.ADMIN,), |
||||
] |
||||
|
||||
assert session.query(User).all() == expected |
||||
|
||||
|
||||
def test_adapter_behaviour(session): |
||||
from ordr3.models import OrderItem, OrderCategory, OrderStatus, LogItem |
||||
|
||||
order = OrderItem( |
||||
None, |
||||
"Ethanol", |
||||
"1-23", |
||||
"VWR", |
||||
OrderCategory.SOLVENT, |
||||
"5 l", |
||||
20.0, |
||||
2, |
||||
"EUR", |
||||
) |
||||
session.add(order) |
||||
|
||||
log_entry = LogItem(order.id, OrderStatus.OPEN, "ME", 123) |
||||
order.add_to_log(log_entry) |
||||
|
||||
from_db = session.query(OrderItem).first() |
||||
|
||||
assert len(from_db.log) == 1 |
||||
assert from_db.created_by == "ME" |
||||
assert from_db.created_on == from_db.log[0].date |
||||
assert from_db.status == OrderStatus.OPEN |
@ -1,75 +1,78 @@
@@ -1,75 +1,78 @@
|
||||
import pytest |
||||
|
||||
|
||||
@pytest.fixture |
||||
def prefilled_repo(): |
||||
from datetime import datetime, timedelta |
||||
from itertools import count |
||||
from ordr3.models import OrderItem, OrderStatus |
||||
from ordr3.repo import FakeOrderRepository |
||||
|
||||
i = count() |
||||
catalog = { |
||||
1: "Ethanol", |
||||
2: "Aceton", |
||||
3: "NaOH", |
||||
4: "Coffee", |
||||
5: "Water", |
||||
} |
||||
|
||||
def _create_order(item, date, status): |
||||
order = OrderItem(next(i), catalog[item], item, "", "", "", "", "") |
||||
order.created_on = date |
||||
order.status = status |
||||
return order |
||||
|
||||
month = timedelta(days=30) |
||||
today = datetime.now() |
||||
|
||||
repo = FakeOrderRepository() |
||||
# should be consumables |
||||
repo.add(_create_order(1, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(1, today - month * 2, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(1, today - month * 3, OrderStatus.COMPLETED)) |
||||
repo.add(_create_order(2, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(2, today - month * 2, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(2, today - month * 3, OrderStatus.COMPLETED)) |
||||
# no consumable, only two repeats |
||||
repo.add(_create_order(3, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(3, today - month * 2, OrderStatus.ORDERED)) |
||||
# no consumable, only two repeats in the last two years |
||||
repo.add(_create_order(4, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(4, today - month * 2, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(4, today - month * 50, OrderStatus.ORDERED)) |
||||
# no consumable, one order on hold |
||||
repo.add(_create_order(5, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(5, today - month * 2, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(5, today - month * 3, OrderStatus.HOLD)) |
||||
|
||||
return repo |
||||
|
||||
|
||||
def test_fakerepo_add(): |
||||
from ordr3.repo import FakeOrderRepository |
||||
|
||||
repo = FakeOrderRepository() |
||||
repo.add("Something") |
||||
|
||||
assert len(repo._orders) == 1 |
||||
assert repo._orders[0] == "Something" |
||||
|
||||
|
||||
def test_fakerepo_get(prefilled_repo): |
||||
reference = 8 |
||||
|
||||
result = prefilled_repo.get(reference) |
||||
|
||||
assert result.id == reference |
||||
|
||||
|
||||
def test_fakerepo_find_consumables(prefilled_repo): |
||||
|
||||
result = prefilled_repo.find_consumables() |
||||
|
||||
assert len(result) == 2 |
||||
assert result == [prefilled_repo._orders[3], prefilled_repo._orders[0]] |
||||
@pytest.fixture() |
||||
def example_orders(): |
||||
from datetime import datetime |
||||
from ordr3.models import OrderItem, OrderCategory, OrderStatus |
||||
|
||||
return [ |
||||
OrderItem( |
||||
1, |
||||
"Ethanol", |
||||
"1-23", |
||||
"VWR", |
||||
OrderCategory.SOLVENT, |
||||
"5 l", |
||||
20.0, |
||||
2, |
||||
"EUR", |
||||
"DFG", |
||||
"urgent", |
||||
datetime(2020, 2, 3, 15, 14, 13), |
||||
"me", |
||||
OrderStatus.OPEN, |
||||
), |
||||
OrderItem( |
||||
2, |
||||
"Gloves", |
||||
"12-3", |
||||
"Carl Roth", |
||||
OrderCategory.DISPOSABLE, |
||||
"100 St.", |
||||
40.0, |
||||
5, |
||||
"USD", |
||||
"BMBF", |
||||
"no comment", |
||||
datetime(2020, 2, 4, 15, 14, 13), |
||||
"you", |
||||
OrderStatus.APPROVAL, |
||||
), |
||||
] |
||||
|
||||
|
||||
def test_sql_repo_add(session, example_orders): |
||||
from ordr3.repo import SqlAlchemyRepository |
||||
from ordr3.models import OrderItem |
||||
|
||||
repo = SqlAlchemyRepository(session) |
||||
repo.add(example_orders[0]) |
||||
session.flush() |
||||
|
||||
order = session.query(OrderItem).first() |
||||
|
||||
assert order == example_orders[0] |
||||
|
||||
|
||||
def test_sql_repo_get(session, example_orders): |
||||
from ordr3.repo import SqlAlchemyRepository |
||||
|
||||
repo = SqlAlchemyRepository(session) |
||||
repo.add(example_orders[0]) |
||||
repo.add(example_orders[1]) |
||||
session.flush() |
||||
|
||||
assert example_orders[1] == repo.get(2) |
||||
|
||||
|
||||
def test_sql_repo_list(session, example_orders): |
||||
from ordr3.repo import SqlAlchemyRepository |
||||
|
||||
earlier, later = example_orders |
||||
repo = SqlAlchemyRepository(session) |
||||
repo.add(earlier) |
||||
repo.add(later) |
||||
session.flush() |
||||
|
||||
assert repo.list() == [later, earlier] |
||||
|
@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
|
||||
from datetime import datetime, timedelta |
||||
|
||||
import pytest |
||||
|
||||
from ordr3.repo import AbstractOrderRepository |
||||
|
||||
|
||||
class FakeOrderRepository(AbstractOrderRepository): |
||||
""" Repository implementation for testing """ |
||||
|
||||
def __init__(self, session): |
||||
self._orders = set() |
||||
|
||||
def add(self, order): |
||||
""" add an order to the datastore """ |
||||
self._orders.add(order) |
||||
|
||||
def get(self, reference): |
||||
""" retrieve an order from the datastore """ |
||||
return next(o for o in self._orders if o.id == reference) |
||||
|
||||
def list(self): |
||||
return sorted(self._orders, reverse=True, key=lambda x: x.created_on) |
||||
|
||||
|
||||
@pytest.fixture |
||||
def prefilled_repo(): |
||||
from itertools import count |
||||
from ordr3.models import OrderItem, OrderStatus |
||||
|
||||
i = count() |
||||
catalog = { |
||||
1: "Ethanol", |
||||
2: "Aceton", |
||||
3: "NaOH", |
||||
4: "Coffee", |
||||
5: "Water", |
||||
} |
||||
|
||||
def _create_order(item, date, status): |
||||
order = OrderItem(next(i), catalog[item], item, "", "", "", "", "") |
||||
order.created_on = date |
||||
order.status = status |
||||
return order |
||||
|
||||
month = timedelta(days=30) |
||||
today = datetime.now() |
||||
|
||||
repo = FakeOrderRepository(session=None) |
||||
# should be consumables |
||||
repo.add(_create_order(1, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(1, today - month * 2, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(1, today - month * 3, OrderStatus.COMPLETED)) |
||||
repo.add(_create_order(2, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(2, today - month * 2, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(2, today - month * 3, OrderStatus.COMPLETED)) |
||||
# no consumable, only two repeats |
||||
repo.add(_create_order(3, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(3, today - month * 2, OrderStatus.ORDERED)) |
||||
# no consumable, only two repeats in the last two years |
||||
repo.add(_create_order(4, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(4, today - month * 2, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(4, today - month * 50, OrderStatus.ORDERED)) |
||||
# no consumable, one order on hold |
||||
repo.add(_create_order(5, today - month * 1, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(5, today - month * 2, OrderStatus.ORDERED)) |
||||
repo.add(_create_order(5, today - month * 3, OrderStatus.HOLD)) |
||||
|
||||
return repo |
||||
|
||||
|
||||
def test_service_find_consumables(prefilled_repo): |
||||
from ordr3.services import find_consumables |
||||
|
||||
result = find_consumables(prefilled_repo) |
||||
|
||||
assert len(result) == 2 |
||||
assert [o.id for o in result] == [3, 0] |
||||
|
||||
|
||||
def test_create_log_entry(prefilled_repo): |
||||
from ordr3.services import create_log_entry |
||||
from ordr3.models import OrderStatus, User |
||||
|
||||
order = prefilled_repo.get(1) |
||||
user = User(*list("ABCDEFG")) |
||||
|
||||
create_log_entry(order, OrderStatus.APPROVAL, user) |
||||
|
||||
assert len(order.log) == 1 |
||||
log_entry = order.log[0] |
||||
assert log_entry.order_id == order.id |
||||
assert log_entry.status == OrderStatus.APPROVAL |
||||
assert log_entry.by == "B" |
||||
assert log_entry.user_id == "A" |
||||
assert isinstance(log_entry.date, datetime) |
||||
assert order.status == log_entry.status |
||||
assert order.created_by == log_entry.by |
||||
assert order.created_on == log_entry.date |
Loading…
Reference in new issue