Skip to content

Commit

Permalink
Database notifications on stock changes
Browse files Browse the repository at this point in the history
Adds triggers to send database notifications for changes to stock lines,
stock types and stock items.

Adds a command ('monitor') to output received notifications to the
console. This also listens for previously defined notifications.

This implements #276 on github. Apart from the 'monitor' command, the
first consumer of these notifications is expected to be the EMF 2024 bar
website.
  • Loading branch information
sde1000 committed Feb 9, 2024
1 parent ecac9f6 commit bd19fe1
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 0 deletions.
105 changes: 105 additions & 0 deletions quicktill/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,27 @@ def locations(cls, session):
.order_by(cls.location).all()]


add_ddl(StockLine.__table__, """
CREATE OR REPLACE FUNCTION notify_stockline_change() RETURNS trigger AS $$
DECLARE
BEGIN
IF (TG_OP = 'DELETE') THEN
PERFORM pg_notify('stockline_change', CAST(OLD.stocklineid AS text));
ELSE
PERFORM pg_notify('stockline_change', CAST(NEW.stocklineid AS text));
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER stockline_changed
AFTER INSERT OR UPDATE OR DELETE ON stocklines
FOR EACH ROW EXECUTE PROCEDURE notify_stockline_change();
""", """
DROP TRIGGER stockline_changed ON stocklines;
DROP FUNCTION notify_stockline_change();
""")


plu_seq = Sequence('plu_seq')


Expand Down Expand Up @@ -2365,6 +2386,27 @@ def calculate_sale(self, qty):
return (sell, unallocated, remaining)


add_ddl(StockType.__table__, """
CREATE OR REPLACE FUNCTION notify_stocktype_change() RETURNS trigger AS $$
DECLARE
BEGIN
IF (TG_OP = 'DELETE') THEN
PERFORM pg_notify('stocktype_change', CAST(OLD.stocktype AS text));
ELSE
PERFORM pg_notify('stocktype_change', CAST(NEW.stocktype AS text));
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER stocktype_changed
AFTER INSERT OR UPDATE OR DELETE ON stocktypes
FOR EACH ROW EXECUTE PROCEDURE notify_stocktype_change();
""", """
DROP TRIGGER stocktype_changed ON stocktypes;
DROP FUNCTION notify_stocktype_change();
""")


class FinishCode(Base):
__tablename__ = 'stockfinish'
id = Column('finishcode', String(8), nullable=False, primary_key=True)
Expand Down Expand Up @@ -2430,6 +2472,27 @@ def document(self, contents):
)


add_ddl(StockTypeMeta.__table__, """
CREATE OR REPLACE FUNCTION notify_stocktype_meta_change() RETURNS trigger AS $$
DECLARE
BEGIN
IF (TG_OP = 'DELETE') THEN
PERFORM pg_notify('stocktype_change', CAST(OLD.stocktype AS text));
ELSE
PERFORM pg_notify('stocktype_change', CAST(NEW.stocktype AS text));
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER stocktype_meta_changed
AFTER INSERT OR UPDATE OR DELETE ON stocktype_meta
FOR EACH ROW EXECUTE PROCEDURE notify_stocktype_meta_change();
""", """
DROP TRIGGER stocktype_meta_changed ON stocktype_meta;
DROP FUNCTION notify_stocktype_meta_change();
""")


stock_seq = Sequence('stock_seq')


Expand Down Expand Up @@ -2620,6 +2683,27 @@ def tillweb_nav(self):
f"{self.stocktype.name})", self.get_absolute_url())]


add_ddl(StockItem.__table__, """
CREATE OR REPLACE FUNCTION notify_stockitem_change() RETURNS trigger AS $$
DECLARE
BEGIN
IF (TG_OP = 'DELETE') THEN
PERFORM pg_notify('stockitem_change', CAST(OLD.stockid AS text));
ELSE
PERFORM pg_notify('stockitem_change', CAST(NEW.stockid AS text));
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER stockitem_changed
AFTER INSERT OR UPDATE OR DELETE ON stock
FOR EACH ROW EXECUTE PROCEDURE notify_stockitem_change();
""", """
DROP TRIGGER stockitem_changed ON stock;
DROP FUNCTION notify_stockitem_change();
""")


