From 19aee59c76bed7775ee92e83ff28f9416e322021 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Sat, 28 Oct 2023 23:04:05 +0200
Subject: [PATCH 01/18] removed color from users

---
 aocp/schema.sql | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/aocp/schema.sql b/aocp/schema.sql
index 324a90e..96b1b94 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -2,8 +2,7 @@ CREATE TABLE IF NOT EXISTS users (
   userid TEXT PRIMARY KEY DEFAULT ( lower(hex(randomblob(6))) ),
   gitlabid TEXT UNIQUE NOT NULL,
   email TEXT UNIQUE NOT NULL,
-  username TEXT,
-  color TEXT
+  username TEXT
 );
 
 /*
-- 
GitLab


From 40fdc8fa6c2a0c04ff17ab0403718a36da1efa0e Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Sat, 28 Oct 2023 23:12:19 +0200
Subject: [PATCH 02/18] updated db.py

---
 aocp/db.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/aocp/db.py b/aocp/db.py
index 9453f5b..2f2e3ce 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -61,7 +61,6 @@ class DB:
         # Try to register user
         try:
             with self.cnx:
-                # TODO assign random color
                 self.cnx.execute("INSERT OR FAIL INTO users (gitlabid,email) VALUES (?,?)", (gitlab_id,gitlab_email))
         except sqlite3.IntegrityError:
             # User has already been registered
-- 
GitLab


From 088c76833250d28a15e55612b544296770fc830e Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Sun, 29 Oct 2023 00:42:12 +0200
Subject: [PATCH 03/18] added db.fetch_all_users()

---
 aocp/__init__.py          |  2 ++
 aocp/db.py                | 15 ++++++++++++++-
 aocp/templates/index.html |  2 ++
 requirements.txt          |  1 +
 4 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/aocp/__init__.py b/aocp/__init__.py
index 4024761..73ef628 100644
--- a/aocp/__init__.py
+++ b/aocp/__init__.py
@@ -51,6 +51,8 @@ def create_app(test_config=None):
         else:
             context["userid"] = None
 
+        context["users"] = db.get_db().fetch_all_users().to_dict('index')
+
         return context
 
     # a simple page that says hello
diff --git a/aocp/db.py b/aocp/db.py
index 2f2e3ce..da013bd 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -2,7 +2,8 @@ from . import auth
 
 import sqlite3
 import click
-from typing import Tuple
+import pandas as pd
+from typing import List, Tuple
 from flask import current_app, g
 
 class DB:
@@ -78,6 +79,18 @@ class DB:
             userid: str = res[0]
             return (userid, first_login)
 
+    def fetch_all_users(self) -> pd.DataFrame:
+        """Fetch information about all users.
+
+        Returns: A DataFrame with the following columns:
+        userid   -- str        -- The users id in our database. (Index)
+        email    -- str        --  The users email adress.
+        username -- str | None -- The users email adress.
+        """
+        query = "SELECT ALL userid,email,username FROM users"
+        return pd.read_sql_query(query, self.cnx, index_col="userid")
+
+
 
 
 def get_db() -> DB:
diff --git a/aocp/templates/index.html b/aocp/templates/index.html
index 46d6971..91e74e0 100644
--- a/aocp/templates/index.html
+++ b/aocp/templates/index.html
@@ -21,5 +21,7 @@
 		{% else %}
 		<p>You are currently NOT logged in.</p>
 		{% endif %}
+		<h2>Users</h2>
+		<pre>{{ users | tojson(indent=4) | e }}{# REMOVE IN PRODUCTION! #}</pre>
 	</body>
 </html>
diff --git a/requirements.txt b/requirements.txt
index 5c510f0..de3815d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,4 @@ packaging>=23.2
 Werkzeug>=3.0.0
 requests>=2.31.0
 authlib>=1.2.1
+pandas>=2.1.2
-- 
GitLab


From a559d3bce355ce02b1a1b37470211a84c89cc73d Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Sun, 29 Oct 2023 01:33:33 +0200
Subject: [PATCH 04/18] added days table

---
 aocp/db.py      | 68 +++++++++++++++++++++++++++++++++++++++++++++++++
 aocp/schema.sql |  8 +++++-
 2 files changed, 75 insertions(+), 1 deletion(-)

diff --git a/aocp/db.py b/aocp/db.py
index da013bd..ec0396a 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -90,6 +90,74 @@ class DB:
         query = "SELECT ALL userid,email,username FROM users"
         return pd.read_sql_query(query, self.cnx, index_col="userid")
 
+    def set_day_puzzle(self, day: int, puzzle: bytes):
+        """Set the puzzle for a day.
+
+        Arguments:
+        day      -- The AOC day. (1-25)
+        puzzle   -- The puzzle for that day.
+
+        See also:
+        - fetch_day_puzzle()
+        """
+        with self.cnx:
+            query = "INSERT OR IGNORE INTO days (day,puzzle) VALUES (?,?)"
+            self.cnx.execute(query, (day,puzzle))
+
+    def fetch_day_puzzle(self, day: int) -> bytes | None:
+        """Fetch the puzzle for a day.
+
+        Returns:
+        Either the puzzle as bytes or None if no puzzle is known for that day.
+
+        See also:
+        - set_day_puzzle()
+        """
+        with self.cnx:
+            query = "SELECT puzzle FROM days WHERE day = ?"
+            cursor = self.cnx.execute(query, (day,))
+            res : sqlite3.Row = cursor.fetchone()
+            if res:
+                return res[0] # Puzzle found
+            else:
+                return None # No puzzle known
+
+    def set_day_solution(self, day: int, solution: str):
+        """Set the solution for a day.
+
+        Bug: This function does nothing IF a puzzle hasn't been set for that day yet using set_day_puzzle().
+
+        Arguments:
+        day      -- The AOC day. (1-25)
+        solution -- The correct solution for that day.
+
+        See also:
+        - fetch_day_solution()
+        - set_day_puzzle()
+        """
+        with self.cnx:
+            query = "UPDATE days SET solution = ? WHERE day = ?"
+            self.cnx.execute(query, (solution,day))
+
+    def fetch_day_solution(self, day: int) -> str | None:
+        """Fetch the solution for a day.
+
+        Returns:
+        Either the solution as a string or None if no solution is known for that day.
+
+        See also:
+        - set_day_solution()
+        """
+        with self.cnx:
+            query = "SELECT solution FROM days WHERE day = ?"
+            cursor = self.cnx.execute(query, (day,))
+            res : sqlite3.Row = cursor.fetchone()
+            if res:
+                return res[0] # Solution found
+            else:
+                return None # No solution known
+
+
 
 
 
diff --git a/aocp/schema.sql b/aocp/schema.sql
index 96b1b94..46ea527 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -2,7 +2,13 @@ CREATE TABLE IF NOT EXISTS users (
   userid TEXT PRIMARY KEY DEFAULT ( lower(hex(randomblob(6))) ),
   gitlabid TEXT UNIQUE NOT NULL,
   email TEXT UNIQUE NOT NULL,
-  username TEXT
+  username TEXT DEFAULT NULL
+);
+
+CREATE TABLE IF NOT EXISTS days (
+  day INTEGER PRIMARY KEY AUTOINCREMENT,
+  solution TEXT DEFAULT NULL,
+  puzzle BLOB DEFAULT NULL
 );
 
 /*
-- 
GitLab


From b8ea87ca7b404c8159b3735cafadce9c3cd274dc Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Sun, 29 Oct 2023 02:13:25 +0200
Subject: [PATCH 05/18] fixed typo

---
 aocp/db.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/aocp/db.py b/aocp/db.py
index ec0396a..ff10729 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -47,7 +47,7 @@ class DB:
 
         Arguments:
         gitlab_id    -- The users id on the identity provider.
-        gitlab_email -- The users email adress on the identity provider.
+        gitlab_email -- The users email address on the identity provider.
 
         Returns: (user_id, first_login)
         user_id     -- The users id in our database.
@@ -84,8 +84,8 @@ class DB:
 
         Returns: A DataFrame with the following columns:
         userid   -- str        -- The users id in our database. (Index)
-        email    -- str        --  The users email adress.
-        username -- str | None -- The users email adress.
+        email    -- str        -- The users email address.
+        username -- str | None -- The users display name.
         """
         query = "SELECT ALL userid,email,username FROM users"
         return pd.read_sql_query(query, self.cnx, index_col="userid")
-- 
GitLab


From 3118329573c1c995add4c8667c6a2d45168aac64 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Sun, 29 Oct 2023 02:29:53 +0200
Subject: [PATCH 06/18] enforce foreign_keys

---
 aocp/db.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/aocp/db.py b/aocp/db.py
index ff10729..9d22942 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -22,6 +22,11 @@ class DB:
         #self.cnx.row_factory = sqlite3.Row
         self.is_closed = False
 
+        # Set PRAGMAs
+        cursor = self.cnx.cursor()
+        cursor.execute("PRAGMA foreign_keys=ON") # Enforce foreign_keys
+        cursor.close()
+
     def init(self):
         """
         Initialize the database.
-- 
GitLab


From 89332e4694e481130f21851c87bcc59acfeea46f Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Sun, 29 Oct 2023 03:28:20 +0100
Subject: [PATCH 07/18] added submissions table

---
 aocp/db.py      | 79 +++++++++++++++++++++++++++++++++++++++++++++++--
 aocp/schema.sql | 10 +++++++
 2 files changed, 87 insertions(+), 2 deletions(-)

diff --git a/aocp/db.py b/aocp/db.py
index 9d22942..97677ae 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -3,7 +3,7 @@ from . import auth
 import sqlite3
 import click
 import pandas as pd
-from typing import List, Tuple
+from typing import Tuple
 from flask import current_app, g
 
 class DB:
@@ -98,6 +98,8 @@ class DB:
     def set_day_puzzle(self, day: int, puzzle: bytes):
         """Set the puzzle for a day.
 
+        This function does nothing if a puzzle is already set for that day.
+
         Arguments:
         day      -- The AOC day. (1-25)
         puzzle   -- The puzzle for that day.
@@ -106,7 +108,7 @@ class DB:
         - fetch_day_puzzle()
         """
         with self.cnx:
-            query = "INSERT OR IGNORE INTO days (day,puzzle) VALUES (?,?)"
+            query = "INSERT INTO days (day,puzzle) VALUES (?,?) ON CONFLICT DO NOTHING"
             self.cnx.execute(query, (day,puzzle))
 
     def fetch_day_puzzle(self, day: int) -> bytes | None:
@@ -162,6 +164,79 @@ class DB:
             else:
                 return None # No solution known
 
+    def add_submission(self, userid: str, day: int, value: str):
+        """Add a users submission for a specific day.
+
+        The timestamp of the submission will automatically be generated in the database.
+        The status of the submission will be set to "PENDING".
+
+        This function does nothing if the user already submitted this value for this day
+
+        Arguments:
+        userid -- The users id in our database.
+        day    -- The AOC day. (1-25)
+        value  -- The submission value.
+        """
+        with self.cnx:
+            query = "INSERT INTO submissions (userid,day,value) VALUES (?,?,?) ON CONFLICT DO NOTHING"
+            self.cnx.execute(query, (userid,day,value))
+
+    def update_submission_status(self, userid: str, day: int, value: str, status: str):
+        """Update the status of a submission.
+
+        This function CANNOT be used to add a submission. Use add_submission() instead.
+
+        Arguments:
+        userid -- The users id in our database.
+        day    -- The AOC day. (1-25)
+        value  -- The submission value.
+        status -- The new status. Must be one of:
+                  - PENDING
+                  - INVALID
+                  - CORRECT
+                  - INCORRECT
+        """
+        with self.cnx:
+            query = "UPDATE submissions SET status = ? WHERE userid = ? AND day = ? AND value = ?"
+            self.cnx.execute(query, (status,userid,day,value))
+
+    def fetch_submissions(self, userid: str | None = None, status: str | None = None) -> pd.DataFrame:
+        """Fetch multiple submissions.
+
+        The submissions can be filtered by ONE of the arguments.
+        All submissions will be fetched, if none of the arguments are set.
+        An error is thrown if multiple arguments are set.
+
+        Arguments:
+        userid -- If set, only fetch submissions by this user.
+        status -- If set, only fetch submissions with this status.
+
+        Returns: A multiindexed DataFrame with the following columns:
+        day       -- int        -- The AOC day. (Index)
+        userid    -- str        -- The users id in our database. (Index)
+        value     -- str        -- The submission value. (Index)
+        status    -- str        -- The status of the submission.
+        timestamp -- datetime   -- Date and time of the submission. (Ordered by (ascending))
+        """
+        query = "SELECT ALL day,userid,value,status,timestamp FROM submissions"
+
+        if userid is not None and status is not None:
+            raise Exception("Cannot fetch submissions with multiple filters set.")
+        elif userid is not None:
+            query += " WHERE userid = :userid"
+        elif status is not None:
+            query += " WHERE status = :status"
+
+        query += " ORDER BY timestamp ASC"
+
+        params = {
+            "userid": userid,
+            "status": status
+        }
+        return pd.read_sql_query(query, self.cnx, index_col=["userid","day","value"], params=params, parse_dates={"timestamp":"s"})
+
+
+
 
 
 
diff --git a/aocp/schema.sql b/aocp/schema.sql
index 46ea527..ae5fc94 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -11,6 +11,16 @@ CREATE TABLE IF NOT EXISTS days (
   puzzle BLOB DEFAULT NULL
 );
 
+CREATE TABLE IF NOT EXISTS submissions (
+  userid TEXT NOT NULL,
+  day INTEGER NOT NULL,
+  value TEXT NOT NULL,
+  status TEXT DEFAULT "PENDING",
+  timestamp INTEGER DEFAULT (strftime('%s')),
+  PRIMARY KEY (userid, day, value),
+  FOREIGN KEY (userid) REFERENCES users (userid) ON DELETE CASCADE
+);
+
 /*
 CREATE TABLE user (
   id INTEGER PRIMARY KEY AUTOINCREMENT,
-- 
GitLab


From 54a968f5095c9e74138f6874a2432621537f40c7 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Sun, 29 Oct 2023 03:29:53 +0100
Subject: [PATCH 08/18] tested submissions

---
 aocp/__init__.py          | 22 +++++++++++++++++++++-
 aocp/templates/index.html | 19 +++++++++++++++++++
 2 files changed, 40 insertions(+), 1 deletion(-)

diff --git a/aocp/__init__.py b/aocp/__init__.py
index 73ef628..9ce84a4 100644
--- a/aocp/__init__.py
+++ b/aocp/__init__.py
@@ -5,7 +5,7 @@ import os
 import json
 import pprint
 
-from flask import Flask, render_template, session
+from flask import Flask, abort, redirect, render_template, request, session
 
 
 def create_app(test_config=None):
@@ -52,6 +52,13 @@ def create_app(test_config=None):
             context["userid"] = None
 
         context["users"] = db.get_db().fetch_all_users().to_dict('index')
+        context["users_html"] = db.get_db().fetch_all_users().to_html()
+
+        #context["submissions"] = db.get_db().fetch_submissions().to_dict('split')
+        context["submissions"] = db.get_db().fetch_submissions().to_html()
+        context["submissions_me"] = db.get_db().fetch_submissions(userid=context["userid"]).to_html()
+        context["submissions_someone"] = db.get_db().fetch_submissions(userid="yeet").to_html()
+        context["submissions_pending"] = db.get_db().fetch_submissions(status="PENDING").to_html()
 
         return context
 
@@ -60,6 +67,19 @@ def create_app(test_config=None):
     def hello():
         return 'Hello, World!'
 
+    @app.route('/submit', methods=["GET", "POST"])
+    def submit():
+        # THIS IS CODE FOR TESTING! THIS FUNCTION IS NOT PRODUCTION READY! IT HAS MULTIPLE SECURITY FLAWS!
+        userid = request.form.get("userid", None, type=str)
+        value = request.form.get("value", None, type=str)
+        day = request.form.get("day", type=int)
+
+        if userid is None or value is None or day is None:
+            abort(400)
+
+        db.get_db().add_submission(userid=userid, value=value, day=day)
+        return redirect("/", code=302)
+
     @app.route('/')
     def index():
         context = get_context()
diff --git a/aocp/templates/index.html b/aocp/templates/index.html
index 91e74e0..70233cd 100644
--- a/aocp/templates/index.html
+++ b/aocp/templates/index.html
@@ -23,5 +23,24 @@
 		{% endif %}
 		<h2>Users</h2>
 		<pre>{{ users | tojson(indent=4) | e }}{# REMOVE IN PRODUCTION! #}</pre>
+		<pre>{{ users_html | safe }}{# REMOVE IN PRODUCTION! #}</pre>
+		<h2>Submissions</h2>
+		<form action="/submit" method="post">
+			<label for="userid">UserID:</label>
+			<input type="text" name="userid" value="{{ userid | e}}" required />
+			<label for="value">Value:</label>
+			<input type="text" name="value" required />
+			<label for="day">Day:</label>
+			<input type="number" name="day" min="1" max="25" value="3" required />
+			<input type="submit" value="Submit">
+		</form>
+		<h3>All</h3>
+		<pre>{{ submissions | safe }}{# REMOVE IN PRODUCTION! #}</pre>
+		<h3>Mine</h3>
+		<pre>{{ submissions_me | safe }}{# REMOVE IN PRODUCTION! #}</pre>
+		<h3>Someone else</h3>
+		<pre>{{ submissions_someone | safe }}{# REMOVE IN PRODUCTION! #}</pre>
+		<h3>Pending</h3>
+		<pre>{{ submissions_pending | safe }}{# REMOVE IN PRODUCTION! #}</pre>
 	</body>
 </html>
-- 
GitLab


From 72d57dfa37de5344f8b0327c4da6daf4314c6a1d Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 18:33:44 +0100
Subject: [PATCH 09/18] updated schema.sql

---
 aocp/schema.sql | 16 ----------------
 1 file changed, 16 deletions(-)

diff --git a/aocp/schema.sql b/aocp/schema.sql
index ae5fc94..8956ded 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -21,19 +21,3 @@ CREATE TABLE IF NOT EXISTS submissions (
   FOREIGN KEY (userid) REFERENCES users (userid) ON DELETE CASCADE
 );
 
-/*
-CREATE TABLE user (
-  id INTEGER PRIMARY KEY AUTOINCREMENT,
-  username TEXT UNIQUE NOT NULL,
-  password TEXT NOT NULL
-);
-
-CREATE TABLE post (
-  id INTEGER PRIMARY KEY AUTOINCREMENT,
-  author_id INTEGER NOT NULL,
-  created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-  title TEXT NOT NULL,
-  body TEXT NOT NULL,
-  FOREIGN KEY (author_id) REFERENCES user (id)
-);
-*/
-- 
GitLab


