From ef5336703508d5a58c0fb72034d40ca3ff7c371b Mon Sep 17 00:00:00 2001
From: s3lph <s3lph@kabelsalat.ch>
Date: Sat, 7 Dec 2024 19:48:20 +0100
Subject: [PATCH 1/3] fix: recreate users table with case-insensitive username

---
 matemat/__init__.py      |  2 +-
 matemat/db/migrations.py | 28 ++++++++++++
 matemat/db/schemas.py    | 93 ++++++++++++++++++++++++++++++++++++++++
 matemat/db/wrapper.py    |  7 +--
 4 files changed, 126 insertions(+), 4 deletions(-)

diff --git a/matemat/__init__.py b/matemat/__init__.py
index 489c7e1..4cedeb6 100644
--- a/matemat/__init__.py
+++ b/matemat/__init__.py
@@ -1,2 +1,2 @@
 
-__version__ = '0.4.1'
+__version__ = '0.4.2'
diff --git a/matemat/db/migrations.py b/matemat/db/migrations.py
index d9f8c64..286f805 100644
--- a/matemat/db/migrations.py
+++ b/matemat/db/migrations.py
@@ -309,3 +309,31 @@ def migrate_schema_8_to_9(c: sqlite3.Cursor):
         ON DELETE CASCADE ON UPDATE CASCADE
     )
     ''')
+
+
+def migrate_schema_9_to_10(c: sqlite3.Cursor):
+    c.execute('''
+    ALTER TABLE users RENAME TO users_old
+    ''')
+    c.execute('''
+    CREATE TABLE users (
+      user_id INTEGER PRIMARY KEY,
+      username TEXT UNIQUE NOT NULL COLLATE NOCASE,
+      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
+    )
+    ''')
+    c.execute('''
+    INSERT INTO users SELECT * FROM users_old
+    ''')
+    c.execute('''
+    DROP TABLE users_old
+    ''')
diff --git a/matemat/db/schemas.py b/matemat/db/schemas.py
index 54d0980..5481fc8 100644
--- a/matemat/db/schemas.py
+++ b/matemat/db/schemas.py
@@ -669,3 +669,96 @@ SCHEMAS[9] = [
         ON DELETE CASCADE ON UPDATE CASCADE
     );
     ''']
+
+
+SCHEMAS[10] = [
+    '''
+    CREATE TABLE users (
+      user_id INTEGER PRIMARY KEY,
+      username TEXT UNIQUE NOT NULL COLLATE NOCASE,
+      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,
+      ean TEXT UNIQUE DEFAULT NULL
+    );
+    ''',
+    '''
+    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
+    );
+    ''',
+    '''
+    CREATE TABLE tokens (  -- authentication tokens such as barcodes
+      token_id INTEGER PRIMARY KEY,
+      user_id INTEGER NOT NULL,
+      token TEXT UNIQUE NOT NULL,
+      name TEXT UNIQUE NOT NULL,
+      date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
+      FOREIGN KEY (user_id) REFERENCES users(user_id)
+        ON DELETE CASCADE ON UPDATE CASCADE
+    );
+    ''']
diff --git a/matemat/db/wrapper.py b/matemat/db/wrapper.py
index 2493b86..30fd6e5 100644
--- a/matemat/db/wrapper.py
+++ b/matemat/db/wrapper.py
@@ -40,7 +40,7 @@ class DatabaseTransaction(object):
 
 class DatabaseWrapper(object):
 
-    SCHEMA_VERSION = 9
+    SCHEMA_VERSION = 10
 
     def __init__(self, filename: str) -> None:
         self._filename: str = filename
@@ -78,8 +78,7 @@ class DatabaseWrapper(object):
 
     def _upgrade(self, from_version: int, to_version: int) -> None:
         with self.transaction() as c:
-            # Note to future s3lph: If there are further migrations, also consider upgrades like 1 -> 3
-            if from_version == 1 and to_version >= 2:
+            if from_version <= 1 and to_version >= 2:
                 migrate_schema_1_to_2(c)
             if from_version <= 2 and to_version >= 3:
                 migrate_schema_2_to_3(c)
@@ -97,6 +96,8 @@ class DatabaseWrapper(object):
                 migrate_schema_7_to_8(c)
             if from_version <= 8 and to_version >= 9:
                 migrate_schema_8_to_9(c)
+            if from_version <= 9 and to_version >= 10:
+                migrate_schema_9_to_10(c)
 
     def connect(self) -> None:
         if self.is_connected():

From dd65b5c4d0961eec3a62b03ac11665bc912f861d Mon Sep 17 00:00:00 2001
From: s3lph <s3lph@kabelsalat.ch>
Date: Sat, 7 Dec 2024 20:38:24 +0100
Subject: [PATCH 2/3] feat: use button groups in admin tables

---
 templates/admin.html | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/templates/admin.html b/templates/admin.html
index 8ef87a6..3cf55c1 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -50,8 +50,10 @@
 	      <td>{{ '✓' if user.is_admin else '✗' }}</td>
 	      <td>{{ '✓' if user.logout_after_purchase else '✗' }}</td>
 	      <td>
-		<a class="btn btn-primary" href="/moduser?userid={{ user.id }}">Edit</a>
-		<a class="btn btn-danger" href="/moduser?userid={{ user.id }}&change=del">Delete</a>
+		<div class="btn-group" role="group">
+                  <a class="btn btn-primary" href="/moduser?userid={{ user.id }}">Edit</a>
+                  <a class="btn btn-danger" href="/moduser?userid={{ user.id }}&change=del">Delete</a>
+		</div>
 	      </td>
 	    </tr>
 	  {% endfor %}
@@ -104,8 +106,10 @@
 	      <td>{{ '✓' if product.stockable else '✗' }}</td>
 	      <td><img style="height: 2em;" src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Picture of {{ product.name }}" draggable="false"></td>
 	      <td>
