forked from s3lph/matemat
Compare commits
9 commits
transactio
...
main
Author | SHA1 | Date | |
---|---|---|---|
bfc503c5d3 | |||
d41484e69a | |||
c8243fd9d5 | |||
677c1e681b | |||
d54aa2bc57 | |||
1e561fd9cd | |||
97df130768 | |||
1b45a21210 | |||
abd70b9cc6 |
25 changed files with 343 additions and 162 deletions
52
.forgejo/workflows/package.yml
Normal file
52
.forgejo/workflows/package.yml
Normal file
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
|
||||
build_wheel:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- name: Build Python wheel
|
||||
run: |
|
||||
apt update; apt install -y python3-pip
|
||||
pip3 install --break-system-packages -e .[test]
|
||||
python3 setup.py egg_info bdist_wheel
|
||||
- uses: https://git.kabelsalat.ch/s3lph/forgejo-action-wheel-package-upload@v3
|
||||
with:
|
||||
username: ${{ secrets.API_USERNAME }}
|
||||
password: ${{ secrets.API_PASSWORD }}
|
||||
|
||||
build_debian:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- name: Prepare package
|
||||
run: |
|
||||
# The Python package name provided by the python3-magic Debian package is "python-magic" rather than "file-magic".
|
||||
sed -re 's/file-magic/python-magic/' -i setup.py
|
||||
cp -r static/ package/debian/matemat/usr/lib/matemat/static/
|
||||
cp -r templates/ package/debian/matemat/usr/lib/matemat/templates/
|
||||
mkdir -p package/debian/matemat/var/lib/matemat/themes/
|
||||
cp -r themes/ package/debian/matemat/usr/lib/matemat/themes/
|
||||
|
||||
- uses: https://git.kabelsalat.ch/s3lph/forgejo-action-python-debian-package@v5
|
||||
with:
|
||||
python_module: matemat
|
||||
package_name: matemat
|
||||
package_root: package/debian/matemat
|
||||
package_output_path: package/debian
|
||||
pre_package_hook: |
|
||||
mv matemat/usr/bin/matemat matemat/usr/lib/matemat/matemat
|
||||
rm -rf matemat/usr/bin
|
||||
chmod +x matemat/usr/lib/matemat/matemat
|
||||
|
||||
- uses: https://git.kabelsalat.ch/s3lph/forgejo-action-debian-package-upload@v2
|
||||
with:
|
||||
username: ${{ secrets.API_USERNAME }}
|
||||
password: ${{ secrets.API_PASSWORD }}
|
||||
deb: "package/debian/*.deb"
|
27
.forgejo/workflows/test.yml
Normal file
27
.forgejo/workflows/test.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- name: test
|
||||
run: |
|
||||
apt update; apt install --yes python3-pip
|
||||
pip3 install --break-system-packages -e .[test]
|
||||
python3 -m coverage run --rcfile=setup.cfg -m unittest discover matemat
|
||||
python3 -m coverage combine
|
||||
python3 -m coverage report --rcfile=setup.cfg
|
||||
|
||||
codestyle:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- name: codestyle
|
||||
run: |
|
||||
apt update; apt install --yes python3-pip
|
||||
pip3 install --break-system-packages -e .[test]
|
||||
pycodestyle matemat
|
|
@ -1,91 +0,0 @@
|
|||
---
|
||||
|
||||
steps:
|
||||
|
||||
test:
|
||||
image: python:3.11-bookworm
|
||||
group: test
|
||||
commands:
|
||||
- pip3 install -e .[test]
|
||||
- python3 -m coverage run --rcfile=setup.cfg -m unittest discover matemat
|
||||
- python3 -m coverage combine
|
||||
- python3 -m coverage report --rcfile=setup.cfg
|
||||
|
||||
codestyle:
|
||||
image: python:3.11-bookworm
|
||||
group: test
|
||||
commands:
|
||||
- pip3 install -e .[test]
|
||||
- pycodestyle matemat
|
||||
|
||||
build_wheel:
|
||||
image: python:3.11-bookworm
|
||||
group: package
|
||||
when:
|
||||
- event: tag
|
||||
secrets:
|
||||
- GITEA_API_REPOSITORY_PYPI
|
||||
- GITEA_API_USERNAME
|
||||
- GITEA_API_PASSWORD
|
||||
commands:
|
||||
- pip3 install -e .[test]
|
||||
- python3 setup.py egg_info bdist_wheel
|
||||
- |
|
||||
cat > ~/.pypirc <<EOF
|
||||
[distutils]
|
||||
index-servers = gitea
|
||||
|
||||
[gitea]
|
||||
repository = $${GITEA_API_REPOSITORY_PYPI}
|
||||
username = $${GITEA_API_USERNAME}
|
||||
password = $${GITEA_API_PASSWORD}
|
||||
EOF
|
||||
- python3 -m twine upload --repository gitea dist/*.whl
|
||||
|
||||
build_debian:
|
||||
image: python:3.11-bookworm
|
||||
group: package
|
||||
when:
|
||||
- event: tag
|
||||
secrets:
|
||||
- GITEA_API_REPOSITORY_DEB
|
||||
- GITEA_API_USERNAME
|
||||
- GITEA_API_PASSWORD
|
||||
commands:
|
||||
- apt update; apt install -y lintian rsync sudo curl
|
||||
- export MATEMAT_VERSION=$(python -c 'import matemat; print(matemat.__version__)')
|
||||
# The Python package name provided by the python3-magic Debian package is "python-magic" rather than "file-magic".
|
||||
- sed -re 's/file-magic/python-magic/' -i setup.py
|
||||
- |
|
||||
(for version in "$(cat CHANGELOG.md | grep '<!-- BEGIN CHANGES' | cut -d ' ' -f 4)"; do
|
||||
echo "matemat ($${version}-1) stable; urgency=medium\n"
|
||||
cat CHANGELOG.md | grep -A 1000 "<"'!'"-- BEGIN CHANGES $${version} -->" | grep -B 1000 "<"'!'"-- END CHANGES $${version} -->" | tail -n +2 | head -n -1 | sed -re 's/^-/ */g'
|
||||
echo "\n -- s3lph@kabelsalat.ch $(date -R)\n"
|
||||
done) > package/debian/matemat/usr/share/doc/matemat/changelog
|
||||
- gzip -9n package/debian/matemat/usr/share/doc/matemat/changelog
|
||||
- cp -r static/ package/debian/matemat/usr/lib/matemat/static/
|
||||
- cp -r templates/ package/debian/matemat/usr/lib/matemat/templates/
|
||||
- mkdir -p package/debian/matemat/var/lib/matemat/themes/
|
||||
- cp -r themes/ package/debian/matemat/usr/lib/matemat/themes/
|
||||
- python3 setup.py egg_info install --root=package/debian/matemat/ --prefix=/usr --optimize=1
|
||||
- cd package/debian
|
||||
- mkdir -p matemat/usr/lib/python3/dist-packages/
|
||||
- rsync -a matemat/usr/lib/python3.11/site-packages/ matemat/usr/lib/python3/dist-packages/
|
||||
- rm -rf matemat/usr/lib/python3.11/
|
||||
- find matemat/usr/lib/python3/dist-packages -name __pycache__ -exec rm -r {} \; 2>/dev/null || true
|
||||
- find matemat/usr/lib/python3/dist-packages -name '*.pyc' -exec rm {} \;
|
||||
- mv matemat/usr/bin/matemat matemat/usr/lib/matemat/matemat
|
||||
- rm -rf matemat/usr/bin
|
||||
- sed -re 's$#!/usr/local/bin/python3$#!/usr/bin/python3$' -i matemat/usr/lib/matemat/matemat
|
||||
- find matemat -type f -exec chmod 0644 {} \;
|
||||
- find matemat -type d -exec chmod 755 {} \;
|
||||
- chmod +x matemat/usr/lib/matemat/matemat matemat/DEBIAN/postinst matemat/DEBIAN/prerm matemat/DEBIAN/postrm
|
||||
- sed -re "s/__VERSION__/$${MATEMAT_VERSION}-1/g" -i matemat/DEBIAN/control
|
||||
- dpkg-deb --build matemat
|
||||
- mv matemat.deb "matemat_$${MATEMAT_VERSION}-1_all.deb"
|
||||
- sudo -u nobody lintian "matemat_$${MATEMAT_VERSION}-1_all.deb" || true
|
||||
- >-
|
||||
curl
|
||||
--user "$${GITEA_API_USERNAME}:$${GITEA_API_PASSWORD}"
|
||||
--upload-file "matemat_$${EXPORTER_VERSION}-1_all.deb"
|
||||
$${GITEA_API_REPOSITORY_DEB}
|
66
CHANGELOG.md
66
CHANGELOG.md
|
@ -1,5 +1,71 @@
|
|||
# Matemat Changelog
|
||||
|
||||
<!-- BEGIN RELEASE v0.3.12 -->
|
||||
## Version 0.3.12
|
||||
|
||||
Sort products
|
||||
|
||||
### Changes
|
||||
|
||||
<!-- BEGIN CHANGES 0.3.12 -->
|
||||
- Sort products in the list
|
||||
<!-- END CHANGES 0.3.12 -->
|
||||
|
||||
<!-- END RELEASE v0.3.12 -->
|
||||
|
||||
<!-- BEGIN RELEASE v0.3.11 -->
|
||||
## Version 0.3.11
|
||||
|
||||
Improve auto-logout
|
||||
|
||||
### Changes
|
||||
|
||||
<!-- BEGIN CHANGES 0.3.11 -->
|
||||
- Show purchase overlay after logout
|
||||
- Fix state of auto-logout checkbox after changing user settings
|
||||
<!-- END CHANGES 0.3.11 -->
|
||||
|
||||
<!-- END RELEASE v0.3.11 -->
|
||||
|
||||
<!-- BEGIN RELEASE v0.3.10 -->
|
||||
## Version 0.3.10
|
||||
|
||||
Add option to log out users automatically after completing a purchase
|
||||
|
||||
### Changes
|
||||
|
||||
<!-- BEGIN CHANGES 0.3.10 -->
|
||||
- Add option to log out users automatically after completing a purchase
|
||||
<!-- END CHANGES 0.3.10 -->
|
||||
|
||||
<!-- END RELEASE v0.3.10 -->
|
||||
|
||||
<!-- BEGIN RELEASE v0.3.9 -->
|
||||
## Version 0.3.9
|
||||
|
||||
Improve UX on small touchscreens
|
||||
|
||||
### Changes
|
||||
|
||||
<!-- BEGIN CHANGES 0.3.9 -->
|
||||
- Improve link sizes for touchscreens
|
||||
<!-- END CHANGES 0.3.9 -->
|
||||
|
||||
<!-- END RELEASE v0.3.9 -->
|
||||
|
||||
<!-- BEGIN RELEASE v0.3.8 -->
|
||||
## Version 0.3.8
|
||||
|
||||
Migrate from Woodpecker CI to Forgejo Actions
|
||||
|
||||
### Changes
|
||||
|
||||
<!-- BEGIN CHANGES 0.3.8 -->
|
||||
- Migrate from Woodpecker CI to Forgejo Actions
|
||||
<!-- END CHANGES 0.3.8 -->
|
||||
|
||||
<!-- END RELEASE v0.3.8 -->
|
||||
|
||||
<!-- BEGIN RELEASE v0.3.7 -->
|
||||
## Version 0.3.7
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# Matemat
|
||||
|
||||
[![status-badge](https://woodpecker.kabelsalat.ch/api/badges/80/status.svg)](https://woodpecker.kabelsalat.ch/repos/80)
|
||||
|
||||
A web service for automated stock-keeping of a soda machine written in Python.
|
||||
It provides a touch-input-friendly user interface (as most input happens through the
|
||||
soda machine's touch screen).
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
|
||||
__version__ = '0.3.7'
|
||||
__version__ = '0.3.12'
|
||||
|
|
|
@ -84,7 +84,7 @@ class MatematDatabase(object):
|
|||
users: List[User] = []
|
||||
with self.db.transaction(exclusive=False) as c:
|
||||
for row in c.execute('''
|
||||
SELECT user_id, username, email, is_admin, is_member, balance, receipt_pref
|
||||
SELECT user_id, username, email, is_admin, is_member, balance, receipt_pref, logout_after_purchase
|
||||
FROM users
|
||||
WHERE touchkey IS NOT NULL OR NOT :must_have_touchkey
|
||||
ORDER BY username COLLATE NOCASE ASC
|
||||
|
@ -92,12 +92,13 @@ class MatematDatabase(object):
|
|||
'must_have_touchkey': with_touchkey
|
||||
}):
|
||||
# Decompose each row and put the values into a User object
|
||||
user_id, username, email, is_admin, is_member, balance, receipt_p = row
|
||||
user_id, username, email, is_admin, is_member, balance, receipt_p, logout_after_purchase = row
|
||||
try:
|
||||
receipt_pref: ReceiptPreference = ReceiptPreference(receipt_p)
|
||||
except ValueError:
|
||||
raise DatabaseConsistencyError(f'{receipt_p} is not a valid ReceiptPreference')
|
||||
users.append(User(user_id, username, balance, email, is_admin, is_member, receipt_pref))
|
||||
users.append(User(user_id, username, balance, email, is_admin, is_member, receipt_pref,
|
||||
logout_after_purchase))
|
||||
return users
|
||||
|
||||
def get_user(self, uid: int) -> User:
|
||||
|
@ -108,7 +109,7 @@ class MatematDatabase(object):
|
|||
with self.db.transaction(exclusive=False) as c:
|
||||
# Fetch all values to construct the user
|
||||
c.execute('''
|
||||
SELECT user_id, username, email, is_admin, is_member, balance, receipt_pref
|
||||
SELECT user_id, username, email, is_admin, is_member, balance, receipt_pref, logout_after_purchase
|
||||
FROM users
|
||||
WHERE user_id = ?
|
||||
''',
|
||||
|
@ -117,19 +118,20 @@ class MatematDatabase(object):
|
|||
if row is None:
|
||||
raise ValueError(f'No user with user ID {uid} exists.')
|
||||
# Unpack the row and construct the user
|
||||
user_id, username, email, is_admin, is_member, balance, receipt_p = row
|
||||
user_id, username, email, is_admin, is_member, balance, receipt_p, logout_after_purchase = row
|
||||
try:
|
||||
receipt_pref: ReceiptPreference = ReceiptPreference(receipt_p)
|
||||
except ValueError:
|
||||
raise DatabaseConsistencyError(f'{receipt_p} is not a valid ReceiptPreference')
|
||||
return User(user_id, username, balance, email, is_admin, is_member, receipt_pref)
|
||||
return User(user_id, username, balance, email, is_admin, is_member, receipt_pref, logout_after_purchase)
|
||||
|
||||
def create_user(self,
|
||||
username: str,
|
||||
password: str,
|
||||
email: Optional[str] = None,
|
||||
admin: bool = False,
|
||||
member: bool = True) -> User:
|
||||
member: bool = True,
|
||||
logout_after_purchase: bool = False) -> User:
|
||||
"""
|
||||
Create a new user.
|
||||
:param username: The name of the new user.
|
||||
|
@ -137,6 +139,7 @@ class MatematDatabase(object):
|
|||
:param email: The user's email address, defaults to None.
|
||||
:param admin: Whether the user is an administrator, defaults to False.
|
||||
:param member: Whether the user is a member, defaults to True.
|
||||
:param logout_after_purchase: Whether the user should be logged out after completing a purchase.
|
||||
:return: A User object representing the created user.
|
||||
:raises ValueError: If a user with the same name already exists.
|
||||
"""
|
||||
|
@ -150,14 +153,17 @@ class MatematDatabase(object):
|
|||
raise ValueError(f'A user with the name \'{username}\' already exists.')
|
||||
# Insert the user into the database.
|
||||
c.execute('''
|
||||
INSERT INTO users (username, email, password, balance, is_admin, is_member, lastchange, created)
|
||||
VALUES (:username, :email, :pwhash, 0, :admin, :member, STRFTIME('%s', 'now'), STRFTIME('%s', 'now'))
|
||||
INSERT INTO users (username, email, password, balance, is_admin, is_member, lastchange, created,
|
||||
logout_after_purchase)
|
||||
VALUES (:username, :email, :pwhash, 0, :admin, :member, STRFTIME('%s', 'now'), STRFTIME('%s', 'now'),
|
||||
:logout_after_purchase)
|
||||
''', {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'pwhash': pwhash,
|
||||
'admin': admin,
|
||||
'member': member
|
||||
'member': member,
|
||||
'logout_after_purchase': logout_after_purchase
|
||||
})
|
||||
# Fetch the new user's rowid.
|
||||
c.execute('SELECT last_insert_rowid()')
|
||||
|
@ -179,14 +185,15 @@ class MatematDatabase(object):
|
|||
raise ValueError('Exactly one of password and touchkey must be provided')
|
||||
with self.db.transaction(exclusive=False) as c:
|
||||
c.execute('''
|
||||
SELECT user_id, username, email, password, touchkey, is_admin, is_member, balance, receipt_pref
|
||||
SELECT user_id, username, email, password, touchkey, is_admin, is_member, balance, receipt_pref,
|
||||
logout_after_purchase
|
||||
FROM users
|
||||
WHERE username = ?
|
||||
''', [username])
|
||||
row = c.fetchone()
|
||||
if row is None:
|
||||
raise AuthenticationError('User does not exist')
|
||||
user_id, username, email, pwhash, tkhash, admin, member, balance, receipt_p = row
|
||||
user_id, username, email, pwhash, tkhash, admin, member, balance, receipt_p, logout_after_purchase = row
|
||||
if password is not None and not compare_digest(crypt.crypt(password, pwhash), pwhash):
|
||||
raise AuthenticationError('Password mismatch')
|
||||
elif touchkey is not None \
|
||||
|
@ -199,7 +206,7 @@ class MatematDatabase(object):
|
|||
receipt_pref: ReceiptPreference = ReceiptPreference(receipt_p)
|
||||
except ValueError:
|
||||
raise DatabaseConsistencyError(f'{receipt_p} is not a valid ReceiptPreference')
|
||||
return User(user_id, username, balance, email, admin, member, receipt_pref)
|
||||
return User(user_id, username, balance, email, admin, member, receipt_pref, logout_after_purchase)
|
||||
|
||||
def change_password(self, user: User, oldpass: str, newpass: str, verify_password: bool = True) -> None:
|
||||
"""
|
||||
|
@ -280,6 +287,8 @@ class MatematDatabase(object):
|
|||
balance: int = kwargs['balance'] if 'balance' in kwargs else user.balance
|
||||
balance_reason: Optional[str] = kwargs['balance_reason'] if 'balance_reason' in kwargs else None
|
||||
receipt_pref: ReceiptPreference = kwargs['receipt_pref'] if 'receipt_pref' in kwargs else user.receipt_pref
|
||||
logout_after_purchase: bool = \
|
||||
kwargs['logout_after_purchase'] if 'logout_after_purchase' in kwargs else user.logout_after_purchase
|
||||
with self.db.transaction() as c:
|
||||
c.execute('SELECT balance FROM users WHERE user_id = :user_id', {'user_id': user.id})
|
||||
row = c.fetchone()
|
||||
|
@ -312,7 +321,8 @@ class MatematDatabase(object):
|
|||
is_admin = :is_admin,
|
||||
is_member = :is_member,
|
||||
receipt_pref = :receipt_pref,
|
||||
lastchange = STRFTIME('%s', 'now')
|
||||
lastchange = STRFTIME('%s', 'now'),
|
||||
logout_after_purchase = :logout_after_purchase
|
||||
WHERE user_id = :user_id
|
||||
''', {
|
||||
'user_id': user.id,
|
||||
|
@ -321,7 +331,8 @@ class MatematDatabase(object):
|
|||
'balance': balance,
|
||||
'is_admin': is_admin,
|
||||
'is_member': is_member,
|
||||
'receipt_pref': receipt_pref.value
|
||||
'receipt_pref': receipt_pref.value,
|
||||
'logout_after_purchase': logout_after_purchase
|
||||
})
|
||||
# Only update the actual user object after the changes in the database succeeded
|
||||
user.name = name
|
||||
|
@ -329,6 +340,7 @@ class MatematDatabase(object):
|
|||
user.balance = balance
|
||||
user.is_admin = is_admin
|
||||
user.is_member = is_member
|
||||
user.logout_after_purchase = logout_after_purchase
|
||||
user.receipt_pref = receipt_pref
|
||||
|
||||
def delete_user(self, user: User) -> None:
|
||||
|
@ -356,7 +368,7 @@ class MatematDatabase(object):
|
|||
with self.db.transaction(exclusive=False) as c:
|
||||
for row in c.execute('''
|
||||
SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable
|
||||
FROM products
|
||||
FROM products ORDER BY name
|
||||
'''):
|
||||
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))
|
||||
|
|
|
@ -276,3 +276,11 @@ def migrate_schema_5_to_6(c: sqlite3.Cursor):
|
|||
ALTER TABLE products ADD COLUMN
|
||||
custom_price INTEGER(1) DEFAULT 0;
|
||||
''')
|
||||
|
||||
|
||||
def migrate_schema_6_to_7(c: sqlite3.Cursor):
|
||||
# Add custom_price column
|
||||
c.execute('''
|
||||
ALTER TABLE users ADD COLUMN
|
||||
logout_after_purchase INTEGER(1) DEFAULT 0;
|
||||
''')
|
||||
|
|
|
@ -26,7 +26,8 @@ class User:
|
|||
email: Optional[str] = None,
|
||||
is_admin: bool = False,
|
||||
is_member: bool = False,
|
||||
receipt_pref: ReceiptPreference = ReceiptPreference.NONE) -> None:
|
||||
receipt_pref: ReceiptPreference = ReceiptPreference.NONE,
|
||||
logout_after_purchase: bool = False) -> None:
|
||||
self.id: int = _id
|
||||
self.name: str = name
|
||||
self.balance: int = balance
|
||||
|
@ -34,6 +35,7 @@ class User:
|
|||
self.is_admin: bool = is_admin
|
||||
self.is_member: bool = is_member
|
||||
self.receipt_pref: ReceiptPreference = receipt_pref
|
||||
self.logout_after_purchase: bool = logout_after_purchase
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, User):
|
||||
|
@ -44,7 +46,9 @@ class User:
|
|||
self.email == other.email and \
|
||||
self.is_admin == other.is_admin and \
|
||||
self.is_member == other.is_member and \
|
||||
self.receipt_pref == other.receipt_pref
|
||||
self.receipt_pref == other.receipt_pref and \
|
||||
self.logout_after_purchase == other.logout_after_purchase
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.id, self.name, self.balance, self.email, self.is_admin, self.is_member, self.receipt_pref))
|
||||
return hash((self.id, self.name, self.balance, self.email, self.is_admin, self.is_member, self.receipt_pref,
|
||||
self.logout_after_purchase))
|
||||
|
|
|
@ -413,3 +413,84 @@ SCHEMAS[6] = [
|
|||
ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
''']
|
||||
|
||||
|
||||
SCHEMAS[7] = [
|
||||
'''
|
||||
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,
|
||||
logout_after_purchase INTEGER(1) 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
|
||||
);
|
||||
''']
|
||||
|
|
|
@ -342,9 +342,9 @@ class DatabaseTest(unittest.TestCase):
|
|||
def test_transfer(self) -> None:
|
||||
with self.db as db:
|
||||
with db.transaction() as c:
|
||||
user = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True)
|
||||
user2 = db.create_user('testuser2', 'supersecurepassword', 'testuser@example.com', True, True)
|
||||
user3 = db.create_user('testuser3', 'supersecurepassword', 'testuser@example.com', True, True)
|
||||
user = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True, False)
|
||||
user2 = db.create_user('testuser2', 'supersecurepassword', 'testuser@example.com', True, True, False)
|
||||
user3 = db.create_user('testuser3', 'supersecurepassword', 'testuser@example.com', True, True, True)
|
||||
c.execute('''SELECT balance FROM users WHERE user_id = ?''', [user.id])
|
||||
self.assertEqual(0, c.fetchone()[0])
|
||||
c.execute('''SELECT balance FROM users WHERE user_id = ?''', [user2.id])
|
||||
|
|
|
@ -53,12 +53,12 @@ class DatabaseTest(unittest.TestCase):
|
|||
with self.db as db:
|
||||
with db.transaction() as c:
|
||||
c.execute('''
|
||||
INSERT INTO users VALUES (1, 'testuser', NULL, 'supersecurepassword', NULL, 1, 1, 0, 42, 0, 0)
|
||||
INSERT INTO users VALUES (1, 'testuser', NULL, 'supersecurepassword', NULL, 1, 1, 0, 42, 0, 0, 0)
|
||||
''')
|
||||
c = db._sqlite_db.cursor()
|
||||
c.execute("SELECT * FROM users")
|
||||
user = c.fetchone()
|
||||
self.assertEqual((1, 'testuser', None, 'supersecurepassword', None, 1, 1, 0, 42, 0, 0), user)
|
||||
self.assertEqual((1, 'testuser', None, 'supersecurepassword', None, 1, 1, 0, 42, 0, 0, 0), user)
|
||||
|
||||
def test_transaction_rollback(self) -> None:
|
||||
"""
|
||||
|
@ -68,7 +68,7 @@ class DatabaseTest(unittest.TestCase):
|
|||
try:
|
||||
with db.transaction() as c:
|
||||
c.execute('''
|
||||
INSERT INTO users VALUES (1, 'testuser', NULL, 'supersecurepassword', NULL, 1, 1, 0, 42, 0, 0)
|
||||
INSERT INTO users VALUES (1, 'testuser', NULL, 'supersecurepassword', NULL, 1, 1, 0, 42, 0, 0, 0)
|
||||
''')
|
||||
raise ValueError('This should trigger a rollback')
|
||||
except ValueError as e:
|
||||
|
|
|
@ -40,7 +40,7 @@ class DatabaseTransaction(object):
|
|||
|
||||
class DatabaseWrapper(object):
|
||||
|
||||
SCHEMA_VERSION = 6
|
||||
SCHEMA_VERSION = 7
|
||||
|
||||
def __init__(self, filename: str) -> None:
|
||||
self._filename: str = filename
|
||||
|
@ -89,6 +89,8 @@ class DatabaseWrapper(object):
|
|||
migrate_schema_4_to_5(c)
|
||||
if from_version <= 5 and to_version >= 6:
|
||||
migrate_schema_5_to_6(c)
|
||||
if from_version <= 6 and to_version >= 7:
|
||||
migrate_schema_6_to_7(c)
|
||||
|
||||
def connect(self) -> None:
|
||||
if self.is_connected():
|
||||
|
|
|
@ -75,6 +75,7 @@ def handle_change(args: FormsDict, files: FormsDict, user: User, db: MatematData
|
|||
return
|
||||
username = str(args.username)
|
||||
email = str(args.email)
|
||||
logout_after_purchase = 'logout_after_purchase' in args
|
||||
# An empty e-mail field should be interpreted as NULL
|
||||
if len(email) == 0:
|
||||
email = None
|
||||
|
@ -84,7 +85,8 @@ def handle_change(args: FormsDict, files: FormsDict, user: User, db: MatematData
|
|||
return
|
||||
# Attempt to update username, e-mail and receipt preference
|
||||
try:
|
||||
db.change_user(user, agent=None, name=username, email=email, receipt_pref=receipt_pref)
|
||||
db.change_user(user, agent=None, name=username, email=email, receipt_pref=receipt_pref,
|
||||
logout_after_purchase=logout_after_purchase)
|
||||
except DatabaseConsistencyError:
|
||||
return
|
||||
|
||||
|
@ -176,8 +178,10 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
|
|||
password = str(args.password)
|
||||
is_member = 'ismember' in args
|
||||
is_admin = 'isadmin' in args
|
||||
logout_after_purchase = 'logout_after_purchase' in args
|
||||
# Create the user in the database
|
||||
newuser: User = db.create_user(username, password, email, member=is_member, admin=is_admin)
|
||||
newuser: User = db.create_user(username, password, email, member=is_member, admin=is_admin,
|
||||
logout_after_purchase=logout_after_purchase)
|
||||
|
||||
# If a default avatar is set, copy it to the user's avatar path
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ def buy():
|
|||
# If no user is logged in, redirect to the main page, as a purchase must always be bound to a user
|
||||
if not session.has(session_id, 'authenticated_user'):
|
||||
redirect('/')
|
||||
authlevel: int = session.get(session_id, 'authentication_level')
|
||||
# Connect to the database
|
||||
with MatematDatabase(config['DatabaseFile']) as db:
|
||||
# Fetch the authenticated user from the database
|
||||
|
@ -34,6 +35,9 @@ def buy():
|
|||
stock_provider = c.get_stock_provider()
|
||||
if stock_provider.needs_update():
|
||||
stock_provider.update_stock(product, -1)
|
||||
# Logout user if configured, logged in via touchkey and no price entry input was shown
|
||||
if user.logout_after_purchase and authlevel < 2 and not product.custom_price:
|
||||
redirect(f'/logout?lastaction=buy&lastproduct={pid}&lastprice={price}')
|
||||
# Redirect to the main page (where this request should have come from)
|
||||
redirect(f'/?lastaction=buy&lastproduct={pid}&lastprice={price}')
|
||||
redirect('/')
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from bottle import get, post, redirect
|
||||
import urllib.parse
|
||||
|
||||
from bottle import get, post, redirect, request
|
||||
|
||||
from matemat.webserver import session
|
||||
|
||||
|
@ -16,4 +18,4 @@ def logout():
|
|||
# Reset the authlevel session variable (0 = none, 1 = touchkey, 2 = password login)
|
||||
session.put(session_id, 'authentication_level', 0)
|
||||
# Redirect to the main page, showing the user list
|
||||
redirect('/')
|
||||
redirect(f'/?{urllib.parse.urlencode(request.query)}')
|
||||
|
|
|
@ -16,6 +16,13 @@ def main_page():
|
|||
session_id: str = session.start()
|
||||
now = str(int(datetime.utcnow().timestamp()))
|
||||
with MatematDatabase(config['DatabaseFile']) as db:
|
||||
# Fetch the list of products to display
|
||||
products = db.list_products()
|
||||
if request.params.lastproduct:
|
||||
lastproduct = db.get_product(request.params.lastproduct)
|
||||
else:
|
||||
lastproduct = None
|
||||
lastprice = int(request.params.lastprice) if request.params.lastprice else None
|
||||
# Check whether a user is logged in
|
||||
if session.has(session_id, 'authenticated_user'):
|
||||
# Fetch the user id and authentication level (touchkey vs password) from the session storage
|
||||
|
@ -24,13 +31,6 @@ def main_page():
|
|||
# Fetch the user object from the database (for name display, price calculation and admin check)
|
||||
users = db.list_users()
|
||||
user = db.get_user(uid)
|
||||
# Fetch the list of products to display
|
||||
products = db.list_products()
|
||||
if request.params.lastproduct:
|
||||
lastproduct = db.get_product(request.params.lastproduct)
|
||||
else:
|
||||
lastproduct = None
|
||||
lastprice = int(request.params.lastprice) if request.params.lastprice else None
|
||||
# Prepare a response with a jinja2 template
|
||||
return template.render('productlist.html',
|
||||
authuser=user, users=users, products=products, authlevel=authlevel,
|
||||
|
@ -44,4 +44,5 @@ def main_page():
|
|||
users = db.list_users(with_touchkey=True)
|
||||
return template.render('userlist.html',
|
||||
users=users, setupname=config['InstanceName'], now=now,
|
||||
signup=(config.get('SignupEnabled', '0') == '1'))
|
||||
signup=(config.get('SignupEnabled', '0') == '1'),
|
||||
lastaction=request.params.lastaction, lastprice=lastprice, lastproduct=lastproduct)
|
||||
|
|
|
@ -111,6 +111,7 @@ def handle_change(args: FormsDict, files: FormsDict, user: User, authuser: User,
|
|||
balance_reason = None
|
||||
is_member = 'ismember' in args
|
||||
is_admin = 'isadmin' in args
|
||||
logout_after_purchase = 'logout_after_purchase' in args
|
||||
# An empty e-mail field should be interpreted as NULL
|
||||
if len(email) == 0:
|
||||
email = None
|
||||
|
@ -121,7 +122,8 @@ def handle_change(args: FormsDict, files: FormsDict, user: User, authuser: User,
|
|||
db.change_password(user, '', password, verify_password=False)
|
||||
# Write the user detail changes
|
||||
db.change_user(user, agent=authuser, name=username, email=email, is_member=is_member, is_admin=is_admin,
|
||||
balance=balance, balance_reason=balance_reason, receipt_pref=receipt_pref)
|
||||
balance=balance, balance_reason=balance_reason, receipt_pref=receipt_pref,
|
||||
logout_after_purchase=logout_after_purchase)
|
||||
except DatabaseConsistencyError:
|
||||
return
|
||||
# If a new avatar was uploaded, process it
|
||||
|
|
|
@ -13,8 +13,8 @@ nav div {
|
|||
|
||||
.thumblist-item {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
margin: 5px 5px 10px 10px;
|
||||
padding: 15px;
|
||||
background: #f0f0f0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -331,4 +331,4 @@ aside#overlay > img {
|
|||
aside#overlay > div.price {
|
||||
padding-top: 30px;
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
<label for="admin-myaccount-isadmin">Admin: </label>
|
||||
<input id="admin-myaccount-isadmin" type="checkbox" disabled="disabled" {% if authuser.is_admin %} checked="checked" {% endif %}/><br/>
|
||||
|
||||
<label for="admin-myaccount-logout-after-purchase">Logout after purchase: </label>
|
||||
<input id="admin-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" {% if authuser.logout_after_purchase %} checked="checked" {% endif %}/><br/>
|
||||
|
||||
<input type="submit" value="Save changes" />
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
<label for="admin-newuser-isadmin">Admin: </label>
|
||||
<input id="admin-newuser-isadmin" type="checkbox" name="isadmin" /><br/>
|
||||
|
||||
<label for="admin-myaccount-logout-after-purchase">Logout after purchase: </label>
|
||||
<input id="admin-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" /><br/>
|
||||
|
||||
<input type="submit" value="Create User" />
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
@ -13,17 +13,34 @@
|
|||
<body>
|
||||
|
||||
{% block overlay %}
|
||||
{% if lastaction is defined and lastaction is not none %}
|
||||
{% if lastaction == 'buy' %}
|
||||
<aside id="overlay">
|
||||
<h2>{{ lastproduct.name }}</h2>
|
||||
<img src="/static/upload/thumbnails/products/{{ lastproduct.id }}.png?cacheBuster={{ now }}" alt="Picture of {{ lastproduct.name }}" draggable="false"/>
|
||||
{% if lastprice is not none %}
|
||||
<div class="price">{{ lastprice|chf }}</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
{% elif lastaction == 'deposit' %}
|
||||
<aside id="overlay">
|
||||
<h2>Deposit</h2>
|
||||
{% if lastprice is not none %}
|
||||
<div class="price">{{ lastprice|chf }}</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
<header>
|
||||
{% block header %}
|
||||
|
||||
{# Always show a link to the home page, either a list of users or of products. #}
|
||||
<nav class="navbarbutton">
|
||||
<div class="selected"><a href="/">Home</a></div>
|
||||
<nav class="navbarbutton">
|
||||
{# Show a link to the settings, if a user logged in via password (authlevel 2). #}
|
||||
{% if authlevel|default(0) > 1 %}
|
||||
{% if authuser is defined %}
|
||||
<a href="/">Home</a>
|
||||
{# If a user is an admin, call the link "Administration". Otherwise, call it "Settings". #}
|
||||
{% if authuser.is_admin %}
|
||||
<a href="/admin">Administration</a>
|
||||
|
|
|
@ -35,6 +35,9 @@
|
|||
<label for="moduser-account-isadmin">Admin: </label>
|
||||
<input id="moduser-account-isadmin" name="isadmin" type="checkbox" {% if user.is_admin %} checked="checked" {% endif %}/><br/>
|
||||
|
||||
<label for="admin-myaccount-logout-after-purchase">Logout after purchase: </label>
|
||||
<input id="admin-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" {% if user.logout_after_purchase %} checked="checked" {% endif %}/><br/>
|
||||
|
||||
<label for="moduser-account-balance">Balance: </label>
|
||||
CHF <input id="moduser-account-balance" name="balance" type="number" step="0.01" value="{{ user.balance|chf(False) }}" /><br/>
|
||||
|
||||
|
|
|
@ -6,27 +6,6 @@
|
|||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block overlay %}
|
||||
{% if lastaction is not none %}
|
||||
{% if lastaction == 'buy' %}
|
||||
<aside id="overlay">
|
||||
<h2>{{ lastproduct.name }}</h2>
|
||||
<img src="/static/upload/thumbnails/products/{{ lastproduct.id }}.png?cacheBuster={{ now }}" alt="Picture of {{ lastproduct.name }}" draggable="false"/>
|
||||
{% if lastprice is not none %}
|
||||
<div class="price">{{ lastprice|chf }}</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
{% elif lastaction == 'deposit' %}
|
||||
<aside id="overlay">
|
||||
<h2>Deposit</h2>
|
||||
{% if lastprice is not none %}
|
||||
<div class="price">{{ lastprice|chf }}</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{# Show the users current balance #}
|
||||
|
|
|
@ -22,9 +22,13 @@
|
|||
|
||||
<br/>
|
||||
{# Link to the password login #}
|
||||
<a href="/login">Password login</a>
|
||||
<div class="thumblist-item">
|
||||
<a href="/login">Password login</a>
|
||||
</div>
|
||||
{% if signup %}
|
||||
<a href="/signup">Create account</a>
|
||||
<div class="thumblist-item">
|
||||
<a href="/signup">Create account</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ super() }}
|
||||
|
|
Loading…
Reference in a new issue