From e1d68a8eda93e41f45bb9e8e49b7e6e52e72a355 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 19:44:56 +0100
Subject: [PATCH 10/18] added groups table

---
 aocp/db.py      | 89 ++++++++++++++++++++++++++++++++++++++++---------
 aocp/schema.sql | 15 +++++++--
 2 files changed, 86 insertions(+), 18 deletions(-)

diff --git a/aocp/db.py b/aocp/db.py
index 97677ae..185419b 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -91,8 +91,9 @@ class DB:
         userid   -- str        -- The users id in our database. (Index)
         email    -- str        -- The users email address.
         username -- str | None -- The users display name.
+        groupid  -- str | None -- The id of the group the user is currently a memeber of or None.
         """
-        query = "SELECT ALL userid,email,username FROM users"
+        query = "SELECT ALL userid,email,username,groupid FROM users"
         return pd.read_sql_query(query, self.cnx, index_col="userid")
 
     def set_day_puzzle(self, day: int, puzzle: bytes):
@@ -169,6 +170,7 @@ class DB:
 
         The timestamp of the submission will automatically be generated in the database.
         The status of the submission will be set to "PENDING".
+        The group of the submission will automatically be set to the current group of the user.
 
         This function does nothing if the user already submitted this value for this day
 
@@ -178,8 +180,16 @@ class DB:
         value  -- The submission value.
         """
         with self.cnx:
-            query = "INSERT INTO submissions (userid,day,value) VALUES (?,?,?) ON CONFLICT DO NOTHING"
-            self.cnx.execute(query, (userid,day,value))
+            # fetch the groupid the user is currently in
+            query = "SELECT groupid FROM users WHERE userid = ?"
+            cursor = self.cnx.execute(query, (userid,))
+            res : sqlite3.Row = cursor.fetchone()
+            groupid = None
+            if res:
+                groupid = res[0]
+
+            query = "INSERT INTO submissions (userid,day,value,groupid) VALUES (?,?,?,?) ON CONFLICT DO NOTHING"
+            self.cnx.execute(query, (userid,day,value,groupid))
 
     def update_submission_status(self, userid: str, day: int, value: str, status: str):
         """Update the status of a submission.
@@ -200,16 +210,16 @@ class DB:
             query = "UPDATE submissions SET status = ? WHERE userid = ? AND day = ? AND value = ?"
             self.cnx.execute(query, (status,userid,day,value))
 
-    def fetch_submissions(self, userid: str | None = None, status: str | None = None) -> pd.DataFrame:
+    def fetch_submissions(self, userid: str | None = None, status: str | None = None, groupid: str | None = None) -> pd.DataFrame:
         """Fetch multiple submissions.
 