-		<a class="btn btn-primary" href="/modproduct?productid={{ product.id }}">Edit</a>
-		<a class="btn btn-danger" href="/modproduct?productid={{ product.id }}&change=del">Delete</a>
+		<div class="btn-group" role="group">
+                  <a class="btn btn-primary" href="/modproduct?productid={{ product.id }}">Edit</a>
+                  <a class="btn btn-danger" href="/modproduct?productid={{ product.id }}&change=del">Delete</a>
+		</div>
 	      </td>
 	    </tr>
 	  {% endfor %}

From 9a1c22081330e2df354f50b0835b69a6921b32f3 Mon Sep 17 00:00:00 2001
From: Valentin Weber <valentin@wv2.ch>
Date: Sat, 7 Dec 2024 21:07:15 +0100
Subject: [PATCH 3/3] feat: refactor user settings part 1

---
 templates/settings.html | 76 ++++++++++++++++++++++++++---------------
 1 file changed, 48 insertions(+), 28 deletions(-)

diff --git a/templates/settings.html b/templates/settings.html
index 69713be..443396a 100644
--- a/templates/settings.html
+++ b/templates/settings.html
@@ -25,37 +25,57 @@
     <h2>My Account</h2>
 
     <form id="settings-myaccount-form" method="post" action="/settings?change=account" accept-charset="UTF-8">
-      <label class="form-label" for="settings-myaccount-username">Username: </label>
-      <input class="form-control" id="settings-myaccount-username" type="text" name="username" value="{{ authuser.name }}" /><br/>
-
-      <label class="form-label" for="settings-myaccount-email">E-Mail: </label>
-      <input class="form-control" id="settings-myaccount-email" type="text" name="email" value="{% if authuser.email is not none %}{{ authuser.email }}{% endif %}" /><br/>
-
-      <label class="form-label" for="settings-myaccount-receipt-pref">Receipts: </label>
-      <select class="form-select" id="settings-myaccount-receipt-pref" name="receipt_pref">
-        {% for pref in receipt_preference_class %}
-          <option value="{{ pref.value }}" {% if authuser.receipt_pref == pref %} selected="selected" {% endif %}>{{ pref.human_readable }}</option>
-        {% endfor %}
-      </select>
-      {% if config_smtp_enabled != '1' %}Sending receipts is disabled by your administrator.{% endif %}
-      <br/>
-
-      <div class="form-check">
-      <input class="form-check-input" id="settings-myaccount-ismember" type="checkbox" disabled="disabled" {% if authuser.is_member %} checked="checked" {% endif %}/>
-      <label class="form-check-label" for="settings-myaccount-ismember">Member</label>
+      <div class="row g-2">
+        <div class="col-md">
+          <div class="form-floating">
+            <input class="form-control" id="settings-myaccount-username" type="text" name="username" value="{{ authuser.name }}">
+            <label for="settings-myaccount-username">Username</label>
+          </div>
+        </div>
+        <div class="col-md">
+          <div class="form-floating">
+            <input class="form-control" id="settings-myaccount-email" type="email" name="email" value="{% if authuser.email is not none %}{{ authuser.email }}{% endif %}">
+            <label for="settings-myaccount-email">E-Mail</label>
+          </div>
+        </div>
+        {% if config_smtp_enabled == '1' %}
+        <div class="col-md">
+          <div class="form-floating">
+            <select class="form-select" id="settings-myaccount-receipt-pref" name="receipt_pref">
+            {% for pref in receipt_preference_class %}
+              <option value="{{ pref.value }}" {% if authuser.receipt_pref == pref %} selected {% endif %}>{{ pref.human_readable }}</option>
+            {% endfor %}
+            </select>
+            <label for="settings-myaccount-receipt-pref">Receipts</label>
+          </div>
+        </div>
+        {% endif %}
       </div>
-
-      <div class="form-check">
-      <input class="form-check-input" id="settings-myaccount-isadmin" type="checkbox" disabled="disabled" {% if authuser.is_admin %} checked="checked" {% endif %}/>
-      <label class="form-check-label" for="settings-myaccount-isadmin">Admin</label>
+      <div class="row g-2">
+        <div class="col">
+          <div class="form-check">
+            <input class="form-check-input" id="settings-myaccount-ismember" type="checkbox" disabled="disabled" {% if authuser.is_member %} checked="checked" {% endif %}>
+            <label class="form-check-label" for="settings-myaccount-ismember">Member</label>
+          </div>
+        </div>
+        <div class="col">
+          <div class="form-check">
+            <input class="form-check-input" id="settings-myaccount-isadmin" type="checkbox" disabled="disabled" {% if authuser.is_admin %} checked="checked" {% endif %}>
+            <label class="form-check-label" for="settings-myaccount-isadmin">Admin</label>
+          </div>
+        </div>
+        <div class="col">
+          <div class="form-check">
+            <input class="form-check-input" id="settings-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" {% if authuser.logout_after_purchase %} checked="checked" {% endif %}>
+            <label class="form-check-label" for="settings-myaccount-logout-after-purchase">Logout after purchase</label>
+          </div>
+        </div>
       </div>
-
-      <div class="form-check">
-      <input class="form-check-input" id="settings-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" {% if authuser.logout_after_purchase %} checked="checked" {% endif %}/>
-      <label class="form-check-label" for="settings-myaccount-logout-after-purchase">Logout after purchase</label>
+      <div class="row g-2">
+        <div class="col">
+          <input class="btn btn-primary" type="submit" value="Save changes">
+        </div>
       </div>
-
-      <input class="btn btn-primary" type="submit" value="Save changes" />
     </form>
 
     <h2>Avatar</h2>