Prepare 0.2.8 release
This commit is contained in:
parent
ea32a33cef
commit
ddcdc0f816
25 changed files with 219 additions and 194 deletions
|
@ -95,28 +95,6 @@ build_debian:
|
|||
only:
|
||||
- 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.xz > SHA256SUMS
|
||||
artifacts:
|
||||
paths:
|
||||
- "package/archlinux/*.pkg.tar.xz"
|
||||
- package/archlinux/SHA256SUMS
|
||||
only:
|
||||
- tags
|
||||
|
||||
|
||||
release:
|
||||
stage: deploy
|
||||
|
|
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,5 +1,20 @@
|
|||
# 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 -->
|
||||
## Version 0.2.7
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
|
||||
__version__ = '0.2.7'
|
||||
__version__ = '0.2.8'
|
||||
|
|
|
@ -354,11 +354,11 @@ class MatematDatabase(object):
|
|||
products: List[Product] = []
|
||||
with self.db.transaction(exclusive=False) as c:
|
||||
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
|
||||
'''):
|
||||
product_id, name, price_member, price_external, stock, stockable = row
|
||||
products.append(Product(product_id, name, price_member, price_external, stockable, stock))
|
||||
product_id, name, price_member, price_external, custom_price, stock, stockable = row
|
||||
products.append(Product(product_id, name, price_member, price_external, custom_price, stockable, stock))
|
||||
return products
|
||||
|
||||
def get_product(self, pid: int) -> Product:
|
||||
|
@ -369,7 +369,7 @@ class MatematDatabase(object):
|
|||
with self.db.transaction(exclusive=False) as c:
|
||||
# Fetch all values to construct the product
|
||||
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
|
||||
WHERE product_id = ?''',
|
||||
[pid])
|
||||
|
@ -377,15 +377,16 @@ class MatematDatabase(object):
|
|||
if row is None:
|
||||
raise ValueError(f'No product with product ID {pid} exists.')
|
||||
# Unpack the row and construct the product
|
||||
product_id, name, price_member, price_non_member, stock, stockable = row
|
||||
return Product(product_id, name, price_member, price_non_member, stockable, stock)
|
||||
product_id, name, price_member, price_non_member, custom_price, stock, stockable = row
|
||||
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.
|
||||
:param name: Name of the product.
|
||||
:param price_member: Price of the product for 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.
|
||||
:return: A Product object representing the created product.
|
||||
:raises ValueError: If a product with the same name already exists.
|
||||
|
@ -396,17 +397,18 @@ class MatematDatabase(object):
|
|||
if c.fetchone() is not None:
|
||||
raise ValueError(f'A product with the name \'{name}\' already exists.')
|
||||
c.execute('''
|
||||
INSERT INTO products (name, price_member, price_non_member, stock, stockable)
|
||||
VALUES (:name, :price_member, :price_non_member, 0, :stockable)
|
||||
INSERT INTO products (name, price_member, price_non_member, custom_price, stock, stockable)
|
||||
VALUES (:name, :price_member, :price_non_member, :custom_price, 0, :stockable)
|
||||
''', {
|
||||
'name': name,
|
||||
'price_member': price_member,
|
||||
'price_non_member': price_non_member,
|
||||
'custom_price': custom_price,
|
||||
'stockable': stockable
|
||||
})
|
||||
c.execute('SELECT last_insert_rowid()')
|
||||
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:
|
||||
"""
|
||||
|
@ -422,6 +424,7 @@ class MatematDatabase(object):
|
|||
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_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
|
||||
stockable: bool = kwargs['stockable'] if 'stockable' in kwargs else product.stockable
|
||||
with self.db.transaction() as c:
|
||||
|
@ -431,6 +434,7 @@ class MatematDatabase(object):
|
|||
name = :name,
|
||||
price_member = :price_member,
|
||||
price_non_member = :price_non_member,
|
||||
custom_price = :custom_price,
|
||||
stock = :stock,
|
||||
stockable = :stockable
|
||||
WHERE product_id = :product_id
|
||||
|
@ -439,6 +443,7 @@ class MatematDatabase(object):
|
|||
'name': name,
|
||||
'price_member': price_member,
|
||||
'price_non_member': price_non_member,
|
||||
'custom_price': custom_price,
|
||||
'stock': stock,
|
||||
'stockable': stockable
|
||||
})
|
||||
|
@ -450,6 +455,7 @@ class MatematDatabase(object):
|
|||
product.name = name
|
||||
product.price_member = price_member
|
||||
product.price_non_member = price_non_member
|
||||
product.custom_price = custom_price
|
||||
product.stock = stock
|
||||
product.stockable = stockable
|
||||
|
||||
|
@ -469,15 +475,18 @@ class MatematDatabase(object):
|
|||
raise DatabaseConsistencyError(
|
||||
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.
|
||||
|
||||
:param user: The user buying a product.
|
||||
: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.
|
||||
"""
|
||||
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:
|
||||
c.execute('''
|
||||
INSERT INTO transactions (user_id, value, old_balance)
|
||||
|
|
|
@ -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
|
||||
''')
|
||||
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;
|
||||
''')
|
||||
|
|
|
@ -8,17 +8,19 @@ class Product:
|
|||
:param name: The product's name.
|
||||
: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 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 stockable: Whether this product is stockable.
|
||||
"""
|
||||
|
||||
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:
|
||||
self.id: int = _id
|
||||
self.name: str = name
|
||||
self.price_member: int = price_member
|
||||
self.price_non_member: int = price_non_member
|
||||
self.custom_price: bool = custom_price
|
||||
self.stock: int = stock
|
||||
self.stockable: bool = stockable
|
||||
|
||||
|
@ -29,8 +31,9 @@ class Product:
|
|||
self.name == other.name and \
|
||||
self.price_member == other.price_member and \
|
||||
self.price_non_member == other.price_non_member and \
|
||||
self.custom_price == other.custom_price and \
|
||||
self.stock == other.stock and \
|
||||
self.stockable == other.stockable
|
||||
|
||||
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))
|
||||
|
|
|
@ -333,3 +333,83 @@ SCHEMAS[5] = [
|
|||
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
|
||||
);
|
||||
''']
|
||||
|
|
|
@ -220,7 +220,7 @@ class DatabaseTest(unittest.TestCase):
|
|||
def test_create_product(self) -> None:
|
||||
with self.db as db:
|
||||
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")
|
||||
row = c.fetchone()
|
||||
self.assertEqual('Club Mate', row[1])
|
||||
|
@ -228,13 +228,14 @@ class DatabaseTest(unittest.TestCase):
|
|||
self.assertEqual(1, row[3])
|
||||
self.assertEqual(200, row[4])
|
||||
self.assertEqual(200, row[5])
|
||||
self.assertEqual(1, row[6])
|
||||
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:
|
||||
with self.db as db:
|
||||
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)
|
||||
self.assertEqual('Club Mate', product.name)
|
||||
self.assertEqual(150, product.price_member)
|
||||
|
@ -248,9 +249,9 @@ class DatabaseTest(unittest.TestCase):
|
|||
# Test empty list
|
||||
products = db.list_products()
|
||||
self.assertEqual(0, len(products))
|
||||
db.create_product('Club Mate', 200, 200, True)
|
||||
db.create_product('Flora Power Mate', 200, 200, False)
|
||||
db.create_product('Fritz Mate', 200, 250, True)
|
||||
db.create_product('Club Mate', 200, 200, False, True)
|
||||
db.create_product('Flora Power Mate', 200, 200, False, False)
|
||||
db.create_product('Fritz Mate', 200, 250, False, True)
|
||||
products = db.list_products()
|
||||
self.assertEqual(3, len(products))
|
||||
productcheck = {}
|
||||
|
@ -272,13 +273,14 @@ class DatabaseTest(unittest.TestCase):
|
|||
|
||||
def test_change_product(self) -> None:
|
||||
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,
|
||||
stock=None, stockable=False)
|
||||
custom_price=True, stock=None, stockable=False)
|
||||
# Changes must be reflected in the passed object
|
||||
self.assertEqual('Flora Power Mate', product.name)
|
||||
self.assertEqual(150, product.price_member)
|
||||
self.assertEqual(250, product.price_non_member)
|
||||
self.assertEqual(True, product.custom_price)
|
||||
self.assertEqual(None, product.stock)
|
||||
self.assertEqual(False, product.stockable)
|
||||
# Changes must be reflected in the database
|
||||
|
@ -286,11 +288,13 @@ class DatabaseTest(unittest.TestCase):
|
|||
self.assertEqual('Flora Power Mate', checkproduct.name)
|
||||
self.assertEqual(150, checkproduct.price_member)
|
||||
self.assertEqual(250, checkproduct.price_non_member)
|
||||
self.assertEqual(True, checkproduct.custom_price)
|
||||
self.assertEqual(None, checkproduct.stock)
|
||||
self.assertEqual(False, checkproduct.stockable)
|
||||
product.id = -1
|
||||
with self.assertRaises(DatabaseConsistencyError):
|
||||
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'
|
||||
with self.assertRaises(DatabaseConsistencyError):
|
||||
# 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:
|
||||
with self.db as db:
|
||||
product = db.create_product('Club Mate', 200, 200, True)
|
||||
product2 = db.create_product('Flora Power Mate', 200, 200, False)
|
||||
product = db.create_product('Club Mate', 200, 200, False, True)
|
||||
product2 = db.create_product('Flora Power Mate', 200, 200, False, False)
|
||||
|
||||
self.assertEqual(2, len(db.list_products()))
|
||||
db.delete_product(product)
|
||||
|
@ -344,9 +348,9 @@ class DatabaseTest(unittest.TestCase):
|
|||
db.deposit(user1, 1337)
|
||||
db.deposit(user2, 4242)
|
||||
db.deposit(user3, 1234)
|
||||
clubmate = db.create_product('Club Mate', 200, 200, True)
|
||||
florapowermate = db.create_product('Flora Power Mate', 150, 250, True)
|
||||
fritzmate = db.create_product('Fritz Mate', 200, 200, True)
|
||||
clubmate = db.create_product('Club Mate', 200, 200, False , True)
|
||||
florapowermate = db.create_product('Flora Power Mate', 150, 250, False, True)
|
||||
fritzmate = db.create_product('Fritz Mate', 200, 200, False, True)
|
||||
|
||||
# user1 is somewhat addicted to caffeine
|
||||
for _ in range(3):
|
||||
|
@ -472,7 +476,7 @@ class DatabaseTest(unittest.TestCase):
|
|||
|
||||
admin: User = db.create_user('admin', 'supersecurepassword', 'admin@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
|
||||
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)
|
||||
user3: User = db.create_user('user3', 'supersecurepassword', 'user3@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)
|
||||
club: Product = db.create_product('Club Mate', 200, 200, False)
|
||||
flora: Product = db.create_product('Flora Power Mate', 200, 200, False, True)
|
||||
club: Product = db.create_product('Club Mate', 200, 200, False, False)
|
||||
|
||||
# Create some transactions
|
||||
db.deposit(user1, 1337)
|
||||
|
|
|
@ -40,7 +40,7 @@ class DatabaseTransaction(object):
|
|||
|
||||
class DatabaseWrapper(object):
|
||||
|
||||
SCHEMA_VERSION = 5
|
||||
SCHEMA_VERSION = 6
|
||||
|
||||
def __init__(self, filename: str) -> None:
|
||||
self._filename: str = filename
|
||||
|
@ -87,6 +87,8 @@ class DatabaseWrapper(object):
|
|||
migrate_schema_3_to_4(c)
|
||||
if from_version <= 4 and to_version >= 5:
|
||||
migrate_schema_4_to_5(c)
|
||||
if from_version <= 5 and to_version >= 6:
|
||||
migrate_schema_5_to_6(c)
|
||||
|
||||
def connect(self) -> None:
|
||||
if self.is_connected():
|
||||
|
|
|
@ -4,5 +4,4 @@ from matemat.interfacing.dispenser import Dispenser
|
|||
class NullDispenser(Dispenser):
|
||||
|
||||
def dispense(self, product, amount):
|
||||
# no-op
|
||||
pass
|
||||
return True
|
||||
|
|
|
@ -198,9 +198,10 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
|
|||
name = str(args.name)
|
||||
price_member = parse_chf(str(args.pricemember))
|
||||
price_non_member = parse_chf(str(args.pricenonmember))
|
||||
custom_price = 'custom_price' in args
|
||||
stockable = 'stockable' in args
|
||||
# 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
|
||||
image = files.image.file.read() if 'image' in files else None
|
||||
if image is not None and len(image) > 0:
|
||||
|
|
|
@ -26,8 +26,11 @@ def buy():
|
|||
pid = int(str(request.params.pid))
|
||||
product = db.get_product(pid)
|
||||
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
|
||||
db.increment_consumption(user, product)
|
||||
db.increment_consumption(user, product, price)
|
||||
stock_provider = c.get_stock_provider()
|
||||
if stock_provider.needs_update():
|
||||
stock_provider.update_stock(product, -1)
|
||||
|
|
|
@ -93,13 +93,14 @@ def handle_change(args: FormsDict, files: FormsDict, product: Product, db: Matem
|
|||
name = str(args.name)
|
||||
price_member = parse_chf(str(args.pricemember))
|
||||
price_non_member = parse_chf(str(args.pricenonmember))
|
||||
custom_price = 'custom_price' in args
|
||||
stock = int(str(args.stock))
|
||||
stockable = 'stockable' in args
|
||||
# Attempt to write the changes to the database
|
||||
try:
|
||||
db.change_product(product,
|
||||
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()
|
||||
if stock_provider.needs_update() and product.stockable:
|
||||
stock_provider.set_stock(product, stock)
|
||||
|
|
|
@ -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/
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
|
@ -1,5 +1,5 @@
|
|||
Package: matemat
|
||||
Version: 0.2.7
|
||||
Version: 0.2.8
|
||||
Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
||||
Section: web
|
||||
Priority: optional
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
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():
|
||||
api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN')
|
||||
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)
|
||||
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()}
|
||||
|
||||
|
@ -133,7 +124,6 @@ def main():
|
|||
|
||||
- [Python Wheel]({wheel_url}) ([sha256]({wheel_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}'''
|
||||
|
||||
post_body: str = json.dumps({'description': augmented_changelog})
|
||||
|
|
|
@ -91,18 +91,32 @@
|
|||
color: blue;
|
||||
}
|
||||
|
||||
#deposit-amount {
|
||||
#deposit-output {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
text-align: right;
|
||||
grid-row: 1;
|
||||
font-size: 50px;
|
||||
line-heigt: 100px;
|
||||
padding: 10px 0;
|
||||
font-family: monospace;
|
||||
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 {
|
||||
background: #f0f0f0;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -4,25 +4,42 @@ Number.prototype.pad = function(size) {
|
|||
return s;
|
||||
}
|
||||
|
||||
let product_id = null;
|
||||
let deposit = '0';
|
||||
let button = document.createElement('div');
|
||||
let input = document.getElementById('deposit-wrapper');
|
||||
let amount = document.getElementById('deposit-amount');
|
||||
let title = document.getElementById('deposit-title');
|
||||
button.classList.add('thumblist-item');
|
||||
button.classList.add('fakelink');
|
||||
button.innerText = 'Deposit';
|
||||
button.onclick = (ev) => {
|
||||
product_id = null;
|
||||
deposit = '0';
|
||||
title.innerText = 'Deposit';
|
||||
amount.innerText = (Math.floor(parseInt(deposit) / 100)) + '.' + (parseInt(deposit) % 100).pad();
|
||||
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) => {
|
||||
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';
|
||||
product_id = null;
|
||||
input.classList.remove('show');
|
||||
} else if (k == 'del') {
|
||||
if (deposit == '0') {
|
||||
product_id = null;
|
||||
input.classList.remove('show');
|
||||
}
|
||||
deposit = deposit.substr(0, deposit.length - 1);
|
||||
|
|
|
@ -49,6 +49,9 @@
|
|||
<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/>
|
||||
|
||||
<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>
|
||||
<input id="admin-newproduct-stockable" type="checkbox" name="stockable" checked="checked" /><br/>
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
<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/>
|
||||
|
||||
<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>
|
||||
<input id="modproduct-stockable" type="checkbox" name="stockable" {% if product.stockable %} checked="checked" {% endif %} /><br/>
|
||||
|
||||
|
|
|
@ -26,7 +26,10 @@
|
|||
</div>
|
||||
<div id="deposit-wrapper">
|
||||
<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', '✓')] %}
|
||||
<div class="numpad" id="numpad-{{ i.0 }}" onclick="deposit_key('{{ i.0 }}');">{{ i.1 }}</div>
|
||||
{% endfor %}
|
||||
|
@ -38,8 +41,15 @@
|
|||
{% for product in products %}
|
||||
{# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #}
|
||||
<div class="thumblist-item">
|
||||
{% if product.custom_price %}
|
||||
<a onclick="setup_custom_price({{ product.id }}, '{{ product.name}}');">
|
||||
{% else %}
|
||||
<a href="/buy?pid={{ product.id }}">
|
||||
{% endif %}
|
||||
<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:
|
||||
{% if authuser.is_member %}
|
||||
{{ product.price_member|chf }}
|
||||
|
@ -47,6 +57,7 @@
|
|||
{{ product.price_non_member|chf }}
|
||||
{% endif %}
|
||||
</span><br/>
|
||||
{% endif %}
|
||||
<div class="imgcontainer">
|
||||
<img src="/static/upload/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}"/>
|
||||
{% set pstock = stock.get_stock(product) %}
|
||||
|
|
Loading…
Reference in a new issue