StockItem.checked = column_property(
select([
func.coalesce(
Expand Down Expand Up @@ -2724,6 +2808,27 @@ class StockOut(Base, Logged):
)


add_ddl(StockOut.__table__, """
CREATE OR REPLACE FUNCTION notify_stockout_change() RETURNS trigger AS $$
DECLARE
BEGIN
IF (TG_OP = 'DELETE') THEN
PERFORM pg_notify('stockitem_change', CAST(OLD.stockid AS text));
ELSE
PERFORM pg_notify('stockitem_change', CAST(NEW.stockid AS text));
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER stockout_changed
AFTER INSERT OR UPDATE OR DELETE ON stockout
FOR EACH ROW EXECUTE PROCEDURE notify_stockout_change();
""", """
DROP TRIGGER stockout_changed ON stockout;
DROP FUNCTION notify_stockout_change();
""")


# These are added to the StockItem class here because they refer
# directly to the StockOut class, defined just above.
StockItem.used = column_property(
Expand Down
143 changes: 143 additions & 0 deletions quicktill/monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Monitor till events
"""

from .cmdline import command
from . import listen
from . import td
from . import event
from .models import LogEntry, User, StockType, StockLine, StockItem
from .models import Config, KeyCap
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import undefer


class monitor(command):
@staticmethod
def run(args):
mainloop = event.SelectorsMainLoop()
listener = listen.db_listener(mainloop, td.engine)

# Start listening for all the types of notification we understand
listener.listen_for("log", monitor.notify_log)
listener.listen_for("user_register", monitor.notify_user_register)
listener.listen_for("group_membership_changed",
monitor.notify_group_membership_changed)
listener.listen_for("group_grants_changed",
monitor.notify_group_grants_changed)
listener.listen_for("stockline_change", monitor.notify_stockline_change)
listener.listen_for("stocktype_change", monitor.notify_stocktype_change)
listener.listen_for("stockitem_change", monitor.notify_stockitem_change)
listener.listen_for("keycaps", monitor.notify_keycaps)
listener.listen_for("config", monitor.notify_config)
listener.listen_for("update", monitor.notify_update)

while True:
mainloop.iterate()

@staticmethod
def notify_log(id_str):
try:
id = int(id_str)
except Exception:
return
with td.orm_session():
logentry = td.s.query(LogEntry).get(id)
if not logentry:
return
print(f"log: {logentry.id} {logentry.time} "
f"{logentry.loguser.fullname}: {logentry}")

@staticmethod
def notify_user_register(id_str):
try:
id = int(id_str)
except Exception:
return
with td.orm_session():
user = td.s.query(User).get(id)
if not user:
return
print(f"user_register: {user.fullname} now at register "
f"{user.register} with transaction {user.trans_id}")

@staticmethod
def notify_group_membership_changed(blank):
# This notification doesn't have a payload
print("group_membership_changed: a group has changed permissions")

@staticmethod
def notify_group_grants_changed(blank):
# This notification doesn't have a payload
print("group_grants_change: a user's groups have changed")

@staticmethod
def notify_stockline_change(id_str):
try:
id = int(id_str)
except Exception:
return
with td.orm_session():
stockline = td.s.query(StockLine).get(id)
if not stockline:
print(f"stockline: id {id} deleted")
return
print(f"stockline: {stockline.name} note '{stockline.note}'")

@staticmethod
def notify_stocktype_change(id_str):
try:
id = int(id_str)
except Exception:
return
with td.orm_session():
stocktype = td.s.query(StockType)\
.options(joinedload('meta'))\
.get(id)
if not stocktype:
print(f"stocktype: id {id} deleted")
return
print(f"stocktype: {stocktype}, price {stocktype.saleprice}, "
f"dept {stocktype.department.description}")
if stocktype.meta:
print(f" metadata: {', '.join(stocktype.meta.keys())}")

@staticmethod
def notify_stockitem_change(id_str):
try:
id = int(id_str)
except Exception:
return
with td.orm_session():
stock = td.s.query(StockItem)\
.options(undefer('remaining'))\
.options(joinedload('stocktype'),
joinedload('stocktype.unit'))\
.get(id)
if not stock:
print(f"stock: id {id} deleted")
return
print(f"stock: {stock.id} {stock.stocktype} "
f"{stock.remaining}/{stock.size} {stock.stocktype.unit.name}")
if stock.finished:
print(f" stock finished {stock.finished}")

@staticmethod
def notify_keycaps(keycode):
with td.orm_session():
keycap = td.s.query(KeyCap).get(keycode)
if not keycap:
return
print(f"keycap: {keycap.keycode} => '{keycap.keycap}' "
f"class '{keycap.css_class}'")

@staticmethod
def notify_config(key):
with td.orm_session():
config = td.s.query(Config).get(key)
if not config:
return
print(f"config: {config.key} = {config.value}")

@staticmethod
def notify_update(blank):
print("update: till update notified")
1 change: 1 addition & 0 deletions quicktill/till.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from . import dbutils # noqa: F401
from . import foodcheck # noqa: F401
from . import secretstore # noqa: F401
from . import monitor # noqa: F401
# End of subcommand imports

log = logging.getLogger(__name__)
Expand Down

0 comments on commit bd19fe1

Please sign in to comment.