Merge branch 'staging'

This commit is contained in:
s3lph 2021-04-07 10:05:01 +02:00
commit bc7f04f360
25 changed files with 221 additions and 194 deletions

View file

@ -95,28 +95,6 @@ build_debian:
only: only:
- tags - tags
build_archlinux:
stage: build
image: archlinux/base:latest # Use an archlinux image instead of the customized debian image.
script:
- pacman -Sy --noconfirm python python-setuptools python-pip python-wheel python-bottle python-jinja python-pillow python-magic base-devel
- export MATEMAT_VERSION=$(python -c 'import matemat; print(matemat.__version__)')
- cp -r static/ package/archlinux/matemat/usr/lib/matemat/static/
- cp -r templates/ package/archlinux/matemat/usr/lib/matemat/templates/
- python3 setup.py egg_info -d -b +master install --root=package/archlinux/matemat/ --prefix=/usr --optimize=1
- cd package/archlinux
- mv matemat/usr/bin/matemat matemat/usr/lib/matemat/matemat
- rm -rf matemat/usr/bin
- sed -re "s/__VERSION__/${MATEMAT_VERSION}/g" -i PKGBUILD
- sudo -u nobody makepkg -c
- sha256sum *.pkg.tar.zst > SHA256SUMS
artifacts:
paths:
- "package/archlinux/*.pkg.tar.zst"
- package/archlinux/SHA256SUMS
only:
- tags
release: release:
stage: deploy stage: deploy

View file

@ -1,5 +1,20 @@
# Matemat Changelog # Matemat Changelog
<!-- BEGIN RELEASE v0.2.8 -->
## Version 0.2.8
Feature release
### Changes
<!-- BEGIN CHANGES 0.2.8 -->
- Feature: Add "custom price" products
- Fix: Buying not working when using the NullDispenser
- Breaking: Remove Arch Linux packaging
<!-- END CHANGES 0.2.8 -->
<!-- END RELEASE v0.2.8 -->
<!-- BEGIN RELEASE v0.2.7 --> <!-- BEGIN RELEASE v0.2.7 -->
## Version 0.2.7 ## Version 0.2.7

View file

@ -1,2 +1,2 @@
__version__ = '0.2.7' __version__ = '0.2.8'

View file

