forked from s3lph/matemat
feat: add option to log out users automatically after completing a purchase
This commit is contained in:
parent
1e561fd9cd
commit
d54aa2bc57
12 changed files with 158 additions and 21 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,5 +1,18 @@
|
|||
# Matemat Changelog
|
||||
|
||||
<!-- 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
|
||||
|
||||
|
|
|
@ -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,8 +153,10 @@ 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,
|
||||
|
@ -179,14 +184,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 +205,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 +286,7 @@ 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 +319,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 +329,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 +338,7 @@ class MatematDatabase(object):
|
|||
user.balance = balance
|
||||
user.is_admin = is_admin
|
||||
user.is_member = is_member
|
||||
user.logout_after_purchase = user.logout_after_purchase
|
||||
user.receipt_pref = receipt_pref
|
||||
|
||||
def delete_user(self, user: User) -> None:
|
||||
|
|
|
@ -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
|
||||
);
|
||||
''']
|
||||
|
|
|
@ -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('/logout')
|
||||
# Redirect to the main page (where this request should have come from)
|
||||
redirect(f'/?lastaction=buy&lastproduct={pid}&lastprice={price}')
|
||||
redirect('/')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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/>
|
||||
|
||||
|
|
Loading…
Reference in a new issue