-        The submissions can be filtered by ONE of the arguments.
+        The submissions can be filtered using one or more of the arguments.
         All submissions will be fetched, if none of the arguments are set.
-        An error is thrown if multiple arguments are set.
 
         Arguments:
-        userid -- If set, only fetch submissions by this user.
-        status -- If set, only fetch submissions with this status.
+        userid   -- If set, only fetch submissions by this user.
+        status   -- If set, only fetch submissions with this status.
+        groupid  -- If set, only fetch submissions for this group.
 
         Returns: A multiindexed DataFrame with the following columns:
         day       -- int        -- The AOC day. (Index)
@@ -217,25 +227,72 @@ class DB:
         value     -- str        -- The submission value. (Index)
         status    -- str        -- The status of the submission.
         timestamp -- datetime   -- Date and time of the submission. (Ordered by (ascending))
+        groupid   -- str | None -- The id of the group the submission was made for.
         """
-        query = "SELECT ALL day,userid,value,status,timestamp FROM submissions"
+        query = "SELECT ALL day,userid,value,status,timestamp,groupid FROM submissions"
 
-        if userid is not None and status is not None:
-            raise Exception("Cannot fetch submissions with multiple filters set.")
-        elif userid is not None:
-            query += " WHERE userid = :userid"
-        elif status is not None:
-            query += " WHERE status = :status"
+        filters = []
+        if userid is not None:
+            filters.append("userid = :userid")
+        if status is not None:
+            filters.append("status = :status")
+        if groupid is not None:
+            filters.append("groupid = :groupid")
+
+        if len(filters) > 0:
+            query += " WHERE "
+            query += " AND ".join(filters)
 
         query += " ORDER BY timestamp ASC"
 
         params = {
             "userid": userid,
-            "status": status
+            "status": status,
+            "groupid": groupid
         }
         return pd.read_sql_query(query, self.cnx, index_col=["userid","day","value"], params=params, parse_dates={"timestamp":"s"})
 
+    def create_group(self, userid: str, groupid: str, name: str, color: str):
+        """Create a new group.
+
+        Arguments:
+        userid  -- The id of the user creating this group.
+        groupid -- The id the group should have. This should also be the short form version of the groups name.
+        name    -- The name the group should have.
+        color   -- The color the group should have.
+
+        Throws:
+        - sqlite3.IntegrityError: If a group by that id or name already exists.
+        """
+        with self.cnx:
+            self.cnx.execute("INSERT OR FAIL INTO groups (groupid,creator,name,color) VALUES (?,?,?,?)", (groupid,userid,name,color))
+
+    def update_group(self, groupid: str, name: str, color: str):
+        """Update the name and color of a group.
 