@ -354,11 +354,11 @@ class MatematDatabase(object):
products: List[Product] = [] products: List[Product] = []
with self.db.transaction(exclusive=False) as c: with self.db.transaction(exclusive=False) as c:
for row in c.execute(''' for row in c.execute('''
SELECT product_id, name, price_member, price_non_member, stock, stockable SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable
FROM products FROM products
'''): '''):
product_id, name, price_member, price_external, stock, stockable = row product_id, name, price_member, price_external, custom_price, stock, stockable = row
products.append(Product(product_id, name, price_member, price_external, stockable, stock)) products.append(Product(product_id, name, price_member, price_external, custom_price, stockable, stock))
return products return products
def get_product(self, pid: int) -> Product: def get_product(self, pid: int) -> Product:
@ -369,7 +369,7 @@ class MatematDatabase(object):
with self.db.transaction(exclusive=False) as c: with self.db.transaction(exclusive=False) as c:
# Fetch all values to construct the product # Fetch all values to construct the product
c.execute(''' c.execute('''
SELECT product_id, name, price_member, price_non_member, stock, stockable SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable
FROM products FROM products
WHERE product_id = ?''', WHERE product_id = ?''',
[pid]) [pid])
@ -377,15 +377,17 @@ class MatematDatabase(object):
if row is None: if row is None:
raise ValueError(f'No product with product ID {pid} exists.') raise ValueError(f'No product with product ID {pid} exists.')
# Unpack the row and construct the product # Unpack the row and construct the product
product_id, name, price_member, price_non_member, stock, stockable = row product_id, name, price_member, price_non_member, custom_price, stock, stockable = row
return Product(product_id, name, price_member, price_non_member, stockable, stock) return Product(product_id, name, price_member, price_non_member, custom_price, stockable, stock)
def create_product(self, name: str, price_member: int, price_non_member: int, stockable: bool) -> Product: def create_product(self, name: str, price_member: int, price_non_member: int, custom_price:
bool, stockable: bool) -> Product:
""" """
Creates a new product. Creates a new product.
:param name: Name of the product. :param name: Name of the product.
:param price_member: Price of the product for members. :param price_member: Price of the product for members.
:param price_non_member: Price of the product for non-members. :param price_non_member: Price of the product for non-members.
:param custom_price: Whether the price is customizable. If yes, the price values are understood as minimum.
:param stockable: True if the product should be stockable, false otherwise. :param stockable: True if the product should be stockable, false otherwise.
:return: A Product object representing the created product. :return: A Product object representing the created product.
:raises ValueError: If a product with the same name already exists. :raises ValueError: If a product with the same name already exists.
@ -396,17 +398,18 @@ class MatematDatabase(object):
if c.fetchone() is not None: if c.fetchone() is not None:
raise ValueError(f'A product with the name \'{name}\' already exists.') raise ValueError(f'A product with the name \'{name}\' already exists.')
c.execute(''' c.execute('''
INSERT INTO products (name, price_member, price_non_member, stock, stockable) INSERT INTO products (name, price_member, price_non_member, custom_price, stock, stockable)
VALUES (:name, :price_member, :price_non_member, 0, :stockable) VALUES (:name, :price_member, :price_non_member, :custom_price, 0, :stockable)
''', { ''', {
'name': name, 'name': name,
'price_member': price_member, 'price_member': price_member,
'price_non_member': price_non_member, 'price_non_member': price_non_member,
'custom_price': custom_price,
'stockable': stockable 'stockable': stockable
}) })
c.execute('SELECT last_insert_rowid()') c.execute('SELECT last_insert_rowid()')
product_id = int(c.fetchone()[0]) product_id = int(c.fetchone()[0])
return Product(product_id, name, price_member, price_non_member, stockable, 0) return Product(product_id, name, price_member, price_non_member, custom_price, stockable, 0)
def change_product(self, product: Product, **kwargs) -> None: def change_product(self, product: Product, **kwargs) -> None:
""" """
@ -422,6 +425,7 @@ class MatematDatabase(object):
name: str = kwargs['name'] if 'name' in kwargs else product.name name: str = kwargs['name'] if 'name' in kwargs else product.name
price_member: int = kwargs['price_member'] if 'price_member' in kwargs else product.price_member price_member: int = kwargs['price_member'] if 'price_member' in kwargs else product.price_member
price_non_member: int = kwargs['price_non_member'] if 'price_non_member' in kwargs else product.price_non_member price_non_member: int = kwargs['price_non_member'] if 'price_non_member' in kwargs else product.price_non_member
custom_price: int = kwargs['custom_price'] if 'custom_price' in kwargs else product.custom_price
stock: int = kwargs['stock'] if 'stock' in kwargs else product.stock stock: int = kwargs['stock'] if 'stock' in kwargs else product.stock
stockable: bool = kwargs['stockable'] if 'stockable' in kwargs else product.stockable stockable: bool = kwargs['stockable'] if 'stockable' in kwargs else product.stockable
with self.db.transaction() as c: with self.db.transaction() as c:
@ -431,6 +435,7 @@ class MatematDatabase(object):
name = :name, name = :name,
price_member = :price_member, price_member = :price_member,
price_non_member = :price_non_member, price_non_member = :price_non_member,
custom_price = :custom_price,
stock = :stock, stock = :stock,
stockable = :stockable stockable = :stockable
WHERE product_id = :product_id WHERE product_id = :product_id
@ -439,6 +444,7 @@ class MatematDatabase(object):
'name': name, 'name': name,
'price_member': price_member, 'price_member': price_member,
'price_non_member': price_non_member, 'price_non_member': price_non_member,
'custom_price': custom_price,
'stock': stock, 'stock': stock,
'stockable': stockable 'stockable': stockable
}) })
@ -450,6 +456,7 @@ class MatematDatabase(object):
product.name = name product.name = name
product.price_member = price_member product.price_member = price_member
product.price_non_member = price_non_member product.price_non_member = price_non_member
product.custom_price = custom_price
product.stock = stock product.stock = stock
product.stockable = stockable product.stockable = stockable
@ -469,15 +476,18 @@ class MatematDatabase(object):
raise DatabaseConsistencyError( raise DatabaseConsistencyError(
f'delete_product should affect 1 products row, but affected {affected}') f'delete_product should affect 1 products row, but affected {affected}')
def increment_consumption(self, user: User, product: Product) -> None: def increment_consumption(self, user: User, product: Product, custom_price: int = None) -> None:
""" """
Decrement the user's balance by the price of the product and create an entry in the statistics table. Decrement the user's balance by the price of the product and create an entry in the statistics table.
:param user: The user buying a product. :param user: The user buying a product.
:param product: The product the user is buying. :param product: The product the user is buying.
:param custom_price: The custom price chosen by the user.
:raises DatabaseConsistencyError: If the user or the product does not exist in the database. :raises DatabaseConsistencyError: If the user or the product does not exist in the database.
""" """
price: int = product.price_member if user.is_member else product.price_non_member price: int = product.price_member if user.is_member else product.price_non_member
if product.custom_price and custom_price is not None:
price = max(price, custom_price)
with self.db.transaction() as c: with self.db.transaction() as c:
c.execute(''' c.execute('''
INSERT INTO transactions (user_id, value, old_balance) INSERT INTO transactions (user_id, value, old_balance)

View file

@ -268,3 +268,11 @@ def migrate_schema_4_to_5(c: sqlite3.Cursor):
INSERT INTO products SELECT product_id, name, stock, 1, price_member, price_non_member FROM products_temp INSERT INTO products SELECT product_id, name, stock, 1, price_member, price_non_member FROM products_temp
''') ''')
c.execute('DROP TABLE products_temp') c.execute('DROP TABLE products_temp')
def migrate_schema_5_to_6(c: sqlite3.Cursor):
# Add custom_price column
c.execute('''
ALTER TABLE products ADD COLUMN
custom_price INTEGER(1) DEFAULT 0;
''')

