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