+        Arguments:
+        groupid -- The id the group to edit.
+        name    -- The name the group should have.
+        color   -- The color the group should have.
+
+        Throws:
+        - sqlite3.IntegrityError: If another group with that name already exists.
+        """
+        with self.cnx:
+            query = "UPDATE OR FAIL groups SET name = ? , color = ? WHERE groupid = ?"
+            self.cnx.execute(query, (name,color,groupid))
+
+    def delete_group(self, groupid: str):
+        """Delete a group.
+
+        All submissions made for that group will forget their group association.
+
+        Arguments:
+        groupid -- The id the group to delete.
+        """
+        with self.cnx:
+            query = "DELETE FROM groups WHERE groupid = ? LIMIT 1"
+            self.cnx.execute(query, (groupid,))
 
 
 
diff --git a/aocp/schema.sql b/aocp/schema.sql
index 8956ded..c19d3bc 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -2,7 +2,9 @@ CREATE TABLE IF NOT EXISTS users (
   userid TEXT PRIMARY KEY DEFAULT ( lower(hex(randomblob(6))) ),
   gitlabid TEXT UNIQUE NOT NULL,
   email TEXT UNIQUE NOT NULL,
-  username TEXT DEFAULT NULL
+  username TEXT DEFAULT NULL,
+  groupid TEXT DEFAULT NULL,
+  FOREIGN KEY (groupid) REFERENCES groups (groupid) ON DELETE SET DEFAULT
 );
 
 CREATE TABLE IF NOT EXISTS days (
@@ -17,7 +19,16 @@ CREATE TABLE IF NOT EXISTS submissions (
   value TEXT NOT NULL,
   status TEXT DEFAULT "PENDING",
   timestamp INTEGER DEFAULT (strftime('%s')),
+  groupid TEXT DEFAULT NULL,
   PRIMARY KEY (userid, day, value),
-  FOREIGN KEY (userid) REFERENCES users (userid) ON DELETE CASCADE
+  FOREIGN KEY (userid) REFERENCES users (userid) ON DELETE CASCADE,
+  FOREIGN KEY (groupid) REFERENCES groups (groupid) ON DELETE SET DEFAULT
 );
 
+CREATE TABLE IF NOT EXISTS groups (
+  groupid TEXT PRIMARY KEY NOT NULL,
+  creator TEXT NOT NULL,
+  name TEXT UNIQUE ON CONFLICT FAIL NOT NULL,
+  color TEXT NOT NULL,
+  FOREIGN KEY (creator) REFERENCES users (userid) ON DELETE CASCADE,
+);
-- 
GitLab


From 5de645659ee823eb886f0c78adb105e15ed3d174 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 19:46:09 +0100
Subject: [PATCH 11/18] fixed syntax error

---
 aocp/schema.sql | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/aocp/schema.sql b/aocp/schema.sql
index c19d3bc..2d99bf6 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -30,5 +30,5 @@ CREATE TABLE IF NOT EXISTS groups (
   creator TEXT NOT NULL,
   name TEXT UNIQUE ON CONFLICT FAIL NOT NULL,
   color TEXT NOT NULL,
-  FOREIGN KEY (creator) REFERENCES users (userid) ON DELETE CASCADE,
+  FOREIGN KEY (creator) REFERENCES users (userid) ON DELETE CASCADE
 );
-- 
GitLab


From 9798e3e6b94a1ca72619b35e796ada460dca863b Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 19:48:26 +0100
Subject: [PATCH 12/18] added option to filter by day

---
 aocp/db.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/aocp/db.py b/aocp/db.py
index 185419b..4858a1c 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -210,7 +210,7 @@ class DB:
             query = "UPDATE submissions SET status = ? WHERE userid = ? AND day = ? AND value = ?"
             self.cnx.execute(query, (status,userid,day,value))
 
-    def fetch_submissions(self, userid: str | None = None, status: str | None = None, groupid: str | None = None) -> pd.DataFrame:
+    def fetch_submissions(self, userid: str | None = None, status: str | None = None, day: int | None = None, groupid: str | None = None) -> pd.DataFrame:
         """Fetch multiple submissions.
 
         The submissions can be filtered using one or more of the arguments.