View file

@ -8,17 +8,19 @@ class Product:
:param name: The product's name. :param name: The product's name.
:param price_member: The price of a unit of this product for users marked as "members". :param price_member: The price of a unit of this product for users marked as "members".
:param price_non_member: The price of a unit of this product for users NOT marked as "members". :param price_non_member: The price of a unit of this product for users NOT marked as "members".
:param custom_price: If true, the user can choose the price to pay, but at least the regular price.
:param stock: The number of items of this product currently in stock, or None if not stockable. :param stock: The number of items of this product currently in stock, or None if not stockable.
:param stockable: Whether this product is stockable. :param stockable: Whether this product is stockable.
""" """
def __init__(self, _id: int, name: str, def __init__(self, _id: int, name: str,
price_member: int, price_non_member: int, price_member: int, price_non_member: int, custom_price: bool,
stockable: bool, stock: int) -> None: stockable: bool, stock: int) -> None:
self.id: int = _id self.id: int = _id
self.name: str = name self.name: str = name
self.price_member: int = price_member self.price_member: int = price_member
self.price_non_member: int = price_non_member self.price_non_member: int = price_non_member
self.custom_price: bool = custom_price
self.stock: int = stock self.stock: int = stock
self.stockable: bool = stockable self.stockable: bool = stockable
@ -29,8 +31,10 @@ class Product:
self.name == other.name and \ self.name == other.name and \
self.price_member == other.price_member and \ self.price_member == other.price_member and \
self.price_non_member == other.price_non_member and \ self.price_non_member == other.price_non_member and \
self.custom_price == other.custom_price and \
self.stock == other.stock and \ self.stock == other.stock and \
self.stockable == other.stockable self.stockable == other.stockable
def __hash__(self) -> int: def __hash__(self) -> int:
return hash((self.id, self.name, self.price_member, self.price_non_member, self.stock, self.stockable)) return hash((self.id, self.name, self.price_member, self.price_non_member, self.custom_price,
self.stock, self.stockable))

View file

@ -333,3 +333,83 @@ SCHEMAS[5] = [
ON DELETE SET NULL ON UPDATE CASCADE ON DELETE SET NULL ON UPDATE CASCADE
); );
'''] ''']
SCHEMAS[6] = [
'''
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT DEFAULT NULL,
password TEXT NOT NULL,
touchkey TEXT DEFAULT NULL,
is_admin INTEGER(1) NOT NULL DEFAULT 0,
is_member INTEGER(1) NOT NULL DEFAULT 1,
balance INTEGER(8) NOT NULL DEFAULT 0,
lastchange INTEGER(8) NOT NULL DEFAULT 0,
receipt_pref INTEGER(1) NOT NULL DEFAULT 0,
created INTEGER(8) NOT NULL DEFAULT 0
);
''',
'''
CREATE TABLE products (
product_id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
stock INTEGER(8) DEFAULT 0,
stockable INTEGER(1) DEFAULT 1,
price_member INTEGER(8) NOT NULL,
price_non_member INTEGER(8) NOT NULL,
custom_price INTEGER(1) DEFAULT 0
);
''',
'''
CREATE TABLE transactions ( -- "superclass" of the following 3 tables
ta_id INTEGER PRIMARY KEY,
user_id INTEGER DEFAULT NULL,
value INTEGER(8) NOT NULL,
old_balance INTEGER(8) NOT NULL,
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE SET NULL ON UPDATE CASCADE
);
''',
'''
CREATE TABLE consumptions ( -- transactions involving buying a product
ta_id INTEGER PRIMARY KEY,
product TEXT NOT NULL,
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
ON DELETE CASCADE ON UPDATE CASCADE
);
''',
'''
CREATE TABLE deposits ( -- transactions involving depositing cash
ta_id INTEGER PRIMARY KEY,
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
ON DELETE CASCADE ON UPDATE CASCADE
);
''',
'''
CREATE TABLE modifications ( -- transactions involving balance modification by an admin
ta_id INTEGER NOT NULL,
agent TEXT NOT NULL,
reason TEXT DEFAULT NULL,
PRIMARY KEY (ta_id),
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
ON DELETE CASCADE ON UPDATE CASCADE
);
''',
'''
CREATE TABLE receipts ( -- receipts sent to the users
receipt_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
first_ta_id INTEGER DEFAULT NULL,
last_ta_id INTEGER DEFAULT NULL,
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (first_ta_id) REFERENCES transactions(ta_id)
ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (last_ta_id) REFERENCES transactions(ta_id)
ON DELETE SET NULL ON UPDATE CASCADE
);
''']

