treewide: types, types and more types
This commit is contained in:
+10
-3
@@ -4,7 +4,8 @@ import tomllib
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
DEFAULT_CONFIG_PATH = Path('/etc/dibbler/dibbler.toml')
|
DEFAULT_CONFIG_PATH = Path("/etc/dibbler/dibbler.toml")
|
||||||
|
|
||||||
|
|
||||||
def default_config_path_submissive_and_readable() -> bool:
|
def default_config_path_submissive_and_readable() -> bool:
|
||||||
return DEFAULT_CONFIG_PATH.is_file() and any(
|
return DEFAULT_CONFIG_PATH.is_file() and any(
|
||||||
@@ -21,8 +22,10 @@ def default_config_path_submissive_and_readable() -> bool:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
config: dict[str, dict[str, Any]] = dict()
|
config: dict[str, dict[str, Any]] = dict()
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path: Path | None = None):
|
def load_config(config_path: Path | None = None):
|
||||||
global config
|
global config
|
||||||
if config_path is not None:
|
if config_path is not None:
|
||||||
@@ -32,9 +35,13 @@ def load_config(config_path: Path | None = None):
|
|||||||
with DEFAULT_CONFIG_PATH.open("rb") as file:
|
with DEFAULT_CONFIG_PATH.open("rb") as file:
|
||||||
config = tomllib.load(file)
|
config = tomllib.load(file)
|
||||||
else:
|
else:
|
||||||
print("Could not read config file, it was neither provided nor readable in default location", file=sys.stderr)
|
print(
|
||||||
|
"Could not read config file, it was neither provided nor readable in default location",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def config_db_string() -> str:
|
def config_db_string() -> str:
|
||||||
db_type = config["database"]["type"]
|
db_type = config["database"]["type"]
|
||||||
|
|
||||||
@@ -54,7 +61,7 @@ def config_db_string() -> str:
|
|||||||
elif "password" in config["database"]["postgresql"]:
|
elif "password" in config["database"]["postgresql"]:
|
||||||
password = config["database"]["postgresql"]["password"]
|
password = config["database"]["postgresql"]["password"]
|
||||||
else:
|
else:
|
||||||
password = ''
|
password = ""
|
||||||
|
|
||||||
if host.startswith("/"):
|
if host.startswith("/"):
|
||||||
return f"postgresql+psycopg2://{username}:{password}@/{dbname}?host={host}"
|
return f"postgresql+psycopg2://{username}:{password}@/{dbname}?host={host}"
|
||||||
|
|||||||
+34
-13
@@ -1,15 +1,21 @@
|
|||||||
import pwd
|
|
||||||
import subprocess
|
|
||||||
import os
|
import os
|
||||||
|
import pwd
|
||||||
import signal
|
import signal
|
||||||
|
import subprocess
|
||||||
|
from typing import Any, Callable, Literal
|
||||||
|
|
||||||
from sqlalchemy import or_, and_
|
from sqlalchemy import and_, not_, or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..models import User, Product
|
from ..models import Product, User
|
||||||
|
|
||||||
|
|
||||||
def search_user(string, sql_session: Session, ignorethisflag=None):
|
def search_user(
|
||||||
|
string: str,
|
||||||
|
sql_session: Session,
|
||||||
|
ignorethisflag=None,
|
||||||
|
):
|
||||||
|
assert sql_session is not None
|
||||||
string = string.lower()
|
string = string.lower()
|
||||||
exact_match = (
|
exact_match = (
|
||||||
sql_session.query(User)
|
sql_session.query(User)
|
||||||
@@ -32,7 +38,12 @@ def search_user(string, sql_session: Session, ignorethisflag=None):
|
|||||||
return user_list
|
return user_list
|
||||||
|
|
||||||
|
|
||||||
def search_product(string, sql_session: Session, find_hidden_products=True):
|
def search_product(
|
||||||
|
string: str,
|
||||||
|
sql_session: Session,
|
||||||
|
find_hidden_products: bool = True,
|
||||||
|
):
|
||||||
|
assert sql_session is not None
|
||||||
if find_hidden_products:
|
if find_hidden_products:
|
||||||
exact_match = (
|
exact_match = (
|
||||||
sql_session.query(Product)
|
sql_session.query(Product)
|
||||||
@@ -45,7 +56,10 @@ def search_product(string, sql_session: Session, find_hidden_products=True):
|
|||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
Product.bar_code == string,
|
Product.bar_code == string,
|
||||||
and_(Product.name == string, Product.hidden is False),
|
and_(
|
||||||
|
Product.name == string,
|
||||||
|
not_(Product.hidden),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
@@ -65,11 +79,14 @@ def search_product(string, sql_session: Session, find_hidden_products=True):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
product_list = (
|
product_list = (
|
||||||
sql_ession.query(Product)
|
sql_session.query(Product)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
Product.bar_code.ilike(f"%{string}%"),
|
Product.bar_code.ilike(f"%{string}%"),
|
||||||
and_(Product.name.ilike(f"%{string}%"), Product.hidden is False),
|
and_(
|
||||||
|
Product.name.ilike(f"%{string}%"),
|
||||||
|
not_(Product.hidden),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
@@ -77,7 +94,7 @@ def search_product(string, sql_session: Session, find_hidden_products=True):
|
|||||||
return product_list
|
return product_list
|
||||||
|
|
||||||
|
|
||||||
def system_user_exists(username):
|
def system_user_exists(username: str) -> bool:
|
||||||
try:
|
try:
|
||||||
pwd.getpwnam(username)
|
pwd.getpwnam(username)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -88,7 +105,7 @@ def system_user_exists(username):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def guess_data_type(string):
|
def guess_data_type(string: str) -> Literal["card", "rfid", "bar_code", "username"] | None:
|
||||||
if string.startswith("ntnu") and string[4:].isdigit():
|
if string.startswith("ntnu") and string[4:].isdigit():
|
||||||
return "card"
|
return "card"
|
||||||
if string.isdigit() and len(string) == 10:
|
if string.isdigit() and len(string) == 10:
|
||||||
@@ -102,7 +119,11 @@ def guess_data_type(string):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def argmax(d, all=False, value=None):
|
def argmax(
|
||||||
|
d,
|
||||||
|
all: bool = False,
|
||||||
|
value: Callable[[Any], Any] | None = None,
|
||||||
|
):
|
||||||
maxarg = None
|
maxarg = None
|
||||||
if value is not None:
|
if value is not None:
|
||||||
dd = d
|
dd = d
|
||||||
@@ -117,7 +138,7 @@ def argmax(d, all=False, value=None):
|
|||||||
return maxarg
|
return maxarg
|
||||||
|
|
||||||
|
|
||||||
def less(string):
|
def less(string: str) -> None:
|
||||||
"""
|
"""
|
||||||
Run less with string as input; wait until it finishes.
|
Run less with string as input; wait until it finishes.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ..models import Transaction
|
|||||||
|
|
||||||
|
|
||||||
def getUser(sql_session: Session):
|
def getUser(sql_session: Session):
|
||||||
|
assert sql_session is not None
|
||||||
while 1:
|
while 1:
|
||||||
string = input("user? ")
|
string = input("user? ")
|
||||||
user = search_user(string, sql_session)
|
user = search_user(string, sql_session)
|
||||||
@@ -38,6 +39,7 @@ def getUser(sql_session: Session):
|
|||||||
|
|
||||||
|
|
||||||
def getProduct(sql_session: Session):
|
def getProduct(sql_session: Session):
|
||||||
|
assert sql_session is not None
|
||||||
while 1:
|
while 1:
|
||||||
string = input("product? ")
|
string = input("product? ")
|
||||||
product = search_product(string, sql_session)
|
product = search_product(string, sql_session)
|
||||||
@@ -237,6 +239,7 @@ def addLineToDatabase(database, inputLine):
|
|||||||
|
|
||||||
|
|
||||||
def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session):
|
def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session):
|
||||||
|
assert sql_session is not None
|
||||||
sdate = input("enter start date (yyyy-mm-dd)? ")
|
sdate = input("enter start date (yyyy-mm-dd)? ")
|
||||||
edate = input("enter end date (yyyy-mm-dd)? ")
|
edate = input("enter end date (yyyy-mm-dd)? ")
|
||||||
print("building database...")
|
print("building database...")
|
||||||
@@ -463,6 +466,7 @@ def printGlobal(database, dateLine, n):
|
|||||||
|
|
||||||
|
|
||||||
def alt4menuTextOnly(database, dateLine, sql_session: Session):
|
def alt4menuTextOnly(database, dateLine, sql_session: Session):
|
||||||
|
assert sql_session is not None
|
||||||
n = 10
|
n = 10
|
||||||
while 1:
|
while 1:
|
||||||
print(
|
print(
|
||||||
@@ -491,6 +495,7 @@ def alt4menuTextOnly(database, dateLine, sql_session: Session):
|
|||||||
|
|
||||||
|
|
||||||
def statisticsTextOnly(sql_session: Session):
|
def statisticsTextOnly(sql_session: Session):
|
||||||
|
assert sql_session is not None
|
||||||
inputType = 4
|
inputType = 4
|
||||||
product = ""
|
product = ""
|
||||||
user = ""
|
user = ""
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ from dibbler.models import (
|
|||||||
Transaction,
|
Transaction,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .helpermenus import Menu
|
from .helpermenus import Menu
|
||||||
|
|
||||||
|
|
||||||
class AddStockMenu(Menu):
|
class AddStockMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Add stock and adjust credit", sql_session=sql_session, uses_db=True)
|
super().__init__("Add stock and adjust credit", sql_session)
|
||||||
self.help_text = """
|
self.help_text = """
|
||||||
Enter what you have bought for PVVVV here, along with your user name and how
|
Enter what you have bought for PVVVV here, along with your user name and how
|
||||||
much money you're due in credits for the purchase when prompted.\n"""
|
much money you're due in credits for the purchase when prompted.\n"""
|
||||||
@@ -110,7 +111,12 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
print(f"{self.products[product][0]}".rjust(width - len(product.name)))
|
print(f"{self.products[product][0]}".rjust(width - len(product.name)))
|
||||||
print(width * "-")
|
print(width * "-")
|
||||||
|
|
||||||
def add_thing_to_pending(self, thing, amount, price):
|
def add_thing_to_pending(
|
||||||
|
self,
|
||||||
|
thing: User | Product,
|
||||||
|
amount: int,
|
||||||
|
price: int,
|
||||||
|
):
|
||||||
if isinstance(thing, User):
|
if isinstance(thing, User):
|
||||||
self.users.append(thing)
|
self.users.append(thing)
|
||||||
elif thing in list(self.products.keys()):
|
elif thing in list(self.products.keys()):
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ from .helpermenus import Menu
|
|||||||
|
|
||||||
|
|
||||||
class BuyMenu(Menu):
|
class BuyMenu(Menu):
|
||||||
|
superfast_mode: bool
|
||||||
|
purchase: Purchase
|
||||||
|
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Buy", sql_session=sql_session, uses_db=True)
|
super().__init__("Buy", sql_session)
|
||||||
self.superfast_mode = False
|
self.superfast_mode = False
|
||||||
self.help_text = """
|
self.help_text = """
|
||||||
Each purchase may contain one or more products and one or more buyers.
|
Each purchase may contain one or more products and one or more buyers.
|
||||||
@@ -27,7 +30,7 @@ addition, and you can type 'what' at any time to redisplay it.
|
|||||||
When finished, write an empty line to confirm the purchase.\n"""
|
When finished, write an empty line to confirm the purchase.\n"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def credit_check(user):
|
def credit_check(user: User):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param user:
|
:param user:
|
||||||
@@ -38,7 +41,11 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
|
|
||||||
return user.credit > config["limits"]["low_credit_warning_limit"]
|
return user.credit > config["limits"]["low_credit_warning_limit"]
|
||||||
|
|
||||||
def low_credit_warning(self, user, timeout=False):
|
def low_credit_warning(
|
||||||
|
self,
|
||||||
|
user: User,
|
||||||
|
timeout: bool = False,
|
||||||
|
):
|
||||||
assert isinstance(user, User)
|
assert isinstance(user, User)
|
||||||
|
|
||||||
print("***********************************************************************")
|
print("***********************************************************************")
|
||||||
@@ -70,7 +77,11 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
else:
|
else:
|
||||||
return self.confirm(prompt=">", default=True)
|
return self.confirm(prompt=">", default=True)
|
||||||
|
|
||||||
def add_thing_to_purchase(self, thing, amount=1):
|
def add_thing_to_purchase(
|
||||||
|
self,
|
||||||
|
thing: User | Product,
|
||||||
|
amount: int = 1,
|
||||||
|
) -> bool:
|
||||||
if isinstance(thing, User):
|
if isinstance(thing, User):
|
||||||
if thing.is_anonymous():
|
if thing.is_anonymous():
|
||||||
print("---------------------------------------------")
|
print("---------------------------------------------")
|
||||||
@@ -79,7 +90,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
print("---------------------------------------------")
|
print("---------------------------------------------")
|
||||||
|
|
||||||
if not self.credit_check(thing):
|
if not self.credit_check(thing):
|
||||||
if self.low_credit_warning(user=thing, timeout=self.superfast_mode):
|
if self.low_credit_warning(
|
||||||
|
user=thing,
|
||||||
|
timeout=self.superfast_mode,
|
||||||
|
):
|
||||||
Transaction(thing, purchase=self.purchase, penalty=2)
|
Transaction(thing, purchase=self.purchase, penalty=2)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -94,7 +108,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
PurchaseEntry(self.purchase, thing, amount)
|
PurchaseEntry(self.purchase, thing, amount)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _execute(self, initial_contents=None):
|
def _execute(
|
||||||
|
self,
|
||||||
|
initial_contents: list[tuple[User | Product, int]] | None = None,
|
||||||
|
):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
self.purchase = Purchase()
|
self.purchase = Purchase()
|
||||||
self.exit_confirm_msg = None
|
self.exit_confirm_msg = None
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ __all__ = [
|
|||||||
|
|
||||||
class AddUserMenu(Menu):
|
class AddUserMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Add user", sql_session=sql_session, uses_db=True)
|
super().__init__("Add user", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -41,7 +41,7 @@ class AddUserMenu(Menu):
|
|||||||
|
|
||||||
class EditUserMenu(Menu):
|
class EditUserMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Edit user", sql_session=sql_session, uses_db=True)
|
super().__init__("Edit user", sql_session)
|
||||||
self.help_text = """
|
self.help_text = """
|
||||||
The only editable part of a user is its card number and rfid.
|
The only editable part of a user is its card number and rfid.
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ user, then rfid (write an empty line to remove the card number or rfid).
|
|||||||
|
|
||||||
class AddProductMenu(Menu):
|
class AddProductMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Add product", sql_session=sql_session, uses_db=True)
|
super().__init__("Add product", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -99,7 +99,7 @@ class AddProductMenu(Menu):
|
|||||||
|
|
||||||
class EditProductMenu(Menu):
|
class EditProductMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Edit product", sql_session=sql_session, uses_db=True)
|
super().__init__("Edit product", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -108,6 +108,7 @@ class EditProductMenu(Menu):
|
|||||||
while True:
|
while True:
|
||||||
selector = Selector(
|
selector = Selector(
|
||||||
f"Do what with {product.name}?",
|
f"Do what with {product.name}?",
|
||||||
|
sql_session=self.sql_session,
|
||||||
items=[
|
items=[
|
||||||
("name", "Edit name"),
|
("name", "Edit name"),
|
||||||
("price", "Edit price"),
|
("price", "Edit price"),
|
||||||
@@ -152,7 +153,7 @@ class EditProductMenu(Menu):
|
|||||||
|
|
||||||
class AdjustStockMenu(Menu):
|
class AdjustStockMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Adjust stock", sql_session=sql_session, uses_db=True)
|
super().__init__("Adjust stock", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -182,7 +183,7 @@ class AdjustStockMenu(Menu):
|
|||||||
|
|
||||||
class CleanupStockMenu(Menu):
|
class CleanupStockMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Stock Cleanup", sql_session=sql_session, uses_db=True)
|
super().__init__("Stock Cleanup", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
|
|||||||
+23
-8
@@ -1,11 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .helpermenus import MessageMenu, Menu
|
from .helpermenus import Menu, MessageMenu
|
||||||
|
|
||||||
|
|
||||||
class FAQMenu(Menu):
|
class FAQMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Frequently Asked Questions")
|
super().__init__("Frequently Asked Questions", sql_session)
|
||||||
self.items = [
|
self.items = [
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"What is the meaning with this program?",
|
"What is the meaning with this program?",
|
||||||
@@ -17,19 +18,25 @@ class FAQMenu(Menu):
|
|||||||
|
|
||||||
Dibbler stores a "credit" amount for each user. When you register a
|
Dibbler stores a "credit" amount for each user. When you register a
|
||||||
purchase in Dibbler, this amount is decreased. To increase your
|
purchase in Dibbler, this amount is decreased. To increase your
|
||||||
credit, purchase products for dibbler, and register them using "Add
|
credit, purchase products for dibbler, and register them using "Add
|
||||||
stock and adjust credit".
|
stock and adjust credit".
|
||||||
Alternatively, add money to the money box and use "Adjust credit" to
|
Alternatively, add money to the money box and use "Adjust credit" to
|
||||||
tell Dibbler about it.
|
tell Dibbler about it.
|
||||||
""",
|
""",
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Can I still pay for stuff using cash?",
|
"Can I still pay for stuff using cash?",
|
||||||
"""
|
"""
|
||||||
Please put money in the money box and use "Adjust Credit" so that
|
Please put money in the money box and use "Adjust Credit" so that
|
||||||
dibbler can keep track of credit and purchases.""",
|
dibbler can keep track of credit and purchases.""",
|
||||||
|
sql_session,
|
||||||
|
),
|
||||||
|
MessageMenu(
|
||||||
|
"How do I exit from a submenu/dialog/thing?",
|
||||||
|
'Type "exit", "q", or ^d.',
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu("How do I exit from a submenu/dialog/thing?", 'Type "exit", "q", or ^d.'),
|
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
'What does "." mean?',
|
'What does "." mean?',
|
||||||
"""
|
"""
|
||||||
@@ -41,6 +48,7 @@ class FAQMenu(Menu):
|
|||||||
line containing only a period, you should read the lines above and
|
line containing only a period, you should read the lines above and
|
||||||
then press enter to continue.
|
then press enter to continue.
|
||||||
""",
|
""",
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Why is the user interface so terribly unintuitive?",
|
"Why is the user interface so terribly unintuitive?",
|
||||||
@@ -52,25 +60,30 @@ class FAQMenu(Menu):
|
|||||||
|
|
||||||
Answer #3: YOU are unintuitive.
|
Answer #3: YOU are unintuitive.
|
||||||
""",
|
""",
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Why is there no help command?",
|
"Why is there no help command?",
|
||||||
'There is. Have you tried typing "help"?',
|
'There is. Have you tried typing "help"?',
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
'Where are the easter eggs? I tried saying "moo", but nothing happened.',
|
'Where are the easter eggs? I tried saying "moo", but nothing happened.',
|
||||||
'Don\'t say "moo".',
|
'Don\'t say "moo".',
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Why does the program speak English when all the users are Norwegians?",
|
"Why does the program speak English when all the users are Norwegians?",
|
||||||
"Godt spørsmål. Det virket sikkert som en god idé der og da.",
|
"Godt spørsmål. Det virket sikkert som en god idé der og da.",
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Why does the screen have strange colours?",
|
"Why does the screen have strange colours?",
|
||||||
"""
|
"""
|
||||||
Type "c" on the main menu to change the colours of the display, or
|
Type "c" on the main menu to change the colours of the display, or
|
||||||
"cs" if you are a boring person.
|
"cs" if you are a boring person.
|
||||||
""",
|
""",
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"I found a bug; is there a reward?",
|
"I found a bug; is there a reward?",
|
||||||
@@ -101,6 +114,7 @@ class FAQMenu(Menu):
|
|||||||
6. Type "restart" in Dibbler to replace the running process by a new
|
6. Type "restart" in Dibbler to replace the running process by a new
|
||||||
one using the updated files.
|
one using the updated files.
|
||||||
""",
|
""",
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"My question isn't listed here; what do I do?",
|
"My question isn't listed here; what do I do?",
|
||||||
@@ -125,5 +139,6 @@ class FAQMenu(Menu):
|
|||||||
5. Type "restart" in Dibbler to replace the running process by a new
|
5. Type "restart" in Dibbler to replace the running process by a new
|
||||||
one using the updated files.
|
one using the updated files.
|
||||||
""",
|
""",
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
+183
-89
@@ -1,46 +1,57 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from select import select
|
from select import select
|
||||||
|
from typing import Any, Callable, Iterable, Literal, Self
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.models import User
|
|
||||||
from dibbler.lib.helpers import (
|
from dibbler.lib.helpers import (
|
||||||
search_user,
|
|
||||||
search_product,
|
|
||||||
guess_data_type,
|
|
||||||
argmax,
|
argmax,
|
||||||
|
guess_data_type,
|
||||||
|
search_product,
|
||||||
|
search_user,
|
||||||
)
|
)
|
||||||
|
from dibbler.models import Product, User
|
||||||
|
|
||||||
exit_commands = ["exit", "abort", "quit", "bye", "eat flaming death", "q"]
|
exit_commands: list[str] = ["exit", "abort", "quit", "bye", "eat flaming death", "q"]
|
||||||
help_commands = ["help", "?"]
|
help_commands: list[str] = ["help", "?"]
|
||||||
context_commands = ["what", "??"]
|
context_commands: list[str] = ["what", "??"]
|
||||||
local_help_commands = ["help!", "???"]
|
local_help_commands: list[str] = ["help!", "???"]
|
||||||
|
|
||||||
|
|
||||||
class ExitMenu(Exception):
|
class ExitMenuException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Menu(object):
|
class Menu(object):
|
||||||
|
name: str
|
||||||
|
sql_session: Session
|
||||||
|
items: list[Self | tuple | str]
|
||||||
|
prompt: str | None
|
||||||
|
end_prompt: str | None
|
||||||
|
return_index: bool
|
||||||
|
exit_msg: str | None
|
||||||
|
exit_confirm_msg: str | None
|
||||||
|
exit_disallowed_msg: str | None
|
||||||
|
help_text: str | None
|
||||||
|
context: str | None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name,
|
name: str,
|
||||||
items=None,
|
sql_session: Session,
|
||||||
prompt=None,
|
items: list[Self | tuple[Any, str] | str] | None = None,
|
||||||
end_prompt="> ",
|
prompt: str | None = None,
|
||||||
return_index=True,
|
end_prompt: str | None = "> ",
|
||||||
exit_msg=None,
|
return_index: bool = True,
|
||||||
exit_confirm_msg=None,
|
exit_msg: str | None = None,
|
||||||
exit_disallowed_msg=None,
|
exit_confirm_msg: str | None = None,
|
||||||
help_text=None,
|
exit_disallowed_msg: str | None = None,
|
||||||
uses_db=False,
|
help_text: str | None = None,
|
||||||
sql_session: Session | None=None,
|
|
||||||
):
|
):
|
||||||
self.name = name
|
self.name: str = name
|
||||||
|
self.sql_session: Session = sql_session
|
||||||
self.items = items if items is not None else []
|
self.items = items if items is not None else []
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
self.end_prompt = end_prompt
|
self.end_prompt = end_prompt
|
||||||
@@ -50,48 +61,54 @@ class Menu(object):
|
|||||||
self.exit_disallowed_msg = exit_disallowed_msg
|
self.exit_disallowed_msg = exit_disallowed_msg
|
||||||
self.help_text = help_text
|
self.help_text = help_text
|
||||||
self.context = None
|
self.context = None
|
||||||
self.uses_db = uses_db
|
|
||||||
self.sql_session: Session | None = sql_session
|
|
||||||
|
|
||||||
assert not (self.uses_db and self.sql_session is None)
|
assert name is not None
|
||||||
|
assert self.sql_session is not None
|
||||||
|
|
||||||
def exit_menu(self):
|
def exit_menu(self) -> None:
|
||||||
if self.exit_disallowed_msg is not None:
|
if self.exit_disallowed_msg is not None:
|
||||||
print(self.exit_disallowed_msg)
|
print(self.exit_disallowed_msg)
|
||||||
return
|
return
|
||||||
if self.exit_confirm_msg is not None:
|
if self.exit_confirm_msg is not None:
|
||||||
if not self.confirm(self.exit_confirm_msg, default=True):
|
if not self.confirm(self.exit_confirm_msg, default=True):
|
||||||
return
|
return
|
||||||
raise ExitMenu()
|
raise ExitMenuException()
|
||||||
|
|
||||||
def at_exit(self):
|
def at_exit(self) -> None:
|
||||||
if self.exit_msg:
|
if self.exit_msg:
|
||||||
print(self.exit_msg)
|
print(self.exit_msg)
|
||||||
|
|
||||||
def set_context(self, string, display=True):
|
def set_context(
|
||||||
|
self,
|
||||||
|
string: str | None,
|
||||||
|
display: bool = True,
|
||||||
|
) -> None:
|
||||||
self.context = string
|
self.context = string
|
||||||
if self.context is not None and display:
|
if self.context is not None and display:
|
||||||
print(self.context)
|
print(self.context)
|
||||||
|
|
||||||
def add_to_context(self, string):
|
def add_to_context(self, string: str) -> None:
|
||||||
self.context += string
|
if self.context is not None:
|
||||||
|
self.context += string
|
||||||
|
else:
|
||||||
|
self.context = string
|
||||||
|
|
||||||
def printc(self, string):
|
def printc(self, string: str) -> None:
|
||||||
print(string)
|
print(string)
|
||||||
if self.context is None:
|
if self.context is None:
|
||||||
self.context = string
|
self.context = string
|
||||||
else:
|
else:
|
||||||
self.context += "\n" + string
|
self.context += "\n" + string
|
||||||
|
|
||||||
def show_context(self):
|
def show_context(self) -> None:
|
||||||
print(self.header())
|
print(self.header())
|
||||||
if self.context is not None:
|
if self.context is not None:
|
||||||
print(self.context)
|
print(self.context)
|
||||||
|
|
||||||
def item_is_submenu(self, i):
|
def item_is_submenu(self, i: int) -> bool:
|
||||||
return isinstance(self.items[i], Menu)
|
return isinstance(self.items[i], Menu)
|
||||||
|
|
||||||
def item_name(self, i):
|
def item_name(self, i: int) -> str:
|
||||||
if self.item_is_submenu(i):
|
if self.item_is_submenu(i):
|
||||||
return self.items[i].name
|
return self.items[i].name
|
||||||
elif isinstance(self.items[i], tuple):
|
elif isinstance(self.items[i], tuple):
|
||||||
@@ -99,7 +116,7 @@ class Menu(object):
|
|||||||
else:
|
else:
|
||||||
return self.items[i]
|
return self.items[i]
|
||||||
|
|
||||||
def item_value(self, i):
|
def item_value(self, i: int):
|
||||||
if isinstance(self.items[i], tuple):
|
if isinstance(self.items[i], tuple):
|
||||||
return self.items[i][0]
|
return self.items[i][0]
|
||||||
if self.return_index:
|
if self.return_index:
|
||||||
@@ -108,11 +125,11 @@ class Menu(object):
|
|||||||
|
|
||||||
def input_str(
|
def input_str(
|
||||||
self,
|
self,
|
||||||
prompt=None,
|
prompt: str | None = None,
|
||||||
end_prompt=None,
|
end_prompt: str | None = None,
|
||||||
regex=None,
|
regex: str | None = None,
|
||||||
length_range=(None, None),
|
length_range=(None, None),
|
||||||
empty_string_is_none=False,
|
empty_string_is_none: bool = False,
|
||||||
timeout=None,
|
timeout=None,
|
||||||
default=None,
|
default=None,
|
||||||
):
|
):
|
||||||
@@ -181,7 +198,7 @@ class Menu(object):
|
|||||||
continue
|
continue
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def special_input_options(self, result):
|
def special_input_options(self, result) -> bool:
|
||||||
"""
|
"""
|
||||||
Handles special, magic input for input_str
|
Handles special, magic input for input_str
|
||||||
|
|
||||||
@@ -191,7 +208,7 @@ class Menu(object):
|
|||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def special_input_choice(self, in_str):
|
def special_input_choice(self, in_str: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Handle choices which are not simply menu items.
|
Handle choices which are not simply menu items.
|
||||||
|
|
||||||
@@ -201,7 +218,12 @@ class Menu(object):
|
|||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def input_choice(self, number_of_choices, prompt=None, end_prompt=None):
|
def input_choice(
|
||||||
|
self,
|
||||||
|
number_of_choices: int,
|
||||||
|
prompt: str | None = None,
|
||||||
|
end_prompt: str | None = None,
|
||||||
|
):
|
||||||
while True:
|
while True:
|
||||||
result = self.input_str(prompt, end_prompt)
|
result = self.input_str(prompt, end_prompt)
|
||||||
if result == "":
|
if result == "":
|
||||||
@@ -216,7 +238,7 @@ class Menu(object):
|
|||||||
if not self.special_input_choice(result):
|
if not self.special_input_choice(result):
|
||||||
self.invalid_menu_choice(result)
|
self.invalid_menu_choice(result)
|
||||||
|
|
||||||
def invalid_menu_choice(self, in_str):
|
def invalid_menu_choice(self, in_str: str):
|
||||||
print("Please enter a valid choice.")
|
print("Please enter a valid choice.")
|
||||||
|
|
||||||
def input_int(
|
def input_int(
|
||||||
@@ -256,32 +278,40 @@ class Menu(object):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
print("Please enter an integer")
|
print("Please enter an integer")
|
||||||
|
|
||||||
def input_user(self, prompt=None, end_prompt=None):
|
def input_user(
|
||||||
|
self,
|
||||||
|
prompt: str | None = None,
|
||||||
|
end_prompt: str | None = None,
|
||||||
|
) -> User:
|
||||||
user = None
|
user = None
|
||||||
while user is None:
|
while user is None:
|
||||||
user = self.retrieve_user(self.input_str(prompt, end_prompt))
|
user = self.retrieve_user(self.input_str(prompt, end_prompt))
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def retrieve_user(self, search_str):
|
def retrieve_user(self, search_str: str) -> User | None:
|
||||||
return self.search_ui(search_user, search_str, "user")
|
return self.search_ui(search_user, search_str, "user")
|
||||||
|
|
||||||
def input_product(self, prompt=None, end_prompt=None):
|
def input_product(
|
||||||
|
self,
|
||||||
|
prompt: str | None = None,
|
||||||
|
end_prompt: str | None = None,
|
||||||
|
) -> Product:
|
||||||
product = None
|
product = None
|
||||||
while product is None:
|
while product is None:
|
||||||
product = self.retrieve_product(self.input_str(prompt, end_prompt))
|
product = self.retrieve_product(self.input_str(prompt, end_prompt))
|
||||||
return product
|
return product
|
||||||
|
|
||||||
def retrieve_product(self, search_str):
|
def retrieve_product(self, search_str: str) -> Product | None:
|
||||||
return self.search_ui(search_product, search_str, "product")
|
return self.search_ui(search_product, search_str, "product")
|
||||||
|
|
||||||
def input_thing(
|
def input_thing(
|
||||||
self,
|
self,
|
||||||
prompt=None,
|
prompt: str | None = None,
|
||||||
end_prompt=None,
|
end_prompt: str | None = None,
|
||||||
permitted_things=("user", "product"),
|
permitted_things: Iterable[str] = ("user", "product"),
|
||||||
add_nonexisting=(),
|
add_nonexisting=(),
|
||||||
empty_input_permitted=False,
|
empty_input_permitted: bool = False,
|
||||||
find_hidden_products=True,
|
find_hidden_products: bool = True,
|
||||||
):
|
):
|
||||||
result = None
|
result = None
|
||||||
while result is None:
|
while result is None:
|
||||||
@@ -289,19 +319,22 @@ class Menu(object):
|
|||||||
if search_str == "" and empty_input_permitted:
|
if search_str == "" and empty_input_permitted:
|
||||||
return None
|
return None
|
||||||
result = self.search_for_thing(
|
result = self.search_for_thing(
|
||||||
search_str, permitted_things, add_nonexisting, find_hidden_products
|
search_str,
|
||||||
|
permitted_things,
|
||||||
|
add_nonexisting,
|
||||||
|
find_hidden_products,
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def input_multiple(
|
def input_multiple(
|
||||||
self,
|
self,
|
||||||
prompt=None,
|
prompt: str | None = None,
|
||||||
end_prompt=None,
|
end_prompt: str | None = None,
|
||||||
permitted_things=("user", "product"),
|
permitted_things: Iterable[str] = ("user", "product"),
|
||||||
add_nonexisting=(),
|
add_nonexisting=(),
|
||||||
empty_input_permitted=False,
|
empty_input_permitted: bool = False,
|
||||||
find_hidden_products=True,
|
find_hidden_products: bool = True,
|
||||||
):
|
) -> tuple[User | Product, int] | None:
|
||||||
result = None
|
result = None
|
||||||
num = 0
|
num = 0
|
||||||
while result is None:
|
while result is None:
|
||||||
@@ -311,7 +344,10 @@ class Menu(object):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
result = self.search_for_thing(
|
result = self.search_for_thing(
|
||||||
search_str, permitted_things, add_nonexisting, find_hidden_products
|
search_str,
|
||||||
|
permitted_things,
|
||||||
|
add_nonexisting,
|
||||||
|
find_hidden_products,
|
||||||
)
|
)
|
||||||
num = 1
|
num = 1
|
||||||
|
|
||||||
@@ -333,12 +369,15 @@ class Menu(object):
|
|||||||
|
|
||||||
def search_for_thing(
|
def search_for_thing(
|
||||||
self,
|
self,
|
||||||
search_str,
|
search_str: str,
|
||||||
permitted_things=("user", "product"),
|
permitted_things=("user", "product"),
|
||||||
add_non_existing=(),
|
add_non_existing=(),
|
||||||
find_hidden_products=True,
|
find_hidden_products: bool = True,
|
||||||
):
|
) -> User | Product | None:
|
||||||
search_fun = {"user": search_user, "product": search_product}
|
search_fun = {
|
||||||
|
"user": search_user,
|
||||||
|
"product": search_product,
|
||||||
|
}
|
||||||
results = {}
|
results = {}
|
||||||
result_values = {}
|
result_values = {}
|
||||||
for thing in permitted_things:
|
for thing in permitted_things:
|
||||||
@@ -357,10 +396,14 @@ class Menu(object):
|
|||||||
return self.search_add(search_str)
|
return self.search_add(search_str)
|
||||||
# print('No match found for "%s".' % search_str)
|
# print('No match found for "%s".' % search_str)
|
||||||
return None
|
return None
|
||||||
return self.search_ui2(search_str, results[selected_thing], selected_thing)
|
return self.search_ui2(
|
||||||
|
search_str,
|
||||||
|
results[selected_thing],
|
||||||
|
selected_thing,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_result_value(result):
|
def search_result_value(result) -> Literal[0, 1, 2, 3]:
|
||||||
if result is None:
|
if result is None:
|
||||||
return 0
|
return 0
|
||||||
if not isinstance(result, list):
|
if not isinstance(result, list):
|
||||||
@@ -371,7 +414,7 @@ class Menu(object):
|
|||||||
return 2
|
return 2
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def search_add(self, string):
|
def search_add(self, string: str) -> User | None:
|
||||||
type_guess = guess_data_type(string)
|
type_guess = guess_data_type(string)
|
||||||
if type_guess == "username":
|
if type_guess == "username":
|
||||||
print(f'"{string}" looks like a username, but no such user exists.')
|
print(f'"{string}" looks like a username, but no such user exists.')
|
||||||
@@ -383,6 +426,7 @@ class Menu(object):
|
|||||||
if type_guess == "card":
|
if type_guess == "card":
|
||||||
selector = Selector(
|
selector = Selector(
|
||||||
f'"{string}" looks like a card number, but no user with that card number exists.',
|
f'"{string}" looks like a card number, but no user with that card number exists.',
|
||||||
|
self.sql_session,
|
||||||
[
|
[
|
||||||
("create", f"Create user with card number {string}"),
|
("create", f"Create user with card number {string}"),
|
||||||
("set", f"Set card number of an existing user to {string}"),
|
("set", f"Set card number of an existing user to {string}"),
|
||||||
@@ -409,11 +453,21 @@ class Menu(object):
|
|||||||
print(f'"{string}" looks like the bar code for a product, but no such product exists.')
|
print(f'"{string}" looks like the bar code for a product, but no such product exists.')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def search_ui(self, search_fun, search_str, thing):
|
def search_ui(
|
||||||
|
self,
|
||||||
|
search_fun: Callable[[str, Session], list[Any] | Any],
|
||||||
|
search_str: str,
|
||||||
|
thing: str,
|
||||||
|
) -> Any:
|
||||||
result = search_fun(search_str, self.sql_session)
|
result = search_fun(search_str, self.sql_session)
|
||||||
return self.search_ui2(search_str, result, thing)
|
return self.search_ui2(search_str, result, thing)
|
||||||
|
|
||||||
def search_ui2(self, search_str, result, thing):
|
def search_ui2(
|
||||||
|
self,
|
||||||
|
search_str: str,
|
||||||
|
result: list[Any] | Any,
|
||||||
|
thing: str,
|
||||||
|
) -> Any:
|
||||||
if not isinstance(result, list):
|
if not isinstance(result, list):
|
||||||
return result
|
return result
|
||||||
if len(result) == 0:
|
if len(result) == 0:
|
||||||
@@ -433,21 +487,37 @@ class Menu(object):
|
|||||||
else:
|
else:
|
||||||
select_header = f'{len(result):d} {thing}s matching "{search_str}"'
|
select_header = f'{len(result):d} {thing}s matching "{search_str}"'
|
||||||
select_items = result
|
select_items = result
|
||||||
selector = Selector(select_header, items=select_items, return_index=False)
|
selector = Selector(
|
||||||
|
select_header,
|
||||||
|
self.sql_session,
|
||||||
|
items=select_items,
|
||||||
|
return_index=False,
|
||||||
|
)
|
||||||
return selector.execute()
|
return selector.execute()
|
||||||
|
|
||||||
@staticmethod
|
def confirm(
|
||||||
def confirm(prompt, end_prompt=None, default=None, timeout=None):
|
self,
|
||||||
return ConfirmMenu(prompt, end_prompt=None, default=default, timeout=timeout).execute()
|
prompt: str,
|
||||||
|
end_prompt: str | None = None,
|
||||||
|
default: bool | None = None,
|
||||||
|
timeout: int | None = None,
|
||||||
|
) -> bool:
|
||||||
|
return ConfirmMenu(
|
||||||
|
self.sql_session,
|
||||||
|
prompt,
|
||||||
|
end_prompt=None,
|
||||||
|
default=default,
|
||||||
|
timeout=timeout,
|
||||||
|
).execute()
|
||||||
|
|
||||||
def header(self):
|
def header(self) -> str:
|
||||||
return f"[{self.name}]"
|
return f"[{self.name}]"
|
||||||
|
|
||||||
def print_header(self):
|
def print_header(self) -> None:
|
||||||
print("")
|
print("")
|
||||||
print(self.header())
|
print(self.header())
|
||||||
|
|
||||||
def pause(self):
|
def pause(self) -> None:
|
||||||
self.input_str(".", end_prompt="")
|
self.input_str(".", end_prompt="")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -489,7 +559,7 @@ class Menu(object):
|
|||||||
self.set_context(None)
|
self.set_context(None)
|
||||||
try:
|
try:
|
||||||
return self._execute(**kwargs)
|
return self._execute(**kwargs)
|
||||||
except ExitMenu:
|
except ExitMenuException:
|
||||||
self.at_exit()
|
self.at_exit()
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
@@ -515,8 +585,17 @@ class Menu(object):
|
|||||||
|
|
||||||
|
|
||||||
class MessageMenu(Menu):
|
class MessageMenu(Menu):
|
||||||
def __init__(self, name, message, pause_after_message=True):
|
message: str
|
||||||
Menu.__init__(self, name)
|
pause_after_message: bool
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
message: str,
|
||||||
|
sql_session: Session,
|
||||||
|
pause_after_message: bool = True,
|
||||||
|
):
|
||||||
|
super().__init__(name, sql_session)
|
||||||
self.message = message.strip()
|
self.message = message.strip()
|
||||||
self.pause_after_message = pause_after_message
|
self.pause_after_message = pause_after_message
|
||||||
|
|
||||||
@@ -529,10 +608,17 @@ class MessageMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class ConfirmMenu(Menu):
|
class ConfirmMenu(Menu):
|
||||||
def __init__(self, prompt="confirm? ", end_prompt=": ", default=None, timeout=0):
|
def __init__(
|
||||||
Menu.__init__(
|
self,
|
||||||
self,
|
sql_session: Session,
|
||||||
|
prompt: str = "confirm? ",
|
||||||
|
end_prompt: str | None = ": ",
|
||||||
|
default: bool | None = None,
|
||||||
|
timeout: int | None = 0,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
"question",
|
"question",
|
||||||
|
sql_session,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
end_prompt=end_prompt,
|
end_prompt=end_prompt,
|
||||||
exit_disallowed_msg="Please answer yes or no",
|
exit_disallowed_msg="Please answer yes or no",
|
||||||
@@ -560,7 +646,8 @@ class ConfirmMenu(Menu):
|
|||||||
class Selector(Menu):
|
class Selector(Menu):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name,
|
name: str,
|
||||||
|
sql_session: Session,
|
||||||
items=None,
|
items=None,
|
||||||
prompt="select",
|
prompt="select",
|
||||||
return_index=True,
|
return_index=True,
|
||||||
@@ -570,15 +657,22 @@ class Selector(Menu):
|
|||||||
):
|
):
|
||||||
if items is None:
|
if items is None:
|
||||||
items = []
|
items = []
|
||||||
Menu.__init__(self, name, items, prompt, return_index=return_index, exit_msg=exit_msg)
|
super().__init__(
|
||||||
|
name,
|
||||||
|
sql_session,
|
||||||
|
items,
|
||||||
|
prompt,
|
||||||
|
return_index=return_index,
|
||||||
|
exit_msg=exit_msg,
|
||||||
|
)
|
||||||
|
|
||||||
def header(self):
|
def header(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def print_header(self):
|
def print_header(self) -> None:
|
||||||
print(self.header())
|
print(self.header())
|
||||||
|
|
||||||
def local_help(self):
|
def local_help(self) -> None:
|
||||||
if self.help_text is None:
|
if self.help_text is None:
|
||||||
print("This is a selection menu. Enter one of the listed numbers, or")
|
print("This is a selection menu. Enter one of the listed numbers, or")
|
||||||
print("'exit' to go out and do something else.")
|
print("'exit' to go out and do something else.")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def restart():
|
|||||||
|
|
||||||
|
|
||||||
class MainMenu(Menu):
|
class MainMenu(Menu):
|
||||||
def special_input_choice(self, in_str):
|
def special_input_choice(self, in_str: str) -> bool:
|
||||||
mv = in_str.split()
|
mv = in_str.split()
|
||||||
if len(mv) == 2 and mv[0].isdigit():
|
if len(mv) == 2 and mv[0].isdigit():
|
||||||
num = int(mv[0])
|
num = int(mv[0])
|
||||||
@@ -35,9 +35,9 @@ class MainMenu(Menu):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def special_input_options(self, result):
|
def special_input_options(self, result: str) -> bool:
|
||||||
if result in faq_commands:
|
if result in faq_commands:
|
||||||
FAQMenu().execute()
|
FAQMenu(self.sql_session).execute()
|
||||||
return True
|
return True
|
||||||
if result in restart_commands:
|
if result in restart_commands:
|
||||||
if self.confirm("Restart Dibbler?"):
|
if self.confirm("Restart Dibbler?"):
|
||||||
@@ -62,5 +62,5 @@ class MainMenu(Menu):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def invalid_menu_choice(self, in_str):
|
def invalid_menu_choice(self, in_str: str) -> None:
|
||||||
print(self.show_context())
|
print(self.show_context())
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from .helpermenus import Menu, Selector
|
|||||||
|
|
||||||
class TransferMenu(Menu):
|
class TransferMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Transfer credit between users", sql_session=sql_session, uses_db=True)
|
super().__init__("Transfer credit between users", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -42,7 +42,7 @@ class TransferMenu(Menu):
|
|||||||
|
|
||||||
class ShowUserMenu(Menu):
|
class ShowUserMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Show user", sql_session=sql_session, uses_db=True)
|
super().__init__("Show user", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -53,6 +53,7 @@ class ShowUserMenu(Menu):
|
|||||||
print(f"Credit: {user.credit} kr")
|
print(f"Credit: {user.credit} kr")
|
||||||
selector = Selector(
|
selector = Selector(
|
||||||
f"What do you want to know about {user.name}?",
|
f"What do you want to know about {user.name}?",
|
||||||
|
self.sql_session,
|
||||||
items=[
|
items=[
|
||||||
(
|
(
|
||||||
"transactions",
|
"transactions",
|
||||||
@@ -75,7 +76,7 @@ class ShowUserMenu(Menu):
|
|||||||
print("What what?")
|
print("What what?")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def print_transactions(user, limit=None):
|
def print_transactions(user: User, limit: int | None = None) -> None:
|
||||||
num_trans = len(user.transactions)
|
num_trans = len(user.transactions)
|
||||||
if limit is None:
|
if limit is None:
|
||||||
limit = num_trans
|
limit = num_trans
|
||||||
@@ -99,13 +100,13 @@ class ShowUserMenu(Menu):
|
|||||||
string += ")"
|
string += ")"
|
||||||
if t.penalty > 1:
|
if t.penalty > 1:
|
||||||
string += f" * {t.penalty:d}x penalty applied"
|
string += f" * {t.penalty:d}x penalty applied"
|
||||||
else:
|
elif t.description is not None:
|
||||||
string += t.description
|
string += t.description
|
||||||
string += "\n"
|
string += "\n"
|
||||||
less(string)
|
less(string)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def print_purchased_products(user):
|
def print_purchased_products(user: User) -> None:
|
||||||
products = []
|
products = []
|
||||||
for ref in user.products:
|
for ref in user.products:
|
||||||
product = ref.product
|
product = ref.product
|
||||||
@@ -125,7 +126,7 @@ class ShowUserMenu(Menu):
|
|||||||
|
|
||||||
class UserListMenu(Menu):
|
class UserListMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "User list", sql_session=sql_session, uses_db=True)
|
super().__init__("User list", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -146,7 +147,7 @@ class UserListMenu(Menu):
|
|||||||
|
|
||||||
class AdjustCreditMenu(Menu):
|
class AdjustCreditMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Adjust credit", sql_session=sql_session, uses_db=True)
|
super().__init__("Adjust credit", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -176,7 +177,7 @@ class AdjustCreditMenu(Menu):
|
|||||||
|
|
||||||
class ProductListMenu(Menu):
|
class ProductListMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Product list", sql_session=sql_session, uses_db=True)
|
super().__init__("Product list", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -206,7 +207,7 @@ class ProductListMenu(Menu):
|
|||||||
|
|
||||||
class ProductSearchMenu(Menu):
|
class ProductSearchMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Product search", sql_session=sql_session, uses_db=True)
|
super().__init__("Product search", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from .helpermenus import Menu
|
|||||||
|
|
||||||
class PrintLabelMenu(Menu):
|
class PrintLabelMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Print a label", sql_session=sql_session, uses_db=True)
|
super().__init__("Print a label", sql_session)
|
||||||
self.help_text = """
|
self.help_text = """
|
||||||
Prints out a product bar code on the printer
|
Prints out a product bar code on the printer
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ __all__ = [
|
|||||||
|
|
||||||
class ProductPopularityMenu(Menu):
|
class ProductPopularityMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Products by popularity", sql_session=sql_session, uses_db=True)
|
super().__init__("Products by popularity", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -50,7 +50,7 @@ class ProductPopularityMenu(Menu):
|
|||||||
|
|
||||||
class ProductRevenueMenu(Menu):
|
class ProductRevenueMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Products by revenue", sql_session=sql_session, uses_db=True)
|
super().__init__("Products by revenue", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -88,7 +88,7 @@ class ProductRevenueMenu(Menu):
|
|||||||
|
|
||||||
class BalanceMenu(Menu):
|
class BalanceMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Total balance of PVVVV", sql_session=sql_session, uses_db=True)
|
super().__init__("Total balance of PVVVV", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
@@ -121,7 +121,7 @@ class BalanceMenu(Menu):
|
|||||||
|
|
||||||
class LoggedStatisticsMenu(Menu):
|
class LoggedStatisticsMenu(Menu):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
Menu.__init__(self, "Statistics from log", sql_session=sql_session, uses_db=True)
|
super().__init__("Statistics from log", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self):
|
||||||
statisticsTextOnly(self.sql_session)
|
statisticsTextOnly(self.sql_session)
|
||||||
|
|||||||
@@ -36,12 +36,19 @@ class Product(Base):
|
|||||||
name_re = r".+"
|
name_re = r".+"
|
||||||
name_length = 45
|
name_length = 45
|
||||||
|
|
||||||
def __init__(self, bar_code, name, price, stock=0, hidden=False):
|
def __init__(
|
||||||
|
self,
|
||||||
|
bar_code: str,
|
||||||
|
name: str,
|
||||||
|
price: int,
|
||||||
|
stock: int = 0,
|
||||||
|
hidden: bool = False,
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.bar_code = bar_code
|
self.bar_code = bar_code
|
||||||
self.price = price
|
self.price = price
|
||||||
self.stock = stock
|
self.stock = stock
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@@ -36,16 +36,16 @@ class Purchase(Base):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_complete(self):
|
def is_complete(self) -> bool:
|
||||||
return len(self.transactions) > 0 and len(self.entries) > 0
|
return len(self.transactions) > 0 and len(self.entries) > 0
|
||||||
|
|
||||||
def price_per_transaction(self, round_up=True):
|
def price_per_transaction(self, round_up: bool = True) -> int:
|
||||||
if round_up:
|
if round_up:
|
||||||
return int(math.ceil(float(self.price) / len(self.transactions)))
|
return int(math.ceil(float(self.price) / len(self.transactions)))
|
||||||
else:
|
else:
|
||||||
return int(math.floor(float(self.price) / len(self.transactions)))
|
return int(math.floor(float(self.price) / len(self.transactions)))
|
||||||
|
|
||||||
def set_price(self, round_up=True):
|
def set_price(self, round_up: bool = True) -> None:
|
||||||
self.price = 0
|
self.price = 0
|
||||||
for entry in self.entries:
|
for entry in self.entries:
|
||||||
self.price += entry.amount * entry.product.price
|
self.price += entry.amount * entry.product.price
|
||||||
@@ -53,16 +53,16 @@ class Purchase(Base):
|
|||||||
for t in self.transactions:
|
for t in self.transactions:
|
||||||
t.amount = self.price_per_transaction(round_up=round_up)
|
t.amount = self.price_per_transaction(round_up=round_up)
|
||||||
|
|
||||||
def perform_purchase(self, ignore_penalty=False, round_up=True):
|
def perform_purchase(self, ignore_penalty: bool = False, round_up: bool = True) -> None:
|
||||||
self.time = datetime.datetime.now()
|
self.time = datetime.now()
|
||||||
self.set_price(round_up=round_up)
|
self.set_price(round_up=round_up)
|
||||||
for t in self.transactions:
|
for t in self.transactions:
|
||||||
t.perform_transaction(ignore_penalty=ignore_penalty)
|
t.perform_transaction(ignore_penalty=ignore_penalty)
|
||||||
for entry in self.entries:
|
for entry in self.entries:
|
||||||
entry.product.stock -= entry.amount
|
entry.product.stock -= entry.amount
|
||||||
|
|
||||||
def perform_soft_purchase(self, price, round_up=True):
|
def perform_soft_purchase(self, price: int, round_up: bool = True) -> None:
|
||||||
self.time = datetime.datetime.now()
|
self.time = datetime.now()
|
||||||
self.price = price
|
self.price = price
|
||||||
for t in self.transactions:
|
for t in self.transactions:
|
||||||
t.amount = self.price_per_transaction(round_up=round_up)
|
t.amount = self.price_per_transaction(round_up=round_up)
|
||||||
|
|||||||
@@ -38,15 +38,22 @@ class Transaction(Base):
|
|||||||
user: Mapped[User] = relationship(lazy="joined")
|
user: Mapped[User] = relationship(lazy="joined")
|
||||||
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
||||||
|
|
||||||
def __init__(self, user, amount=0, description=None, purchase=None, penalty=1):
|
def __init__(
|
||||||
|
self,
|
||||||
|
user: User,
|
||||||
|
amount: int = 0,
|
||||||
|
description: str | None = None,
|
||||||
|
purchase: Purchase | None = None,
|
||||||
|
penalty: int = 1,
|
||||||
|
):
|
||||||
self.user = user
|
self.user = user
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.description = description
|
self.description = description
|
||||||
self.purchase = purchase
|
self.purchase = purchase
|
||||||
self.penalty = penalty
|
self.penalty = penalty
|
||||||
|
|
||||||
def perform_transaction(self, ignore_penalty=False):
|
def perform_transaction(self, ignore_penalty: bool = False) -> None:
|
||||||
self.time = datetime.datetime.now()
|
self.time = datetime.now()
|
||||||
if not ignore_penalty:
|
if not ignore_penalty:
|
||||||
self.amount *= self.penalty
|
self.amount *= self.penalty
|
||||||
self.user.credit -= self.amount
|
self.user.credit -= self.amount
|
||||||
|
|||||||
@@ -25,14 +25,20 @@ class User(Base):
|
|||||||
card: Mapped[str | None] = mapped_column(String(20))
|
card: Mapped[str | None] = mapped_column(String(20))
|
||||||
rfid: Mapped[str | None] = mapped_column(String(20))
|
rfid: Mapped[str | None] = mapped_column(String(20))
|
||||||
|
|
||||||
products: Mapped[set[UserProducts]] = relationship(back_populates="user")
|
products: Mapped[list[UserProducts]] = relationship(back_populates="user")
|
||||||
transactions: Mapped[set[Transaction]] = relationship(back_populates="user")
|
transactions: Mapped[list[Transaction]] = relationship(back_populates="user")
|
||||||
|
|
||||||
name_re = r"[a-z]+"
|
name_re = r"[a-z]+"
|
||||||
card_re = r"(([Nn][Tt][Nn][Uu])?[0-9]+)?"
|
card_re = r"(([Nn][Tt][Nn][Uu])?[0-9]+)?"
|
||||||
rfid_re = r"[0-9a-fA-F]*"
|
rfid_re = r"[0-9a-fA-F]*"
|
||||||
|
|
||||||
def __init__(self, name, card, rfid=None, credit=0):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
card: str | None,
|
||||||
|
rfid: str | None = None,
|
||||||
|
credit: int = 0,
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
if card == "":
|
if card == "":
|
||||||
card = None
|
card = None
|
||||||
|
|||||||
@@ -4,25 +4,56 @@
|
|||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
from signal import (
|
||||||
|
SIG_IGN,
|
||||||
|
SIGQUIT,
|
||||||
|
SIGTSTP,
|
||||||
|
)
|
||||||
|
from signal import (
|
||||||
|
signal as set_signal_handler,
|
||||||
|
)
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..conf import config
|
from ..conf import config
|
||||||
from ..lib.helpers import *
|
from ..menus import (
|
||||||
from ..menus import *
|
AddProductMenu,
|
||||||
|
AddStockMenu,
|
||||||
|
AddUserMenu,
|
||||||
|
AdjustCreditMenu,
|
||||||
|
AdjustStockMenu,
|
||||||
|
BalanceMenu,
|
||||||
|
BuyMenu,
|
||||||
|
CleanupStockMenu,
|
||||||
|
EditProductMenu,
|
||||||
|
EditUserMenu,
|
||||||
|
FAQMenu,
|
||||||
|
LoggedStatisticsMenu,
|
||||||
|
MainMenu,
|
||||||
|
Menu,
|
||||||
|
PrintLabelMenu,
|
||||||
|
ProductListMenu,
|
||||||
|
ProductPopularityMenu,
|
||||||
|
ProductRevenueMenu,
|
||||||
|
ProductSearchMenu,
|
||||||
|
ShowUserMenu,
|
||||||
|
TransferMenu,
|
||||||
|
UserListMenu,
|
||||||
|
)
|
||||||
|
|
||||||
random.seed()
|
random.seed()
|
||||||
|
|
||||||
|
|
||||||
def main(sql_session: Session):
|
def main(sql_session: Session):
|
||||||
if not config["general"]["stop_allowed"]:
|
if not config["general"]["stop_allowed"]:
|
||||||
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
|
set_signal_handler(SIGQUIT, SIG_IGN)
|
||||||
|
|
||||||
if not config["general"]["stop_allowed"]:
|
if not config["general"]["stop_allowed"]:
|
||||||
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
set_signal_handler(SIGTSTP, SIG_IGN)
|
||||||
|
|
||||||
main = MainMenu(
|
main = MainMenu(
|
||||||
"Dibbler main menu",
|
"Dibbler main menu",
|
||||||
|
sql_session,
|
||||||
items=[
|
items=[
|
||||||
BuyMenu(sql_session),
|
BuyMenu(sql_session),
|
||||||
ProductListMenu(sql_session),
|
ProductListMenu(sql_session),
|
||||||
@@ -33,6 +64,7 @@ def main(sql_session: Session):
|
|||||||
AddStockMenu(sql_session),
|
AddStockMenu(sql_session),
|
||||||
Menu(
|
Menu(
|
||||||
"Add/edit",
|
"Add/edit",
|
||||||
|
sql_session,
|
||||||
items=[
|
items=[
|
||||||
AddUserMenu(sql_session),
|
AddUserMenu(sql_session),
|
||||||
EditUserMenu(sql_session),
|
EditUserMenu(sql_session),
|
||||||
@@ -45,6 +77,7 @@ def main(sql_session: Session):
|
|||||||
ProductSearchMenu(sql_session),
|
ProductSearchMenu(sql_session),
|
||||||
Menu(
|
Menu(
|
||||||
"Statistics",
|
"Statistics",
|
||||||
|
sql_session,
|
||||||
items=[
|
items=[
|
||||||
ProductPopularityMenu(sql_session),
|
ProductPopularityMenu(sql_session),
|
||||||
ProductRevenueMenu(sql_session),
|
ProductRevenueMenu(sql_session),
|
||||||
@@ -52,7 +85,7 @@ def main(sql_session: Session):
|
|||||||
LoggedStatisticsMenu(sql_session),
|
LoggedStatisticsMenu(sql_session),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
FAQMenu(),
|
FAQMenu(sql_session),
|
||||||
PrintLabelMenu(sql_session),
|
PrintLabelMenu(sql_session),
|
||||||
],
|
],
|
||||||
exit_msg="happy happy joy joy",
|
exit_msg="happy happy joy joy",
|
||||||
|
|||||||
Reference in New Issue
Block a user