@@ -219,6 +219,7 @@ class DB:
         Arguments:
         userid   -- If set, only fetch submissions by this user.
         status   -- If set, only fetch submissions with this status.
+        day      -- If set, only fetch submissions for this day.
         groupid  -- If set, only fetch submissions for this group.
 
         Returns: A multiindexed DataFrame with the following columns:
@@ -238,6 +239,8 @@ class DB:
             filters.append("status = :status")
         if groupid is not None:
             filters.append("groupid = :groupid")
+        if day is not None:
+            filters.append("day = :day")
 
         if len(filters) > 0:
             query += " WHERE "
@@ -248,6 +251,7 @@ class DB:
         params = {
             "userid": userid,
             "status": status,
+            "day": day,
             "groupid": groupid
         }
         return pd.read_sql_query(query, self.cnx, index_col=["userid","day","value"], params=params, parse_dates={"timestamp":"s"})
-- 
GitLab


From a4b87dfeb7e73fa3f9280abc31a1cb994a2a882f Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 20:09:47 +0100
Subject: [PATCH 13/18] don't indirectly leak email adresses of other users

---
 aocp/schema.sql | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/aocp/schema.sql b/aocp/schema.sql
index 2d99bf6..99c9d43 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -1,7 +1,7 @@
 CREATE TABLE IF NOT EXISTS users (
   userid TEXT PRIMARY KEY DEFAULT ( lower(hex(randomblob(6))) ),
   gitlabid TEXT UNIQUE NOT NULL,
-  email TEXT UNIQUE NOT NULL,
+  email TEXT NOT NULL,
   username TEXT DEFAULT NULL,
   groupid TEXT DEFAULT NULL,
   FOREIGN KEY (groupid) REFERENCES groups (groupid) ON DELETE SET DEFAULT
-- 
GitLab


From 4a9f8c1fc042e3c134027f256051876e97df35ce Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 20:10:40 +0100
Subject: [PATCH 14/18] added functions for user management

---
 aocp/db.py | 38 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 36 insertions(+), 2 deletions(-)

diff --git a/aocp/db.py b/aocp/db.py
index 4858a1c..4d5ae63 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -96,6 +96,40 @@ class DB:
         query = "SELECT ALL userid,email,username,groupid FROM users"
         return pd.read_sql_query(query, self.cnx, index_col="userid")
 
+    def delete_user(self, userid: str):
+        """Delete a user.
+
+        All submissions and groups made by that user will also be deleted.
+
+        Arguments:
+        userid -- The id of the user to delete.
+        """
+        with self.cnx:
+            query = "DELETE FROM users WHERE userid = ? LIMIT 1"
+            self.cnx.execute(query, (userid,))
+
+    def update_user_name(self, userid: str, username: str | None):
+        """Change or delete a username.
+
+        Arguments:
+        userid   -- The id of the user to edit.
+        username -- The new username or None if you want to remove it.
+        """
+        with self.cnx:
+            query = "UPDATE users SET username = ? WHERE userid = ?"
+            self.cnx.execute(query, (username,userid))
+
+    def update_user_group(self, userid: str, groupid: str | None):
+        """Change which group the user is a memeber of or just remove them from the current group.
+
+        Arguments:
+        userid  -- The id of the user to edit.
+        groupid -- The new groupid or None if the user should be in no group.
+        """
+        with self.cnx:
+            query = "UPDATE users SET groupid = ? WHERE userid = ?"
+            self.cnx.execute(query, (groupid,userid))
+
     def set_day_puzzle(self, day: int, puzzle: bytes):
         """Set the puzzle for a day.
 
@@ -275,7 +309,7 @@ class DB:
         """Update the name and color of a group.
 
         Arguments:
-        groupid -- The id the group to edit.
+        groupid -- The id of the group to edit.
         name    -- The name the group should have.
         color   -- The color the group should have.
 
@@ -292,7 +326,7 @@ class DB:
         All submissions made for that group will forget their group association.
 
         Arguments:
-        groupid -- The id the group to delete.
+        groupid -- The id of the group to delete.
         """
         with self.cnx:
             query = "DELETE FROM groups WHERE groupid = ? LIMIT 1"
-- 
GitLab


From e39b4dd8e0e06cc9619186c8a043cc2f9ac42eed Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 20:14:10 +0100
Subject: [PATCH 15/18] use parameters instead of arguments in documentation

---
 aocp/db.py | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/aocp/db.py b/aocp/db.py
index 4d5ae63..81c3e13 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -50,7 +50,7 @@ class DB:
         """
         Login or register a user via the database.
 
-        Arguments:
+        Parameters:
         gitlab_id    -- The users id on the identity provider.
         gitlab_email -- The users email address on the identity provider.
 
@@ -101,7 +101,7 @@ class DB:
 
         All submissions and groups made by that user will also be deleted.
 
-        Arguments:
+        Parameters:
         userid -- The id of the user to delete.
         """
         with self.cnx:
@@ -111,7 +111,7 @@ class DB:
     def update_user_name(self, userid: str, username: str | None):
         """Change or delete a username.
 
-        Arguments:
+        Parameters:
         userid   -- The id of the user to edit.
         username -- The new username or None if you want to remove it.
         """
@@ -122,7 +122,7 @@ class DB:
     def update_user_group(self, userid: str, groupid: str | None):
         """Change which group the user is a memeber of or just remove them from the current group.
 
-        Arguments:
+        Parameters:
         userid  -- The id of the user to edit.
         groupid -- The new groupid or None if the user should be in no group.
         """
@@ -135,7 +135,7 @@ class DB:
 
         This function does nothing if a puzzle is already set for that day.
 
-        Arguments:
+        Parameters:
         day      -- The AOC day. (1-25)
         puzzle   -- The puzzle for that day.
 
@@ -169,7 +169,7 @@ class DB:
 
         Bug: This function does nothing IF a puzzle hasn't been set for that day yet using set_day_puzzle().
 
-        Arguments:
+        Parameters:
         day      -- The AOC day. (1-25)
         solution -- The correct solution for that day.
 
@@ -208,7 +208,7 @@ class DB:
 
         This function does nothing if the user already submitted this value for this day
 
-        Arguments:
+        Parameters:
         userid -- The users id in our database.
         day    -- The AOC day. (1-25)
         value  -- The submission value.
@@ -230,7 +230,7 @@ class DB:
 
         This function CANNOT be used to add a submission. Use add_submission() instead.
 
-        Arguments:
+        Parameters:
         userid -- The users id in our database.
         day    -- The AOC day. (1-25)
         value  -- The submission value.
@@ -247,10 +247,10 @@ class DB:
     def fetch_submissions(self, userid: str | None = None, status: str | None = None, day: int | None = None, groupid: str | None = None) -> pd.DataFrame:
         """Fetch multiple submissions.
 
-        The submissions can be filtered using one or more of the arguments.
-        All submissions will be fetched, if none of the arguments are set.
+        The submissions can be filtered using one or more of the parameters.
+        All submissions will be fetched, if none of the parameters are set.
 
-        Arguments:
+        Parameters:
         userid   -- If set, only fetch submissions by this user.
         status   -- If set, only fetch submissions with this status.
         day      -- If set, only fetch submissions for this day.
@@ -293,7 +293,7 @@ class DB:
     def create_group(self, userid: str, groupid: str, name: str, color: str):
         """Create a new group.
 
-        Arguments:
+        Parameters:
         userid  -- The id of the user creating this group.
         groupid -- The id the group should have. This should also be the short form version of the groups name.
         name    -- The name the group should have.
@@ -308,7 +308,7 @@ class DB:
     def update_group(self, groupid: str, name: str, color: str):
         """Update the name and color of a group.
 
-        Arguments:
+        Parameters:
         groupid -- The id of the group to edit.
         name    -- The name the group should have.
         color   -- The color the group should have.
@@ -325,7 +325,7 @@ class DB:
 
         All submissions made for that group will forget their group association.
 
-        Arguments:
+        Parameters:
         groupid -- The id of the group to delete.
         """
         with self.cnx:
-- 
GitLab


From 8d541278a9db86526f49286eb8012c5b5ac59779 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 20:17:42 +0100
Subject: [PATCH 16/18] updated schema.sql

---
 aocp/schema.sql | 1 +
 1 file changed, 1 insertion(+)

diff --git a/aocp/schema.sql b/aocp/schema.sql
index 99c9d43..18807aa 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS submissions (
   timestamp INTEGER DEFAULT (strftime('%s')),
   groupid TEXT DEFAULT NULL,
   PRIMARY KEY (userid, day, value),
+  FOREIGN KEY (day) REFERENCES days (day),
   FOREIGN KEY (userid) REFERENCES users (userid) ON DELETE CASCADE,
   FOREIGN KEY (groupid) REFERENCES groups (groupid) ON DELETE SET DEFAULT
 );
-- 
GitLab


From dce7307aff358f29a6d139811b1582376148bd59 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 20:33:39 +0100
Subject: [PATCH 17/18] add admin column to users

---
 aocp/db.py      | 29 +++++++++++++++++++++++++++--
 aocp/schema.sql |  1 +
 2 files changed, 28 insertions(+), 2 deletions(-)

diff --git a/aocp/db.py b/aocp/db.py
index 81c3e13..879d00a 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -92,9 +92,20 @@ class DB:
         email    -- str        -- The users email address.
         username -- str | None -- The users display name.
         groupid  -- str | None -- The id of the group the user is currently a memeber of or None.
+        admin    -- bool       -- Whether the user has admin permissions.
         """
-        query = "SELECT ALL userid,email,username,groupid FROM users"
-        return pd.read_sql_query(query, self.cnx, index_col="userid")
+        query = "SELECT ALL userid,email,username,groupid,admin FROM users"
+        df = pd.read_sql_query(query, self.cnx, index_col="userid")
+        df = df.astype({"admin": bool})
+        #print("==="*5)
+        #print(df.dtypes)
+        #print(df)
+        #print("===")
+        #print(df.iloc[0])
+        #print(df.iloc[0]["email"])
+        #print(type(df.iloc[0]["email"]))
+        #print("===")
+        return df
 
     def delete_user(self, userid: str):
         """Delete a user.
@@ -130,6 +141,20 @@ class DB:
             query = "UPDATE users SET groupid = ? WHERE userid = ?"
             self.cnx.execute(query, (groupid,userid))
 
+    def update_user_admin(self, userid: str, admin: bool):
+        """Update whether the user is an admin or not.
+
+        Parameters:
+        userid   -- The id of the user to edit.
+        admin    -- Whether the user is supposed to be an admin.
+        """
+        with self.cnx:
+            adminint = 0
+            if admin == True: # Very explicit as a mistake here could be fatal.
+                adminint = 1
+            query = "UPDATE users SET admin = ? WHERE userid = ?"
+            self.cnx.execute(query, (adminint,userid))
+
     def set_day_puzzle(self, day: int, puzzle: bytes):
         """Set the puzzle for a day.
 
diff --git a/aocp/schema.sql b/aocp/schema.sql
index 18807aa..66325d8 100644
--- a/aocp/schema.sql
+++ b/aocp/schema.sql
@@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS users (
   email TEXT NOT NULL,
   username TEXT DEFAULT NULL,
   groupid TEXT DEFAULT NULL,
+  admin INTEGER DEFAULT 0,
   FOREIGN KEY (groupid) REFERENCES groups (groupid) ON DELETE SET DEFAULT
 );
 
-- 
GitLab


From 3742affb305f3cf8beffec14b3f054209d6aedcf Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Mon, 30 Oct 2023 21:05:32 +0100
Subject: [PATCH 18/18] use Europe/Berlin as timezone for datetimes

---
 aocp/db.py | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/aocp/db.py b/aocp/db.py
index 879d00a..8b49cac 100644
--- a/aocp/db.py
+++ b/aocp/db.py
@@ -5,6 +5,8 @@ import click
 import pandas as pd
 from typing import Tuple
 from flask import current_app, g
+from zoneinfo import ZoneInfo
+
 
 class DB:
     """
@@ -286,11 +288,12 @@ class DB:
         userid    -- str        -- The users id in our database. (Index)
         value     -- str        -- The submission value. (Index)
         status    -- str        -- The status of the submission.
-        timestamp -- datetime   -- Date and time of the submission. (Ordered by (ascending))
+        timestamp -- datetime   -- Date and time of the submission. (Timezone: Europe/Berlin) (Ordered by ascending)
         groupid   -- str | None -- The id of the group the submission was made for.
         """
         query = "SELECT ALL day,userid,value,status,timestamp,groupid FROM submissions"
 
+        # Add filters to query
         filters = []
         if userid is not None:
             filters.append("userid = :userid")
@@ -307,13 +310,22 @@ class DB:
 
         query += " ORDER BY timestamp ASC"
 
+        # Run SQL query
         params = {
             "userid": userid,
             "status": status,
             "day": day,
             "groupid": groupid
         }
-        return pd.read_sql_query(query, self.cnx, index_col=["userid","day","value"], params=params, parse_dates={"timestamp":"s"})
+        df = pd.read_sql_query(query, self.cnx, index_col=["userid","day","value"], params=params)
+
+        # Convert timestamp to datetime with correct timezone
+        tz = ZoneInfo("Europe/Berlin")
+        df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True, unit="s", origin="unix")
+        #df["timestamp"] = df["timestamp"].tz_convert("Europe/Berlin") # Bugged.
+        df["timestamp"] = df["timestamp"].map(lambda d: d.astimezone(tz=tz))
+
+        return df
 
     def create_group(self, userid: str, groupid: str, name: str, color: str):
         """Create a new group.
-- 
GitLab