View file

@ -220,7 +220,7 @@ class DatabaseTest(unittest.TestCase):
def test_create_product(self) -> None: def test_create_product(self) -> None:
with self.db as db: with self.db as db:
with db.transaction() as c: with db.transaction() as c:
db.create_product('Club Mate', 200, 200, True) db.create_product('Club Mate', 200, 200, True, True)
c.execute("SELECT * FROM products") c.execute("SELECT * FROM products")
row = c.fetchone() row = c.fetchone()
self.assertEqual('Club Mate', row[1]) self.assertEqual('Club Mate', row[1])
@ -228,13 +228,14 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual(1, row[3]) self.assertEqual(1, row[3])
self.assertEqual(200, row[4]) self.assertEqual(200, row[4])
self.assertEqual(200, row[5]) self.assertEqual(200, row[5])
self.assertEqual(1, row[6])
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
db.create_product('Club Mate', 250, 250, False) db.create_product('Club Mate', 250, 250, False, False)
def test_get_product(self) -> None: def test_get_product(self) -> None:
with self.db as db: with self.db as db:
with db.transaction(exclusive=False): with db.transaction(exclusive=False):
created = db.create_product('Club Mate', 150, 250, False) created = db.create_product('Club Mate', 150, 250, False, False)
product = db.get_product(created.id) product = db.get_product(created.id)
self.assertEqual('Club Mate', product.name) self.assertEqual('Club Mate', product.name)
self.assertEqual(150, product.price_member) self.assertEqual(150, product.price_member)
@ -248,9 +249,9 @@ class DatabaseTest(unittest.TestCase):
# Test empty list # Test empty list
products = db.list_products() products = db.list_products()
self.assertEqual(0, len(products)) self.assertEqual(0, len(products))
db.create_product('Club Mate', 200, 200, True) db.create_product('Club Mate', 200, 200, False, True)
db.create_product('Flora Power Mate', 200, 200, False) db.create_product('Flora Power Mate', 200, 200, False, False)
db.create_product('Fritz Mate', 200, 250, True) db.create_product('Fritz Mate', 200, 250, False, True)
products = db.list_products() products = db.list_products()
self.assertEqual(3, len(products)) self.assertEqual(3, len(products))
productcheck = {} productcheck = {}
@ -272,13 +273,14 @@ class DatabaseTest(unittest.TestCase):
def test_change_product(self) -> None: def test_change_product(self) -> None:
with self.db as db: with self.db as db:
product = db.create_product('Club Mate', 200, 200, True) product = db.create_product('Club Mate', 200, 200, False, True)
db.change_product(product, name='Flora Power Mate', price_member=150, price_non_member=250, db.change_product(product, name='Flora Power Mate', price_member=150, price_non_member=250,
stock=None, stockable=False) custom_price=True, stock=None, stockable=False)
# Changes must be reflected in the passed object # Changes must be reflected in the passed object
self.assertEqual('Flora Power Mate', product.name) self.assertEqual('Flora Power Mate', product.name)
self.assertEqual(150, product.price_member) self.assertEqual(150, product.price_member)
self.assertEqual(250, product.price_non_member) self.assertEqual(250, product.price_non_member)
self.assertEqual(True, product.custom_price)
self.assertEqual(None, product.stock) self.assertEqual(None, product.stock)
self.assertEqual(False, product.stockable) self.assertEqual(False, product.stockable)
# Changes must be reflected in the database # Changes must be reflected in the database
@ -286,11 +288,13 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual('Flora Power Mate', checkproduct.name) self.assertEqual('Flora Power Mate', checkproduct.name)
self.assertEqual(150, checkproduct.price_member) self.assertEqual(150, checkproduct.price_member)
self.assertEqual(250, checkproduct.price_non_member) self.assertEqual(250, checkproduct.price_non_member)
self.assertEqual(True, checkproduct.custom_price)
self.assertEqual(None, checkproduct.stock) self.assertEqual(None, checkproduct.stock)
self.assertEqual(False, checkproduct.stockable)
product.id = -1 product.id = -1
with self.assertRaises(DatabaseConsistencyError): with self.assertRaises(DatabaseConsistencyError):
db.change_product(product) db.change_product(product)
product2 = db.create_product('Club Mate', 200, 200, True) product2 = db.create_product('Club Mate', 200, 200, False, True)
product2.name = 'Flora Power Mate' product2.name = 'Flora Power Mate'
with self.assertRaises(DatabaseConsistencyError): with self.assertRaises(DatabaseConsistencyError):
# Should fail, as a product with the same name already exists. # Should fail, as a product with the same name already exists.
@ -298,8 +302,8 @@ class DatabaseTest(unittest.TestCase):
def test_delete_product(self) -> None: def test_delete_product(self) -> None:
with self.db as db: with self.db as db:
product = db.create_product('Club Mate', 200, 200, True) product = db.create_product('Club Mate', 200, 200, False, True)
product2 = db.create_product('Flora Power Mate', 200, 200, False) product2 = db.create_product('Flora Power Mate', 200, 200, False, False)
self.assertEqual(2, len(db.list_products())) self.assertEqual(2, len(db.list_products()))
db.delete_product(product) db.delete_product(product)
@ -344,9 +348,9 @@ class DatabaseTest(unittest.TestCase):
db.deposit(user1, 1337) db.deposit(user1, 1337)
db.deposit(user2, 4242) db.deposit(user2, 4242)
db.deposit(user3, 1234) db.deposit(user3, 1234)
clubmate = db.create_product('Club Mate', 200, 200, True) clubmate = db.create_product('Club Mate', 200, 200, False, True)
florapowermate = db.create_product('Flora Power Mate', 150, 250, True) florapowermate = db.create_product('Flora Power Mate', 150, 250, False, True)
fritzmate = db.create_product('Fritz Mate', 200, 200, True) fritzmate = db.create_product('Fritz Mate', 200, 200, False, True)
# user1 is somewhat addicted to caffeine # user1 is somewhat addicted to caffeine
for _ in range(3): for _ in range(3):
@ -472,7 +476,7 @@ class DatabaseTest(unittest.TestCase):
admin: User = db.create_user('admin', 'supersecurepassword', 'admin@example.com', True, True) admin: User = db.create_user('admin', 'supersecurepassword', 'admin@example.com', True, True)
user: User = db.create_user('user', 'supersecurepassword', 'user@example.com', True, True) user: User = db.create_user('user', 'supersecurepassword', 'user@example.com', True, True)
product: Product = db.create_product('Flora Power Mate', 200, 200, True) product: Product = db.create_product('Flora Power Mate', 200, 200, False, True)
# Create some transactions # Create some transactions
db.change_user(user, agent=admin, db.change_user(user, agent=admin,
@ -561,8 +565,8 @@ class DatabaseTest(unittest.TestCase):
user2: User = db.create_user('user2', 'supersecurepassword', 'user2@example.com', True, False) user2: User = db.create_user('user2', 'supersecurepassword', 'user2@example.com', True, False)
user3: User = db.create_user('user3', 'supersecurepassword', 'user3@example.com', True, False) user3: User = db.create_user('user3', 'supersecurepassword', 'user3@example.com', True, False)
user4: User = db.create_user('user4', 'supersecurepassword', 'user4@example.com', True, False) user4: User = db.create_user('user4', 'supersecurepassword', 'user4@example.com', True, False)
flora: Product = db.create_product('Flora Power Mate', 200, 200, True) flora: Product = db.create_product('Flora Power Mate', 200, 200, False, True)
club: Product = db.create_product('Club Mate', 200, 200, False) club: Product = db.create_product('Club Mate', 200, 200, False, False)
# Create some transactions # Create some transactions
db.deposit(user1, 1337) db.deposit(user1, 1337)

View file

@ -40,7 +40,7 @@ class DatabaseTransaction(object):
class DatabaseWrapper(object): class DatabaseWrapper(object):
SCHEMA_VERSION = 5 SCHEMA_VERSION = 6
def __init__(self, filename: str) -> None: def __init__(self, filename: str) -> None:
self._filename: str = filename self._filename: str = filename
@ -87,6 +87,8 @@ class DatabaseWrapper(object):
migrate_schema_3_to_4(c) migrate_schema_3_to_4(c)
if from_version <= 4 and to_version >= 5: if from_version <= 4 and to_version >= 5:
migrate_schema_4_to_5(c) migrate_schema_4_to_5(c)
if from_version <= 5 and to_version >= 6:
migrate_schema_5_to_6(c)
def connect(self) -> None: def connect(self) -> None:
if self.is_connected(): if self.is_connected():

View file

@ -4,5 +4,4 @@ from matemat.interfacing.dispenser import Dispenser
class NullDispenser(Dispenser): class NullDispenser(Dispenser):
def dispense(self, product, amount): def dispense(self, product, amount):
# no-op return True
pass

View file

@ -198,9 +198,10 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
name = str(args.name) name = str(args.name)
price_member = parse_chf(str(args.pricemember)) price_member = parse_chf(str(args.pricemember))
price_non_member = parse_chf(str(args.pricenonmember)) price_non_member = parse_chf(str(args.pricenonmember))
custom_price = 'custom_price' in args
stockable = 'stockable' in args stockable = 'stockable' in args
# Create the user in the database # Create the user in the database
newproduct = db.create_product(name, price_member, price_non_member, stockable) newproduct = db.create_product(name, price_member, price_non_member, custom_price, stockable)
# If a new product image was uploaded, process it # If a new product image was uploaded, process it
image = files.image.file.read() if 'image' in files else None image = files.image.file.read() if 'image' in files else None
if image is not None and len(image) > 0: if image is not None and len(image) > 0:

View file

@ -26,8 +26,11 @@ def buy():
pid = int(str(request.params.pid)) pid = int(str(request.params.pid))
product = db.get_product(pid) product = db.get_product(pid)
if c.get_dispenser().dispense(product, 1): if c.get_dispenser().dispense(product, 1):
price = None
if 'price' in request.params:
price = int(str(request.params.price))
# Create a consumption entry for the (user, product) combination # Create a consumption entry for the (user, product) combination
db.increment_consumption(user, product) db.increment_consumption(user, product, price)
stock_provider = c.get_stock_provider() stock_provider = c.get_stock_provider()
if stock_provider.needs_update(): if stock_provider.needs_update():
stock_provider.update_stock(product, -1) stock_provider.update_stock(product, -1)

View file

@ -93,13 +93,14 @@ def handle_change(args: FormsDict, files: FormsDict, product: Product, db: Matem
name = str(args.name) name = str(args.name)
price_member = parse_chf(str(args.pricemember)) price_member = parse_chf(str(args.pricemember))
price_non_member = parse_chf(str(args.pricenonmember)) price_non_member = parse_chf(str(args.pricenonmember))
custom_price = 'custom_price' in args
stock = int(str(args.stock)) stock = int(str(args.stock))
stockable = 'stockable' in args stockable = 'stockable' in args
# Attempt to write the changes to the database # Attempt to write the changes to the database
try: try:
db.change_product(product, db.change_product(product,
name=name, price_member=price_member, price_non_member=price_non_member, name=name, price_member=price_member, price_non_member=price_non_member,
stock=stock, stockable=stockable) custom_price=custom_price, stock=stock, stockable=stockable)
stock_provider = get_stock_provider() stock_provider = get_stock_provider()
if stock_provider.needs_update() and product.stockable: if stock_provider.needs_update() and product.stockable:
stock_provider.set_stock(product, stock) stock_provider.set_stock(product, stock)

View file

@ -1,30 +0,0 @@
# Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
pkgname=matemat
pkgver=0.2.4
pkgrel=1
arch=('any')
pkgdesc='A soda machine stock-keeping webservice'
url='https://gitlab.com/s3lph/matemat'
licence=('MIT')
depends=(
'python'
'python-bottle'
'python-jinja'
'python-pillow'
'python-magic'
'file'
)
backup=(
'etc/matemat.conf'
)
install=$pkgname.install
package() {
cp -r ../matemat/* ../pkg/matemat/
}

View file

@ -1,31 +0,0 @@
post_install() {
if ! getent group matemat >/dev/null; then
groupadd --system matemat
fi
if ! getent passwd matemat >/dev/null; then
useradd --system --create-home --gid matemat --home-dir /var/lib/matemat --shell /usr/bin/nologin matemat
fi
chown matemat:matemat /var/lib/matemat
chmod 0750 /var/lib/matemat
ln -sf /var/lib/matemat/upload /usr/lib/matemat/static/upload
systemctl daemon-reload
}
pre_remove() {
systemctl stop matemat.service
userdel matemat
}
post_remove() {
systemctl daemon-reload
}

View file

@ -1,32 +0,0 @@
[Matemat]
# The IP address to listen on
Address=::
# The TCP port to listen on
Port=80
# The log level, one of NONE, DEBUG, INFO, WARNING, ERROR, CRITICAL
LogLevel=DEBUG
[Pagelets]
# Name of the Matemat instance, shown in the web app
InstanceName=Matemat
#
# Configure SMTP credentials
#
# SmtpFrom=matemat@example.com
# SmtpSubj=Matemat Receipt
# SmtpHost=exmaple.com
# SmtpPort=587
# SmtpUser=matemat@example.com
# SmtpPass=supersecurepassword
#
# Enable to allow users to receive receipts via email
#
# SmtpSendReceipts=1
# Add static HTTP headers in this section
# [HttpHeaders]

View file

@ -1,11 +0,0 @@
[Matemat]
StaticPath=/usr/lib/matemat/static
TemplatePath=/usr/lib/matemat/templates
LogTarget=stdout
[Pagelets]
UploadDir=/var/lib/matemat/upload
DatabaseFile=/var/lib/matemat/matemat.db

View file

@ -1,12 +0,0 @@
[Unit]
Description=matemat
After=networking.target
[Service]
ExecStart=/usr/bin/python -m matemat /etc/matemat.conf /usr/lib/matemat/matemat.conf
User=matemat
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target

View file

@ -1,5 +1,5 @@
Package: matemat Package: matemat
Version: 0.2.7 Version: 0.2.8
Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol> Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
Section: web Section: web
Priority: optional Priority: optional

View file

@ -84,14 +84,6 @@ def fetch_debian_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[s
return debian_url, debian_sha_url return debian_url, debian_sha_url
def fetch_arch_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
mybase: str = f'{base_url}/jobs/{job_ids["build_archlinux"]}/artifacts/raw'
arch_sha_url: str = f'{mybase}/package/archlinux/SHA256SUMS'
arch_filename: str = fetch_single_shafile(arch_sha_url)
arch_url: str = f'{mybase}/package/archlinux/{arch_filename}'
return arch_url, arch_sha_url
def main(): def main():
api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN') api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN')
release_tag: Optional[str] = os.getenv('CI_COMMIT_TAG') release_tag: Optional[str] = os.getenv('CI_COMMIT_TAG')
@ -125,7 +117,6 @@ def main():
wheel_url, wheel_sha_url = fetch_wheel_url(base_url, job_ids) wheel_url, wheel_sha_url = fetch_wheel_url(base_url, job_ids)
debian_url, debian_sha_url = fetch_debian_url(base_url, job_ids) debian_url, debian_sha_url = fetch_debian_url(base_url, job_ids)
arch_url, arch_sha_url = fetch_arch_url(base_url, job_ids)
augmented_changelog = f'''{changelog.strip()} augmented_changelog = f'''{changelog.strip()}
@ -133,7 +124,6 @@ def main():
- [Python Wheel]({wheel_url}) ([sha256]({wheel_sha_url})) - [Python Wheel]({wheel_url}) ([sha256]({wheel_sha_url}))
- [Debian Package]({debian_url}) ([sha256]({debian_sha_url})) - [Debian Package]({debian_url}) ([sha256]({debian_sha_url}))
- [Arch Linux Package]({arch_url}) ([sha256]({arch_sha_url}))
- Docker image: registry.gitlab.com/{project_name}:{release_tag}''' - Docker image: registry.gitlab.com/{project_name}:{release_tag}'''
post_body: str = json.dumps({'description': augmented_changelog}) post_body: str = json.dumps({'description': augmented_changelog})

View file

@ -91,18 +91,32 @@
color: blue; color: blue;
} }
#deposit-amount { #deposit-output {
grid-column-start: 1; grid-column-start: 1;
grid-column-end: 4; grid-column-end: 4;
text-align: right;
grid-row: 1; grid-row: 1;
font-size: 50px;
line-heigt: 100px;
padding: 10px 0;
font-family: monospace;
background: #ffffff; background: #ffffff;
} }
#deposit-title {
display: block;
font-family: sans-serif;
padding: 0 10px;
line-height: 30px;
font-size: 25px;
height: 30px;
}
#deposit-amount {
display: block;
text-align: right;
font-size: 50px;
line-height: 70px;
height: 70px;
padding: 0 10px;
font-family: monospace;
}
.numpad { .numpad {
background: #f0f0f0; background: #f0f0f0;
text-decoration: none; text-decoration: none;

View file

@ -4,25 +4,42 @@ Number.prototype.pad = function(size) {
return s; return s;
} }
let product_id = null;
let deposit = '0'; let deposit = '0';
let button = document.createElement('div'); let button = document.createElement('div');
let input = document.getElementById('deposit-wrapper'); let input = document.getElementById('deposit-wrapper');
let amount = document.getElementById('deposit-amount'); let amount = document.getElementById('deposit-amount');
let title = document.getElementById('deposit-title');
button.classList.add('thumblist-item'); button.classList.add('thumblist-item');
button.classList.add('fakelink'); button.classList.add('fakelink');
button.innerText = 'Deposit'; button.innerText = 'Deposit';
button.onclick = (ev) => { button.onclick = (ev) => {
product_id = null;
deposit = '0'; deposit = '0';
title.innerText = 'Deposit';
amount.innerText = (Math.floor(parseInt(deposit) / 100)) + '.' + (parseInt(deposit) % 100).pad(); amount.innerText = (Math.floor(parseInt(deposit) / 100)) + '.' + (parseInt(deposit) % 100).pad();
input.classList.add('show'); input.classList.add('show');
}; };
setup_custom_price = (pid, pname) => {
product_id = pid;
title.innerText = pname;
deposit = '0';
amount.innerText = (Math.floor(parseInt(deposit) / 100)) + '.' + (parseInt(deposit) % 100).pad();
input.classList.add('show');
};
deposit_key = (k) => { deposit_key = (k) => {
if (k == 'ok') { if (k == 'ok') {
window.location.href = '/deposit?n=' + parseInt(deposit); if (product_id === null) {
window.location.href = '/deposit?n=' + parseInt(deposit);
} else {
window.location.href = '/buy?pid=' + product_id + '&price=' + parseInt(deposit);
}
deposit = '0'; deposit = '0';
product_id = null;
input.classList.remove('show'); input.classList.remove('show');
} else if (k == 'del') { } else if (k == 'del') {
if (deposit == '0') { if (deposit == '0') {
product_id = null;
input.classList.remove('show'); input.classList.remove('show');
} }
deposit = deposit.substr(0, deposit.length - 1); deposit = deposit.substr(0, deposit.length - 1);

View file

@ -49,6 +49,9 @@
<label for="admin-newproduct-price-non-member">Non-member price: </label> <label for="admin-newproduct-price-non-member">Non-member price: </label>
CHF <input id="admin-newproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="0" /><br/> CHF <input id="admin-newproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="0" /><br/>
<label for="admin-custom-price"><abbr title="When 'Custom Price' is enabled, users choose the price to pay, but at least the prices given above">Custom Price</abbr>: </label>
<input id="admin-custom-price" type="checkbox" name="custom_price" /><br/>
<label for="admin-newproduct-stockable">Stockable: </label> <label for="admin-newproduct-stockable">Stockable: </label>
<input id="admin-newproduct-stockable" type="checkbox" name="stockable" checked="checked" /><br/> <input id="admin-newproduct-stockable" type="checkbox" name="stockable" checked="checked" /><br/>

View file

@ -20,6 +20,9 @@
<label for="modproduct-price-non-member">Non-member price: </label> <label for="modproduct-price-non-member">Non-member price: </label>
CHF <input id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" /><br/> CHF <input id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" /><br/>
<label for="modproduct-custom-price"><abbr title="When 'Custom Price' is enabled, users choose the price to pay, but at least the prices given above">Custom Price</abbr>: </label>
<input id="modproduct-custom-price" type="checkbox" name="custom_price" {% if product.custom_price %} checked="checked" {% endif %} /><br/>
<label for="modproduct-stockable">Stockable: </label> <label for="modproduct-stockable">Stockable: </label>
<input id="modproduct-stockable" type="checkbox" name="stockable" {% if product.stockable %} checked="checked" {% endif %} /><br/> <input id="modproduct-stockable" type="checkbox" name="stockable" {% if product.stockable %} checked="checked" {% endif %} /><br/>

View file

@ -26,7 +26,10 @@
</div> </div>
<div id="deposit-wrapper"> <div id="deposit-wrapper">
<div id="deposit-input"> <div id="deposit-input">
<span id="deposit-amount">0.00</span> <div id="deposit-output">
<span id="deposit-title"></span>
<span id="deposit-amount">0.00</span>
</div>
{% for i in [('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'), ('8', '8'), ('9', '9'), ('del', '✗'), ('0', '0'), ('ok', '✓')] %} {% for i in [('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'), ('8', '8'), ('9', '9'), ('del', '✗'), ('0', '0'), ('ok', '✓')] %}
<div class="numpad" id="numpad-{{ i.0 }}" onclick="deposit_key('{{ i.0 }}');">{{ i.1 }}</div> <div class="numpad" id="numpad-{{ i.0 }}" onclick="deposit_key('{{ i.0 }}');">{{ i.1 }}</div>
{% endfor %} {% endfor %}
@ -38,8 +41,15 @@
{% for product in products %} {% for product in products %}
{# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #} {# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #}
<div class="thumblist-item"> <div class="thumblist-item">
{% if product.custom_price %}
<a onclick="setup_custom_price({{ product.id }}, '{{ product.name}}');">
{% else %}
<a href="/buy?pid={{ product.id }}"> <a href="/buy?pid={{ product.id }}">
{% endif %}
<span class="thumblist-title">{{ product.name }}</span> <span class="thumblist-title">{{ product.name }}</span>
{% if product.custom_price %}
<span class="thumblist-detail">Custom Price</span><br/>
{% else %}
<span class="thumblist-detail">Price: <span class="thumblist-detail">Price:
{% if authuser.is_member %} {% if authuser.is_member %}
{{ product.price_member|chf }} {{ product.price_member|chf }}
@ -47,6 +57,7 @@
{{ product.price_non_member|chf }} {{ product.price_non_member|chf }}
{% endif %} {% endif %}
</span><br/> </span><br/>
{% endif %}
<div class="imgcontainer"> <div class="imgcontainer">
<img src="/static/upload/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}"/> <img src="/static/upload/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}"/>
{% set pstock = stock.get_stock(product) %} {% set pstock = stock.get_stock(product) %}