From 0a294830fa22aa874c6f1c2cd59d7b6aac7645f9 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:00:17 +0200 Subject: [PATCH] delete files --- curr.txt | 0 database_testing.py | 18 - db_testing.py | 45 - dbfunctions | Bin 2140 -> 0 bytes docs - Kopie/database.html | 4266 ---------------------------------- docs - Kopie/database.md | 327 --- docs - Kopie/db.html | 132 -- docs - Kopie/db.md | 0 docs - Kopie/index.md | 18 - docs/database.html | 4266 ---------------------------------- docs/db.html | 132 -- documentation/database.html | 3071 ------------------------ documentation/database.md | 327 --- resources.qrc | 16 - ruff.toml | 77 - sap.db | Bin 647168 -> 0 bytes semap.db | 0 test/__init__.py | 0 test/bookgrabbertest.py | 66 - test/database_test.py | 17 - test/many_webrequest_test.py | 10 - test/rds_test.py | 103 - test/semestergen_test.py | 7 - test/test_database.py | 4 - test/webrequest_test.py | 18 - 25 files changed, 12920 deletions(-) delete mode 100644 curr.txt delete mode 100644 database_testing.py delete mode 100644 db_testing.py delete mode 100644 dbfunctions delete mode 100644 docs - Kopie/database.html delete mode 100644 docs - Kopie/database.md delete mode 100644 docs - Kopie/db.html delete mode 100644 docs - Kopie/db.md delete mode 100644 docs - Kopie/index.md delete mode 100644 docs/database.html delete mode 100644 docs/db.html delete mode 100644 documentation/database.html delete mode 100644 documentation/database.md delete mode 100644 resources.qrc delete mode 100644 ruff.toml delete mode 100644 sap.db delete mode 100644 semap.db delete mode 100644 test/__init__.py delete mode 100644 test/bookgrabbertest.py delete mode 100644 test/database_test.py delete mode 100644 test/many_webrequest_test.py delete mode 100644 test/rds_test.py delete mode 100644 test/semestergen_test.py delete mode 100644 test/test_database.py delete mode 100644 test/webrequest_test.py diff --git a/curr.txt b/curr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/database_testing.py b/database_testing.py deleted file mode 100644 index 7e6cdcb..0000000 --- a/database_testing.py +++ /dev/null @@ -1,18 +0,0 @@ -from src.backend.database import Database -from src.logic.dataclass import ApparatData - -apparat = ApparatData() -apparat.appname = "testapparat123" -apparat.appnr = 155 -apparat.dauerapp = True -apparat.profname = "Mustermanns, Max" -apparat.subject = "Physik" -apparat.semester = "SoSe 2021" - - -files = {"name": "test.png", "type": "png", "path": r"C:\Users\aky547\Desktop\test.png"} -db = Database() -# print(db.recreate_file("testfile.pdf",files,3)) -# db.insert_file(files,3) -# recreate_file("test.pdf",files,1))#insert_file(files,1)) -db.get_apparats_name(70) diff --git a/db_testing.py b/db_testing.py deleted file mode 100644 index 4d963d3..0000000 --- a/db_testing.py +++ /dev/null @@ -1,45 +0,0 @@ -from codebase import Database -from codebase.pickles import load_pickle, make_pickle -from omegaconf import OmegaConf -from webrequest import BibTextTransformer, WebRequest - -config = OmegaConf.load("config.yaml") -db = Database() -# # # f = db.get_media(1, 1) -# # # dataclass_objects = [] - -# # # for dataclass_str in f: -# # # print(f"dataclass {dataclass_str}") -# # # # dataclass_obj = ast.literal_eval(dataclass_str[0]) -# # # dataclass_objects.append(dataclass_str) - -# # # cla = BookData().from_string(dataclass_objects[0]) -# # # print(type(cla)) -# # book = ( -# # BibTextTransformer("ARRAY") -# # .get_data(WebRequest().get_ppn("ST 250 U42 (15)").get_data()) -# # .return_data() -# # ) -# # print(book) - -# # bpickle = make_pickle(book) -# # print(bpickle) - -# # print(load_pickle(bpickle)) - - -# # # print(pickle.dumps(book), type(pickle.dumps(book))) - -# # # db.add_medium(book, "2", "1") -# # # db.get_app_data("1", "Testapparat") - -# # books = db.get_media(1, 1, 0) - -# # print(len(books)) -# book = db.get_specific_book(16) - -# print(book) - - -if __name__ == "__main__": - print(db.get_media(15, 2)) diff --git a/dbfunctions b/dbfunctions deleted file mode 100644 index 1ad65f3f3eba9a3550e91fda748b4f43f278a4aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2140 zcmah~%Wi`}4D`8D|Dm^B`bSlT2#}P9M+%{7e!lI@uAF^B6a~b3Y|q-W-oM{B*-0ri zeg)p0w0O=~dzBZ@(n|wGD~t45+YFyljzGnpGko^)2}>mx&w7?KGB)yqs52~nPTESD z@aK8~{~Mk&(BwgM1FL4L-*<57=rVdc#a6+WT#G2q!nusn^2QTJvD-*uR zqk&(E=#E()vqud_FwR?R6RU)sRbI<;2J=x4D>0P4W~dRT!+(ooh{<&`MWj6qF)@z= z_Ee#^9hk6Rx>w~%bzDppRb}jD&e{dF_O;<+Sp8I}(}F%S?zT!|&vkY-a_ea)^i^dR zP_EP08fHK*bG!7F^w_SuiYl6z_(J7wabk_&O zppsL|2^}7hW56?3OlQuNoeQkf79Ul9nMARZMW#4`u+|&-vm}#7hEhbKjx2s{CBb?>AO#ZB=3OSdog^+|3qSIn%*j?= zC?DP!ygTeYzlAGgkJlei6M^ruZb4ZvR!hu=Nkb{e^^@{Z%JWA8@Wm5`;@VnO%Rd+mk_F?vGc2^gZb QPHsN8c5x@~@+?pC4<2=SfB*mh diff --git a/docs - Kopie/database.html b/docs - Kopie/database.html deleted file mode 100644 index b3ff3a4..0000000 --- a/docs - Kopie/database.html +++ /dev/null @@ -1,4266 +0,0 @@ - - - - - - -database API documentation - - - - - - - - - - - -
-
-
-

Module database

-
-
-
- -Expand source code - -
import datetime
-import os
-import re
-import sqlite3 as sql
-import tempfile
-from src.logic.log import MyLogger
-#from icecream import ic
-from typing import List, Tuple, Dict, Any, Optional, Union
-from pathlib import Path
-from omegaconf import OmegaConf
-from src.backend.db import CREATE_TABLE_APPARAT, CREATE_TABLE_MESSAGES, CREATE_TABLE_MEDIA, CREATE_TABLE_APPKONTOS, CREATE_TABLE_FILES, CREATE_TABLE_PROF, CREATE_TABLE_USER, CREATE_TABLE_SUBJECTS
-from src.logic.constants import SEMAP_MEDIA_ACCOUNTS
-from src.logic.dataclass import ApparatData, BookData
-from src.errors import NoResultError, AppPresentError
-from src.utils import load_pickle, dump_pickle,create_blob
-config = OmegaConf.load("config.yaml")
-logger = MyLogger(__name__)
-
-
-class Database:
-    """
-    Initialize the database and create the tables if they do not exist.
-    """
-    def __init__(self, db_path: str = None):
-        """
-        Default constructor for the database class
-
-        Args:
-            db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None.
-        """
-        if db_path is None:
-            self.db_path = config.database.path + config.database.name
-            self.db_path = self.db_path.replace("~", str(Path.home()))
-        else:
-            self.db_path = db_path
-        if self.get_db_contents() is None:
-            logger.log_critical("Database does not exist, creating tables")
-            self.create_tables()
-    def get_db_contents(self)->Union[List[Tuple], None]:
-        """
-        Get the contents of the 
-
-        Returns:
-            Union[List[Tuple], None]: _description_
-        """
-        try:
-            with sql.connect(self.db_path) as conn:
-                cursor = conn.cursor()
-                cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-                return cursor.fetchall()
-        except sql.OperationalError:
-            return None
-    def connect(self)->sql.Connection:
-        """
-        Connect to the database
-
-        Returns:
-            sql.Connection: The active connection to the database
-        """
-        return sql.connect(self.db_path)
-    def close_connection(self, conn: sql.Connection):
-        """
-        closes the connection to the database
-        
-        Args:
-        ----
-            - conn (sql.Connection): the connection to be closed
-        """
-        conn.close()
-    def create_tables(self):
-        """
-        Create the tables in the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        cursor.execute(CREATE_TABLE_APPARAT)
-        cursor.execute(CREATE_TABLE_MESSAGES)
-        cursor.execute(CREATE_TABLE_MEDIA)
-        cursor.execute(CREATE_TABLE_APPKONTOS)
-        cursor.execute(CREATE_TABLE_FILES)
-        cursor.execute(CREATE_TABLE_PROF)
-        cursor.execute(CREATE_TABLE_USER)
-        cursor.execute(CREATE_TABLE_SUBJECTS)
-        conn.commit()
-        self.close_connection(conn)
-    def insertInto(self, query:str, params:Tuple) -> None:
-        """
-        Insert sent data into the database
-
-        Args:
-            query (str): The query to be executed
-            params (Tuple): the parameters to be inserted into the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Inserting {params} into database with query {query}")
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-        """
-        Query the Database for the sent query.
-
-        Args:
-            query (str): The query to be executed
-            args (Tuple, optional): The arguments for the query. Defaults to ().
-            one (bool, optional): Return the first result only. Defaults to False.
-
-        Returns:
-            Union[Typle|List[Tuple]]: Returns the result of the query
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Querying database with query {query}, args: {args}")
-        cursor.execute(query, args)
-        rv = cursor.fetchall()
-        conn.commit()
-        self.close_connection(conn)
-        return (rv[0] if rv else None) if one else rv
-
-    # Books
-    def addBookToDatabase(self, bookdata:BookData,app_id:Union[str,int], prof_id:Union[str,int]):
-        """
-        Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-        Args:
-            bookdata (BookData): The metadata of the book to be added
-            app_id (str): The apparat id where the book should be added to
-            prof_id (str): The id of the professor where the book should be added to.
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        t_query = (
-            f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-        )
-        # print(t_query)
-        result = cursor.execute(t_query).fetchall()
-        result = [load_pickle(i[0]) for i in result]
-        if bookdata in result:
-            print("Bookdata already in database")
-            # check if the book was deleted in the apparat
-            query = (
-                "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-            )
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            result = cursor.execute(query, params).fetchone()
-            if result[0] == 1:
-                print("Book was deleted, updating bookdata")
-                query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-                params = (app_id, prof_id, dump_pickle(bookdata))
-                cursor.execute(query, params)
-                conn.commit()
-            return
-
-        query = (
-            "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-        )
-        converted = dump_pickle(bookdata)
-        params = (converted, app_id, prof_id, 0)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getBookIdBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->int:
-        """
-        Get a book id based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            int: The id of the book
-        """
-        result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [(load_pickle(i[0]),i[1]) for i in result]
-        book = [i for i in books if i[0].signature == signature][0][1]
-        return book
-    def getBookBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->BookData:
-        """
-        Get the book based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            BookData: The total metadata of the book wrapped in a BookData object
-        """
-        result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [load_pickle(i[0]) for i in result]
-        book = [i for i in books if i.signature == signature][0]
-        return book
-    def getLastBookId(self)->int:
-        """
-        Get the last book id in the database
-
-        Returns:
-            int: ID of the last book in the database
-        """
-        return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-    def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-        """
-        Search a book in the database based on the sent data.
-
-        Args:
-            data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-            - signature: The signature of the book
-            - title: The title of the book
-
-        Returns:
-            list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-        """
-        rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-        #ic(rdata, len(rdata))
-        mode = 0
-        if len(data)== 1:
-            if "signature" in data.keys():
-                mode = 1
-            elif "title" in data.keys():
-                mode = 2
-        elif len(data) == 2:
-            mode = 3
-        else:
-            return None
-        ret = []
-        for book in rdata:
-            bookdata = load_pickle(book[1])
-            app_id = book[2]
-            prof_id = book[3]
-            if mode == 1:
-                if data["signature"] in bookdata.signature:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 2:
-                if data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 3:
-                if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-        #ic(ret)
-        return ret
-    def setAvailability(self, book_id:str, available:str):
-        """
-        Set the availability of a book in the database
-
-        Args:
-            book_id (str): The id of the book
-            available (str): The availability of the book
-        """
-        self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-    def getBookId(self, bookdata:BookData, app_id:Union[str,int], prof_id:Union[str,int])->int:
-        """
-        Get the id of a book based on the metadata of the book
-
-        Args:
-            bookdata (BookData): The wrapped metadata of the book
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-
-        Returns:
-            int: ID of the book
-        """
-        result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-        return result[0]
-    def getBook(self,book_id:int)->BookData:
-        """
-        Get the book based on the id in the database
-
-        Args:
-            book_id (int): The id of the book
-
-        Returns:
-            BookData: The metadata of the book wrapped in a BookData object
-        """
-        return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-    def getBooks(self, app_id:Union[str,int], prof_id:Union[str,int], deleted=0)->list[dict[int, BookData, int]]:
-        """
-        Get the Books based on the apparat id and the professor id
-
-        Args:
-            app_id (str): The ID of the apparat
-            prof_id (str): The ID of the professor
-            deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-        Returns:
-            list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-        """
-        qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-        ret_result = []
-        for result_a in qdata:
-            data = {"id": int, "bookdata": BookData, "available": int}
-            data["id"] = result_a[0]
-            data["bookdata"] = load_pickle(result_a[1])
-            data["available"] = result_a[2]
-            ret_result.append(data)
-        return ret_result
-    def updateBookdata(self, book_id, bookdata:BookData):
-        """
-        Update the bookdata in the database
-
-        Args:
-            book_id (str): The id of the book
-            bookdata (BookData): The new metadata of the book
-        """
-        self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-    def deleteBook(self, book_id):
-        """
-        Delete a book from the database
-
-        Args:
-            book_id (str): ID of the book
-        """
-        self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-    # File Interactions
-    def getBlob(self, filename, app_id:Union[str,int]):
-        """
-        Get a blob from the database
-
-        Args:
-            filename (str): The name of the file
-            app_id (str): ID of the apparat
-
-        Returns:
-            bytes: The file stored in 
-        """
-        return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-    def insertFile(self, file: list[dict], app_id:Union[str,int], prof_id:Union[str,int]):
-        """Instert a list of files into the database
-
-        Args:
-            file (list[dict]): a list containing all the files to be inserted
-            Structured: [{"name": "filename", "path": "path", "type": "filetype"}]
-            app_id (int): the id of the apparat
-            prof_id (str): the id of the professor
-        """
-        for f in file:
-            filename = f["name"]
-            path = f["path"]
-            filetyp = f["type"]
-            if path == "Database":
-                continue
-            blob = create_blob(path)
-            query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-            self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-    def recreateFile(self, filename:str, app_id:Union[str,int],filetype:str)->str:
-        """Recreate a file from the database
-
-        Args:
-            filename (str): the name of the file
-            app_id (Union[str,int]): the id of the apparat
-            filetype (str): the extension of the file to be created
-
-        Returns:
-            str: The filename of the recreated file 
-        """
-        blob = self.getBlob(filename, app_id)
-        tempdir = config.database.tempdir
-        tempdir = tempdir.replace("~", str(Path.home()))
-        tempdir_path = Path(tempdir)        
-        if not os.path.exists(tempdir_path):
-            os.mkdir(tempdir_path)
-        file = tempfile.NamedTemporaryFile(
-            delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-        )
-        file.write(blob)
-        print("file created")
-        return file.name
-    def getFiles(self, app_id:Union[str,int], prof_id:int)->list[tuple]:
-        """Get all the files associated with the apparat and the professor
-
-        Args:
-            app_id (Union[str,int]): The id of the apparat
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            list[tuple]: a list of tuples containing the filename and the filetype for the corresponding apparat and professor
-        """
-        return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-    def getSemersters(self)->list[str]:
-        """Return all the unique semesters in the database
-
-        Returns:
-            list: a list of strings containing the semesters
-        """
-        data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-        return [i[0] for i in data]
-
-    def getSubjects(self):
-        """Get all the subjects in the database
-
-        Returns:
-            list[tuple]: a list of tuples containing the subjects
-        """
-        return self.query_db("SELECT * FROM subjects")
-
-    # Messages
-    def addMessage(self, message:dict,user:str, app_id:Union[str,int]):
-        """add a Message to the database
-
-        Args:
-            message (dict): the message to be added
-            user (str): the user who added the message
-            app_id (Union[str,int]): the id of the apparat
-        """
-        def __getUserId(user):
-            return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-        user_id = __getUserId(user)
-        self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],app_id))
-    def getMessages(self, date:str)->list[dict[str, str, str, str]]:
-        """Get all the messages for a specific date
-
-        Args:
-            date (str): a date.datetime object formatted as a string in the format "YYYY-MM-DD"
-        
-        Returns:
-            list[dict[str, str, str, str]]: a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message
-        """
-        def __get_user_name(user_id):
-            return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-        messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-        ret = [
-            {
-                "message": i[2],
-                "user": __get_user_name(i[4]),
-                "appnr": i[5],
-                "id": i[0]
-            }
-            for i in messages
-        ]
-        return ret
-    def deleteMessage(self, message_id):
-        """Delete a message from the database
-
-        Args:
-            message_id (str): the id of the message
-        """
-        self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-    # Prof data
-    def getProfNameById(self, prof_id:Union[str,int],add_title:bool=False)->str:
-        """Get a professor name based on the id
-
-        Args:
-            prof_id (Union[str,int]): The id of the professor
-            add_title (bool, optional): wether to add the title or no. Defaults to False.
-
-        Returns:
-            str: The name of the professor
-        """
-        prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-        if add_title:
-            return f"{self.getTitleById(prof_id)}{prof[0]}"
-        else:
-            return prof[0]
-    def getTitleById(self, prof_id:Union[str,int])->str:
-        """get the title of a professor based on the id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            str: the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned
-        """
-        title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-        return f"{title} " if title is not None else ""
-    def getProfByName(self, prof_name:str)->tuple:
-        """get all the data of a professor based on the name
-
-        Args:
-            prof_name (str): the name of the professor
-
-        Returns:
-            tuple: the data of the professor
-        """
-        return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-    def getProfId(self, prof_name:str)->Optional[int]:
-        """Get the id of a professor based on the name
-
-        Args:
-            prof_name (str): the name of the professor
-
-        Returns:
-            Optional[int]: the id of the professor, if the professor is not found, None is returned
-        """
-        
-        data = self.getProfByName(prof_name.replace(",", ""))
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def getSpecificProfData(self, prof_id:Union[str,int], fields:List[str])->tuple:
-        """A customisable function to get specific data of a professor based on the id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-            fields (List[str]): a list of fields to be returned
-
-        Returns:
-            tuple: a tuple containing the requested data
-        """
-        query = "SELECT "
-        for field in fields:
-            query += f"{field},"
-        query = query[:-1]
-        query += " FROM prof WHERE id=?"
-        return self.query_db(query, (prof_id,), one=True)[0]
-    def getProfData(self, profname:str):
-        """Get mail, telephone number and title of a professor based on the name
-
-        Args:
-            profname (str): name of the professor
-
-        Returns:
-            tuple: the mail, telephone number and title of the professor
-        """
-        data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-        return data
-    def createProf(self, prof_details:dict):
-        """Create a professor in the database
-
-        Args:
-            prof_details (dict): a dictionary containing the details of the professor
-        """
-        prof_title = prof_details["prof_title"]
-        prof_fname = prof_details["profname"].split(",")[1]
-        prof_fname = prof_fname.strip()
-        prof_lname = prof_details["profname"].split(",")[0]
-        prof_lname = prof_lname.strip()
-        prof_fullname = prof_details["profname"].replace(",", "")
-        prof_mail = prof_details["prof_mail"]
-        prof_tel = prof_details["prof_tel"]
-        params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-        query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-        self.insertInto(query=query, params=params)
-    def getProfs(self)->list[tuple]:
-        """Return all the professors in the database
-
-        Returns:
-            list[tuple]: a list containing all the professors in individual tuples
-        """
-        return self.query_db("SELECT * FROM prof")
-
-    # Apparat
-    def getAllAparats(self,deleted=0)->list[tuple]:
-        """Get all the apparats in the database
-
-        Args:
-            deleted (int, optional): Switch the result to use . Defaults to 0.
-
-        Returns:
-            list[tuple]: a list of tuples containing the apparats
-        """
-        return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-    def getApparatData(self, appnr, appname)->ApparatData:
-        """Get the Apparat data based on the apparat number and the name
-
-        Args:
-            appnr (str): the apparat number
-            appname (str): the name of the apparat
-
-        Raises:
-            NoResultError: an error is raised if no result is found
-
-        Returns:
-            ApparatData: the appended data of the apparat wrapped in an ApparatData object
-        """
-        result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-        if result is None:
-            raise NoResultError("No result found")
-        apparat = ApparatData()
-        apparat.appname = result[1]
-        apparat.appnr = result[4]
-        apparat.dauerapp = True if result[7] == 1 else False
-        prof_data = self.getProfData(self.getProfNameById(result[2]))
-        apparat.profname = self.getProfNameById(result[2])
-        apparat.prof_mail = prof_data[0]
-        apparat.prof_tel = prof_data[1]
-        apparat.prof_title = prof_data[2]
-        apparat.app_fach = result[3]
-        apparat.erstellsemester = result[5]
-        apparat.semester = result[8]
-        apparat.deleted = result[9]
-        apparat.apparat_adis_id = result[11]
-        apparat.prof_adis_id = result[12]
-        return apparat
-    def getUnavailableApparatNumbers(self)->List[int]:
-        """Get a list of all the apparat numbers in the database that are currently in use
-
-        Returns:
-            List[int]: the list of used apparat numbers
-        """
-        numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-        numbers = [i[0] for i in numbers]
-        logger.log_info(f"Currently used apparat numbers: {numbers}")
-        return numbers
-    def setNewSemesterDate(self, app_id:Union[str,int], newDate, dauerapp=False):
-        """Set the new semester date for an apparat
-
-        Args:
-            app_id (Union[str,int]): the id of the apparat
-            newDate (str): the new date
-            dauerapp (bool, optional): if the apparat was changed to dauerapparat. Defaults to False.
-        """
-        date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-        if dauerapp:
-            self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,app_id))
-        else:
-            self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,app_id))
-    def getApparatId(self, apparat_name)->Optional[int]:
-        """get the id of an apparat based on the name
-
-        Args:
-            apparat_name (str): the name of the apparat e.g. "Semesterapparat 1"
-
-        Returns:
-            Optional[int]: the id of the apparat, if the apparat is not found, None is returned
-        """
-        data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def createApparat(self, apparat:ApparatData)->int:
-        """create the apparat in the database
-
-        Args:
-            apparat (ApparatData): the wrapped metadata of the apparat
-
-        Raises:
-            AppPresentError: an error describing that the apparats chosen id is already present in the database
-
-        Returns:
-            Optional[int]: the id of the apparat
-        """
-                
-        prof_id = self.getProfId(apparat.profname)
-        app_id = self.getApparatId(apparat.appname)
-        if app_id:
-            raise AppPresentError(app_id)
-
-        self.createProf(apparat.get_prof_details())
-        prof_id = self.getProfId(apparat.profname)
-        #ic(prof_id)
-        query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-        logger.log_info(query)
-        self.query_db(query)
-        return self.getApparatId(apparat.appname)
-    def getApparatsByProf(self, prof_id:Union[str,int])->list[tuple]:
-        """Get all apparats based on the professor id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            list[tuple]: a list of tuples containing the apparats
-        """
-        return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-    def getApparatsBySemester(self, semester:str)->dict[list]:
-        """get all apparats based on the semester
-
-        Args:
-            semester (str): the selected semester
-
-        Returns:
-            dict[list]: a list off all created and deleted apparats for the selected semester
-        """
-        data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-        conn = self.connect()
-        cursor = conn.cursor()
-        c_tmp = []
-        for i in data:
-            c_tmp.append((i[0], self.getProfNameById(i[1])))
-        query = (
-            f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-        )
-        result = cursor.execute(query).fetchall()
-        d_tmp = []
-        for i in result:
-            d_tmp.append((i[0], self.getProfNameById(i[1])))
-        # group the apparats by prof
-        c_ret = {}
-        for i in c_tmp:
-            if i[1] not in c_ret.keys():
-                c_ret[i[1]] = [i[0]]
-            else:
-                c_ret[i[1]].append(i[0])
-        d_ret = {}
-        for i in d_tmp:
-            if i[1] not in d_ret.keys():
-                d_ret[i[1]] = [i[0]]
-            else:
-                d_ret[i[1]].append(i[0])
-        self.close_connection(conn)
-        return {"created": c_ret, "deleted": d_ret}
-    def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-        """get a list of all apparats created and deleted by semester
-
-        Returns:
-            tuple[list[str],list[int]]: a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        semesters = self.getSemersters()
-        created = []
-        deleted = []
-        for semester in semesters:
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-            result = cursor.execute(query).fetchone()
-            created.append(result[0])
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-            result = cursor.execute(query).fetchone()
-            deleted.append(result[0])
-        # store data in a tuple
-        ret = []
-        e_tuple = ()
-        for sem in semesters:
-            e_tuple = (
-                sem,
-                created[semesters.index(sem)],
-                deleted[semesters.index(sem)],
-            )
-            ret.append(e_tuple)
-        self.close_connection(conn)
-        return ret 
-    def deleteApparat(self, app_id:Union[str,int], semester:str):
-        """Delete an apparat from the database
-
-        Args:
-            app_id (Union[str, int]): the id of the apparat
-            semester (str): the semester the apparat should be deleted from
-        """
-        self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,app_id)) 
-    def isEternal(self, id):
-        """check if the apparat is eternal (dauerapparat)
-
-        Args:
-            id (int): the id of the apparat to be checked
-
-        Returns:
-            int: the state of the apparat
-        """
-        return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-    def getApparatName(self, app_id:Union[str,int], prof_id:Union[str,int]):
-        """get the name of the apparat based on the id
-
-        Args:
-            app_id (Union[str,int]): the id of the apparat
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            str: the name of the apparat
-        """
-        return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-    def updateApparat(self, apparat_data:ApparatData):
-        """Update an apparat in the database
-
-        Args:
-            apparat_data (ApparatData): the new metadata of the apparat
-        """
-        query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-        params = (
-            apparat_data.appname,
-            apparat_data.app_fach,
-            apparat_data.dauerapp,
-            self.getProfId(apparat_data.profname),
-            apparat_data.appnr,        
-        )
-        self.query_db(query, params)
-    def checkApparatExists(self, apparat_name:str):
-        """check if the apparat is already present in the database based on the name
-
-        Args:
-            apparat_name (str): the name of the apparat
-
-        Returns:
-            bool: True if the apparat is present, False if not
-        """
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-    def checkApparatExistsById(self, app_id:Union[str,int])->bool:
-        """a check to see if the apparat is already present in the database, based on the id
-
-        Args:
-            app_id (Union[str, int]): the id of the apparat
-
-        Returns:
-            bool: True if the apparat is present, False if not
-        """
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (app_id,), one=True) else False
-    # Statistics
-    def statistic_request(self, **kwargs: Any):
-        """Take n amount of kwargs and return the result of the query
-        """
-        def __query(query):
-            """execute the query and return the result
-
-            Args:
-                query (str): the constructed query
-
-            Returns:
-                list: the result of the query
-            """
-            conn = self.connect()
-            cursor = conn.cursor()
-            result = cursor.execute(query).fetchall()
-            for result_a in result:
-                orig_value = result_a
-                prof_name = self.getProfNameById(result_a[2])
-                # replace the prof_id with the prof_name
-                result_a = list(result_a)
-                result_a[2] = prof_name
-                result_a = tuple(result_a)
-                result[result.index(orig_value)] = result_a
-            self.close_connection(conn)
-            return result
-        if "deletable" in kwargs.keys():
-            query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-            return __query(query)
-        if "dauer" in kwargs.keys():
-            kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-        query = "SELECT * FROM semesterapparat WHERE "
-        for key, value in kwargs.items() if kwargs.items() is not None else {}:
-            print(key, value)
-            query += f"{key}='{value}' AND "
-            print(query)
-        # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-        if "deletesemester" in kwargs.keys():
-            query = query.replace(
-                f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-            )
-        if "endsemester" in kwargs.keys():
-            if "erstellsemester" in kwargs.keys():
-                query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-                query = query.replace(
-                    f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-                )
-            else:
-                query = query.replace(
-                    f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-                )
-                print("replaced")
-            query = query.replace(
-                "xyz",
-                f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-            )
-        # remove all x="" parts from the query where x is a key in kwargs
-        query = query[:-5]
-        print(query)
-        return __query(query)
-
-    # Admin data
-    def getUser(self):
-        """Get a single user from the database"""
-        return self.query_db("SELECT * FROM user", one=True)
-    def getUsers(self)->list[tuple]:
-        """Return a list of tuples of all the users in the database"""
-        return self.query_db("SELECT * FROM user")
-
-    def login(self, user, hashed_password):
-        """try to login the user.
-        The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database 
-
-        Args:
-            user (str): username that tries to login
-            hashed_password (str): the password the user tries to login with
-
-        Returns:
-            bool: True if the login was successful, False if not
-        """
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        if salt is None:
-            return False
-        hashed_password = salt + hashed_password
-        password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-        if password == hashed_password:
-            return True
-        else:
-            return False
-    def changePassword(self, user, new_password):
-        """change the password of a user.
-        The password will be added with the salt and then committed to the database
-
-        Args:
-            user (str): username
-            new_password (str): the hashed password
-        """
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        new_password = salt + new_password
-        self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-    def getRole(self, user):
-        """get the role of the user
-
-        Args:
-            user (str): username
-
-        Returns:
-            str: the name of the role
-        """
-        return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-    def getRoles(self)->list[tuple]:
-        """get all the roles in the database
-
-        Returns:
-            list[str]: a list of all the roles
-        """
-        return self.query_db("SELECT role FROM user")
-    def checkUsername(self, user)->bool:
-        """a check to see if the username is already present in the database
-
-        Args:
-            user (str): the username
-
-        Returns:
-            bool: True if the username is present, False if not
-        """
-        data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-        return True if data is not None else False
-    def createUser(self, user, password, role, salt):
-        """create an user from the AdminCommands class.
-
-        Args:
-            user (str): the username of the user
-            password (str): a hashed password
-            role (str): the role of the user
-            salt (str): a salt for the password
-        """
-        self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-    def deleteUser(self, user):
-        """delete an unser
-
-        Args:
-            user (str): username of the user
-        """
-        self.query_db("DELETE FROM user WHERE username=?", (user,))
-    def updateUser(self, username, data:dict[str, str]):
-        """changge the data of a user
-
-        Args:
-            username (str): the username of the user
-            data (dict[str, str]): the data to be changed
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        query = "UPDATE user SET "
-        for key,value in data.items():
-            if key == "username":
-                continue
-            query += f"{key}='{value}',"
-        query = query[:-1]
-        query += " WHERE username=?"
-        params = (username,)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getFacultyMember(self, name:str)->tuple:
-        """get a faculty member based on the name
-
-        Args:
-            name (str): the name to be searched for
-
-        Returns:
-            tuple: a tuple containing the data of the faculty member
-        """
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-    def updateFacultyMember(self, data:dict, oldlname:str, oldfname:str):
-        """update the data of a faculty member
-
-        Args:
-            data (dict): a dictionary containing the data to be updated
-            oldlname (str): the old last name of the faculty member
-            oldfname (str): the old first name of the faculty member
-        """
-        placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-        query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-        data["oldlname"] = oldlname
-        data["oldfname"] = oldfname
-        self.query_db(query, data)
-    def getFacultyMembers(self):
-        """get a list of all faculty members
-
-        Returns:
-            list[tuple]: a list of tuples containing the faculty members
-        """
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class Database -(db_path: str = None) -
-
-

Initialize the database and create the tables if they do not exist.

-

Default constructor for the database class

-

Args

-
-
db_path : str, optional
-
Optional Path for testing / specific purposes. Defaults to None.
-
-
- -Expand source code - -
class Database:
-    """
-    Initialize the database and create the tables if they do not exist.
-    """
-    def __init__(self, db_path: str = None):
-        """
-        Default constructor for the database class
-
-        Args:
-            db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None.
-        """
-        if db_path is None:
-            self.db_path = config.database.path + config.database.name
-            self.db_path = self.db_path.replace("~", str(Path.home()))
-        else:
-            self.db_path = db_path
-        if self.get_db_contents() is None:
-            logger.log_critical("Database does not exist, creating tables")
-            self.create_tables()
-    def get_db_contents(self)->Union[List[Tuple], None]:
-        """
-        Get the contents of the 
-
-        Returns:
-            Union[List[Tuple], None]: _description_
-        """
-        try:
-            with sql.connect(self.db_path) as conn:
-                cursor = conn.cursor()
-                cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-                return cursor.fetchall()
-        except sql.OperationalError:
-            return None
-    def connect(self)->sql.Connection:
-        """
-        Connect to the database
-
-        Returns:
-            sql.Connection: The active connection to the database
-        """
-        return sql.connect(self.db_path)
-    def close_connection(self, conn: sql.Connection):
-        """
-        closes the connection to the database
-        
-        Args:
-        ----
-            - conn (sql.Connection): the connection to be closed
-        """
-        conn.close()
-    def create_tables(self):
-        """
-        Create the tables in the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        cursor.execute(CREATE_TABLE_APPARAT)
-        cursor.execute(CREATE_TABLE_MESSAGES)
-        cursor.execute(CREATE_TABLE_MEDIA)
-        cursor.execute(CREATE_TABLE_APPKONTOS)
-        cursor.execute(CREATE_TABLE_FILES)
-        cursor.execute(CREATE_TABLE_PROF)
-        cursor.execute(CREATE_TABLE_USER)
-        cursor.execute(CREATE_TABLE_SUBJECTS)
-        conn.commit()
-        self.close_connection(conn)
-    def insertInto(self, query:str, params:Tuple) -> None:
-        """
-        Insert sent data into the database
-
-        Args:
-            query (str): The query to be executed
-            params (Tuple): the parameters to be inserted into the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Inserting {params} into database with query {query}")
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-        """
-        Query the Database for the sent query.
-
-        Args:
-            query (str): The query to be executed
-            args (Tuple, optional): The arguments for the query. Defaults to ().
-            one (bool, optional): Return the first result only. Defaults to False.
-
-        Returns:
-            Union[Typle|List[Tuple]]: Returns the result of the query
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Querying database with query {query}, args: {args}")
-        cursor.execute(query, args)
-        rv = cursor.fetchall()
-        conn.commit()
-        self.close_connection(conn)
-        return (rv[0] if rv else None) if one else rv
-
-    # Books
-    def addBookToDatabase(self, bookdata:BookData,app_id:Union[str,int], prof_id:Union[str,int]):
-        """
-        Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-        Args:
-            bookdata (BookData): The metadata of the book to be added
-            app_id (str): The apparat id where the book should be added to
-            prof_id (str): The id of the professor where the book should be added to.
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        t_query = (
-            f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-        )
-        # print(t_query)
-        result = cursor.execute(t_query).fetchall()
-        result = [load_pickle(i[0]) for i in result]
-        if bookdata in result:
-            print("Bookdata already in database")
-            # check if the book was deleted in the apparat
-            query = (
-                "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-            )
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            result = cursor.execute(query, params).fetchone()
-            if result[0] == 1:
-                print("Book was deleted, updating bookdata")
-                query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-                params = (app_id, prof_id, dump_pickle(bookdata))
-                cursor.execute(query, params)
-                conn.commit()
-            return
-
-        query = (
-            "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-        )
-        converted = dump_pickle(bookdata)
-        params = (converted, app_id, prof_id, 0)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getBookIdBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->int:
-        """
-        Get a book id based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            int: The id of the book
-        """
-        result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [(load_pickle(i[0]),i[1]) for i in result]
-        book = [i for i in books if i[0].signature == signature][0][1]
-        return book
-    def getBookBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->BookData:
-        """
-        Get the book based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            BookData: The total metadata of the book wrapped in a BookData object
-        """
-        result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [load_pickle(i[0]) for i in result]
-        book = [i for i in books if i.signature == signature][0]
-        return book
-    def getLastBookId(self)->int:
-        """
-        Get the last book id in the database
-
-        Returns:
-            int: ID of the last book in the database
-        """
-        return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-    def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-        """
-        Search a book in the database based on the sent data.
-
-        Args:
-            data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-            - signature: The signature of the book
-            - title: The title of the book
-
-        Returns:
-            list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-        """
-        rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-        #ic(rdata, len(rdata))
-        mode = 0
-        if len(data)== 1:
-            if "signature" in data.keys():
-                mode = 1
-            elif "title" in data.keys():
-                mode = 2
-        elif len(data) == 2:
-            mode = 3
-        else:
-            return None
-        ret = []
-        for book in rdata:
-            bookdata = load_pickle(book[1])
-            app_id = book[2]
-            prof_id = book[3]
-            if mode == 1:
-                if data["signature"] in bookdata.signature:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 2:
-                if data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 3:
-                if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-        #ic(ret)
-        return ret
-    def setAvailability(self, book_id:str, available:str):
-        """
-        Set the availability of a book in the database
-
-        Args:
-            book_id (str): The id of the book
-            available (str): The availability of the book
-        """
-        self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-    def getBookId(self, bookdata:BookData, app_id:Union[str,int], prof_id:Union[str,int])->int:
-        """
-        Get the id of a book based on the metadata of the book
-
-        Args:
-            bookdata (BookData): The wrapped metadata of the book
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-
-        Returns:
-            int: ID of the book
-        """
-        result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-        return result[0]
-    def getBook(self,book_id:int)->BookData:
-        """
-        Get the book based on the id in the database
-
-        Args:
-            book_id (int): The id of the book
-
-        Returns:
-            BookData: The metadata of the book wrapped in a BookData object
-        """
-        return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-    def getBooks(self, app_id:Union[str,int], prof_id:Union[str,int], deleted=0)->list[dict[int, BookData, int]]:
-        """
-        Get the Books based on the apparat id and the professor id
-
-        Args:
-            app_id (str): The ID of the apparat
-            prof_id (str): The ID of the professor
-            deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-        Returns:
-            list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-        """
-        qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-        ret_result = []
-        for result_a in qdata:
-            data = {"id": int, "bookdata": BookData, "available": int}
-            data["id"] = result_a[0]
-            data["bookdata"] = load_pickle(result_a[1])
-            data["available"] = result_a[2]
-            ret_result.append(data)
-        return ret_result
-    def updateBookdata(self, book_id, bookdata:BookData):
-        """
-        Update the bookdata in the database
-
-        Args:
-            book_id (str): The id of the book
-            bookdata (BookData): The new metadata of the book
-        """
-        self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-    def deleteBook(self, book_id):
-        """
-        Delete a book from the database
-
-        Args:
-            book_id (str): ID of the book
-        """
-        self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-    # File Interactions
-    def getBlob(self, filename, app_id:Union[str,int]):
-        """
-        Get a blob from the database
-
-        Args:
-            filename (str): The name of the file
-            app_id (str): ID of the apparat
-
-        Returns:
-            bytes: The file stored in 
-        """
-        return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-    def insertFile(self, file: list[dict], app_id:Union[str,int], prof_id:Union[str,int]):
-        """Instert a list of files into the database
-
-        Args:
-            file (list[dict]): a list containing all the files to be inserted
-            Structured: [{"name": "filename", "path": "path", "type": "filetype"}]
-            app_id (int): the id of the apparat
-            prof_id (str): the id of the professor
-        """
-        for f in file:
-            filename = f["name"]
-            path = f["path"]
-            filetyp = f["type"]
-            if path == "Database":
-                continue
-            blob = create_blob(path)
-            query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-            self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-    def recreateFile(self, filename:str, app_id:Union[str,int],filetype:str)->str:
-        """Recreate a file from the database
-
-        Args:
-            filename (str): the name of the file
-            app_id (Union[str,int]): the id of the apparat
-            filetype (str): the extension of the file to be created
-
-        Returns:
-            str: The filename of the recreated file 
-        """
-        blob = self.getBlob(filename, app_id)
-        tempdir = config.database.tempdir
-        tempdir = tempdir.replace("~", str(Path.home()))
-        tempdir_path = Path(tempdir)        
-        if not os.path.exists(tempdir_path):
-            os.mkdir(tempdir_path)
-        file = tempfile.NamedTemporaryFile(
-            delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-        )
-        file.write(blob)
-        print("file created")
-        return file.name
-    def getFiles(self, app_id:Union[str,int], prof_id:int)->list[tuple]:
-        """Get all the files associated with the apparat and the professor
-
-        Args:
-            app_id (Union[str,int]): The id of the apparat
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            list[tuple]: a list of tuples containing the filename and the filetype for the corresponding apparat and professor
-        """
-        return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-    def getSemersters(self)->list[str]:
-        """Return all the unique semesters in the database
-
-        Returns:
-            list: a list of strings containing the semesters
-        """
-        data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-        return [i[0] for i in data]
-
-    def getSubjects(self):
-        """Get all the subjects in the database
-
-        Returns:
-            list[tuple]: a list of tuples containing the subjects
-        """
-        return self.query_db("SELECT * FROM subjects")
-
-    # Messages
-    def addMessage(self, message:dict,user:str, app_id:Union[str,int]):
-        """add a Message to the database
-
-        Args:
-            message (dict): the message to be added
-            user (str): the user who added the message
-            app_id (Union[str,int]): the id of the apparat
-        """
-        def __getUserId(user):
-            return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-        user_id = __getUserId(user)
-        self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],app_id))
-    def getMessages(self, date:str)->list[dict[str, str, str, str]]:
-        """Get all the messages for a specific date
-
-        Args:
-            date (str): a date.datetime object formatted as a string in the format "YYYY-MM-DD"
-        
-        Returns:
-            list[dict[str, str, str, str]]: a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message
-        """
-        def __get_user_name(user_id):
-            return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-        messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-        ret = [
-            {
-                "message": i[2],
-                "user": __get_user_name(i[4]),
-                "appnr": i[5],
-                "id": i[0]
-            }
-            for i in messages
-        ]
-        return ret
-    def deleteMessage(self, message_id):
-        """Delete a message from the database
-
-        Args:
-            message_id (str): the id of the message
-        """
-        self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-    # Prof data
-    def getProfNameById(self, prof_id:Union[str,int],add_title:bool=False)->str:
-        """Get a professor name based on the id
-
-        Args:
-            prof_id (Union[str,int]): The id of the professor
-            add_title (bool, optional): wether to add the title or no. Defaults to False.
-
-        Returns:
-            str: The name of the professor
-        """
-        prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-        if add_title:
-            return f"{self.getTitleById(prof_id)}{prof[0]}"
-        else:
-            return prof[0]
-    def getTitleById(self, prof_id:Union[str,int])->str:
-        """get the title of a professor based on the id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            str: the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned
-        """
-        title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-        return f"{title} " if title is not None else ""
-    def getProfByName(self, prof_name:str)->tuple:
-        """get all the data of a professor based on the name
-
-        Args:
-            prof_name (str): the name of the professor
-
-        Returns:
-            tuple: the data of the professor
-        """
-        return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-    def getProfId(self, prof_name:str)->Optional[int]:
-        """Get the id of a professor based on the name
-
-        Args:
-            prof_name (str): the name of the professor
-
-        Returns:
-            Optional[int]: the id of the professor, if the professor is not found, None is returned
-        """
-        
-        data = self.getProfByName(prof_name.replace(",", ""))
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def getSpecificProfData(self, prof_id:Union[str,int], fields:List[str])->tuple:
-        """A customisable function to get specific data of a professor based on the id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-            fields (List[str]): a list of fields to be returned
-
-        Returns:
-            tuple: a tuple containing the requested data
-        """
-        query = "SELECT "
-        for field in fields:
-            query += f"{field},"
-        query = query[:-1]
-        query += " FROM prof WHERE id=?"
-        return self.query_db(query, (prof_id,), one=True)[0]
-    def getProfData(self, profname:str):
-        """Get mail, telephone number and title of a professor based on the name
-
-        Args:
-            profname (str): name of the professor
-
-        Returns:
-            tuple: the mail, telephone number and title of the professor
-        """
-        data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-        return data
-    def createProf(self, prof_details:dict):
-        """Create a professor in the database
-
-        Args:
-            prof_details (dict): a dictionary containing the details of the professor
-        """
-        prof_title = prof_details["prof_title"]
-        prof_fname = prof_details["profname"].split(",")[1]
-        prof_fname = prof_fname.strip()
-        prof_lname = prof_details["profname"].split(",")[0]
-        prof_lname = prof_lname.strip()
-        prof_fullname = prof_details["profname"].replace(",", "")
-        prof_mail = prof_details["prof_mail"]
-        prof_tel = prof_details["prof_tel"]
-        params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-        query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-        self.insertInto(query=query, params=params)
-    def getProfs(self)->list[tuple]:
-        """Return all the professors in the database
-
-        Returns:
-            list[tuple]: a list containing all the professors in individual tuples
-        """
-        return self.query_db("SELECT * FROM prof")
-
-    # Apparat
-    def getAllAparats(self,deleted=0)->list[tuple]:
-        """Get all the apparats in the database
-
-        Args:
-            deleted (int, optional): Switch the result to use . Defaults to 0.
-
-        Returns:
-            list[tuple]: a list of tuples containing the apparats
-        """
-        return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-    def getApparatData(self, appnr, appname)->ApparatData:
-        """Get the Apparat data based on the apparat number and the name
-
-        Args:
-            appnr (str): the apparat number
-            appname (str): the name of the apparat
-
-        Raises:
-            NoResultError: an error is raised if no result is found
-
-        Returns:
-            ApparatData: the appended data of the apparat wrapped in an ApparatData object
-        """
-        result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-        if result is None:
-            raise NoResultError("No result found")
-        apparat = ApparatData()
-        apparat.appname = result[1]
-        apparat.appnr = result[4]
-        apparat.dauerapp = True if result[7] == 1 else False
-        prof_data = self.getProfData(self.getProfNameById(result[2]))
-        apparat.profname = self.getProfNameById(result[2])
-        apparat.prof_mail = prof_data[0]
-        apparat.prof_tel = prof_data[1]
-        apparat.prof_title = prof_data[2]
-        apparat.app_fach = result[3]
-        apparat.erstellsemester = result[5]
-        apparat.semester = result[8]
-        apparat.deleted = result[9]
-        apparat.apparat_adis_id = result[11]
-        apparat.prof_adis_id = result[12]
-        return apparat
-    def getUnavailableApparatNumbers(self)->List[int]:
-        """Get a list of all the apparat numbers in the database that are currently in use
-
-        Returns:
-            List[int]: the list of used apparat numbers
-        """
-        numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-        numbers = [i[0] for i in numbers]
-        logger.log_info(f"Currently used apparat numbers: {numbers}")
-        return numbers
-    def setNewSemesterDate(self, app_id:Union[str,int], newDate, dauerapp=False):
-        """Set the new semester date for an apparat
-
-        Args:
-            app_id (Union[str,int]): the id of the apparat
-            newDate (str): the new date
-            dauerapp (bool, optional): if the apparat was changed to dauerapparat. Defaults to False.
-        """
-        date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-        if dauerapp:
-            self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,app_id))
-        else:
-            self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,app_id))
-    def getApparatId(self, apparat_name)->Optional[int]:
-        """get the id of an apparat based on the name
-
-        Args:
-            apparat_name (str): the name of the apparat e.g. "Semesterapparat 1"
-
-        Returns:
-            Optional[int]: the id of the apparat, if the apparat is not found, None is returned
-        """
-        data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def createApparat(self, apparat:ApparatData)->int:
-        """create the apparat in the database
-
-        Args:
-            apparat (ApparatData): the wrapped metadata of the apparat
-
-        Raises:
-            AppPresentError: an error describing that the apparats chosen id is already present in the database
-
-        Returns:
-            Optional[int]: the id of the apparat
-        """
-                
-        prof_id = self.getProfId(apparat.profname)
-        app_id = self.getApparatId(apparat.appname)
-        if app_id:
-            raise AppPresentError(app_id)
-
-        self.createProf(apparat.get_prof_details())
-        prof_id = self.getProfId(apparat.profname)
-        #ic(prof_id)
-        query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-        logger.log_info(query)
-        self.query_db(query)
-        return self.getApparatId(apparat.appname)
-    def getApparatsByProf(self, prof_id:Union[str,int])->list[tuple]:
-        """Get all apparats based on the professor id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            list[tuple]: a list of tuples containing the apparats
-        """
-        return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-    def getApparatsBySemester(self, semester:str)->dict[list]:
-        """get all apparats based on the semester
-
-        Args:
-            semester (str): the selected semester
-
-        Returns:
-            dict[list]: a list off all created and deleted apparats for the selected semester
-        """
-        data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-        conn = self.connect()
-        cursor = conn.cursor()
-        c_tmp = []
-        for i in data:
-            c_tmp.append((i[0], self.getProfNameById(i[1])))
-        query = (
-            f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-        )
-        result = cursor.execute(query).fetchall()
-        d_tmp = []
-        for i in result:
-            d_tmp.append((i[0], self.getProfNameById(i[1])))
-        # group the apparats by prof
-        c_ret = {}
-        for i in c_tmp:
-            if i[1] not in c_ret.keys():
-                c_ret[i[1]] = [i[0]]
-            else:
-                c_ret[i[1]].append(i[0])
-        d_ret = {}
-        for i in d_tmp:
-            if i[1] not in d_ret.keys():
-                d_ret[i[1]] = [i[0]]
-            else:
-                d_ret[i[1]].append(i[0])
-        self.close_connection(conn)
-        return {"created": c_ret, "deleted": d_ret}
-    def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-        """get a list of all apparats created and deleted by semester
-
-        Returns:
-            tuple[list[str],list[int]]: a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        semesters = self.getSemersters()
-        created = []
-        deleted = []
-        for semester in semesters:
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-            result = cursor.execute(query).fetchone()
-            created.append(result[0])
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-            result = cursor.execute(query).fetchone()
-            deleted.append(result[0])
-        # store data in a tuple
-        ret = []
-        e_tuple = ()
-        for sem in semesters:
-            e_tuple = (
-                sem,
-                created[semesters.index(sem)],
-                deleted[semesters.index(sem)],
-            )
-            ret.append(e_tuple)
-        self.close_connection(conn)
-        return ret 
-    def deleteApparat(self, app_id:Union[str,int], semester:str):
-        """Delete an apparat from the database
-
-        Args:
-            app_id (Union[str, int]): the id of the apparat
-            semester (str): the semester the apparat should be deleted from
-        """
-        self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,app_id)) 
-    def isEternal(self, id):
-        """check if the apparat is eternal (dauerapparat)
-
-        Args:
-            id (int): the id of the apparat to be checked
-
-        Returns:
-            int: the state of the apparat
-        """
-        return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-    def getApparatName(self, app_id:Union[str,int], prof_id:Union[str,int]):
-        """get the name of the apparat based on the id
-
-        Args:
-            app_id (Union[str,int]): the id of the apparat
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            str: the name of the apparat
-        """
-        return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-    def updateApparat(self, apparat_data:ApparatData):
-        """Update an apparat in the database
-
-        Args:
-            apparat_data (ApparatData): the new metadata of the apparat
-        """
-        query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-        params = (
-            apparat_data.appname,
-            apparat_data.app_fach,
-            apparat_data.dauerapp,
-            self.getProfId(apparat_data.profname),
-            apparat_data.appnr,        
-        )
-        self.query_db(query, params)
-    def checkApparatExists(self, apparat_name:str):
-        """check if the apparat is already present in the database based on the name
-
-        Args:
-            apparat_name (str): the name of the apparat
-
-        Returns:
-            bool: True if the apparat is present, False if not
-        """
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-    def checkApparatExistsById(self, app_id:Union[str,int])->bool:
-        """a check to see if the apparat is already present in the database, based on the id
-
-        Args:
-            app_id (Union[str, int]): the id of the apparat
-
-        Returns:
-            bool: True if the apparat is present, False if not
-        """
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (app_id,), one=True) else False
-    # Statistics
-    def statistic_request(self, **kwargs: Any):
-        """Take n amount of kwargs and return the result of the query
-        """
-        def __query(query):
-            """execute the query and return the result
-
-            Args:
-                query (str): the constructed query
-
-            Returns:
-                list: the result of the query
-            """
-            conn = self.connect()
-            cursor = conn.cursor()
-            result = cursor.execute(query).fetchall()
-            for result_a in result:
-                orig_value = result_a
-                prof_name = self.getProfNameById(result_a[2])
-                # replace the prof_id with the prof_name
-                result_a = list(result_a)
-                result_a[2] = prof_name
-                result_a = tuple(result_a)
-                result[result.index(orig_value)] = result_a
-            self.close_connection(conn)
-            return result
-        if "deletable" in kwargs.keys():
-            query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-            return __query(query)
-        if "dauer" in kwargs.keys():
-            kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-        query = "SELECT * FROM semesterapparat WHERE "
-        for key, value in kwargs.items() if kwargs.items() is not None else {}:
-            print(key, value)
-            query += f"{key}='{value}' AND "
-            print(query)
-        # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-        if "deletesemester" in kwargs.keys():
-            query = query.replace(
-                f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-            )
-        if "endsemester" in kwargs.keys():
-            if "erstellsemester" in kwargs.keys():
-                query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-                query = query.replace(
-                    f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-                )
-            else:
-                query = query.replace(
-                    f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-                )
-                print("replaced")
-            query = query.replace(
-                "xyz",
-                f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-            )
-        # remove all x="" parts from the query where x is a key in kwargs
-        query = query[:-5]
-        print(query)
-        return __query(query)
-
-    # Admin data
-    def getUser(self):
-        """Get a single user from the database"""
-        return self.query_db("SELECT * FROM user", one=True)
-    def getUsers(self)->list[tuple]:
-        """Return a list of tuples of all the users in the database"""
-        return self.query_db("SELECT * FROM user")
-
-    def login(self, user, hashed_password):
-        """try to login the user.
-        The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database 
-
-        Args:
-            user (str): username that tries to login
-            hashed_password (str): the password the user tries to login with
-
-        Returns:
-            bool: True if the login was successful, False if not
-        """
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        if salt is None:
-            return False
-        hashed_password = salt + hashed_password
-        password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-        if password == hashed_password:
-            return True
-        else:
-            return False
-    def changePassword(self, user, new_password):
-        """change the password of a user.
-        The password will be added with the salt and then committed to the database
-
-        Args:
-            user (str): username
-            new_password (str): the hashed password
-        """
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        new_password = salt + new_password
-        self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-    def getRole(self, user):
-        """get the role of the user
-
-        Args:
-            user (str): username
-
-        Returns:
-            str: the name of the role
-        """
-        return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-    def getRoles(self)->list[tuple]:
-        """get all the roles in the database
-
-        Returns:
-            list[str]: a list of all the roles
-        """
-        return self.query_db("SELECT role FROM user")
-    def checkUsername(self, user)->bool:
-        """a check to see if the username is already present in the database
-
-        Args:
-            user (str): the username
-
-        Returns:
-            bool: True if the username is present, False if not
-        """
-        data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-        return True if data is not None else False
-    def createUser(self, user, password, role, salt):
-        """create an user from the AdminCommands class.
-
-        Args:
-            user (str): the username of the user
-            password (str): a hashed password
-            role (str): the role of the user
-            salt (str): a salt for the password
-        """
-        self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-    def deleteUser(self, user):
-        """delete an unser
-
-        Args:
-            user (str): username of the user
-        """
-        self.query_db("DELETE FROM user WHERE username=?", (user,))
-    def updateUser(self, username, data:dict[str, str]):
-        """changge the data of a user
-
-        Args:
-            username (str): the username of the user
-            data (dict[str, str]): the data to be changed
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        query = "UPDATE user SET "
-        for key,value in data.items():
-            if key == "username":
-                continue
-            query += f"{key}='{value}',"
-        query = query[:-1]
-        query += " WHERE username=?"
-        params = (username,)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getFacultyMember(self, name:str)->tuple:
-        """get a faculty member based on the name
-
-        Args:
-            name (str): the name to be searched for
-
-        Returns:
-            tuple: a tuple containing the data of the faculty member
-        """
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-    def updateFacultyMember(self, data:dict, oldlname:str, oldfname:str):
-        """update the data of a faculty member
-
-        Args:
-            data (dict): a dictionary containing the data to be updated
-            oldlname (str): the old last name of the faculty member
-            oldfname (str): the old first name of the faculty member
-        """
-        placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-        query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-        data["oldlname"] = oldlname
-        data["oldfname"] = oldfname
-        self.query_db(query, data)
-    def getFacultyMembers(self):
-        """get a list of all faculty members
-
-        Returns:
-            list[tuple]: a list of tuples containing the faculty members
-        """
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-

Methods

-
-
-def addBookToDatabase(self, bookdata: src.logic.dataclass.BookData, app_id: Union[str, int], prof_id: Union[str, int]) -
-
-

Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.

-

Args

-
-
bookdata : BookData
-
The metadata of the book to be added
-
app_id : str
-
The apparat id where the book should be added to
-
prof_id : str
-
The id of the professor where the book should be added to.
-
-
- -Expand source code - -
def addBookToDatabase(self, bookdata:BookData,app_id:Union[str,int], prof_id:Union[str,int]):
-    """
-    Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-    Args:
-        bookdata (BookData): The metadata of the book to be added
-        app_id (str): The apparat id where the book should be added to
-        prof_id (str): The id of the professor where the book should be added to.
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    t_query = (
-        f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-    )
-    # print(t_query)
-    result = cursor.execute(t_query).fetchall()
-    result = [load_pickle(i[0]) for i in result]
-    if bookdata in result:
-        print("Bookdata already in database")
-        # check if the book was deleted in the apparat
-        query = (
-            "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-        )
-        params = (app_id, prof_id, dump_pickle(bookdata))
-        result = cursor.execute(query, params).fetchone()
-        if result[0] == 1:
-            print("Book was deleted, updating bookdata")
-            query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            cursor.execute(query, params)
-            conn.commit()
-        return
-
-    query = (
-        "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-    )
-    converted = dump_pickle(bookdata)
-    params = (converted, app_id, prof_id, 0)
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def addMessage(self, message: dict, user: str, app_id: Union[str, int]) -
-
-

add a Message to the database

-

Args

-
-
message : dict
-
the message to be added
-
user : str
-
the user who added the message
-
app_id : Union[str,int]
-
the id of the apparat
-
-
- -Expand source code - -
def addMessage(self, message:dict,user:str, app_id:Union[str,int]):
-    """add a Message to the database
-
-    Args:
-        message (dict): the message to be added
-        user (str): the user who added the message
-        app_id (Union[str,int]): the id of the apparat
-    """
-    def __getUserId(user):
-        return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-    user_id = __getUserId(user)
-    self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],app_id))
-
-
-
-def changePassword(self, user, new_password) -
-
-

change the password of a user. -The password will be added with the salt and then committed to the database

-

Args

-
-
user : str
-
username
-
new_password : str
-
the hashed password
-
-
- -Expand source code - -
def changePassword(self, user, new_password):
-    """change the password of a user.
-    The password will be added with the salt and then committed to the database
-
-    Args:
-        user (str): username
-        new_password (str): the hashed password
-    """
-    salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-    new_password = salt + new_password
-    self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-
-
-
-def checkApparatExists(self, apparat_name: str) -
-
-

check if the apparat is already present in the database based on the name

-

Args

-
-
apparat_name : str
-
the name of the apparat
-
-

Returns

-
-
bool
-
True if the apparat is present, False if not
-
-
- -Expand source code - -
def checkApparatExists(self, apparat_name:str):
-    """check if the apparat is already present in the database based on the name
-
-    Args:
-        apparat_name (str): the name of the apparat
-
-    Returns:
-        bool: True if the apparat is present, False if not
-    """
-    return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-
-
-
-def checkApparatExistsById(self, app_id: Union[str, int]) ‑> bool -
-
-

a check to see if the apparat is already present in the database, based on the id

-

Args

-
-
app_id : Union[str, int]
-
the id of the apparat
-
-

Returns

-
-
bool
-
True if the apparat is present, False if not
-
-
- -Expand source code - -
def checkApparatExistsById(self, app_id:Union[str,int])->bool:
-    """a check to see if the apparat is already present in the database, based on the id
-
-    Args:
-        app_id (Union[str, int]): the id of the apparat
-
-    Returns:
-        bool: True if the apparat is present, False if not
-    """
-    return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (app_id,), one=True) else False
-
-
-
-def checkUsername(self, user) ‑> bool -
-
-

a check to see if the username is already present in the database

-

Args

-
-
user : str
-
the username
-
-

Returns

-
-
bool
-
True if the username is present, False if not
-
-
- -Expand source code - -
def checkUsername(self, user)->bool:
-    """a check to see if the username is already present in the database
-
-    Args:
-        user (str): the username
-
-    Returns:
-        bool: True if the username is present, False if not
-    """
-    data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-    return True if data is not None else False
-
-
-
-def close_connection(self, conn: sqlite3.Connection) -
-
-

closes the connection to the database

-

Args:

-
- conn (sql.Connection): the connection to be closed
-
-
- -Expand source code - -
def close_connection(self, conn: sql.Connection):
-    """
-    closes the connection to the database
-    
-    Args:
-    ----
-        - conn (sql.Connection): the connection to be closed
-    """
-    conn.close()
-
-
-
-def connect(self) ‑> sqlite3.Connection -
-
-

Connect to the database

-

Returns

-
-
sql.Connection
-
The active connection to the database
-
-
- -Expand source code - -
def connect(self)->sql.Connection:
-    """
-    Connect to the database
-
-    Returns:
-        sql.Connection: The active connection to the database
-    """
-    return sql.connect(self.db_path)
-
-
-
-def createApparat(self, apparat: src.logic.dataclass.ApparatData) ‑> int -
-
-

create the apparat in the database

-

Args

-
-
apparat : ApparatData
-
the wrapped metadata of the apparat
-
-

Raises

-
-
AppPresentError
-
an error describing that the apparats chosen id is already present in the database
-
-

Returns

-
-
Optional[int]
-
the id of the apparat
-
-
- -Expand source code - -
def createApparat(self, apparat:ApparatData)->int:
-    """create the apparat in the database
-
-    Args:
-        apparat (ApparatData): the wrapped metadata of the apparat
-
-    Raises:
-        AppPresentError: an error describing that the apparats chosen id is already present in the database
-
-    Returns:
-        Optional[int]: the id of the apparat
-    """
-            
-    prof_id = self.getProfId(apparat.profname)
-    app_id = self.getApparatId(apparat.appname)
-    if app_id:
-        raise AppPresentError(app_id)
-
-    self.createProf(apparat.get_prof_details())
-    prof_id = self.getProfId(apparat.profname)
-    #ic(prof_id)
-    query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-    logger.log_info(query)
-    self.query_db(query)
-    return self.getApparatId(apparat.appname)
-
-
-
-def createProf(self, prof_details: dict) -
-
-

Create a professor in the database

-

Args

-
-
prof_details : dict
-
a dictionary containing the details of the professor
-
-
- -Expand source code - -
def createProf(self, prof_details:dict):
-    """Create a professor in the database
-
-    Args:
-        prof_details (dict): a dictionary containing the details of the professor
-    """
-    prof_title = prof_details["prof_title"]
-    prof_fname = prof_details["profname"].split(",")[1]
-    prof_fname = prof_fname.strip()
-    prof_lname = prof_details["profname"].split(",")[0]
-    prof_lname = prof_lname.strip()
-    prof_fullname = prof_details["profname"].replace(",", "")
-    prof_mail = prof_details["prof_mail"]
-    prof_tel = prof_details["prof_tel"]
-    params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-    query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-    self.insertInto(query=query, params=params)
-
-
-
-def createUser(self, user, password, role, salt) -
-
-

create an user from the AdminCommands class.

-

Args

-
-
user : str
-
the username of the user
-
password : str
-
a hashed password
-
role : str
-
the role of the user
-
salt : str
-
a salt for the password
-
-
- -Expand source code - -
def createUser(self, user, password, role, salt):
-    """create an user from the AdminCommands class.
-
-    Args:
-        user (str): the username of the user
-        password (str): a hashed password
-        role (str): the role of the user
-        salt (str): a salt for the password
-    """
-    self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-
-
-
-def create_tables(self) -
-
-

Create the tables in the database

-
- -Expand source code - -
def create_tables(self):
-    """
-    Create the tables in the database
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    cursor.execute(CREATE_TABLE_APPARAT)
-    cursor.execute(CREATE_TABLE_MESSAGES)
-    cursor.execute(CREATE_TABLE_MEDIA)
-    cursor.execute(CREATE_TABLE_APPKONTOS)
-    cursor.execute(CREATE_TABLE_FILES)
-    cursor.execute(CREATE_TABLE_PROF)
-    cursor.execute(CREATE_TABLE_USER)
-    cursor.execute(CREATE_TABLE_SUBJECTS)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def deleteApparat(self, app_id: Union[str, int], semester: str) -
-
-

Delete an apparat from the database

-

Args

-
-
app_id : Union[str, int]
-
the id of the apparat
-
semester : str
-
the semester the apparat should be deleted from
-
-
- -Expand source code - -
def deleteApparat(self, app_id:Union[str,int], semester:str):
-    """Delete an apparat from the database
-
-    Args:
-        app_id (Union[str, int]): the id of the apparat
-        semester (str): the semester the apparat should be deleted from
-    """
-    self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,app_id)) 
-
-
-
-def deleteBook(self, book_id) -
-
-

Delete a book from the database

-

Args

-
-
book_id : str
-
ID of the book
-
-
- -Expand source code - -
def deleteBook(self, book_id):
-    """
-    Delete a book from the database
-
-    Args:
-        book_id (str): ID of the book
-    """
-    self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-
-
-def deleteMessage(self, message_id) -
-
-

Delete a message from the database

-

Args

-
-
message_id : str
-
the id of the message
-
-
- -Expand source code - -
def deleteMessage(self, message_id):
-    """Delete a message from the database
-
-    Args:
-        message_id (str): the id of the message
-    """
-    self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-
-
-def deleteUser(self, user) -
-
-

delete an unser

-

Args

-
-
user : str
-
username of the user
-
-
- -Expand source code - -
def deleteUser(self, user):
-    """delete an unser
-
-    Args:
-        user (str): username of the user
-    """
-    self.query_db("DELETE FROM user WHERE username=?", (user,))
-
-
-
-def getAllAparats(self, deleted=0) ‑> list[tuple] -
-
-

Get all the apparats in the database

-

Args

-
-
deleted : int, optional
-
Switch the result to use . Defaults to 0.
-
-

Returns

-
-
list[tuple]
-
a list of tuples containing the apparats
-
-
- -Expand source code - -
def getAllAparats(self,deleted=0)->list[tuple]:
-    """Get all the apparats in the database
-
-    Args:
-        deleted (int, optional): Switch the result to use . Defaults to 0.
-
-    Returns:
-        list[tuple]: a list of tuples containing the apparats
-    """
-    return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-
-
-
-def getApparatCountBySemester(self) ‑> tuple[list[str], list[int]] -
-
-

get a list of all apparats created and deleted by semester

-

Returns

-
-
tuple[list[str],list[int]]
-
a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester
-
-
- -Expand source code - -
def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-    """get a list of all apparats created and deleted by semester
-
-    Returns:
-        tuple[list[str],list[int]]: a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    semesters = self.getSemersters()
-    created = []
-    deleted = []
-    for semester in semesters:
-        query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-        result = cursor.execute(query).fetchone()
-        created.append(result[0])
-        query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-        result = cursor.execute(query).fetchone()
-        deleted.append(result[0])
-    # store data in a tuple
-    ret = []
-    e_tuple = ()
-    for sem in semesters:
-        e_tuple = (
-            sem,
-            created[semesters.index(sem)],
-            deleted[semesters.index(sem)],
-        )
-        ret.append(e_tuple)
-    self.close_connection(conn)
-    return ret 
-
-
-
-def getApparatData(self, appnr, appname) ‑> src.logic.dataclass.ApparatData -
-
-

Get the Apparat data based on the apparat number and the name

-

Args

-
-
appnr : str
-
the apparat number
-
appname : str
-
the name of the apparat
-
-

Raises

-
-
NoResultError
-
an error is raised if no result is found
-
-

Returns

-
-
ApparatData
-
the appended data of the apparat wrapped in an ApparatData object
-
-
- -Expand source code - -
def getApparatData(self, appnr, appname)->ApparatData:
-    """Get the Apparat data based on the apparat number and the name
-
-    Args:
-        appnr (str): the apparat number
-        appname (str): the name of the apparat
-
-    Raises:
-        NoResultError: an error is raised if no result is found
-
-    Returns:
-        ApparatData: the appended data of the apparat wrapped in an ApparatData object
-    """
-    result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-    if result is None:
-        raise NoResultError("No result found")
-    apparat = ApparatData()
-    apparat.appname = result[1]
-    apparat.appnr = result[4]
-    apparat.dauerapp = True if result[7] == 1 else False
-    prof_data = self.getProfData(self.getProfNameById(result[2]))
-    apparat.profname = self.getProfNameById(result[2])
-    apparat.prof_mail = prof_data[0]
-    apparat.prof_tel = prof_data[1]
-    apparat.prof_title = prof_data[2]
-    apparat.app_fach = result[3]
-    apparat.erstellsemester = result[5]
-    apparat.semester = result[8]
-    apparat.deleted = result[9]
-    apparat.apparat_adis_id = result[11]
-    apparat.prof_adis_id = result[12]
-    return apparat
-
-
-
-def getApparatId(self, apparat_name) ‑> Optional[int] -
-
-

get the id of an apparat based on the name

-

Args

-
-
apparat_name : str
-
the name of the apparat e.g. "Semesterapparat 1"
-
-

Returns

-
-
Optional[int]
-
the id of the apparat, if the apparat is not found, None is returned
-
-
- -Expand source code - -
def getApparatId(self, apparat_name)->Optional[int]:
-    """get the id of an apparat based on the name
-
-    Args:
-        apparat_name (str): the name of the apparat e.g. "Semesterapparat 1"
-
-    Returns:
-        Optional[int]: the id of the apparat, if the apparat is not found, None is returned
-    """
-    data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-    if data is None:
-        return None
-    else:
-        return data[0]
-
-
-
-def getApparatName(self, app_id: Union[str, int], prof_id: Union[str, int]) -
-
-

get the name of the apparat based on the id

-

Args

-
-
app_id : Union[str,int]
-
the id of the apparat
-
prof_id : Union[str,int]
-
the id of the professor
-
-

Returns

-
-
str
-
the name of the apparat
-
-
- -Expand source code - -
def getApparatName(self, app_id:Union[str,int], prof_id:Union[str,int]):
-    """get the name of the apparat based on the id
-
-    Args:
-        app_id (Union[str,int]): the id of the apparat
-        prof_id (Union[str,int]): the id of the professor
-
-    Returns:
-        str: the name of the apparat
-    """
-    return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-
-
-
-def getApparatsByProf(self, prof_id: Union[str, int]) ‑> list[tuple] -
-
-

Get all apparats based on the professor id

-

Args

-
-
prof_id : Union[str,int]
-
the id of the professor
-
-

Returns

-
-
list[tuple]
-
a list of tuples containing the apparats
-
-
- -Expand source code - -
def getApparatsByProf(self, prof_id:Union[str,int])->list[tuple]:
-    """Get all apparats based on the professor id
-
-    Args:
-        prof_id (Union[str,int]): the id of the professor
-
-    Returns:
-        list[tuple]: a list of tuples containing the apparats
-    """
-    return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-
-
-
-def getApparatsBySemester(self, semester: str) ‑> dict[list] -
-
-

get all apparats based on the semester

-

Args

-
-
semester : str
-
the selected semester
-
-

Returns

-
-
dict[list]
-
a list off all created and deleted apparats for the selected semester
-
-
- -Expand source code - -
def getApparatsBySemester(self, semester:str)->dict[list]:
-    """get all apparats based on the semester
-
-    Args:
-        semester (str): the selected semester
-
-    Returns:
-        dict[list]: a list off all created and deleted apparats for the selected semester
-    """
-    data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-    conn = self.connect()
-    cursor = conn.cursor()
-    c_tmp = []
-    for i in data:
-        c_tmp.append((i[0], self.getProfNameById(i[1])))
-    query = (
-        f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-    )
-    result = cursor.execute(query).fetchall()
-    d_tmp = []
-    for i in result:
-        d_tmp.append((i[0], self.getProfNameById(i[1])))
-    # group the apparats by prof
-    c_ret = {}
-    for i in c_tmp:
-        if i[1] not in c_ret.keys():
-            c_ret[i[1]] = [i[0]]
-        else:
-            c_ret[i[1]].append(i[0])
-    d_ret = {}
-    for i in d_tmp:
-        if i[1] not in d_ret.keys():
-            d_ret[i[1]] = [i[0]]
-        else:
-            d_ret[i[1]].append(i[0])
-    self.close_connection(conn)
-    return {"created": c_ret, "deleted": d_ret}
-
-
-
-def getBlob(self, filename, app_id: Union[str, int]) -
-
-

Get a blob from the database

-

Args

-
-
filename : str
-
The name of the file
-
app_id : str
-
ID of the apparat
-
-

Returns

-
-
bytes
-
The file stored in
-
-
- -Expand source code - -
def getBlob(self, filename, app_id:Union[str,int]):
-    """
-    Get a blob from the database
-
-    Args:
-        filename (str): The name of the file
-        app_id (str): ID of the apparat
-
-    Returns:
-        bytes: The file stored in 
-    """
-    return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-
-
-
-def getBook(self, book_id: int) ‑> src.logic.dataclass.BookData -
-
-

Get the book based on the id in the database

-

Args

-
-
book_id : int
-
The id of the book
-
-

Returns

-
-
BookData
-
The metadata of the book wrapped in a BookData object
-
-
- -Expand source code - -
def getBook(self,book_id:int)->BookData:
-    """
-    Get the book based on the id in the database
-
-    Args:
-        book_id (int): The id of the book
-
-    Returns:
-        BookData: The metadata of the book wrapped in a BookData object
-    """
-    return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-
-
-
-def getBookBasedOnSignature(self, app_id: Union[str, int], prof_id: Union[str, int], signature: str) ‑> src.logic.dataclass.BookData -
-
-

Get the book based on the signature of the book.

-

Args

-
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
signature : str
-
The signature of the book
-
-

Returns

-
-
BookData
-
The total metadata of the book wrapped in a BookData object
-
-
- -Expand source code - -
def getBookBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->BookData:
-    """
-    Get the book based on the signature of the book.
-
-    Args:
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-        signature (str): The signature of the book
-
-    Returns:
-        BookData: The total metadata of the book wrapped in a BookData object
-    """
-    result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-    books = [load_pickle(i[0]) for i in result]
-    book = [i for i in books if i.signature == signature][0]
-    return book
-
-
-
-def getBookId(self, bookdata: src.logic.dataclass.BookData, app_id: Union[str, int], prof_id: Union[str, int]) ‑> int -
-
-

Get the id of a book based on the metadata of the book

-

Args

-
-
bookdata : BookData
-
The wrapped metadata of the book
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
-

Returns

-
-
int
-
ID of the book
-
-
- -Expand source code - -
def getBookId(self, bookdata:BookData, app_id:Union[str,int], prof_id:Union[str,int])->int:
-    """
-    Get the id of a book based on the metadata of the book
-
-    Args:
-        bookdata (BookData): The wrapped metadata of the book
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-
-    Returns:
-        int: ID of the book
-    """
-    result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-    return result[0]
-
-
-
-def getBookIdBasedOnSignature(self, app_id: Union[str, int], prof_id: Union[str, int], signature: str) ‑> int -
-
-

Get a book id based on the signature of the book.

-

Args

-
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
signature : str
-
The signature of the book
-
-

Returns

-
-
int
-
The id of the book
-
-
- -Expand source code - -
def getBookIdBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->int:
-    """
-    Get a book id based on the signature of the book.
-
-    Args:
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-        signature (str): The signature of the book
-
-    Returns:
-        int: The id of the book
-    """
-    result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-    books = [(load_pickle(i[0]),i[1]) for i in result]
-    book = [i for i in books if i[0].signature == signature][0][1]
-    return book
-
-
-
-def getBooks(self, app_id: Union[str, int], prof_id: Union[str, int], deleted=0) ‑> list[dict[int, src.logic.dataclass.BookData, int]] -
-
-

Get the Books based on the apparat id and the professor id

-

Args

-
-
app_id : str
-
The ID of the apparat
-
prof_id : str
-
The ID of the professor
-
deleted : int, optional
-
The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-

Returns

-
-
list[dict[int, BookData, int]]
-
A list of dictionaries containing the id, the metadata of the book and the availability of the book
-
-
- -Expand source code - -
def getBooks(self, app_id:Union[str,int], prof_id:Union[str,int], deleted=0)->list[dict[int, BookData, int]]:
-    """
-    Get the Books based on the apparat id and the professor id
-
-    Args:
-        app_id (str): The ID of the apparat
-        prof_id (str): The ID of the professor
-        deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-    Returns:
-        list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-    """
-    qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-    ret_result = []
-    for result_a in qdata:
-        data = {"id": int, "bookdata": BookData, "available": int}
-        data["id"] = result_a[0]
-        data["bookdata"] = load_pickle(result_a[1])
-        data["available"] = result_a[2]
-        ret_result.append(data)
-    return ret_result
-
-
-
-def getFacultyMember(self, name: str) ‑> tuple -
-
-

get a faculty member based on the name

-

Args

-
-
name : str
-
the name to be searched for
-
-

Returns

-
-
tuple
-
a tuple containing the data of the faculty member
-
-
- -Expand source code - -
def getFacultyMember(self, name:str)->tuple:
-    """get a faculty member based on the name
-
-    Args:
-        name (str): the name to be searched for
-
-    Returns:
-        tuple: a tuple containing the data of the faculty member
-    """
-    return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-
-
-
-def getFacultyMembers(self) -
-
-

get a list of all faculty members

-

Returns

-
-
list[tuple]
-
a list of tuples containing the faculty members
-
-
- -Expand source code - -
def getFacultyMembers(self):
-    """get a list of all faculty members
-
-    Returns:
-        list[tuple]: a list of tuples containing the faculty members
-    """
-    return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-
-
-def getFiles(self, app_id: Union[str, int], prof_id: int) ‑> list[tuple] -
-
-

Get all the files associated with the apparat and the professor

-

Args

-
-
app_id : Union[str,int]
-
The id of the apparat
-
prof_id : Union[str,int]
-
the id of the professor
-
-

Returns

-
-
list[tuple]
-
a list of tuples containing the filename and the filetype for the corresponding apparat and professor
-
-
- -Expand source code - -
def getFiles(self, app_id:Union[str,int], prof_id:int)->list[tuple]:
-    """Get all the files associated with the apparat and the professor
-
-    Args:
-        app_id (Union[str,int]): The id of the apparat
-        prof_id (Union[str,int]): the id of the professor
-
-    Returns:
-        list[tuple]: a list of tuples containing the filename and the filetype for the corresponding apparat and professor
-    """
-    return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-
-
-def getLastBookId(self) ‑> int -
-
-

Get the last book id in the database

-

Returns

-
-
int
-
ID of the last book in the database
-
-
- -Expand source code - -
def getLastBookId(self)->int:
-    """
-    Get the last book id in the database
-
-    Returns:
-        int: ID of the last book in the database
-    """
-    return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-
-
-
-def getMessages(self, date: str) ‑> list[dict[str, str, str, str]] -
-
-

Get all the messages for a specific date

-

Args

-
-
date : str
-
a date.datetime object formatted as a string in the format "YYYY-MM-DD"
-
-

Returns

-
-
list[dict[str, str, str, str]]
-
a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message
-
-
- -Expand source code - -
def getMessages(self, date:str)->list[dict[str, str, str, str]]:
-    """Get all the messages for a specific date
-
-    Args:
-        date (str): a date.datetime object formatted as a string in the format "YYYY-MM-DD"
-    
-    Returns:
-        list[dict[str, str, str, str]]: a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message
-    """
-    def __get_user_name(user_id):
-        return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-    messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-    ret = [
-        {
-            "message": i[2],
-            "user": __get_user_name(i[4]),
-            "appnr": i[5],
-            "id": i[0]
-        }
-        for i in messages
-    ]
-    return ret
-
-
-
-def getProfByName(self, prof_name: str) ‑> tuple -
-
-

get all the data of a professor based on the name

-

Args

-
-
prof_name : str
-
the name of the professor
-
-

Returns

-
-
tuple
-
the data of the professor
-
-
- -Expand source code - -
def getProfByName(self, prof_name:str)->tuple:
-    """get all the data of a professor based on the name
-
-    Args:
-        prof_name (str): the name of the professor
-
-    Returns:
-        tuple: the data of the professor
-    """
-    return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-
-
-
-def getProfData(self, profname: str) -
-
-

Get mail, telephone number and title of a professor based on the name

-

Args

-
-
profname : str
-
name of the professor
-
-

Returns

-
-
tuple
-
the mail, telephone number and title of the professor
-
-
- -Expand source code - -
def getProfData(self, profname:str):
-    """Get mail, telephone number and title of a professor based on the name
-
-    Args:
-        profname (str): name of the professor
-
-    Returns:
-        tuple: the mail, telephone number and title of the professor
-    """
-    data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-    return data
-
-
-
-def getProfId(self, prof_name: str) ‑> Optional[int] -
-
-

Get the id of a professor based on the name

-

Args

-
-
prof_name : str
-
the name of the professor
-
-

Returns

-
-
Optional[int]
-
the id of the professor, if the professor is not found, None is returned
-
-
- -Expand source code - -
def getProfId(self, prof_name:str)->Optional[int]:
-    """Get the id of a professor based on the name
-
-    Args:
-        prof_name (str): the name of the professor
-
-    Returns:
-        Optional[int]: the id of the professor, if the professor is not found, None is returned
-    """
-    
-    data = self.getProfByName(prof_name.replace(",", ""))
-    if data is None:
-        return None
-    else:
-        return data[0]
-
-
-
-def getProfNameById(self, prof_id: Union[str, int], add_title: bool = False) ‑> str -
-
-

Get a professor name based on the id

-

Args

-
-
prof_id : Union[str,int]
-
The id of the professor
-
add_title : bool, optional
-
wether to add the title or no. Defaults to False.
-
-

Returns

-
-
str
-
The name of the professor
-
-
- -Expand source code - -
def getProfNameById(self, prof_id:Union[str,int],add_title:bool=False)->str:
-    """Get a professor name based on the id
-
-    Args:
-        prof_id (Union[str,int]): The id of the professor
-        add_title (bool, optional): wether to add the title or no. Defaults to False.
-
-    Returns:
-        str: The name of the professor
-    """
-    prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-    if add_title:
-        return f"{self.getTitleById(prof_id)}{prof[0]}"
-    else:
-        return prof[0]
-
-
-
-def getProfs(self) ‑> list[tuple] -
-
-

Return all the professors in the database

-

Returns

-
-
list[tuple]
-
a list containing all the professors in individual tuples
-
-
- -Expand source code - -
def getProfs(self)->list[tuple]:
-    """Return all the professors in the database
-
-    Returns:
-        list[tuple]: a list containing all the professors in individual tuples
-    """
-    return self.query_db("SELECT * FROM prof")
-
-
-
-def getRole(self, user) -
-
-

get the role of the user

-

Args

-
-
user : str
-
username
-
-

Returns

-
-
str
-
the name of the role
-
-
- -Expand source code - -
def getRole(self, user):
-    """get the role of the user
-
-    Args:
-        user (str): username
-
-    Returns:
-        str: the name of the role
-    """
-    return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-
-
-
-def getRoles(self) ‑> list[tuple] -
-
-

get all the roles in the database

-

Returns

-
-
list[str]
-
a list of all the roles
-
-
- -Expand source code - -
def getRoles(self)->list[tuple]:
-    """get all the roles in the database
-
-    Returns:
-        list[str]: a list of all the roles
-    """
-    return self.query_db("SELECT role FROM user")
-
-
-
-def getSemersters(self) ‑> list[str] -
-
-

Return all the unique semesters in the database

-

Returns

-
-
list
-
a list of strings containing the semesters
-
-
- -Expand source code - -
def getSemersters(self)->list[str]:
-    """Return all the unique semesters in the database
-
-    Returns:
-        list: a list of strings containing the semesters
-    """
-    data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-    return [i[0] for i in data]
-
-
-
-def getSpecificProfData(self, prof_id: Union[str, int], fields: List[str]) ‑> tuple -
-
-

A customisable function to get specific data of a professor based on the id

-

Args

-
-
prof_id : Union[str,int]
-
the id of the professor
-
fields : List[str]
-
a list of fields to be returned
-
-

Returns

-
-
tuple
-
a tuple containing the requested data
-
-
- -Expand source code - -
def getSpecificProfData(self, prof_id:Union[str,int], fields:List[str])->tuple:
-    """A customisable function to get specific data of a professor based on the id
-
-    Args:
-        prof_id (Union[str,int]): the id of the professor
-        fields (List[str]): a list of fields to be returned
-
-    Returns:
-        tuple: a tuple containing the requested data
-    """
-    query = "SELECT "
-    for field in fields:
-        query += f"{field},"
-    query = query[:-1]
-    query += " FROM prof WHERE id=?"
-    return self.query_db(query, (prof_id,), one=True)[0]
-
-
-
-def getSubjects(self) -
-
-

Get all the subjects in the database

-

Returns

-
-
list[tuple]
-
a list of tuples containing the subjects
-
-
- -Expand source code - -
def getSubjects(self):
-    """Get all the subjects in the database
-
-    Returns:
-        list[tuple]: a list of tuples containing the subjects
-    """
-    return self.query_db("SELECT * FROM subjects")
-
-
-
-def getTitleById(self, prof_id: Union[str, int]) ‑> str -
-
-

get the title of a professor based on the id

-

Args

-
-
prof_id : Union[str,int]
-
the id of the professor
-
-

Returns

-
-
str
-
the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned
-
-
- -Expand source code - -
def getTitleById(self, prof_id:Union[str,int])->str:
-    """get the title of a professor based on the id
-
-    Args:
-        prof_id (Union[str,int]): the id of the professor
-
-    Returns:
-        str: the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned
-    """
-    title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-    return f"{title} " if title is not None else ""
-
-
-
-def getUnavailableApparatNumbers(self) ‑> List[int] -
-
-

Get a list of all the apparat numbers in the database that are currently in use

-

Returns

-
-
List[int]
-
the list of used apparat numbers
-
-
- -Expand source code - -
def getUnavailableApparatNumbers(self)->List[int]:
-    """Get a list of all the apparat numbers in the database that are currently in use
-
-    Returns:
-        List[int]: the list of used apparat numbers
-    """
-    numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-    numbers = [i[0] for i in numbers]
-    logger.log_info(f"Currently used apparat numbers: {numbers}")
-    return numbers
-
-
-
-def getUser(self) -
-
-

Get a single user from the database

-
- -Expand source code - -
def getUser(self):
-    """Get a single user from the database"""
-    return self.query_db("SELECT * FROM user", one=True)
-
-
-
-def getUsers(self) ‑> list[tuple] -
-
-

Return a list of tuples of all the users in the database

-
- -Expand source code - -
def getUsers(self)->list[tuple]:
-    """Return a list of tuples of all the users in the database"""
-    return self.query_db("SELECT * FROM user")
-
-
-
-def get_db_contents(self) ‑> Optional[List[Tuple]] -
-
-

Get the contents of the

-

Returns

-
-
Union[List[Tuple], None]
-
description
-
-
- -Expand source code - -
def get_db_contents(self)->Union[List[Tuple], None]:
-    """
-    Get the contents of the 
-
-    Returns:
-        Union[List[Tuple], None]: _description_
-    """
-    try:
-        with sql.connect(self.db_path) as conn:
-            cursor = conn.cursor()
-            cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-            return cursor.fetchall()
-    except sql.OperationalError:
-        return None
-
-
-
-def insertFile(self, file: list[dict], app_id: Union[str, int], prof_id: Union[str, int]) -
-
-

Instert a list of files into the database

-

Args

-
-
file : list[dict]
-
a list containing all the files to be inserted
-
Structured
-
[{"name": "filename", "path": "path", "type": "filetype"}]
-
app_id : int
-
the id of the apparat
-
prof_id : str
-
the id of the professor
-
-
- -Expand source code - -
def insertFile(self, file: list[dict], app_id:Union[str,int], prof_id:Union[str,int]):
-    """Instert a list of files into the database
-
-    Args:
-        file (list[dict]): a list containing all the files to be inserted
-        Structured: [{"name": "filename", "path": "path", "type": "filetype"}]
-        app_id (int): the id of the apparat
-        prof_id (str): the id of the professor
-    """
-    for f in file:
-        filename = f["name"]
-        path = f["path"]
-        filetyp = f["type"]
-        if path == "Database":
-            continue
-        blob = create_blob(path)
-        query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-        self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-
-
-
-def insertInto(self, query: str, params: Tuple) ‑> None -
-
-

Insert sent data into the database

-

Args

-
-
query : str
-
The query to be executed
-
params : Tuple
-
the parameters to be inserted into the database
-
-
- -Expand source code - -
def insertInto(self, query:str, params:Tuple) -> None:
-    """
-    Insert sent data into the database
-
-    Args:
-        query (str): The query to be executed
-        params (Tuple): the parameters to be inserted into the database
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    logger.log_info(f"Inserting {params} into database with query {query}")
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def isEternal(self, id) -
-
-

check if the apparat is eternal (dauerapparat)

-

Args

-
-
id : int
-
the id of the apparat to be checked
-
-

Returns

-
-
int
-
the state of the apparat
-
-
- -Expand source code - -
def isEternal(self, id):
-    """check if the apparat is eternal (dauerapparat)
-
-    Args:
-        id (int): the id of the apparat to be checked
-
-    Returns:
-        int: the state of the apparat
-    """
-    return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-
-
-
-def login(self, user, hashed_password) -
-
-

try to login the user. -The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database

-

Args

-
-
user : str
-
username that tries to login
-
hashed_password : str
-
the password the user tries to login with
-
-

Returns

-
-
bool
-
True if the login was successful, False if not
-
-
- -Expand source code - -
def login(self, user, hashed_password):
-    """try to login the user.
-    The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database 
-
-    Args:
-        user (str): username that tries to login
-        hashed_password (str): the password the user tries to login with
-
-    Returns:
-        bool: True if the login was successful, False if not
-    """
-    salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-    if salt is None:
-        return False
-    hashed_password = salt + hashed_password
-    password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-    if password == hashed_password:
-        return True
-    else:
-        return False
-
-
-
-def query_db(self, query: str, args: Tuple = (), one: bool = False) ‑> Union[Tuple, List[Tuple]] -
-
-

Query the Database for the sent query.

-

Args

-
-
query : str
-
The query to be executed
-
args : Tuple, optional
-
The arguments for the query. Defaults to ().
-
one : bool, optional
-
Return the first result only. Defaults to False.
-
-

Returns

-

Union[Typle|List[Tuple]]: Returns the result of the query

-
- -Expand source code - -
def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-    """
-    Query the Database for the sent query.
-
-    Args:
-        query (str): The query to be executed
-        args (Tuple, optional): The arguments for the query. Defaults to ().
-        one (bool, optional): Return the first result only. Defaults to False.
-
-    Returns:
-        Union[Typle|List[Tuple]]: Returns the result of the query
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    logger.log_info(f"Querying database with query {query}, args: {args}")
-    cursor.execute(query, args)
-    rv = cursor.fetchall()
-    conn.commit()
-    self.close_connection(conn)
-    return (rv[0] if rv else None) if one else rv
-
-
-
-def recreateFile(self, filename: str, app_id: Union[str, int], filetype: str) ‑> str -
-
-

Recreate a file from the database

-

Args

-
-
filename : str
-
the name of the file
-
app_id : Union[str,int]
-
the id of the apparat
-
filetype : str
-
the extension of the file to be created
-
-

Returns

-
-
str
-
The filename of the recreated file
-
-
- -Expand source code - -
def recreateFile(self, filename:str, app_id:Union[str,int],filetype:str)->str:
-    """Recreate a file from the database
-
-    Args:
-        filename (str): the name of the file
-        app_id (Union[str,int]): the id of the apparat
-        filetype (str): the extension of the file to be created
-
-    Returns:
-        str: The filename of the recreated file 
-    """
-    blob = self.getBlob(filename, app_id)
-    tempdir = config.database.tempdir
-    tempdir = tempdir.replace("~", str(Path.home()))
-    tempdir_path = Path(tempdir)        
-    if not os.path.exists(tempdir_path):
-        os.mkdir(tempdir_path)
-    file = tempfile.NamedTemporaryFile(
-        delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-    )
-    file.write(blob)
-    print("file created")
-    return file.name
-
-
-
-def searchBook(self, data: dict[str, str]) ‑> list[tuple[src.logic.dataclass.BookData, int]] -
-
-

Search a book in the database based on the sent data.

-

Args

-
-
data : dict[str, str]
-
A dictionary containing the data to be searched for. The dictionary can contain the following:
-
-
    -
  • signature: The signature of the book
  • -
  • title: The title of the book
  • -
-

Returns

-
-
list[tuple[BookData, int]]
-
A list of tuples containing the wrapped Metadata and the id of the book
-
-
- -Expand source code - -
def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-    """
-    Search a book in the database based on the sent data.
-
-    Args:
-        data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-        - signature: The signature of the book
-        - title: The title of the book
-
-    Returns:
-        list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-    """
-    rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-    #ic(rdata, len(rdata))
-    mode = 0
-    if len(data)== 1:
-        if "signature" in data.keys():
-            mode = 1
-        elif "title" in data.keys():
-            mode = 2
-    elif len(data) == 2:
-        mode = 3
-    else:
-        return None
-    ret = []
-    for book in rdata:
-        bookdata = load_pickle(book[1])
-        app_id = book[2]
-        prof_id = book[3]
-        if mode == 1:
-            if data["signature"] in bookdata.signature:
-                ret.append((bookdata,app_id,prof_id))
-        elif mode == 2:
-            if data["title"] in bookdata.title:
-                ret.append((bookdata,app_id,prof_id))
-        elif mode == 3:
-            if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                ret.append((bookdata,app_id,prof_id))
-    #ic(ret)
-    return ret
-
-
-
-def setAvailability(self, book_id: str, available: str) -
-
-

Set the availability of a book in the database

-

Args

-
-
book_id : str
-
The id of the book
-
available : str
-
The availability of the book
-
-
- -Expand source code - -
def setAvailability(self, book_id:str, available:str):
-    """
-    Set the availability of a book in the database
-
-    Args:
-        book_id (str): The id of the book
-        available (str): The availability of the book
-    """
-    self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-
-
-
-def setNewSemesterDate(self, app_id: Union[str, int], newDate, dauerapp=False) -
-
-

Set the new semester date for an apparat

-

Args

-
-
app_id : Union[str,int]
-
the id of the apparat
-
newDate : str
-
the new date
-
dauerapp : bool, optional
-
if the apparat was changed to dauerapparat. Defaults to False.
-
-
- -Expand source code - -
def setNewSemesterDate(self, app_id:Union[str,int], newDate, dauerapp=False):
-    """Set the new semester date for an apparat
-
-    Args:
-        app_id (Union[str,int]): the id of the apparat
-        newDate (str): the new date
-        dauerapp (bool, optional): if the apparat was changed to dauerapparat. Defaults to False.
-    """
-    date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-    if dauerapp:
-        self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,app_id))
-    else:
-        self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,app_id))
-
-
-
-def statistic_request(self, **kwargs: Any) -
-
-

Take n amount of kwargs and return the result of the query

-
- -Expand source code - -
def statistic_request(self, **kwargs: Any):
-    """Take n amount of kwargs and return the result of the query
-    """
-    def __query(query):
-        """execute the query and return the result
-
-        Args:
-            query (str): the constructed query
-
-        Returns:
-            list: the result of the query
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        result = cursor.execute(query).fetchall()
-        for result_a in result:
-            orig_value = result_a
-            prof_name = self.getProfNameById(result_a[2])
-            # replace the prof_id with the prof_name
-            result_a = list(result_a)
-            result_a[2] = prof_name
-            result_a = tuple(result_a)
-            result[result.index(orig_value)] = result_a
-        self.close_connection(conn)
-        return result
-    if "deletable" in kwargs.keys():
-        query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-        return __query(query)
-    if "dauer" in kwargs.keys():
-        kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-    query = "SELECT * FROM semesterapparat WHERE "
-    for key, value in kwargs.items() if kwargs.items() is not None else {}:
-        print(key, value)
-        query += f"{key}='{value}' AND "
-        print(query)
-    # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-    if "deletesemester" in kwargs.keys():
-        query = query.replace(
-            f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-        )
-    if "endsemester" in kwargs.keys():
-        if "erstellsemester" in kwargs.keys():
-            query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-            query = query.replace(
-                f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-            )
-        else:
-            query = query.replace(
-                f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-            )
-            print("replaced")
-        query = query.replace(
-            "xyz",
-            f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-        )
-    # remove all x="" parts from the query where x is a key in kwargs
-    query = query[:-5]
-    print(query)
-    return __query(query)
-
-
-
-def updateApparat(self, apparat_data: src.logic.dataclass.ApparatData) -
-
-

Update an apparat in the database

-

Args

-
-
apparat_data : ApparatData
-
the new metadata of the apparat
-
-
- -Expand source code - -
def updateApparat(self, apparat_data:ApparatData):
-    """Update an apparat in the database
-
-    Args:
-        apparat_data (ApparatData): the new metadata of the apparat
-    """
-    query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-    params = (
-        apparat_data.appname,
-        apparat_data.app_fach,
-        apparat_data.dauerapp,
-        self.getProfId(apparat_data.profname),
-        apparat_data.appnr,        
-    )
-    self.query_db(query, params)
-
-
-
-def updateBookdata(self, book_id, bookdata: src.logic.dataclass.BookData) -
-
-

Update the bookdata in the database

-

Args

-
-
book_id : str
-
The id of the book
-
bookdata : BookData
-
The new metadata of the book
-
-
- -Expand source code - -
def updateBookdata(self, book_id, bookdata:BookData):
-    """
-    Update the bookdata in the database
-
-    Args:
-        book_id (str): The id of the book
-        bookdata (BookData): The new metadata of the book
-    """
-    self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-
-
-
-def updateFacultyMember(self, data: dict, oldlname: str, oldfname: str) -
-
-

update the data of a faculty member

-

Args

-
-
data : dict
-
a dictionary containing the data to be updated
-
oldlname : str
-
the old last name of the faculty member
-
oldfname : str
-
the old first name of the faculty member
-
-
- -Expand source code - -
def updateFacultyMember(self, data:dict, oldlname:str, oldfname:str):
-    """update the data of a faculty member
-
-    Args:
-        data (dict): a dictionary containing the data to be updated
-        oldlname (str): the old last name of the faculty member
-        oldfname (str): the old first name of the faculty member
-    """
-    placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-    query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-    data["oldlname"] = oldlname
-    data["oldfname"] = oldfname
-    self.query_db(query, data)
-
-
-
-def updateUser(self, username, data: dict[str, str]) -
-
-

changge the data of a user

-

Args

-
-
username : str
-
the username of the user
-
data : dict[str, str]
-
the data to be changed
-
-
- -Expand source code - -
def updateUser(self, username, data:dict[str, str]):
-    """changge the data of a user
-
-    Args:
-        username (str): the username of the user
-        data (dict[str, str]): the data to be changed
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    query = "UPDATE user SET "
-    for key,value in data.items():
-        if key == "username":
-            continue
-        query += f"{key}='{value}',"
-    query = query[:-1]
-    query += " WHERE username=?"
-    params = (username,)
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs - Kopie/database.md b/docs - Kopie/database.md deleted file mode 100644 index c518b09..0000000 --- a/docs - Kopie/database.md +++ /dev/null @@ -1,327 +0,0 @@ -Module database -=============== - -Classes -------- - -`Database(db_path: str = None)` -: Initialize the database and create the tables if they do not exist. - - Default constructor for the database class - - Args: - db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None. - - ### Methods - - `addBookToDatabase(self, bookdata: src.logic.dataclass.BookData, app_id: str, prof_id: str)` - : Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on. - - Args: - bookdata (BookData): The metadata of the book to be added - app_id (str): The apparat id where the book should be added to - prof_id (str): The id of the professor where the book should be added to. - - `addMessage(self, message: dict, user, appnr)` - : - - `changePassword(self, user, new_password)` - : - - `checkApparatExists(self, apparat_name)` - : - - `checkApparatExistsById(self, apparat_id)` - : - - `checkUsername(self, user)` - : - - `close_connection(self, conn: sqlite3.Connection)` - : closes the connection to the database - - Args: - ---- - - conn (sql.Connection): the connection to be closed - - `connect(self) ‑> sqlite3.Connection` - : Connect to the database - - Returns: - sql.Connection: The active connection to the database - - `createApparat(self, apparat: src.logic.dataclass.ApparatData) ‑> Union[src.errors.DatabaseErrors.AppPresentError, ForwardRef(None), int]` - : - - `createProf(self, prof_details: dict)` - : - - `createUser(self, user, password, role, salt)` - : Create a user based on passed data. - - Args: - ---- - - username (str): Username to be used - - password (str): the salted password - - role (str): Role of the user - - salt (str): a random salt for the user - - `create_tables(self)` - : Create the tables in the database - - `deleteApparat(self, appnr, semester)` - : - - `deleteBook(self, book_id)` - : Delete a book from the database - - Args: - book_id (str): ID of the book - - `deleteMessage(self, message_id)` - : - - `deleteUser(self, user)` - : - - `getAllAparats(self, deleted=0)` - : - - `getApparatCountBySemester(self) ‑> tuple[list[str], list[int]]` - : - - `getApparatData(self, appnr, appname) ‑> src.logic.dataclass.ApparatData` - : - - `getApparatId(self, apparat_name)` - : - - `getApparatName(self, app_id, prof_id)` - : - - `getApparatsByProf(self, prof_id: int) ‑> list[tuple]` - : - - `getApparatsBySemester(self, semester: str) ‑> dict` - : - - `getBlob(self, filename, app_id)` - : Get a blob from the database - - Args: - filename (str): The name of the file - app_id (str): ID of the apparat - - Returns: - bytes: The file stored in - - `getBook(self, book_id: int) ‑> src.logic.dataclass.BookData` - : Get the book based on the id in the database - - Args: - book_id (int): The id of the book - - Returns: - BookData: The metadata of the book wrapped in a BookData object - - `getBookBasedOnSignature(self, app_id: str, prof_id: str, signature: str) ‑> src.logic.dataclass.BookData` - : Get the book based on the signature of the book. - - Args: - app_id (str): The apparat id the book should be associated with - prof_id (str): The professor id the book should be associated with - signature (str): The signature of the book - - Returns: - BookData: The total metadata of the book wrapped in a BookData object - - `getBookId(self, bookdata: src.logic.dataclass.BookData, app_id, prof_id) ‑> int` - : Get the id of a book based on the metadata of the book - - Args: - bookdata (BookData): The wrapped metadata of the book - app_id (str): The apparat id the book should be associated with - prof_id (str): The professor id the book should be associated with - - Returns: - int: ID of the book - - `getBookIdBasedOnSignature(self, app_id: str, prof_id: str, signature: str) ‑> int` - : Get a book id based on the signature of the book. - - Args: - app_id (str): The apparat id the book should be associated with - prof_id (str): The professor id the book should be associated with - signature (str): The signature of the book - - Returns: - int: The id of the book - - `getBooks(self, app_id, prof_id, deleted=0) ‑> list[dict[int, src.logic.dataclass.BookData, int]]` - : Get the Books based on the apparat id and the professor id - - Args: - app_id (str): The ID of the apparat - prof_id (str): The ID of the professor - deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0. - - Returns: - list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book - - `getFacultyMember(self, name: str)` - : - - `getFacultyMembers(self)` - : - - `getFiles(self, app_id: int, prof_id: int) ‑> list[tuple]` - : - - `getLastBookId(self) ‑> int` - : Get the last book id in the database - - Returns: - int: ID of the last book in the database - - `getMessages(self, date: str)` - : - - `getProfByName(self, prof_name: str)` - : - - `getProfData(self, profname: str)` - : - - `getProfId(self, prof_name: str)` - : getProfId _summary_ - - :param prof_name: _description_ - :type prof_name: str - :return: _description_ - :rtype: _type_ - - `getProfNameById(self, prof_id: int, add_title: bool = False)` - : - - `getProfs(self)` - : - - `getRole(self, user)` - : - - `getRoles(self)` - : - - `getSemersters(self)` - : - - `getSpecificProfData(self, prof_id: int, fields: List[str])` - : getSpecificProfData _summary_ - - - - Args: - ---- - - prof_id (int): _description_ - - fields (List[str]): _description_ - - Returns: - ------- - - _type_: _description_ - - `getSubjects(self)` - : - - `getTitleById(self, prof_id: int)` - : - - `getUnavailableApparatNumbers(self) ‑> List[int]` - : getUnavailableApparatNumbers returns a list of all currently used ApparatNumbers - - - - Returns: - ------- - - number(List[int]): a list of all currently used apparat numbers - - `getUser(self)` - : - - `getUsers(self)` - : - - `get_db_contents(self) ‑> Optional[List[Tuple]]` - : Get the contents of the - - Returns: - Union[List[Tuple], None]: _description_ - - `insertFile(self, file: list[dict], app_id: int, prof_id)` - : - - `insertInto(self, query: str, params: Tuple) ‑> None` - : Insert sent data into the database - - Args: - query (str): The query to be executed - params (Tuple): the parameters to be inserted into the database - - `isEternal(self, id)` - : - - `login(self, user, hashed_password)` - : - - `query_db(self, query: str, args: Tuple = (), one: bool = False) ‑> Union[Tuple, List[Tuple]]` - : Query the Database for the sent query. - - Args: - query (str): The query to be executed - args (Tuple, optional): The arguments for the query. Defaults to (). - one (bool, optional): Return the first result only. Defaults to False. - - Returns: - Union[Typle|List[Tuple]]: Returns the result of the query - - `recreateFile(self, filename, app_id, filetype)` - : - - `searchBook(self, data: dict[str, str]) ‑> list[tuple[src.logic.dataclass.BookData, int]]` - : Search a book in the database based on the sent data. - - Args: - data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: - - signature: The signature of the book - - title: The title of the book - - Returns: - list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book - - `setAvailability(self, book_id: str, available: str)` - : Set the availability of a book in the database - - Args: - book_id (str): The id of the book - available (str): The availability of the book - - `setNewSemesterDate(self, appnr, newDate, dauerapp=False)` - : - - `statistic_request(self, **kwargs: Any)` - : - - `updateApparat(self, apparat_data: src.logic.dataclass.ApparatData)` - : - - `updateBookdata(self, book_id, bookdata: src.logic.dataclass.BookData)` - : Update the bookdata in the database - - Args: - book_id (str): The id of the book - bookdata (BookData): The new metadata of the book - - `updateFacultyMember(self, data, oldlname, oldfname)` - : - - `updateUser(self, username, data: dict[str, str])` - : \ No newline at end of file diff --git a/docs - Kopie/db.html b/docs - Kopie/db.html deleted file mode 100644 index f2b125f..0000000 --- a/docs - Kopie/db.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - -db API documentation - - - - - - - - - - - -
-
-
-

Module db

-
-
-

Module db provides the database schema for the semesterapparat application.

-
-
-
- -Expand source code - -
CREATE_TABLE_APPARAT = """CREATE TABLE semesterapparat (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    name TEXT,
-    prof_id INTEGER,
-    fach TEXT,
-    appnr INTEGER,
-    erstellsemester TEXT,
-    verlängert_am TEXT,
-    dauer BOOLEAN,
-    verlängerung_bis TEXT,
-    deletion_status INTEGER,
-    deleted_date TEXT,
-    apparat_id_adis INTEGER,
-    prof_id_adis INTEGER,
-    konto INTEGER REFERENCES app_kontos (id),
-    FOREIGN KEY (prof_id) REFERENCES prof (id)
-  )"""
-CREATE_TABLE_MEDIA = """CREATE TABLE media (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    bookdata BLOB,
-    app_id INTEGER,
-    prof_id INTEGER,
-    deleted INTEGER DEFAULT (0),
-    available BOOLEAN,
-    reservation BOOLEAN,
-    FOREIGN KEY (prof_id) REFERENCES prof (id),
-    FOREIGN KEY (app_id) REFERENCES semesterapparat (id)
-  )"""
-CREATE_TABLE_APPKONTOS = """CREATE TABLE app_kontos (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    app_id INTEGER,
-    konto INTEGER,
-    passwort TEXT,
-    FOREIGN KEY (app_id) REFERENCES semesterapparat (id)
-    )"""
-CREATE_TABLE_FILES = """CREATE TABLE files (
-    id INTEGER PRIMARY KEY,
-    filename TEXT,
-    fileblob BLOB,
-    app_id INTEGER,
-    filetyp TEXT,
-    prof_id INTEGER REFERENCES prof (id),
-    FOREIGN KEY (app_id) REFERENCES semesterapparat (id)
-    )"""
-CREATE_TABLE_MESSAGES = """CREATE TABLE messages (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    created_at date NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    message TEXT NOT NULL,
-    remind_at date NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    user_id INTEGER NOT NULL,
-    appnr INTEGER,
-    FOREIGN KEY (user_id) REFERENCES user (id)
-  )"""
-CREATE_TABLE_PROF = """CREATE TABLE prof (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    titel TEXT,
-    fname TEXT,
-    lname TEXT,
-    fullname TEXT NOT NULL UNIQUE,
-    mail TEXT,
-    telnr TEXT
-  )"""
-CREATE_TABLE_USER = """CREATE TABLE user (
-    id integer NOT NULL PRIMARY KEY AUTOINCREMENT,
-    created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    username TEXT NOT NULL UNIQUE,
-    password TEXT NOT NULL,
-    salt TEXT NOT NULL,
-    role TEXT NOT NULL,
-    email TEXT UNIQUE,
-    name TEXT
-  )"""
-CREATE_TABLE_SUBJECTS = """CREATE TABLE subjects (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    name TEXT NOT NULL UNIQUE
-)"""
-
-
-
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs - Kopie/db.md b/docs - Kopie/db.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs - Kopie/index.md b/docs - Kopie/index.md deleted file mode 100644 index a77c9f7..0000000 --- a/docs - Kopie/index.md +++ /dev/null @@ -1,18 +0,0 @@ -# Welcome to MkDocs - -For full documentation visit [mkdocs.org](https://www.mkdocs.org). - -## Commands - -* `mkdocs new [dir-name]` - Create a new project. -* `mkdocs serve` - Start the live-reloading docs server. -* `mkdocs build` - Build the documentation site. -* `mkdocs -h` - Print help message and exit. - -## Project layout - - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - database.md # Documentation of the Database and it's tables - ... # Other markdown pages, images and other files. diff --git a/docs/database.html b/docs/database.html deleted file mode 100644 index 7be3ffd..0000000 --- a/docs/database.html +++ /dev/null @@ -1,4266 +0,0 @@ - - - - - - -database API documentation - - - - - - - - - - - -
-
-
-

Module database

-
-
-
- -Expand source code - -
import datetime
-import os
-import re
-import sqlite3 as sql
-import tempfile
-from src.logic.log import MyLogger
-from icecream import ic
-from typing import List, Tuple, Dict, Any, Optional, Union
-from pathlib import Path
-from omegaconf import OmegaConf
-from src.backend.db import CREATE_TABLE_APPARAT, CREATE_TABLE_MESSAGES, CREATE_TABLE_MEDIA, CREATE_TABLE_APPKONTOS, CREATE_TABLE_FILES, CREATE_TABLE_PROF, CREATE_TABLE_USER, CREATE_TABLE_SUBJECTS
-from src.logic.constants import SEMAP_MEDIA_ACCOUNTS
-from src.logic.dataclass import ApparatData, BookData
-from src.errors import NoResultError, AppPresentError
-from src.utils import load_pickle, dump_pickle,create_blob
-config = OmegaConf.load("config.yaml")
-logger = MyLogger(__name__)
-
-
-class Database:
-    """
-    Initialize the database and create the tables if they do not exist.
-    """
-    def __init__(self, db_path: str = None):
-        """
-        Default constructor for the database class
-
-        Args:
-            db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None.
-        """
-        if db_path is None:
-            self.db_path = config.database.path + config.database.name
-            self.db_path = self.db_path.replace("~", str(Path.home()))
-        else:
-            self.db_path = db_path
-        if self.get_db_contents() is None:
-            logger.log_critical("Database does not exist, creating tables")
-            self.create_tables()
-    def get_db_contents(self)->Union[List[Tuple], None]:
-        """
-        Get the contents of the 
-
-        Returns:
-            Union[List[Tuple], None]: _description_
-        """
-        try:
-            with sql.connect(self.db_path) as conn:
-                cursor = conn.cursor()
-                cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-                return cursor.fetchall()
-        except sql.OperationalError:
-            return None
-    def connect(self)->sql.Connection:
-        """
-        Connect to the database
-
-        Returns:
-            sql.Connection: The active connection to the database
-        """
-        return sql.connect(self.db_path)
-    def close_connection(self, conn: sql.Connection):
-        """
-        closes the connection to the database
-        
-        Args:
-        ----
-            - conn (sql.Connection): the connection to be closed
-        """
-        conn.close()
-    def create_tables(self):
-        """
-        Create the tables in the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        cursor.execute(CREATE_TABLE_APPARAT)
-        cursor.execute(CREATE_TABLE_MESSAGES)
-        cursor.execute(CREATE_TABLE_MEDIA)
-        cursor.execute(CREATE_TABLE_APPKONTOS)
-        cursor.execute(CREATE_TABLE_FILES)
-        cursor.execute(CREATE_TABLE_PROF)
-        cursor.execute(CREATE_TABLE_USER)
-        cursor.execute(CREATE_TABLE_SUBJECTS)
-        conn.commit()
-        self.close_connection(conn)
-    def insertInto(self, query:str, params:Tuple) -> None:
-        """
-        Insert sent data into the database
-
-        Args:
-            query (str): The query to be executed
-            params (Tuple): the parameters to be inserted into the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Inserting {params} into database with query {query}")
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-        """
-        Query the Database for the sent query.
-
-        Args:
-            query (str): The query to be executed
-            args (Tuple, optional): The arguments for the query. Defaults to ().
-            one (bool, optional): Return the first result only. Defaults to False.
-
-        Returns:
-            Union[Typle|List[Tuple]]: Returns the result of the query
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Querying database with query {query}, args: {args}")
-        cursor.execute(query, args)
-        rv = cursor.fetchall()
-        conn.commit()
-        self.close_connection(conn)
-        return (rv[0] if rv else None) if one else rv
-
-    # Books
-    def addBookToDatabase(self, bookdata:BookData,app_id:Union[str,int], prof_id:Union[str,int]):
-        """
-        Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-        Args:
-            bookdata (BookData): The metadata of the book to be added
-            app_id (str): The apparat id where the book should be added to
-            prof_id (str): The id of the professor where the book should be added to.
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        t_query = (
-            f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-        )
-        # print(t_query)
-        result = cursor.execute(t_query).fetchall()
-        result = [load_pickle(i[0]) for i in result]
-        if bookdata in result:
-            print("Bookdata already in database")
-            # check if the book was deleted in the apparat
-            query = (
-                "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-            )
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            result = cursor.execute(query, params).fetchone()
-            if result[0] == 1:
-                print("Book was deleted, updating bookdata")
-                query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-                params = (app_id, prof_id, dump_pickle(bookdata))
-                cursor.execute(query, params)
-                conn.commit()
-            return
-
-        query = (
-            "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-        )
-        converted = dump_pickle(bookdata)
-        params = (converted, app_id, prof_id, 0)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getBookIdBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->int:
-        """
-        Get a book id based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            int: The id of the book
-        """
-        result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [(load_pickle(i[0]),i[1]) for i in result]
-        book = [i for i in books if i[0].signature == signature][0][1]
-        return book
-    def getBookBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->BookData:
-        """
-        Get the book based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            BookData: The total metadata of the book wrapped in a BookData object
-        """
-        result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [load_pickle(i[0]) for i in result]
-        book = [i for i in books if i.signature == signature][0]
-        return book
-    def getLastBookId(self)->int:
-        """
-        Get the last book id in the database
-
-        Returns:
-            int: ID of the last book in the database
-        """
-        return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-    def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-        """
-        Search a book in the database based on the sent data.
-
-        Args:
-            data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-            - signature: The signature of the book
-            - title: The title of the book
-
-        Returns:
-            list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-        """
-        rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-        ic(rdata, len(rdata))
-        mode = 0
-        if len(data)== 1:
-            if "signature" in data.keys():
-                mode = 1
-            elif "title" in data.keys():
-                mode = 2
-        elif len(data) == 2:
-            mode = 3
-        else:
-            return None
-        ret = []
-        for book in rdata:
-            bookdata = load_pickle(book[1])
-            app_id = book[2]
-            prof_id = book[3]
-            if mode == 1:
-                if data["signature"] in bookdata.signature:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 2:
-                if data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 3:
-                if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-        ic(ret)
-        return ret
-    def setAvailability(self, book_id:str, available:str):
-        """
-        Set the availability of a book in the database
-
-        Args:
-            book_id (str): The id of the book
-            available (str): The availability of the book
-        """
-        self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-    def getBookId(self, bookdata:BookData, app_id:Union[str,int], prof_id:Union[str,int])->int:
-        """
-        Get the id of a book based on the metadata of the book
-
-        Args:
-            bookdata (BookData): The wrapped metadata of the book
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-
-        Returns:
-            int: ID of the book
-        """
-        result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-        return result[0]
-    def getBook(self,book_id:int)->BookData:
-        """
-        Get the book based on the id in the database
-
-        Args:
-            book_id (int): The id of the book
-
-        Returns:
-            BookData: The metadata of the book wrapped in a BookData object
-        """
-        return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-    def getBooks(self, app_id:Union[str,int], prof_id:Union[str,int], deleted=0)->list[dict[int, BookData, int]]:
-        """
-        Get the Books based on the apparat id and the professor id
-
-        Args:
-            app_id (str): The ID of the apparat
-            prof_id (str): The ID of the professor
-            deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-        Returns:
-            list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-        """
-        qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-        ret_result = []
-        for result_a in qdata:
-            data = {"id": int, "bookdata": BookData, "available": int}
-            data["id"] = result_a[0]
-            data["bookdata"] = load_pickle(result_a[1])
-            data["available"] = result_a[2]
-            ret_result.append(data)
-        return ret_result
-    def updateBookdata(self, book_id, bookdata:BookData):
-        """
-        Update the bookdata in the database
-
-        Args:
-            book_id (str): The id of the book
-            bookdata (BookData): The new metadata of the book
-        """
-        self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-    def deleteBook(self, book_id):
-        """
-        Delete a book from the database
-
-        Args:
-            book_id (str): ID of the book
-        """
-        self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-    # File Interactions
-    def getBlob(self, filename, app_id:Union[str,int]):
-        """
-        Get a blob from the database
-
-        Args:
-            filename (str): The name of the file
-            app_id (str): ID of the apparat
-
-        Returns:
-            bytes: The file stored in 
-        """
-        return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-    def insertFile(self, file: list[dict], app_id:Union[str,int], prof_id:Union[str,int]):
-        """Instert a list of files into the database
-
-        Args:
-            file (list[dict]): a list containing all the files to be inserted
-            Structured: [{"name": "filename", "path": "path", "type": "filetype"}]
-            app_id (int): the id of the apparat
-            prof_id (str): the id of the professor
-        """
-        for f in file:
-            filename = f["name"]
-            path = f["path"]
-            filetyp = f["type"]
-            if path == "Database":
-                continue
-            blob = create_blob(path)
-            query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-            self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-    def recreateFile(self, filename:str, app_id:Union[str,int],filetype:str)->str:
-        """Recreate a file from the database
-
-        Args:
-            filename (str): the name of the file
-            app_id (Union[str,int]): the id of the apparat
-            filetype (str): the extension of the file to be created
-
-        Returns:
-            str: The filename of the recreated file 
-        """
-        blob = self.getBlob(filename, app_id)
-        tempdir = config.database.tempdir
-        tempdir = tempdir.replace("~", str(Path.home()))
-        tempdir_path = Path(tempdir)        
-        if not os.path.exists(tempdir_path):
-            os.mkdir(tempdir_path)
-        file = tempfile.NamedTemporaryFile(
-            delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-        )
-        file.write(blob)
-        print("file created")
-        return file.name
-    def getFiles(self, app_id:Union[str,int], prof_id:int)->list[tuple]:
-        """Get all the files associated with the apparat and the professor
-
-        Args:
-            app_id (Union[str,int]): The id of the apparat
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            list[tuple]: a list of tuples containing the filename and the filetype for the corresponding apparat and professor
-        """
-        return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-    def getSemersters(self)->list[str]:
-        """Return all the unique semesters in the database
-
-        Returns:
-            list: a list of strings containing the semesters
-        """
-        data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-        return [i[0] for i in data]
-
-    def getSubjects(self):
-        """Get all the subjects in the database
-
-        Returns:
-            list[tuple]: a list of tuples containing the subjects
-        """
-        return self.query_db("SELECT * FROM subjects")
-
-    # Messages
-    def addMessage(self, message:dict,user:str, app_id:Union[str,int]):
-        """add a Message to the database
-
-        Args:
-            message (dict): the message to be added
-            user (str): the user who added the message
-            app_id (Union[str,int]): the id of the apparat
-        """
-        def __getUserId(user):
-            return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-        user_id = __getUserId(user)
-        self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],app_id))
-    def getMessages(self, date:str)->list[dict[str, str, str, str]]:
-        """Get all the messages for a specific date
-
-        Args:
-            date (str): a date.datetime object formatted as a string in the format "YYYY-MM-DD"
-        
-        Returns:
-            list[dict[str, str, str, str]]: a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message
-        """
-        def __get_user_name(user_id):
-            return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-        messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-        ret = [
-            {
-                "message": i[2],
-                "user": __get_user_name(i[4]),
-                "appnr": i[5],
-                "id": i[0]
-            }
-            for i in messages
-        ]
-        return ret
-    def deleteMessage(self, message_id):
-        """Delete a message from the database
-
-        Args:
-            message_id (str): the id of the message
-        """
-        self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-    # Prof data
-    def getProfNameById(self, prof_id:Union[str,int],add_title:bool=False)->str:
-        """Get a professor name based on the id
-
-        Args:
-            prof_id (Union[str,int]): The id of the professor
-            add_title (bool, optional): wether to add the title or no. Defaults to False.
-
-        Returns:
-            str: The name of the professor
-        """
-        prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-        if add_title:
-            return f"{self.getTitleById(prof_id)}{prof[0]}"
-        else:
-            return prof[0]
-    def getTitleById(self, prof_id:Union[str,int])->str:
-        """get the title of a professor based on the id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            str: the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned
-        """
-        title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-        return f"{title} " if title is not None else ""
-    def getProfByName(self, prof_name:str)->tuple:
-        """get all the data of a professor based on the name
-
-        Args:
-            prof_name (str): the name of the professor
-
-        Returns:
-            tuple: the data of the professor
-        """
-        return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-    def getProfId(self, prof_name:str)->Optional[int]:
-        """Get the id of a professor based on the name
-
-        Args:
-            prof_name (str): the name of the professor
-
-        Returns:
-            Optional[int]: the id of the professor, if the professor is not found, None is returned
-        """
-        
-        data = self.getProfByName(prof_name.replace(",", ""))
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def getSpecificProfData(self, prof_id:Union[str,int], fields:List[str])->tuple:
-        """A customisable function to get specific data of a professor based on the id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-            fields (List[str]): a list of fields to be returned
-
-        Returns:
-            tuple: a tuple containing the requested data
-        """
-        query = "SELECT "
-        for field in fields:
-            query += f"{field},"
-        query = query[:-1]
-        query += " FROM prof WHERE id=?"
-        return self.query_db(query, (prof_id,), one=True)[0]
-    def getProfData(self, profname:str):
-        """Get mail, telephone number and title of a professor based on the name
-
-        Args:
-            profname (str): name of the professor
-
-        Returns:
-            tuple: the mail, telephone number and title of the professor
-        """
-        data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-        return data
-    def createProf(self, prof_details:dict):
-        """Create a professor in the database
-
-        Args:
-            prof_details (dict): a dictionary containing the details of the professor
-        """
-        prof_title = prof_details["prof_title"]
-        prof_fname = prof_details["profname"].split(",")[1]
-        prof_fname = prof_fname.strip()
-        prof_lname = prof_details["profname"].split(",")[0]
-        prof_lname = prof_lname.strip()
-        prof_fullname = prof_details["profname"].replace(",", "")
-        prof_mail = prof_details["prof_mail"]
-        prof_tel = prof_details["prof_tel"]
-        params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-        query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-        self.insertInto(query=query, params=params)
-    def getProfs(self)->list[tuple]:
-        """Return all the professors in the database
-
-        Returns:
-            list[tuple]: a list containing all the professors in individual tuples
-        """
-        return self.query_db("SELECT * FROM prof")
-
-    # Apparat
-    def getAllAparats(self,deleted=0)->list[tuple]:
-        """Get all the apparats in the database
-
-        Args:
-            deleted (int, optional): Switch the result to use . Defaults to 0.
-
-        Returns:
-            list[tuple]: a list of tuples containing the apparats
-        """
-        return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-    def getApparatData(self, appnr, appname)->ApparatData:
-        """Get the Apparat data based on the apparat number and the name
-
-        Args:
-            appnr (str): the apparat number
-            appname (str): the name of the apparat
-
-        Raises:
-            NoResultError: an error is raised if no result is found
-
-        Returns:
-            ApparatData: the appended data of the apparat wrapped in an ApparatData object
-        """
-        result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-        if result is None:
-            raise NoResultError("No result found")
-        apparat = ApparatData()
-        apparat.appname = result[1]
-        apparat.appnr = result[4]
-        apparat.dauerapp = True if result[7] == 1 else False
-        prof_data = self.getProfData(self.getProfNameById(result[2]))
-        apparat.profname = self.getProfNameById(result[2])
-        apparat.prof_mail = prof_data[0]
-        apparat.prof_tel = prof_data[1]
-        apparat.prof_title = prof_data[2]
-        apparat.app_fach = result[3]
-        apparat.erstellsemester = result[5]
-        apparat.semester = result[8]
-        apparat.deleted = result[9]
-        apparat.apparat_adis_id = result[11]
-        apparat.prof_adis_id = result[12]
-        return apparat
-    def getUnavailableApparatNumbers(self)->List[int]:
-        """Get a list of all the apparat numbers in the database that are currently in use
-
-        Returns:
-            List[int]: the list of used apparat numbers
-        """
-        numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-        numbers = [i[0] for i in numbers]
-        logger.log_info(f"Currently used apparat numbers: {numbers}")
-        return numbers
-    def setNewSemesterDate(self, app_id:Union[str,int], newDate, dauerapp=False):
-        """Set the new semester date for an apparat
-
-        Args:
-            app_id (Union[str,int]): the id of the apparat
-            newDate (str): the new date
-            dauerapp (bool, optional): if the apparat was changed to dauerapparat. Defaults to False.
-        """
-        date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-        if dauerapp:
-            self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,app_id))
-        else:
-            self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,app_id))
-    def getApparatId(self, apparat_name)->Optional[int]:
-        """get the id of an apparat based on the name
-
-        Args:
-            apparat_name (str): the name of the apparat e.g. "Semesterapparat 1"
-
-        Returns:
-            Optional[int]: the id of the apparat, if the apparat is not found, None is returned
-        """
-        data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def createApparat(self, apparat:ApparatData)->int:
-        """create the apparat in the database
-
-        Args:
-            apparat (ApparatData): the wrapped metadata of the apparat
-
-        Raises:
-            AppPresentError: an error describing that the apparats chosen id is already present in the database
-
-        Returns:
-            Optional[int]: the id of the apparat
-        """
-                
-        prof_id = self.getProfId(apparat.profname)
-        app_id = self.getApparatId(apparat.appname)
-        if app_id:
-            raise AppPresentError(app_id)
-
-        self.createProf(apparat.get_prof_details())
-        prof_id = self.getProfId(apparat.profname)
-        ic(prof_id)
-        query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-        logger.log_info(query)
-        self.query_db(query)
-        return self.getApparatId(apparat.appname)
-    def getApparatsByProf(self, prof_id:Union[str,int])->list[tuple]:
-        """Get all apparats based on the professor id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            list[tuple]: a list of tuples containing the apparats
-        """
-        return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-    def getApparatsBySemester(self, semester:str)->dict[list]:
-        """get all apparats based on the semester
-
-        Args:
-            semester (str): the selected semester
-
-        Returns:
-            dict[list]: a list off all created and deleted apparats for the selected semester
-        """
-        data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-        conn = self.connect()
-        cursor = conn.cursor()
-        c_tmp = []
-        for i in data:
-            c_tmp.append((i[0], self.getProfNameById(i[1])))
-        query = (
-            f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-        )
-        result = cursor.execute(query).fetchall()
-        d_tmp = []
-        for i in result:
-            d_tmp.append((i[0], self.getProfNameById(i[1])))
-        # group the apparats by prof
-        c_ret = {}
-        for i in c_tmp:
-            if i[1] not in c_ret.keys():
-                c_ret[i[1]] = [i[0]]
-            else:
-                c_ret[i[1]].append(i[0])
-        d_ret = {}
-        for i in d_tmp:
-            if i[1] not in d_ret.keys():
-                d_ret[i[1]] = [i[0]]
-            else:
-                d_ret[i[1]].append(i[0])
-        self.close_connection(conn)
-        return {"created": c_ret, "deleted": d_ret}
-    def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-        """get a list of all apparats created and deleted by semester
-
-        Returns:
-            tuple[list[str],list[int]]: a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        semesters = self.getSemersters()
-        created = []
-        deleted = []
-        for semester in semesters:
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-            result = cursor.execute(query).fetchone()
-            created.append(result[0])
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-            result = cursor.execute(query).fetchone()
-            deleted.append(result[0])
-        # store data in a tuple
-        ret = []
-        e_tuple = ()
-        for sem in semesters:
-            e_tuple = (
-                sem,
-                created[semesters.index(sem)],
-                deleted[semesters.index(sem)],
-            )
-            ret.append(e_tuple)
-        self.close_connection(conn)
-        return ret 
-    def deleteApparat(self, app_id:Union[str,int], semester:str):
-        """Delete an apparat from the database
-
-        Args:
-            app_id (Union[str, int]): the id of the apparat
-            semester (str): the semester the apparat should be deleted from
-        """
-        self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,app_id)) 
-    def isEternal(self, id):
-        """check if the apparat is eternal (dauerapparat)
-
-        Args:
-            id (int): the id of the apparat to be checked
-
-        Returns:
-            int: the state of the apparat
-        """
-        return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-    def getApparatName(self, app_id:Union[str,int], prof_id:Union[str,int]):
-        """get the name of the apparat based on the id
-
-        Args:
-            app_id (Union[str,int]): the id of the apparat
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            str: the name of the apparat
-        """
-        return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-    def updateApparat(self, apparat_data:ApparatData):
-        """Update an apparat in the database
-
-        Args:
-            apparat_data (ApparatData): the new metadata of the apparat
-        """
-        query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-        params = (
-            apparat_data.appname,
-            apparat_data.app_fach,
-            apparat_data.dauerapp,
-            self.getProfId(apparat_data.profname),
-            apparat_data.appnr,        
-        )
-        self.query_db(query, params)
-    def checkApparatExists(self, apparat_name:str):
-        """check if the apparat is already present in the database based on the name
-
-        Args:
-            apparat_name (str): the name of the apparat
-
-        Returns:
-            bool: True if the apparat is present, False if not
-        """
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-    def checkApparatExistsById(self, app_id:Union[str,int])->bool:
-        """a check to see if the apparat is already present in the database, based on the id
-
-        Args:
-            app_id (Union[str, int]): the id of the apparat
-
-        Returns:
-            bool: True if the apparat is present, False if not
-        """
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (app_id,), one=True) else False
-    # Statistics
-    def statistic_request(self, **kwargs: Any):
-        """Take n amount of kwargs and return the result of the query
-        """
-        def __query(query):
-            """execute the query and return the result
-
-            Args:
-                query (str): the constructed query
-
-            Returns:
-                list: the result of the query
-            """
-            conn = self.connect()
-            cursor = conn.cursor()
-            result = cursor.execute(query).fetchall()
-            for result_a in result:
-                orig_value = result_a
-                prof_name = self.getProfNameById(result_a[2])
-                # replace the prof_id with the prof_name
-                result_a = list(result_a)
-                result_a[2] = prof_name
-                result_a = tuple(result_a)
-                result[result.index(orig_value)] = result_a
-            self.close_connection(conn)
-            return result
-        if "deletable" in kwargs.keys():
-            query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-            return __query(query)
-        if "dauer" in kwargs.keys():
-            kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-        query = "SELECT * FROM semesterapparat WHERE "
-        for key, value in kwargs.items() if kwargs.items() is not None else {}:
-            print(key, value)
-            query += f"{key}='{value}' AND "
-            print(query)
-        # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-        if "deletesemester" in kwargs.keys():
-            query = query.replace(
-                f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-            )
-        if "endsemester" in kwargs.keys():
-            if "erstellsemester" in kwargs.keys():
-                query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-                query = query.replace(
-                    f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-                )
-            else:
-                query = query.replace(
-                    f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-                )
-                print("replaced")
-            query = query.replace(
-                "xyz",
-                f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-            )
-        # remove all x="" parts from the query where x is a key in kwargs
-        query = query[:-5]
-        print(query)
-        return __query(query)
-
-    # Admin data
-    def getUser(self):
-        """Get a single user from the database"""
-        return self.query_db("SELECT * FROM user", one=True)
-    def getUsers(self)->list[tuple]:
-        """Return a list of tuples of all the users in the database"""
-        return self.query_db("SELECT * FROM user")
-
-    def login(self, user, hashed_password):
-        """try to login the user.
-        The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database 
-
-        Args:
-            user (str): username that tries to login
-            hashed_password (str): the password the user tries to login with
-
-        Returns:
-            bool: True if the login was successful, False if not
-        """
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        if salt is None:
-            return False
-        hashed_password = salt + hashed_password
-        password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-        if password == hashed_password:
-            return True
-        else:
-            return False
-    def changePassword(self, user, new_password):
-        """change the password of a user.
-        The password will be added with the salt and then committed to the database
-
-        Args:
-            user (str): username
-            new_password (str): the hashed password
-        """
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        new_password = salt + new_password
-        self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-    def getRole(self, user):
-        """get the role of the user
-
-        Args:
-            user (str): username
-
-        Returns:
-            str: the name of the role
-        """
-        return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-    def getRoles(self)->list[tuple]:
-        """get all the roles in the database
-
-        Returns:
-            list[str]: a list of all the roles
-        """
-        return self.query_db("SELECT role FROM user")
-    def checkUsername(self, user)->bool:
-        """a check to see if the username is already present in the database
-
-        Args:
-            user (str): the username
-
-        Returns:
-            bool: True if the username is present, False if not
-        """
-        data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-        return True if data is not None else False
-    def createUser(self, user, password, role, salt):
-        """create an user from the AdminCommands class.
-
-        Args:
-            user (str): the username of the user
-            password (str): a hashed password
-            role (str): the role of the user
-            salt (str): a salt for the password
-        """
-        self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-    def deleteUser(self, user):
-        """delete an unser
-
-        Args:
-            user (str): username of the user
-        """
-        self.query_db("DELETE FROM user WHERE username=?", (user,))
-    def updateUser(self, username, data:dict[str, str]):
-        """changge the data of a user
-
-        Args:
-            username (str): the username of the user
-            data (dict[str, str]): the data to be changed
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        query = "UPDATE user SET "
-        for key,value in data.items():
-            if key == "username":
-                continue
-            query += f"{key}='{value}',"
-        query = query[:-1]
-        query += " WHERE username=?"
-        params = (username,)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getFacultyMember(self, name:str)->tuple:
-        """get a faculty member based on the name
-
-        Args:
-            name (str): the name to be searched for
-
-        Returns:
-            tuple: a tuple containing the data of the faculty member
-        """
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-    def updateFacultyMember(self, data:dict, oldlname:str, oldfname:str):
-        """update the data of a faculty member
-
-        Args:
-            data (dict): a dictionary containing the data to be updated
-            oldlname (str): the old last name of the faculty member
-            oldfname (str): the old first name of the faculty member
-        """
-        placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-        query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-        data["oldlname"] = oldlname
-        data["oldfname"] = oldfname
-        self.query_db(query, data)
-    def getFacultyMembers(self):
-        """get a list of all faculty members
-
-        Returns:
-            list[tuple]: a list of tuples containing the faculty members
-        """
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class Database -(db_path: str = None) -
-
-

Initialize the database and create the tables if they do not exist.

-

Default constructor for the database class

-

Args

-
-
db_path : str, optional
-
Optional Path for testing / specific purposes. Defaults to None.
-
-
- -Expand source code - -
class Database:
-    """
-    Initialize the database and create the tables if they do not exist.
-    """
-    def __init__(self, db_path: str = None):
-        """
-        Default constructor for the database class
-
-        Args:
-            db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None.
-        """
-        if db_path is None:
-            self.db_path = config.database.path + config.database.name
-            self.db_path = self.db_path.replace("~", str(Path.home()))
-        else:
-            self.db_path = db_path
-        if self.get_db_contents() is None:
-            logger.log_critical("Database does not exist, creating tables")
-            self.create_tables()
-    def get_db_contents(self)->Union[List[Tuple], None]:
-        """
-        Get the contents of the 
-
-        Returns:
-            Union[List[Tuple], None]: _description_
-        """
-        try:
-            with sql.connect(self.db_path) as conn:
-                cursor = conn.cursor()
-                cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-                return cursor.fetchall()
-        except sql.OperationalError:
-            return None
-    def connect(self)->sql.Connection:
-        """
-        Connect to the database
-
-        Returns:
-            sql.Connection: The active connection to the database
-        """
-        return sql.connect(self.db_path)
-    def close_connection(self, conn: sql.Connection):
-        """
-        closes the connection to the database
-        
-        Args:
-        ----
-            - conn (sql.Connection): the connection to be closed
-        """
-        conn.close()
-    def create_tables(self):
-        """
-        Create the tables in the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        cursor.execute(CREATE_TABLE_APPARAT)
-        cursor.execute(CREATE_TABLE_MESSAGES)
-        cursor.execute(CREATE_TABLE_MEDIA)
-        cursor.execute(CREATE_TABLE_APPKONTOS)
-        cursor.execute(CREATE_TABLE_FILES)
-        cursor.execute(CREATE_TABLE_PROF)
-        cursor.execute(CREATE_TABLE_USER)
-        cursor.execute(CREATE_TABLE_SUBJECTS)
-        conn.commit()
-        self.close_connection(conn)
-    def insertInto(self, query:str, params:Tuple) -> None:
-        """
-        Insert sent data into the database
-
-        Args:
-            query (str): The query to be executed
-            params (Tuple): the parameters to be inserted into the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Inserting {params} into database with query {query}")
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-        """
-        Query the Database for the sent query.
-
-        Args:
-            query (str): The query to be executed
-            args (Tuple, optional): The arguments for the query. Defaults to ().
-            one (bool, optional): Return the first result only. Defaults to False.
-
-        Returns:
-            Union[Typle|List[Tuple]]: Returns the result of the query
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Querying database with query {query}, args: {args}")
-        cursor.execute(query, args)
-        rv = cursor.fetchall()
-        conn.commit()
-        self.close_connection(conn)
-        return (rv[0] if rv else None) if one else rv
-
-    # Books
-    def addBookToDatabase(self, bookdata:BookData,app_id:Union[str,int], prof_id:Union[str,int]):
-        """
-        Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-        Args:
-            bookdata (BookData): The metadata of the book to be added
-            app_id (str): The apparat id where the book should be added to
-            prof_id (str): The id of the professor where the book should be added to.
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        t_query = (
-            f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-        )
-        # print(t_query)
-        result = cursor.execute(t_query).fetchall()
-        result = [load_pickle(i[0]) for i in result]
-        if bookdata in result:
-            print("Bookdata already in database")
-            # check if the book was deleted in the apparat
-            query = (
-                "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-            )
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            result = cursor.execute(query, params).fetchone()
-            if result[0] == 1:
-                print("Book was deleted, updating bookdata")
-                query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-                params = (app_id, prof_id, dump_pickle(bookdata))
-                cursor.execute(query, params)
-                conn.commit()
-            return
-
-        query = (
-            "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-        )
-        converted = dump_pickle(bookdata)
-        params = (converted, app_id, prof_id, 0)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getBookIdBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->int:
-        """
-        Get a book id based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            int: The id of the book
-        """
-        result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [(load_pickle(i[0]),i[1]) for i in result]
-        book = [i for i in books if i[0].signature == signature][0][1]
-        return book
-    def getBookBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->BookData:
-        """
-        Get the book based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            BookData: The total metadata of the book wrapped in a BookData object
-        """
-        result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [load_pickle(i[0]) for i in result]
-        book = [i for i in books if i.signature == signature][0]
-        return book
-    def getLastBookId(self)->int:
-        """
-        Get the last book id in the database
-
-        Returns:
-            int: ID of the last book in the database
-        """
-        return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-    def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-        """
-        Search a book in the database based on the sent data.
-
-        Args:
-            data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-            - signature: The signature of the book
-            - title: The title of the book
-
-        Returns:
-            list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-        """
-        rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-        ic(rdata, len(rdata))
-        mode = 0
-        if len(data)== 1:
-            if "signature" in data.keys():
-                mode = 1
-            elif "title" in data.keys():
-                mode = 2
-        elif len(data) == 2:
-            mode = 3
-        else:
-            return None
-        ret = []
-        for book in rdata:
-            bookdata = load_pickle(book[1])
-            app_id = book[2]
-            prof_id = book[3]
-            if mode == 1:
-                if data["signature"] in bookdata.signature:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 2:
-                if data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 3:
-                if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-        ic(ret)
-        return ret
-    def setAvailability(self, book_id:str, available:str):
-        """
-        Set the availability of a book in the database
-
-        Args:
-            book_id (str): The id of the book
-            available (str): The availability of the book
-        """
-        self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-    def getBookId(self, bookdata:BookData, app_id:Union[str,int], prof_id:Union[str,int])->int:
-        """
-        Get the id of a book based on the metadata of the book
-
-        Args:
-            bookdata (BookData): The wrapped metadata of the book
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-
-        Returns:
-            int: ID of the book
-        """
-        result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-        return result[0]
-    def getBook(self,book_id:int)->BookData:
-        """
-        Get the book based on the id in the database
-
-        Args:
-            book_id (int): The id of the book
-
-        Returns:
-            BookData: The metadata of the book wrapped in a BookData object
-        """
-        return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-    def getBooks(self, app_id:Union[str,int], prof_id:Union[str,int], deleted=0)->list[dict[int, BookData, int]]:
-        """
-        Get the Books based on the apparat id and the professor id
-
-        Args:
-            app_id (str): The ID of the apparat
-            prof_id (str): The ID of the professor
-            deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-        Returns:
-            list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-        """
-        qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-        ret_result = []
-        for result_a in qdata:
-            data = {"id": int, "bookdata": BookData, "available": int}
-            data["id"] = result_a[0]
-            data["bookdata"] = load_pickle(result_a[1])
-            data["available"] = result_a[2]
-            ret_result.append(data)
-        return ret_result
-    def updateBookdata(self, book_id, bookdata:BookData):
-        """
-        Update the bookdata in the database
-
-        Args:
-            book_id (str): The id of the book
-            bookdata (BookData): The new metadata of the book
-        """
-        self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-    def deleteBook(self, book_id):
-        """
-        Delete a book from the database
-
-        Args:
-            book_id (str): ID of the book
-        """
-        self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-    # File Interactions
-    def getBlob(self, filename, app_id:Union[str,int]):
-        """
-        Get a blob from the database
-
-        Args:
-            filename (str): The name of the file
-            app_id (str): ID of the apparat
-
-        Returns:
-            bytes: The file stored in 
-        """
-        return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-    def insertFile(self, file: list[dict], app_id:Union[str,int], prof_id:Union[str,int]):
-        """Instert a list of files into the database
-
-        Args:
-            file (list[dict]): a list containing all the files to be inserted
-            Structured: [{"name": "filename", "path": "path", "type": "filetype"}]
-            app_id (int): the id of the apparat
-            prof_id (str): the id of the professor
-        """
-        for f in file:
-            filename = f["name"]
-            path = f["path"]
-            filetyp = f["type"]
-            if path == "Database":
-                continue
-            blob = create_blob(path)
-            query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-            self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-    def recreateFile(self, filename:str, app_id:Union[str,int],filetype:str)->str:
-        """Recreate a file from the database
-
-        Args:
-            filename (str): the name of the file
-            app_id (Union[str,int]): the id of the apparat
-            filetype (str): the extension of the file to be created
-
-        Returns:
-            str: The filename of the recreated file 
-        """
-        blob = self.getBlob(filename, app_id)
-        tempdir = config.database.tempdir
-        tempdir = tempdir.replace("~", str(Path.home()))
-        tempdir_path = Path(tempdir)        
-        if not os.path.exists(tempdir_path):
-            os.mkdir(tempdir_path)
-        file = tempfile.NamedTemporaryFile(
-            delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-        )
-        file.write(blob)
-        print("file created")
-        return file.name
-    def getFiles(self, app_id:Union[str,int], prof_id:int)->list[tuple]:
-        """Get all the files associated with the apparat and the professor
-
-        Args:
-            app_id (Union[str,int]): The id of the apparat
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            list[tuple]: a list of tuples containing the filename and the filetype for the corresponding apparat and professor
-        """
-        return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-    def getSemersters(self)->list[str]:
-        """Return all the unique semesters in the database
-
-        Returns:
-            list: a list of strings containing the semesters
-        """
-        data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-        return [i[0] for i in data]
-
-    def getSubjects(self):
-        """Get all the subjects in the database
-
-        Returns:
-            list[tuple]: a list of tuples containing the subjects
-        """
-        return self.query_db("SELECT * FROM subjects")
-
-    # Messages
-    def addMessage(self, message:dict,user:str, app_id:Union[str,int]):
-        """add a Message to the database
-
-        Args:
-            message (dict): the message to be added
-            user (str): the user who added the message
-            app_id (Union[str,int]): the id of the apparat
-        """
-        def __getUserId(user):
-            return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-        user_id = __getUserId(user)
-        self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],app_id))
-    def getMessages(self, date:str)->list[dict[str, str, str, str]]:
-        """Get all the messages for a specific date
-
-        Args:
-            date (str): a date.datetime object formatted as a string in the format "YYYY-MM-DD"
-        
-        Returns:
-            list[dict[str, str, str, str]]: a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message
-        """
-        def __get_user_name(user_id):
-            return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-        messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-        ret = [
-            {
-                "message": i[2],
-                "user": __get_user_name(i[4]),
-                "appnr": i[5],
-                "id": i[0]
-            }
-            for i in messages
-        ]
-        return ret
-    def deleteMessage(self, message_id):
-        """Delete a message from the database
-
-        Args:
-            message_id (str): the id of the message
-        """
-        self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-    # Prof data
-    def getProfNameById(self, prof_id:Union[str,int],add_title:bool=False)->str:
-        """Get a professor name based on the id
-
-        Args:
-            prof_id (Union[str,int]): The id of the professor
-            add_title (bool, optional): wether to add the title or no. Defaults to False.
-
-        Returns:
-            str: The name of the professor
-        """
-        prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-        if add_title:
-            return f"{self.getTitleById(prof_id)}{prof[0]}"
-        else:
-            return prof[0]
-    def getTitleById(self, prof_id:Union[str,int])->str:
-        """get the title of a professor based on the id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            str: the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned
-        """
-        title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-        return f"{title} " if title is not None else ""
-    def getProfByName(self, prof_name:str)->tuple:
-        """get all the data of a professor based on the name
-
-        Args:
-            prof_name (str): the name of the professor
-
-        Returns:
-            tuple: the data of the professor
-        """
-        return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-    def getProfId(self, prof_name:str)->Optional[int]:
-        """Get the id of a professor based on the name
-
-        Args:
-            prof_name (str): the name of the professor
-
-        Returns:
-            Optional[int]: the id of the professor, if the professor is not found, None is returned
-        """
-        
-        data = self.getProfByName(prof_name.replace(",", ""))
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def getSpecificProfData(self, prof_id:Union[str,int], fields:List[str])->tuple:
-        """A customisable function to get specific data of a professor based on the id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-            fields (List[str]): a list of fields to be returned
-
-        Returns:
-            tuple: a tuple containing the requested data
-        """
-        query = "SELECT "
-        for field in fields:
-            query += f"{field},"
-        query = query[:-1]
-        query += " FROM prof WHERE id=?"
-        return self.query_db(query, (prof_id,), one=True)[0]
-    def getProfData(self, profname:str):
-        """Get mail, telephone number and title of a professor based on the name
-
-        Args:
-            profname (str): name of the professor
-
-        Returns:
-            tuple: the mail, telephone number and title of the professor
-        """
-        data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-        return data
-    def createProf(self, prof_details:dict):
-        """Create a professor in the database
-
-        Args:
-            prof_details (dict): a dictionary containing the details of the professor
-        """
-        prof_title = prof_details["prof_title"]
-        prof_fname = prof_details["profname"].split(",")[1]
-        prof_fname = prof_fname.strip()
-        prof_lname = prof_details["profname"].split(",")[0]
-        prof_lname = prof_lname.strip()
-        prof_fullname = prof_details["profname"].replace(",", "")
-        prof_mail = prof_details["prof_mail"]
-        prof_tel = prof_details["prof_tel"]
-        params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-        query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-        self.insertInto(query=query, params=params)
-    def getProfs(self)->list[tuple]:
-        """Return all the professors in the database
-
-        Returns:
-            list[tuple]: a list containing all the professors in individual tuples
-        """
-        return self.query_db("SELECT * FROM prof")
-
-    # Apparat
-    def getAllAparats(self,deleted=0)->list[tuple]:
-        """Get all the apparats in the database
-
-        Args:
-            deleted (int, optional): Switch the result to use . Defaults to 0.
-
-        Returns:
-            list[tuple]: a list of tuples containing the apparats
-        """
-        return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-    def getApparatData(self, appnr, appname)->ApparatData:
-        """Get the Apparat data based on the apparat number and the name
-
-        Args:
-            appnr (str): the apparat number
-            appname (str): the name of the apparat
-
-        Raises:
-            NoResultError: an error is raised if no result is found
-
-        Returns:
-            ApparatData: the appended data of the apparat wrapped in an ApparatData object
-        """
-        result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-        if result is None:
-            raise NoResultError("No result found")
-        apparat = ApparatData()
-        apparat.appname = result[1]
-        apparat.appnr = result[4]
-        apparat.dauerapp = True if result[7] == 1 else False
-        prof_data = self.getProfData(self.getProfNameById(result[2]))
-        apparat.profname = self.getProfNameById(result[2])
-        apparat.prof_mail = prof_data[0]
-        apparat.prof_tel = prof_data[1]
-        apparat.prof_title = prof_data[2]
-        apparat.app_fach = result[3]
-        apparat.erstellsemester = result[5]
-        apparat.semester = result[8]
-        apparat.deleted = result[9]
-        apparat.apparat_adis_id = result[11]
-        apparat.prof_adis_id = result[12]
-        return apparat
-    def getUnavailableApparatNumbers(self)->List[int]:
-        """Get a list of all the apparat numbers in the database that are currently in use
-
-        Returns:
-            List[int]: the list of used apparat numbers
-        """
-        numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-        numbers = [i[0] for i in numbers]
-        logger.log_info(f"Currently used apparat numbers: {numbers}")
-        return numbers
-    def setNewSemesterDate(self, app_id:Union[str,int], newDate, dauerapp=False):
-        """Set the new semester date for an apparat
-
-        Args:
-            app_id (Union[str,int]): the id of the apparat
-            newDate (str): the new date
-            dauerapp (bool, optional): if the apparat was changed to dauerapparat. Defaults to False.
-        """
-        date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-        if dauerapp:
-            self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,app_id))
-        else:
-            self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,app_id))
-    def getApparatId(self, apparat_name)->Optional[int]:
-        """get the id of an apparat based on the name
-
-        Args:
-            apparat_name (str): the name of the apparat e.g. "Semesterapparat 1"
-
-        Returns:
-            Optional[int]: the id of the apparat, if the apparat is not found, None is returned
-        """
-        data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def createApparat(self, apparat:ApparatData)->int:
-        """create the apparat in the database
-
-        Args:
-            apparat (ApparatData): the wrapped metadata of the apparat
-
-        Raises:
-            AppPresentError: an error describing that the apparats chosen id is already present in the database
-
-        Returns:
-            Optional[int]: the id of the apparat
-        """
-                
-        prof_id = self.getProfId(apparat.profname)
-        app_id = self.getApparatId(apparat.appname)
-        if app_id:
-            raise AppPresentError(app_id)
-
-        self.createProf(apparat.get_prof_details())
-        prof_id = self.getProfId(apparat.profname)
-        ic(prof_id)
-        query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-        logger.log_info(query)
-        self.query_db(query)
-        return self.getApparatId(apparat.appname)
-    def getApparatsByProf(self, prof_id:Union[str,int])->list[tuple]:
-        """Get all apparats based on the professor id
-
-        Args:
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            list[tuple]: a list of tuples containing the apparats
-        """
-        return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-    def getApparatsBySemester(self, semester:str)->dict[list]:
-        """get all apparats based on the semester
-
-        Args:
-            semester (str): the selected semester
-
-        Returns:
-            dict[list]: a list off all created and deleted apparats for the selected semester
-        """
-        data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-        conn = self.connect()
-        cursor = conn.cursor()
-        c_tmp = []
-        for i in data:
-            c_tmp.append((i[0], self.getProfNameById(i[1])))
-        query = (
-            f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-        )
-        result = cursor.execute(query).fetchall()
-        d_tmp = []
-        for i in result:
-            d_tmp.append((i[0], self.getProfNameById(i[1])))
-        # group the apparats by prof
-        c_ret = {}
-        for i in c_tmp:
-            if i[1] not in c_ret.keys():
-                c_ret[i[1]] = [i[0]]
-            else:
-                c_ret[i[1]].append(i[0])
-        d_ret = {}
-        for i in d_tmp:
-            if i[1] not in d_ret.keys():
-                d_ret[i[1]] = [i[0]]
-            else:
-                d_ret[i[1]].append(i[0])
-        self.close_connection(conn)
-        return {"created": c_ret, "deleted": d_ret}
-    def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-        """get a list of all apparats created and deleted by semester
-
-        Returns:
-            tuple[list[str],list[int]]: a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        semesters = self.getSemersters()
-        created = []
-        deleted = []
-        for semester in semesters:
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-            result = cursor.execute(query).fetchone()
-            created.append(result[0])
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-            result = cursor.execute(query).fetchone()
-            deleted.append(result[0])
-        # store data in a tuple
-        ret = []
-        e_tuple = ()
-        for sem in semesters:
-            e_tuple = (
-                sem,
-                created[semesters.index(sem)],
-                deleted[semesters.index(sem)],
-            )
-            ret.append(e_tuple)
-        self.close_connection(conn)
-        return ret 
-    def deleteApparat(self, app_id:Union[str,int], semester:str):
-        """Delete an apparat from the database
-
-        Args:
-            app_id (Union[str, int]): the id of the apparat
-            semester (str): the semester the apparat should be deleted from
-        """
-        self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,app_id)) 
-    def isEternal(self, id):
-        """check if the apparat is eternal (dauerapparat)
-
-        Args:
-            id (int): the id of the apparat to be checked
-
-        Returns:
-            int: the state of the apparat
-        """
-        return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-    def getApparatName(self, app_id:Union[str,int], prof_id:Union[str,int]):
-        """get the name of the apparat based on the id
-
-        Args:
-            app_id (Union[str,int]): the id of the apparat
-            prof_id (Union[str,int]): the id of the professor
-
-        Returns:
-            str: the name of the apparat
-        """
-        return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-    def updateApparat(self, apparat_data:ApparatData):
-        """Update an apparat in the database
-
-        Args:
-            apparat_data (ApparatData): the new metadata of the apparat
-        """
-        query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-        params = (
-            apparat_data.appname,
-            apparat_data.app_fach,
-            apparat_data.dauerapp,
-            self.getProfId(apparat_data.profname),
-            apparat_data.appnr,        
-        )
-        self.query_db(query, params)
-    def checkApparatExists(self, apparat_name:str):
-        """check if the apparat is already present in the database based on the name
-
-        Args:
-            apparat_name (str): the name of the apparat
-
-        Returns:
-            bool: True if the apparat is present, False if not
-        """
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-    def checkApparatExistsById(self, app_id:Union[str,int])->bool:
-        """a check to see if the apparat is already present in the database, based on the id
-
-        Args:
-            app_id (Union[str, int]): the id of the apparat
-
-        Returns:
-            bool: True if the apparat is present, False if not
-        """
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (app_id,), one=True) else False
-    # Statistics
-    def statistic_request(self, **kwargs: Any):
-        """Take n amount of kwargs and return the result of the query
-        """
-        def __query(query):
-            """execute the query and return the result
-
-            Args:
-                query (str): the constructed query
-
-            Returns:
-                list: the result of the query
-            """
-            conn = self.connect()
-            cursor = conn.cursor()
-            result = cursor.execute(query).fetchall()
-            for result_a in result:
-                orig_value = result_a
-                prof_name = self.getProfNameById(result_a[2])
-                # replace the prof_id with the prof_name
-                result_a = list(result_a)
-                result_a[2] = prof_name
-                result_a = tuple(result_a)
-                result[result.index(orig_value)] = result_a
-            self.close_connection(conn)
-            return result
-        if "deletable" in kwargs.keys():
-            query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-            return __query(query)
-        if "dauer" in kwargs.keys():
-            kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-        query = "SELECT * FROM semesterapparat WHERE "
-        for key, value in kwargs.items() if kwargs.items() is not None else {}:
-            print(key, value)
-            query += f"{key}='{value}' AND "
-            print(query)
-        # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-        if "deletesemester" in kwargs.keys():
-            query = query.replace(
-                f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-            )
-        if "endsemester" in kwargs.keys():
-            if "erstellsemester" in kwargs.keys():
-                query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-                query = query.replace(
-                    f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-                )
-            else:
-                query = query.replace(
-                    f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-                )
-                print("replaced")
-            query = query.replace(
-                "xyz",
-                f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-            )
-        # remove all x="" parts from the query where x is a key in kwargs
-        query = query[:-5]
-        print(query)
-        return __query(query)
-
-    # Admin data
-    def getUser(self):
-        """Get a single user from the database"""
-        return self.query_db("SELECT * FROM user", one=True)
-    def getUsers(self)->list[tuple]:
-        """Return a list of tuples of all the users in the database"""
-        return self.query_db("SELECT * FROM user")
-
-    def login(self, user, hashed_password):
-        """try to login the user.
-        The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database 
-
-        Args:
-            user (str): username that tries to login
-            hashed_password (str): the password the user tries to login with
-
-        Returns:
-            bool: True if the login was successful, False if not
-        """
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        if salt is None:
-            return False
-        hashed_password = salt + hashed_password
-        password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-        if password == hashed_password:
-            return True
-        else:
-            return False
-    def changePassword(self, user, new_password):
-        """change the password of a user.
-        The password will be added with the salt and then committed to the database
-
-        Args:
-            user (str): username
-            new_password (str): the hashed password
-        """
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        new_password = salt + new_password
-        self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-    def getRole(self, user):
-        """get the role of the user
-
-        Args:
-            user (str): username
-
-        Returns:
-            str: the name of the role
-        """
-        return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-    def getRoles(self)->list[tuple]:
-        """get all the roles in the database
-
-        Returns:
-            list[str]: a list of all the roles
-        """
-        return self.query_db("SELECT role FROM user")
-    def checkUsername(self, user)->bool:
-        """a check to see if the username is already present in the database
-
-        Args:
-            user (str): the username
-
-        Returns:
-            bool: True if the username is present, False if not
-        """
-        data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-        return True if data is not None else False
-    def createUser(self, user, password, role, salt):
-        """create an user from the AdminCommands class.
-
-        Args:
-            user (str): the username of the user
-            password (str): a hashed password
-            role (str): the role of the user
-            salt (str): a salt for the password
-        """
-        self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-    def deleteUser(self, user):
-        """delete an unser
-
-        Args:
-            user (str): username of the user
-        """
-        self.query_db("DELETE FROM user WHERE username=?", (user,))
-    def updateUser(self, username, data:dict[str, str]):
-        """changge the data of a user
-
-        Args:
-            username (str): the username of the user
-            data (dict[str, str]): the data to be changed
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        query = "UPDATE user SET "
-        for key,value in data.items():
-            if key == "username":
-                continue
-            query += f"{key}='{value}',"
-        query = query[:-1]
-        query += " WHERE username=?"
-        params = (username,)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getFacultyMember(self, name:str)->tuple:
-        """get a faculty member based on the name
-
-        Args:
-            name (str): the name to be searched for
-
-        Returns:
-            tuple: a tuple containing the data of the faculty member
-        """
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-    def updateFacultyMember(self, data:dict, oldlname:str, oldfname:str):
-        """update the data of a faculty member
-
-        Args:
-            data (dict): a dictionary containing the data to be updated
-            oldlname (str): the old last name of the faculty member
-            oldfname (str): the old first name of the faculty member
-        """
-        placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-        query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-        data["oldlname"] = oldlname
-        data["oldfname"] = oldfname
-        self.query_db(query, data)
-    def getFacultyMembers(self):
-        """get a list of all faculty members
-
-        Returns:
-            list[tuple]: a list of tuples containing the faculty members
-        """
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-

Methods

-
-
-def addBookToDatabase(self, bookdata: src.logic.dataclass.BookData, app_id: Union[str, int], prof_id: Union[str, int]) -
-
-

Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.

-

Args

-
-
bookdata : BookData
-
The metadata of the book to be added
-
app_id : str
-
The apparat id where the book should be added to
-
prof_id : str
-
The id of the professor where the book should be added to.
-
-
- -Expand source code - -
def addBookToDatabase(self, bookdata:BookData,app_id:Union[str,int], prof_id:Union[str,int]):
-    """
-    Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-    Args:
-        bookdata (BookData): The metadata of the book to be added
-        app_id (str): The apparat id where the book should be added to
-        prof_id (str): The id of the professor where the book should be added to.
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    t_query = (
-        f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-    )
-    # print(t_query)
-    result = cursor.execute(t_query).fetchall()
-    result = [load_pickle(i[0]) for i in result]
-    if bookdata in result:
-        print("Bookdata already in database")
-        # check if the book was deleted in the apparat
-        query = (
-            "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-        )
-        params = (app_id, prof_id, dump_pickle(bookdata))
-        result = cursor.execute(query, params).fetchone()
-        if result[0] == 1:
-            print("Book was deleted, updating bookdata")
-            query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            cursor.execute(query, params)
-            conn.commit()
-        return
-
-    query = (
-        "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-    )
-    converted = dump_pickle(bookdata)
-    params = (converted, app_id, prof_id, 0)
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def addMessage(self, message: dict, user: str, app_id: Union[str, int]) -
-
-

add a Message to the database

-

Args

-
-
message : dict
-
the message to be added
-
user : str
-
the user who added the message
-
app_id : Union[str,int]
-
the id of the apparat
-
-
- -Expand source code - -
def addMessage(self, message:dict,user:str, app_id:Union[str,int]):
-    """add a Message to the database
-
-    Args:
-        message (dict): the message to be added
-        user (str): the user who added the message
-        app_id (Union[str,int]): the id of the apparat
-    """
-    def __getUserId(user):
-        return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-    user_id = __getUserId(user)
-    self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],app_id))
-
-
-
-def changePassword(self, user, new_password) -
-
-

change the password of a user. -The password will be added with the salt and then committed to the database

-

Args

-
-
user : str
-
username
-
new_password : str
-
the hashed password
-
-
- -Expand source code - -
def changePassword(self, user, new_password):
-    """change the password of a user.
-    The password will be added with the salt and then committed to the database
-
-    Args:
-        user (str): username
-        new_password (str): the hashed password
-    """
-    salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-    new_password = salt + new_password
-    self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-
-
-
-def checkApparatExists(self, apparat_name: str) -
-
-

check if the apparat is already present in the database based on the name

-

Args

-
-
apparat_name : str
-
the name of the apparat
-
-

Returns

-
-
bool
-
True if the apparat is present, False if not
-
-
- -Expand source code - -
def checkApparatExists(self, apparat_name:str):
-    """check if the apparat is already present in the database based on the name
-
-    Args:
-        apparat_name (str): the name of the apparat
-
-    Returns:
-        bool: True if the apparat is present, False if not
-    """
-    return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-
-
-
-def checkApparatExistsById(self, app_id: Union[str, int]) ‑> bool -
-
-

a check to see if the apparat is already present in the database, based on the id

-

Args

-
-
app_id : Union[str, int]
-
the id of the apparat
-
-

Returns

-
-
bool
-
True if the apparat is present, False if not
-
-
- -Expand source code - -
def checkApparatExistsById(self, app_id:Union[str,int])->bool:
-    """a check to see if the apparat is already present in the database, based on the id
-
-    Args:
-        app_id (Union[str, int]): the id of the apparat
-
-    Returns:
-        bool: True if the apparat is present, False if not
-    """
-    return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (app_id,), one=True) else False
-
-
-
-def checkUsername(self, user) ‑> bool -
-
-

a check to see if the username is already present in the database

-

Args

-
-
user : str
-
the username
-
-

Returns

-
-
bool
-
True if the username is present, False if not
-
-
- -Expand source code - -
def checkUsername(self, user)->bool:
-    """a check to see if the username is already present in the database
-
-    Args:
-        user (str): the username
-
-    Returns:
-        bool: True if the username is present, False if not
-    """
-    data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-    return True if data is not None else False
-
-
-
-def close_connection(self, conn: sqlite3.Connection) -
-
-

closes the connection to the database

-

Args:

-
- conn (sql.Connection): the connection to be closed
-
-
- -Expand source code - -
def close_connection(self, conn: sql.Connection):
-    """
-    closes the connection to the database
-    
-    Args:
-    ----
-        - conn (sql.Connection): the connection to be closed
-    """
-    conn.close()
-
-
-
-def connect(self) ‑> sqlite3.Connection -
-
-

Connect to the database

-

Returns

-
-
sql.Connection
-
The active connection to the database
-
-
- -Expand source code - -
def connect(self)->sql.Connection:
-    """
-    Connect to the database
-
-    Returns:
-        sql.Connection: The active connection to the database
-    """
-    return sql.connect(self.db_path)
-
-
-
-def createApparat(self, apparat: src.logic.dataclass.ApparatData) ‑> int -
-
-

create the apparat in the database

-

Args

-
-
apparat : ApparatData
-
the wrapped metadata of the apparat
-
-

Raises

-
-
AppPresentError
-
an error describing that the apparats chosen id is already present in the database
-
-

Returns

-
-
Optional[int]
-
the id of the apparat
-
-
- -Expand source code - -
def createApparat(self, apparat:ApparatData)->int:
-    """create the apparat in the database
-
-    Args:
-        apparat (ApparatData): the wrapped metadata of the apparat
-
-    Raises:
-        AppPresentError: an error describing that the apparats chosen id is already present in the database
-
-    Returns:
-        Optional[int]: the id of the apparat
-    """
-            
-    prof_id = self.getProfId(apparat.profname)
-    app_id = self.getApparatId(apparat.appname)
-    if app_id:
-        raise AppPresentError(app_id)
-
-    self.createProf(apparat.get_prof_details())
-    prof_id = self.getProfId(apparat.profname)
-    ic(prof_id)
-    query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-    logger.log_info(query)
-    self.query_db(query)
-    return self.getApparatId(apparat.appname)
-
-
-
-def createProf(self, prof_details: dict) -
-
-

Create a professor in the database

-

Args

-
-
prof_details : dict
-
a dictionary containing the details of the professor
-
-
- -Expand source code - -
def createProf(self, prof_details:dict):
-    """Create a professor in the database
-
-    Args:
-        prof_details (dict): a dictionary containing the details of the professor
-    """
-    prof_title = prof_details["prof_title"]
-    prof_fname = prof_details["profname"].split(",")[1]
-    prof_fname = prof_fname.strip()
-    prof_lname = prof_details["profname"].split(",")[0]
-    prof_lname = prof_lname.strip()
-    prof_fullname = prof_details["profname"].replace(",", "")
-    prof_mail = prof_details["prof_mail"]
-    prof_tel = prof_details["prof_tel"]
-    params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-    query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-    self.insertInto(query=query, params=params)
-
-
-
-def createUser(self, user, password, role, salt) -
-
-

create an user from the AdminCommands class.

-

Args

-
-
user : str
-
the username of the user
-
password : str
-
a hashed password
-
role : str
-
the role of the user
-
salt : str
-
a salt for the password
-
-
- -Expand source code - -
def createUser(self, user, password, role, salt):
-    """create an user from the AdminCommands class.
-
-    Args:
-        user (str): the username of the user
-        password (str): a hashed password
-        role (str): the role of the user
-        salt (str): a salt for the password
-    """
-    self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-
-
-
-def create_tables(self) -
-
-

Create the tables in the database

-
- -Expand source code - -
def create_tables(self):
-    """
-    Create the tables in the database
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    cursor.execute(CREATE_TABLE_APPARAT)
-    cursor.execute(CREATE_TABLE_MESSAGES)
-    cursor.execute(CREATE_TABLE_MEDIA)
-    cursor.execute(CREATE_TABLE_APPKONTOS)
-    cursor.execute(CREATE_TABLE_FILES)
-    cursor.execute(CREATE_TABLE_PROF)
-    cursor.execute(CREATE_TABLE_USER)
-    cursor.execute(CREATE_TABLE_SUBJECTS)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def deleteApparat(self, app_id: Union[str, int], semester: str) -
-
-

Delete an apparat from the database

-

Args

-
-
app_id : Union[str, int]
-
the id of the apparat
-
semester : str
-
the semester the apparat should be deleted from
-
-
- -Expand source code - -
def deleteApparat(self, app_id:Union[str,int], semester:str):
-    """Delete an apparat from the database
-
-    Args:
-        app_id (Union[str, int]): the id of the apparat
-        semester (str): the semester the apparat should be deleted from
-    """
-    self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,app_id)) 
-
-
-
-def deleteBook(self, book_id) -
-
-

Delete a book from the database

-

Args

-
-
book_id : str
-
ID of the book
-
-
- -Expand source code - -
def deleteBook(self, book_id):
-    """
-    Delete a book from the database
-
-    Args:
-        book_id (str): ID of the book
-    """
-    self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-
-
-def deleteMessage(self, message_id) -
-
-

Delete a message from the database

-

Args

-
-
message_id : str
-
the id of the message
-
-
- -Expand source code - -
def deleteMessage(self, message_id):
-    """Delete a message from the database
-
-    Args:
-        message_id (str): the id of the message
-    """
-    self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-
-
-def deleteUser(self, user) -
-
-

delete an unser

-

Args

-
-
user : str
-
username of the user
-
-
- -Expand source code - -
def deleteUser(self, user):
-    """delete an unser
-
-    Args:
-        user (str): username of the user
-    """
-    self.query_db("DELETE FROM user WHERE username=?", (user,))
-
-
-
-def getAllAparats(self, deleted=0) ‑> list[tuple] -
-
-

Get all the apparats in the database

-

Args

-
-
deleted : int, optional
-
Switch the result to use . Defaults to 0.
-
-

Returns

-
-
list[tuple]
-
a list of tuples containing the apparats
-
-
- -Expand source code - -
def getAllAparats(self,deleted=0)->list[tuple]:
-    """Get all the apparats in the database
-
-    Args:
-        deleted (int, optional): Switch the result to use . Defaults to 0.
-
-    Returns:
-        list[tuple]: a list of tuples containing the apparats
-    """
-    return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-
-
-
-def getApparatCountBySemester(self) ‑> tuple[list[str], list[int]] -
-
-

get a list of all apparats created and deleted by semester

-

Returns

-
-
tuple[list[str],list[int]]
-
a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester
-
-
- -Expand source code - -
def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-    """get a list of all apparats created and deleted by semester
-
-    Returns:
-        tuple[list[str],list[int]]: a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    semesters = self.getSemersters()
-    created = []
-    deleted = []
-    for semester in semesters:
-        query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-        result = cursor.execute(query).fetchone()
-        created.append(result[0])
-        query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-        result = cursor.execute(query).fetchone()
-        deleted.append(result[0])
-    # store data in a tuple
-    ret = []
-    e_tuple = ()
-    for sem in semesters:
-        e_tuple = (
-            sem,
-            created[semesters.index(sem)],
-            deleted[semesters.index(sem)],
-        )
-        ret.append(e_tuple)
-    self.close_connection(conn)
-    return ret 
-
-
-
-def getApparatData(self, appnr, appname) ‑> src.logic.dataclass.ApparatData -
-
-

Get the Apparat data based on the apparat number and the name

-

Args

-
-
appnr : str
-
the apparat number
-
appname : str
-
the name of the apparat
-
-

Raises

-
-
NoResultError
-
an error is raised if no result is found
-
-

Returns

-
-
ApparatData
-
the appended data of the apparat wrapped in an ApparatData object
-
-
- -Expand source code - -
def getApparatData(self, appnr, appname)->ApparatData:
-    """Get the Apparat data based on the apparat number and the name
-
-    Args:
-        appnr (str): the apparat number
-        appname (str): the name of the apparat
-
-    Raises:
-        NoResultError: an error is raised if no result is found
-
-    Returns:
-        ApparatData: the appended data of the apparat wrapped in an ApparatData object
-    """
-    result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-    if result is None:
-        raise NoResultError("No result found")
-    apparat = ApparatData()
-    apparat.appname = result[1]
-    apparat.appnr = result[4]
-    apparat.dauerapp = True if result[7] == 1 else False
-    prof_data = self.getProfData(self.getProfNameById(result[2]))
-    apparat.profname = self.getProfNameById(result[2])
-    apparat.prof_mail = prof_data[0]
-    apparat.prof_tel = prof_data[1]
-    apparat.prof_title = prof_data[2]
-    apparat.app_fach = result[3]
-    apparat.erstellsemester = result[5]
-    apparat.semester = result[8]
-    apparat.deleted = result[9]
-    apparat.apparat_adis_id = result[11]
-    apparat.prof_adis_id = result[12]
-    return apparat
-
-
-
-def getApparatId(self, apparat_name) ‑> Optional[int] -
-
-

get the id of an apparat based on the name

-

Args

-
-
apparat_name : str
-
the name of the apparat e.g. "Semesterapparat 1"
-
-

Returns

-
-
Optional[int]
-
the id of the apparat, if the apparat is not found, None is returned
-
-
- -Expand source code - -
def getApparatId(self, apparat_name)->Optional[int]:
-    """get the id of an apparat based on the name
-
-    Args:
-        apparat_name (str): the name of the apparat e.g. "Semesterapparat 1"
-
-    Returns:
-        Optional[int]: the id of the apparat, if the apparat is not found, None is returned
-    """
-    data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-    if data is None:
-        return None
-    else:
-        return data[0]
-
-
-
-def getApparatName(self, app_id: Union[str, int], prof_id: Union[str, int]) -
-
-

get the name of the apparat based on the id

-

Args

-
-
app_id : Union[str,int]
-
the id of the apparat
-
prof_id : Union[str,int]
-
the id of the professor
-
-

Returns

-
-
str
-
the name of the apparat
-
-
- -Expand source code - -
def getApparatName(self, app_id:Union[str,int], prof_id:Union[str,int]):
-    """get the name of the apparat based on the id
-
-    Args:
-        app_id (Union[str,int]): the id of the apparat
-        prof_id (Union[str,int]): the id of the professor
-
-    Returns:
-        str: the name of the apparat
-    """
-    return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-
-
-
-def getApparatsByProf(self, prof_id: Union[str, int]) ‑> list[tuple] -
-
-

Get all apparats based on the professor id

-

Args

-
-
prof_id : Union[str,int]
-
the id of the professor
-
-

Returns

-
-
list[tuple]
-
a list of tuples containing the apparats
-
-
- -Expand source code - -
def getApparatsByProf(self, prof_id:Union[str,int])->list[tuple]:
-    """Get all apparats based on the professor id
-
-    Args:
-        prof_id (Union[str,int]): the id of the professor
-
-    Returns:
-        list[tuple]: a list of tuples containing the apparats
-    """
-    return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-
-
-
-def getApparatsBySemester(self, semester: str) ‑> dict[list] -
-
-

get all apparats based on the semester

-

Args

-
-
semester : str
-
the selected semester
-
-

Returns

-
-
dict[list]
-
a list off all created and deleted apparats for the selected semester
-
-
- -Expand source code - -
def getApparatsBySemester(self, semester:str)->dict[list]:
-    """get all apparats based on the semester
-
-    Args:
-        semester (str): the selected semester
-
-    Returns:
-        dict[list]: a list off all created and deleted apparats for the selected semester
-    """
-    data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-    conn = self.connect()
-    cursor = conn.cursor()
-    c_tmp = []
-    for i in data:
-        c_tmp.append((i[0], self.getProfNameById(i[1])))
-    query = (
-        f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-    )
-    result = cursor.execute(query).fetchall()
-    d_tmp = []
-    for i in result:
-        d_tmp.append((i[0], self.getProfNameById(i[1])))
-    # group the apparats by prof
-    c_ret = {}
-    for i in c_tmp:
-        if i[1] not in c_ret.keys():
-            c_ret[i[1]] = [i[0]]
-        else:
-            c_ret[i[1]].append(i[0])
-    d_ret = {}
-    for i in d_tmp:
-        if i[1] not in d_ret.keys():
-            d_ret[i[1]] = [i[0]]
-        else:
-            d_ret[i[1]].append(i[0])
-    self.close_connection(conn)
-    return {"created": c_ret, "deleted": d_ret}
-
-
-
-def getBlob(self, filename, app_id: Union[str, int]) -
-
-

Get a blob from the database

-

Args

-
-
filename : str
-
The name of the file
-
app_id : str
-
ID of the apparat
-
-

Returns

-
-
bytes
-
The file stored in
-
-
- -Expand source code - -
def getBlob(self, filename, app_id:Union[str,int]):
-    """
-    Get a blob from the database
-
-    Args:
-        filename (str): The name of the file
-        app_id (str): ID of the apparat
-
-    Returns:
-        bytes: The file stored in 
-    """
-    return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-
-
-
-def getBook(self, book_id: int) ‑> src.logic.dataclass.BookData -
-
-

Get the book based on the id in the database

-

Args

-
-
book_id : int
-
The id of the book
-
-

Returns

-
-
BookData
-
The metadata of the book wrapped in a BookData object
-
-
- -Expand source code - -
def getBook(self,book_id:int)->BookData:
-    """
-    Get the book based on the id in the database
-
-    Args:
-        book_id (int): The id of the book
-
-    Returns:
-        BookData: The metadata of the book wrapped in a BookData object
-    """
-    return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-
-
-
-def getBookBasedOnSignature(self, app_id: Union[str, int], prof_id: Union[str, int], signature: str) ‑> src.logic.dataclass.BookData -
-
-

Get the book based on the signature of the book.

-

Args

-
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
signature : str
-
The signature of the book
-
-

Returns

-
-
BookData
-
The total metadata of the book wrapped in a BookData object
-
-
- -Expand source code - -
def getBookBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->BookData:
-    """
-    Get the book based on the signature of the book.
-
-    Args:
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-        signature (str): The signature of the book
-
-    Returns:
-        BookData: The total metadata of the book wrapped in a BookData object
-    """
-    result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-    books = [load_pickle(i[0]) for i in result]
-    book = [i for i in books if i.signature == signature][0]
-    return book
-
-
-
-def getBookId(self, bookdata: src.logic.dataclass.BookData, app_id: Union[str, int], prof_id: Union[str, int]) ‑> int -
-
-

Get the id of a book based on the metadata of the book

-

Args

-
-
bookdata : BookData
-
The wrapped metadata of the book
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
-

Returns

-
-
int
-
ID of the book
-
-
- -Expand source code - -
def getBookId(self, bookdata:BookData, app_id:Union[str,int], prof_id:Union[str,int])->int:
-    """
-    Get the id of a book based on the metadata of the book
-
-    Args:
-        bookdata (BookData): The wrapped metadata of the book
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-
-    Returns:
-        int: ID of the book
-    """
-    result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-    return result[0]
-
-
-
-def getBookIdBasedOnSignature(self, app_id: Union[str, int], prof_id: Union[str, int], signature: str) ‑> int -
-
-

Get a book id based on the signature of the book.

-

Args

-
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
signature : str
-
The signature of the book
-
-

Returns

-
-
int
-
The id of the book
-
-
- -Expand source code - -
def getBookIdBasedOnSignature(self, app_id:Union[str,int], prof_id:Union[str,int],signature:str)->int:
-    """
-    Get a book id based on the signature of the book.
-
-    Args:
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-        signature (str): The signature of the book
-
-    Returns:
-        int: The id of the book
-    """
-    result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-    books = [(load_pickle(i[0]),i[1]) for i in result]
-    book = [i for i in books if i[0].signature == signature][0][1]
-    return book
-
-
-
-def getBooks(self, app_id: Union[str, int], prof_id: Union[str, int], deleted=0) ‑> list[dict[int, src.logic.dataclass.BookData, int]] -
-
-

Get the Books based on the apparat id and the professor id

-

Args

-
-
app_id : str
-
The ID of the apparat
-
prof_id : str
-
The ID of the professor
-
deleted : int, optional
-
The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-

Returns

-
-
list[dict[int, BookData, int]]
-
A list of dictionaries containing the id, the metadata of the book and the availability of the book
-
-
- -Expand source code - -
def getBooks(self, app_id:Union[str,int], prof_id:Union[str,int], deleted=0)->list[dict[int, BookData, int]]:
-    """
-    Get the Books based on the apparat id and the professor id
-
-    Args:
-        app_id (str): The ID of the apparat
-        prof_id (str): The ID of the professor
-        deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-    Returns:
-        list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-    """
-    qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-    ret_result = []
-    for result_a in qdata:
-        data = {"id": int, "bookdata": BookData, "available": int}
-        data["id"] = result_a[0]
-        data["bookdata"] = load_pickle(result_a[1])
-        data["available"] = result_a[2]
-        ret_result.append(data)
-    return ret_result
-
-
-
-def getFacultyMember(self, name: str) ‑> tuple -
-
-

get a faculty member based on the name

-

Args

-
-
name : str
-
the name to be searched for
-
-

Returns

-
-
tuple
-
a tuple containing the data of the faculty member
-
-
- -Expand source code - -
def getFacultyMember(self, name:str)->tuple:
-    """get a faculty member based on the name
-
-    Args:
-        name (str): the name to be searched for
-
-    Returns:
-        tuple: a tuple containing the data of the faculty member
-    """
-    return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-
-
-
-def getFacultyMembers(self) -
-
-

get a list of all faculty members

-

Returns

-
-
list[tuple]
-
a list of tuples containing the faculty members
-
-
- -Expand source code - -
def getFacultyMembers(self):
-    """get a list of all faculty members
-
-    Returns:
-        list[tuple]: a list of tuples containing the faculty members
-    """
-    return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-
-
-def getFiles(self, app_id: Union[str, int], prof_id: int) ‑> list[tuple] -
-
-

Get all the files associated with the apparat and the professor

-

Args

-
-
app_id : Union[str,int]
-
The id of the apparat
-
prof_id : Union[str,int]
-
the id of the professor
-
-

Returns

-
-
list[tuple]
-
a list of tuples containing the filename and the filetype for the corresponding apparat and professor
-
-
- -Expand source code - -
def getFiles(self, app_id:Union[str,int], prof_id:int)->list[tuple]:
-    """Get all the files associated with the apparat and the professor
-
-    Args:
-        app_id (Union[str,int]): The id of the apparat
-        prof_id (Union[str,int]): the id of the professor
-
-    Returns:
-        list[tuple]: a list of tuples containing the filename and the filetype for the corresponding apparat and professor
-    """
-    return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-
-
-def getLastBookId(self) ‑> int -
-
-

Get the last book id in the database

-

Returns

-
-
int
-
ID of the last book in the database
-
-
- -Expand source code - -
def getLastBookId(self)->int:
-    """
-    Get the last book id in the database
-
-    Returns:
-        int: ID of the last book in the database
-    """
-    return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-
-
-
-def getMessages(self, date: str) ‑> list[dict[str, str, str, str]] -
-
-

Get all the messages for a specific date

-

Args

-
-
date : str
-
a date.datetime object formatted as a string in the format "YYYY-MM-DD"
-
-

Returns

-
-
list[dict[str, str, str, str]]
-
a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message
-
-
- -Expand source code - -
def getMessages(self, date:str)->list[dict[str, str, str, str]]:
-    """Get all the messages for a specific date
-
-    Args:
-        date (str): a date.datetime object formatted as a string in the format "YYYY-MM-DD"
-    
-    Returns:
-        list[dict[str, str, str, str]]: a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message
-    """
-    def __get_user_name(user_id):
-        return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-    messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-    ret = [
-        {
-            "message": i[2],
-            "user": __get_user_name(i[4]),
-            "appnr": i[5],
-            "id": i[0]
-        }
-        for i in messages
-    ]
-    return ret
-
-
-
-def getProfByName(self, prof_name: str) ‑> tuple -
-
-

get all the data of a professor based on the name

-

Args

-
-
prof_name : str
-
the name of the professor
-
-

Returns

-
-
tuple
-
the data of the professor
-
-
- -Expand source code - -
def getProfByName(self, prof_name:str)->tuple:
-    """get all the data of a professor based on the name
-
-    Args:
-        prof_name (str): the name of the professor
-
-    Returns:
-        tuple: the data of the professor
-    """
-    return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-
-
-
-def getProfData(self, profname: str) -
-
-

Get mail, telephone number and title of a professor based on the name

-

Args

-
-
profname : str
-
name of the professor
-
-

Returns

-
-
tuple
-
the mail, telephone number and title of the professor
-
-
- -Expand source code - -
def getProfData(self, profname:str):
-    """Get mail, telephone number and title of a professor based on the name
-
-    Args:
-        profname (str): name of the professor
-
-    Returns:
-        tuple: the mail, telephone number and title of the professor
-    """
-    data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-    return data
-
-
-
-def getProfId(self, prof_name: str) ‑> Optional[int] -
-
-

Get the id of a professor based on the name

-

Args

-
-
prof_name : str
-
the name of the professor
-
-

Returns

-
-
Optional[int]
-
the id of the professor, if the professor is not found, None is returned
-
-
- -Expand source code - -
def getProfId(self, prof_name:str)->Optional[int]:
-    """Get the id of a professor based on the name
-
-    Args:
-        prof_name (str): the name of the professor
-
-    Returns:
-        Optional[int]: the id of the professor, if the professor is not found, None is returned
-    """
-    
-    data = self.getProfByName(prof_name.replace(",", ""))
-    if data is None:
-        return None
-    else:
-        return data[0]
-
-
-
-def getProfNameById(self, prof_id: Union[str, int], add_title: bool = False) ‑> str -
-
-

Get a professor name based on the id

-

Args

-
-
prof_id : Union[str,int]
-
The id of the professor
-
add_title : bool, optional
-
wether to add the title or no. Defaults to False.
-
-

Returns

-
-
str
-
The name of the professor
-
-
- -Expand source code - -
def getProfNameById(self, prof_id:Union[str,int],add_title:bool=False)->str:
-    """Get a professor name based on the id
-
-    Args:
-        prof_id (Union[str,int]): The id of the professor
-        add_title (bool, optional): wether to add the title or no. Defaults to False.
-
-    Returns:
-        str: The name of the professor
-    """
-    prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-    if add_title:
-        return f"{self.getTitleById(prof_id)}{prof[0]}"
-    else:
-        return prof[0]
-
-
-
-def getProfs(self) ‑> list[tuple] -
-
-

Return all the professors in the database

-

Returns

-
-
list[tuple]
-
a list containing all the professors in individual tuples
-
-
- -Expand source code - -
def getProfs(self)->list[tuple]:
-    """Return all the professors in the database
-
-    Returns:
-        list[tuple]: a list containing all the professors in individual tuples
-    """
-    return self.query_db("SELECT * FROM prof")
-
-
-
-def getRole(self, user) -
-
-

get the role of the user

-

Args

-
-
user : str
-
username
-
-

Returns

-
-
str
-
the name of the role
-
-
- -Expand source code - -
def getRole(self, user):
-    """get the role of the user
-
-    Args:
-        user (str): username
-
-    Returns:
-        str: the name of the role
-    """
-    return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-
-
-
-def getRoles(self) ‑> list[tuple] -
-
-

get all the roles in the database

-

Returns

-
-
list[str]
-
a list of all the roles
-
-
- -Expand source code - -
def getRoles(self)->list[tuple]:
-    """get all the roles in the database
-
-    Returns:
-        list[str]: a list of all the roles
-    """
-    return self.query_db("SELECT role FROM user")
-
-
-
-def getSemersters(self) ‑> list[str] -
-
-

Return all the unique semesters in the database

-

Returns

-
-
list
-
a list of strings containing the semesters
-
-
- -Expand source code - -
def getSemersters(self)->list[str]:
-    """Return all the unique semesters in the database
-
-    Returns:
-        list: a list of strings containing the semesters
-    """
-    data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-    return [i[0] for i in data]
-
-
-
-def getSpecificProfData(self, prof_id: Union[str, int], fields: List[str]) ‑> tuple -
-
-

A customisable function to get specific data of a professor based on the id

-

Args

-
-
prof_id : Union[str,int]
-
the id of the professor
-
fields : List[str]
-
a list of fields to be returned
-
-

Returns

-
-
tuple
-
a tuple containing the requested data
-
-
- -Expand source code - -
def getSpecificProfData(self, prof_id:Union[str,int], fields:List[str])->tuple:
-    """A customisable function to get specific data of a professor based on the id
-
-    Args:
-        prof_id (Union[str,int]): the id of the professor
-        fields (List[str]): a list of fields to be returned
-
-    Returns:
-        tuple: a tuple containing the requested data
-    """
-    query = "SELECT "
-    for field in fields:
-        query += f"{field},"
-    query = query[:-1]
-    query += " FROM prof WHERE id=?"
-    return self.query_db(query, (prof_id,), one=True)[0]
-
-
-
-def getSubjects(self) -
-
-

Get all the subjects in the database

-

Returns

-
-
list[tuple]
-
a list of tuples containing the subjects
-
-
- -Expand source code - -
def getSubjects(self):
-    """Get all the subjects in the database
-
-    Returns:
-        list[tuple]: a list of tuples containing the subjects
-    """
-    return self.query_db("SELECT * FROM subjects")
-
-
-
-def getTitleById(self, prof_id: Union[str, int]) ‑> str -
-
-

get the title of a professor based on the id

-

Args

-
-
prof_id : Union[str,int]
-
the id of the professor
-
-

Returns

-
-
str
-
the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned
-
-
- -Expand source code - -
def getTitleById(self, prof_id:Union[str,int])->str:
-    """get the title of a professor based on the id
-
-    Args:
-        prof_id (Union[str,int]): the id of the professor
-
-    Returns:
-        str: the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned
-    """
-    title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-    return f"{title} " if title is not None else ""
-
-
-
-def getUnavailableApparatNumbers(self) ‑> List[int] -
-
-

Get a list of all the apparat numbers in the database that are currently in use

-

Returns

-
-
List[int]
-
the list of used apparat numbers
-
-
- -Expand source code - -
def getUnavailableApparatNumbers(self)->List[int]:
-    """Get a list of all the apparat numbers in the database that are currently in use
-
-    Returns:
-        List[int]: the list of used apparat numbers
-    """
-    numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-    numbers = [i[0] for i in numbers]
-    logger.log_info(f"Currently used apparat numbers: {numbers}")
-    return numbers
-
-
-
-def getUser(self) -
-
-

Get a single user from the database

-
- -Expand source code - -
def getUser(self):
-    """Get a single user from the database"""
-    return self.query_db("SELECT * FROM user", one=True)
-
-
-
-def getUsers(self) ‑> list[tuple] -
-
-

Return a list of tuples of all the users in the database

-
- -Expand source code - -
def getUsers(self)->list[tuple]:
-    """Return a list of tuples of all the users in the database"""
-    return self.query_db("SELECT * FROM user")
-
-
-
-def get_db_contents(self) ‑> Optional[List[Tuple]] -
-
-

Get the contents of the

-

Returns

-
-
Union[List[Tuple], None]
-
description
-
-
- -Expand source code - -
def get_db_contents(self)->Union[List[Tuple], None]:
-    """
-    Get the contents of the 
-
-    Returns:
-        Union[List[Tuple], None]: _description_
-    """
-    try:
-        with sql.connect(self.db_path) as conn:
-            cursor = conn.cursor()
-            cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-            return cursor.fetchall()
-    except sql.OperationalError:
-        return None
-
-
-
-def insertFile(self, file: list[dict], app_id: Union[str, int], prof_id: Union[str, int]) -
-
-

Instert a list of files into the database

-

Args

-
-
file : list[dict]
-
a list containing all the files to be inserted
-
Structured
-
[{"name": "filename", "path": "path", "type": "filetype"}]
-
app_id : int
-
the id of the apparat
-
prof_id : str
-
the id of the professor
-
-
- -Expand source code - -
def insertFile(self, file: list[dict], app_id:Union[str,int], prof_id:Union[str,int]):
-    """Instert a list of files into the database
-
-    Args:
-        file (list[dict]): a list containing all the files to be inserted
-        Structured: [{"name": "filename", "path": "path", "type": "filetype"}]
-        app_id (int): the id of the apparat
-        prof_id (str): the id of the professor
-    """
-    for f in file:
-        filename = f["name"]
-        path = f["path"]
-        filetyp = f["type"]
-        if path == "Database":
-            continue
-        blob = create_blob(path)
-        query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-        self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-
-
-
-def insertInto(self, query: str, params: Tuple) ‑> None -
-
-

Insert sent data into the database

-

Args

-
-
query : str
-
The query to be executed
-
params : Tuple
-
the parameters to be inserted into the database
-
-
- -Expand source code - -
def insertInto(self, query:str, params:Tuple) -> None:
-    """
-    Insert sent data into the database
-
-    Args:
-        query (str): The query to be executed
-        params (Tuple): the parameters to be inserted into the database
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    logger.log_info(f"Inserting {params} into database with query {query}")
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def isEternal(self, id) -
-
-

check if the apparat is eternal (dauerapparat)

-

Args

-
-
id : int
-
the id of the apparat to be checked
-
-

Returns

-
-
int
-
the state of the apparat
-
-
- -Expand source code - -
def isEternal(self, id):
-    """check if the apparat is eternal (dauerapparat)
-
-    Args:
-        id (int): the id of the apparat to be checked
-
-    Returns:
-        int: the state of the apparat
-    """
-    return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-
-
-
-def login(self, user, hashed_password) -
-
-

try to login the user. -The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database

-

Args

-
-
user : str
-
username that tries to login
-
hashed_password : str
-
the password the user tries to login with
-
-

Returns

-
-
bool
-
True if the login was successful, False if not
-
-
- -Expand source code - -
def login(self, user, hashed_password):
-    """try to login the user.
-    The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database 
-
-    Args:
-        user (str): username that tries to login
-        hashed_password (str): the password the user tries to login with
-
-    Returns:
-        bool: True if the login was successful, False if not
-    """
-    salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-    if salt is None:
-        return False
-    hashed_password = salt + hashed_password
-    password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-    if password == hashed_password:
-        return True
-    else:
-        return False
-
-
-
-def query_db(self, query: str, args: Tuple = (), one: bool = False) ‑> Union[Tuple, List[Tuple]] -
-
-

Query the Database for the sent query.

-

Args

-
-
query : str
-
The query to be executed
-
args : Tuple, optional
-
The arguments for the query. Defaults to ().
-
one : bool, optional
-
Return the first result only. Defaults to False.
-
-

Returns

-

Union[Typle|List[Tuple]]: Returns the result of the query

-
- -Expand source code - -
def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-    """
-    Query the Database for the sent query.
-
-    Args:
-        query (str): The query to be executed
-        args (Tuple, optional): The arguments for the query. Defaults to ().
-        one (bool, optional): Return the first result only. Defaults to False.
-
-    Returns:
-        Union[Typle|List[Tuple]]: Returns the result of the query
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    logger.log_info(f"Querying database with query {query}, args: {args}")
-    cursor.execute(query, args)
-    rv = cursor.fetchall()
-    conn.commit()
-    self.close_connection(conn)
-    return (rv[0] if rv else None) if one else rv
-
-
-
-def recreateFile(self, filename: str, app_id: Union[str, int], filetype: str) ‑> str -
-
-

Recreate a file from the database

-

Args

-
-
filename : str
-
the name of the file
-
app_id : Union[str,int]
-
the id of the apparat
-
filetype : str
-
the extension of the file to be created
-
-

Returns

-
-
str
-
The filename of the recreated file
-
-
- -Expand source code - -
def recreateFile(self, filename:str, app_id:Union[str,int],filetype:str)->str:
-    """Recreate a file from the database
-
-    Args:
-        filename (str): the name of the file
-        app_id (Union[str,int]): the id of the apparat
-        filetype (str): the extension of the file to be created
-
-    Returns:
-        str: The filename of the recreated file 
-    """
-    blob = self.getBlob(filename, app_id)
-    tempdir = config.database.tempdir
-    tempdir = tempdir.replace("~", str(Path.home()))
-    tempdir_path = Path(tempdir)        
-    if not os.path.exists(tempdir_path):
-        os.mkdir(tempdir_path)
-    file = tempfile.NamedTemporaryFile(
-        delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-    )
-    file.write(blob)
-    print("file created")
-    return file.name
-
-
-
-def searchBook(self, data: dict[str, str]) ‑> list[tuple[src.logic.dataclass.BookData, int]] -
-
-

Search a book in the database based on the sent data.

-

Args

-
-
data : dict[str, str]
-
A dictionary containing the data to be searched for. The dictionary can contain the following:
-
-
    -
  • signature: The signature of the book
  • -
  • title: The title of the book
  • -
-

Returns

-
-
list[tuple[BookData, int]]
-
A list of tuples containing the wrapped Metadata and the id of the book
-
-
- -Expand source code - -
def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-    """
-    Search a book in the database based on the sent data.
-
-    Args:
-        data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-        - signature: The signature of the book
-        - title: The title of the book
-
-    Returns:
-        list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-    """
-    rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-    ic(rdata, len(rdata))
-    mode = 0
-    if len(data)== 1:
-        if "signature" in data.keys():
-            mode = 1
-        elif "title" in data.keys():
-            mode = 2
-    elif len(data) == 2:
-        mode = 3
-    else:
-        return None
-    ret = []
-    for book in rdata:
-        bookdata = load_pickle(book[1])
-        app_id = book[2]
-        prof_id = book[3]
-        if mode == 1:
-            if data["signature"] in bookdata.signature:
-                ret.append((bookdata,app_id,prof_id))
-        elif mode == 2:
-            if data["title"] in bookdata.title:
-                ret.append((bookdata,app_id,prof_id))
-        elif mode == 3:
-            if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                ret.append((bookdata,app_id,prof_id))
-    ic(ret)
-    return ret
-
-
-
-def setAvailability(self, book_id: str, available: str) -
-
-

Set the availability of a book in the database

-

Args

-
-
book_id : str
-
The id of the book
-
available : str
-
The availability of the book
-
-
- -Expand source code - -
def setAvailability(self, book_id:str, available:str):
-    """
-    Set the availability of a book in the database
-
-    Args:
-        book_id (str): The id of the book
-        available (str): The availability of the book
-    """
-    self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-
-
-
-def setNewSemesterDate(self, app_id: Union[str, int], newDate, dauerapp=False) -
-
-

Set the new semester date for an apparat

-

Args

-
-
app_id : Union[str,int]
-
the id of the apparat
-
newDate : str
-
the new date
-
dauerapp : bool, optional
-
if the apparat was changed to dauerapparat. Defaults to False.
-
-
- -Expand source code - -
def setNewSemesterDate(self, app_id:Union[str,int], newDate, dauerapp=False):
-    """Set the new semester date for an apparat
-
-    Args:
-        app_id (Union[str,int]): the id of the apparat
-        newDate (str): the new date
-        dauerapp (bool, optional): if the apparat was changed to dauerapparat. Defaults to False.
-    """
-    date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-    if dauerapp:
-        self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,app_id))
-    else:
-        self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,app_id))
-
-
-
-def statistic_request(self, **kwargs: Any) -
-
-

Take n amount of kwargs and return the result of the query

-
- -Expand source code - -
def statistic_request(self, **kwargs: Any):
-    """Take n amount of kwargs and return the result of the query
-    """
-    def __query(query):
-        """execute the query and return the result
-
-        Args:
-            query (str): the constructed query
-
-        Returns:
-            list: the result of the query
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        result = cursor.execute(query).fetchall()
-        for result_a in result:
-            orig_value = result_a
-            prof_name = self.getProfNameById(result_a[2])
-            # replace the prof_id with the prof_name
-            result_a = list(result_a)
-            result_a[2] = prof_name
-            result_a = tuple(result_a)
-            result[result.index(orig_value)] = result_a
-        self.close_connection(conn)
-        return result
-    if "deletable" in kwargs.keys():
-        query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-        return __query(query)
-    if "dauer" in kwargs.keys():
-        kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-    query = "SELECT * FROM semesterapparat WHERE "
-    for key, value in kwargs.items() if kwargs.items() is not None else {}:
-        print(key, value)
-        query += f"{key}='{value}' AND "
-        print(query)
-    # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-    if "deletesemester" in kwargs.keys():
-        query = query.replace(
-            f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-        )
-    if "endsemester" in kwargs.keys():
-        if "erstellsemester" in kwargs.keys():
-            query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-            query = query.replace(
-                f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-            )
-        else:
-            query = query.replace(
-                f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-            )
-            print("replaced")
-        query = query.replace(
-            "xyz",
-            f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-        )
-    # remove all x="" parts from the query where x is a key in kwargs
-    query = query[:-5]
-    print(query)
-    return __query(query)
-
-
-
-def updateApparat(self, apparat_data: src.logic.dataclass.ApparatData) -
-
-

Update an apparat in the database

-

Args

-
-
apparat_data : ApparatData
-
the new metadata of the apparat
-
-
- -Expand source code - -
def updateApparat(self, apparat_data:ApparatData):
-    """Update an apparat in the database
-
-    Args:
-        apparat_data (ApparatData): the new metadata of the apparat
-    """
-    query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-    params = (
-        apparat_data.appname,
-        apparat_data.app_fach,
-        apparat_data.dauerapp,
-        self.getProfId(apparat_data.profname),
-        apparat_data.appnr,        
-    )
-    self.query_db(query, params)
-
-
-
-def updateBookdata(self, book_id, bookdata: src.logic.dataclass.BookData) -
-
-

Update the bookdata in the database

-

Args

-
-
book_id : str
-
The id of the book
-
bookdata : BookData
-
The new metadata of the book
-
-
- -Expand source code - -
def updateBookdata(self, book_id, bookdata:BookData):
-    """
-    Update the bookdata in the database
-
-    Args:
-        book_id (str): The id of the book
-        bookdata (BookData): The new metadata of the book
-    """
-    self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-
-
-
-def updateFacultyMember(self, data: dict, oldlname: str, oldfname: str) -
-
-

update the data of a faculty member

-

Args

-
-
data : dict
-
a dictionary containing the data to be updated
-
oldlname : str
-
the old last name of the faculty member
-
oldfname : str
-
the old first name of the faculty member
-
-
- -Expand source code - -
def updateFacultyMember(self, data:dict, oldlname:str, oldfname:str):
-    """update the data of a faculty member
-
-    Args:
-        data (dict): a dictionary containing the data to be updated
-        oldlname (str): the old last name of the faculty member
-        oldfname (str): the old first name of the faculty member
-    """
-    placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-    query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-    data["oldlname"] = oldlname
-    data["oldfname"] = oldfname
-    self.query_db(query, data)
-
-
-
-def updateUser(self, username, data: dict[str, str]) -
-
-

changge the data of a user

-

Args

-
-
username : str
-
the username of the user
-
data : dict[str, str]
-
the data to be changed
-
-
- -Expand source code - -
def updateUser(self, username, data:dict[str, str]):
-    """changge the data of a user
-
-    Args:
-        username (str): the username of the user
-        data (dict[str, str]): the data to be changed
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    query = "UPDATE user SET "
-    for key,value in data.items():
-        if key == "username":
-            continue
-        query += f"{key}='{value}',"
-    query = query[:-1]
-    query += " WHERE username=?"
-    params = (username,)
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/db.html b/docs/db.html deleted file mode 100644 index f2b125f..0000000 --- a/docs/db.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - -db API documentation - - - - - - - - - - - -
-
-
-

Module db

-
-
-

Module db provides the database schema for the semesterapparat application.

-
-
-
- -Expand source code - -
CREATE_TABLE_APPARAT = """CREATE TABLE semesterapparat (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    name TEXT,
-    prof_id INTEGER,
-    fach TEXT,
-    appnr INTEGER,
-    erstellsemester TEXT,
-    verlängert_am TEXT,
-    dauer BOOLEAN,
-    verlängerung_bis TEXT,
-    deletion_status INTEGER,
-    deleted_date TEXT,
-    apparat_id_adis INTEGER,
-    prof_id_adis INTEGER,
-    konto INTEGER REFERENCES app_kontos (id),
-    FOREIGN KEY (prof_id) REFERENCES prof (id)
-  )"""
-CREATE_TABLE_MEDIA = """CREATE TABLE media (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    bookdata BLOB,
-    app_id INTEGER,
-    prof_id INTEGER,
-    deleted INTEGER DEFAULT (0),
-    available BOOLEAN,
-    reservation BOOLEAN,
-    FOREIGN KEY (prof_id) REFERENCES prof (id),
-    FOREIGN KEY (app_id) REFERENCES semesterapparat (id)
-  )"""
-CREATE_TABLE_APPKONTOS = """CREATE TABLE app_kontos (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    app_id INTEGER,
-    konto INTEGER,
-    passwort TEXT,
-    FOREIGN KEY (app_id) REFERENCES semesterapparat (id)
-    )"""
-CREATE_TABLE_FILES = """CREATE TABLE files (
-    id INTEGER PRIMARY KEY,
-    filename TEXT,
-    fileblob BLOB,
-    app_id INTEGER,
-    filetyp TEXT,
-    prof_id INTEGER REFERENCES prof (id),
-    FOREIGN KEY (app_id) REFERENCES semesterapparat (id)
-    )"""
-CREATE_TABLE_MESSAGES = """CREATE TABLE messages (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    created_at date NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    message TEXT NOT NULL,
-    remind_at date NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    user_id INTEGER NOT NULL,
-    appnr INTEGER,
-    FOREIGN KEY (user_id) REFERENCES user (id)
-  )"""
-CREATE_TABLE_PROF = """CREATE TABLE prof (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    titel TEXT,
-    fname TEXT,
-    lname TEXT,
-    fullname TEXT NOT NULL UNIQUE,
-    mail TEXT,
-    telnr TEXT
-  )"""
-CREATE_TABLE_USER = """CREATE TABLE user (
-    id integer NOT NULL PRIMARY KEY AUTOINCREMENT,
-    created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    username TEXT NOT NULL UNIQUE,
-    password TEXT NOT NULL,
-    salt TEXT NOT NULL,
-    role TEXT NOT NULL,
-    email TEXT UNIQUE,
-    name TEXT
-  )"""
-CREATE_TABLE_SUBJECTS = """CREATE TABLE subjects (
-    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-    name TEXT NOT NULL UNIQUE
-)"""
-
-
-
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/database.html b/documentation/database.html deleted file mode 100644 index 65ce4cf..0000000 --- a/documentation/database.html +++ /dev/null @@ -1,3071 +0,0 @@ - - - - - - -database API documentation - - - - - - - - - - - -
-
-
-

Module database

-
-
-
- -Expand source code - -
import datetime
-import os
-import re
-import sqlite3 as sql
-import tempfile
-from src.logic.log import MyLogger
-#from icecream import ic
-from typing import List, Tuple, Dict, Any, Optional, Union
-from pathlib import Path
-from omegaconf import OmegaConf
-from src.backend.db import CREATE_TABLE_APPARAT, CREATE_TABLE_MESSAGES, CREATE_TABLE_MEDIA, CREATE_TABLE_APPKONTOS, CREATE_TABLE_FILES, CREATE_TABLE_PROF, CREATE_TABLE_USER, CREATE_TABLE_SUBJECTS
-from src.logic.constants import SEMAP_MEDIA_ACCOUNTS
-from src.logic.dataclass import ApparatData, BookData
-from src.errors import NoResultError, AppPresentError
-from src.utils import load_pickle, dump_pickle,create_blob
-config = OmegaConf.load("config.yaml")
-logger = MyLogger(__name__)
-
-
-class Database:
-    """
-    Initialize the database and create the tables if they do not exist.
-    """
-    def __init__(self, db_path: str = None):
-        """
-        Default constructor for the database class
-
-        Args:
-            db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None.
-        """
-        if db_path is None:
-            self.db_path = config.database.path + config.database.name
-            self.db_path = self.db_path.replace("~", str(Path.home()))
-        else:
-            self.db_path = db_path
-        if self.get_db_contents() is None:
-            logger.log_critical("Database does not exist, creating tables")
-            self.create_tables()
-
-    def get_db_contents(self)->Union[List[Tuple], None]:
-        """
-        Get the contents of the 
-
-        Returns:
-            Union[List[Tuple], None]: _description_
-        """
-        try:
-            with sql.connect(self.db_path) as conn:
-                cursor = conn.cursor()
-                cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-                return cursor.fetchall()
-        except sql.OperationalError:
-            return None
-
-    def connect(self)->sql.Connection:
-        """
-        Connect to the database
-
-        Returns:
-            sql.Connection: The active connection to the database
-        """
-        return sql.connect(self.db_path)
-
-    def close_connection(self, conn: sql.Connection):
-        """
-        closes the connection to the database
-        
-        Args:
-        ----
-            - conn (sql.Connection): the connection to be closed
-        """
-        conn.close()
-
-    def create_tables(self):
-        """
-        Create the tables in the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        cursor.execute(CREATE_TABLE_APPARAT)
-        cursor.execute(CREATE_TABLE_MESSAGES)
-        cursor.execute(CREATE_TABLE_MEDIA)
-        cursor.execute(CREATE_TABLE_APPKONTOS)
-        cursor.execute(CREATE_TABLE_FILES)
-        cursor.execute(CREATE_TABLE_PROF)
-        cursor.execute(CREATE_TABLE_USER)
-        cursor.execute(CREATE_TABLE_SUBJECTS)
-        conn.commit()
-        self.close_connection(conn)
-
-    def insertInto(self, query:str, params:Tuple) -> None:
-        """
-        Insert sent data into the database
-
-        Args:
-            query (str): The query to be executed
-            params (Tuple): the parameters to be inserted into the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Inserting {params} into database with query {query}")
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-
-    def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-        """
-        Query the Database for the sent query.
-
-        Args:
-            query (str): The query to be executed
-            args (Tuple, optional): The arguments for the query. Defaults to ().
-            one (bool, optional): Return the first result only. Defaults to False.
-
-        Returns:
-            Union[Typle|List[Tuple]]: Returns the result of the query
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Querying database with query {query}, args: {args}")
-        cursor.execute(query, args)
-        rv = cursor.fetchall()
-        conn.commit()
-        self.close_connection(conn)
-        return (rv[0] if rv else None) if one else rv
-
-    # Books
-    def addBookToDatabase(self, bookdata:BookData,app_id:str, prof_id:str):
-        """
-        Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-        Args:
-            bookdata (BookData): The metadata of the book to be added
-            app_id (str): The apparat id where the book should be added to
-            prof_id (str): The id of the professor where the book should be added to.
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        t_query = (
-            f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-        )
-        # print(t_query)
-        result = cursor.execute(t_query).fetchall()
-        result = [load_pickle(i[0]) for i in result]
-        if bookdata in result:
-            print("Bookdata already in database")
-            # check if the book was deleted in the apparat
-            query = (
-                "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-            )
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            result = cursor.execute(query, params).fetchone()
-            if result[0] == 1:
-                print("Book was deleted, updating bookdata")
-                query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-                params = (app_id, prof_id, dump_pickle(bookdata))
-                cursor.execute(query, params)
-                conn.commit()
-            return
-
-        query = (
-            "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-        )
-        converted = dump_pickle(bookdata)
-        params = (converted, app_id, prof_id, 0)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getBookIdBasedOnSignature(self, app_id:str, prof_id:str,signature:str)->int:
-        """
-        Get a book id based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            int: The id of the book
-        """
-        result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [(load_pickle(i[0]),i[1]) for i in result]
-        book = [i for i in books if i[0].signature == signature][0][1]
-        return book
-    def getBookBasedOnSignature(self, app_id:str, prof_id:str,signature:str)->BookData:
-        """
-        Get the book based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            BookData: The total metadata of the book wrapped in a BookData object
-        """
-        result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [load_pickle(i[0]) for i in result]
-        book = [i for i in books if i.signature == signature][0]
-        return book
-    def getLastBookId(self)->int:
-        """
-        Get the last book id in the database
-
-        Returns:
-            int: ID of the last book in the database
-        """
-        return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-    def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-        """
-        Search a book in the database based on the sent data.
-
-        Args:
-            data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-            - signature: The signature of the book
-            - title: The title of the book
-
-        Returns:
-            list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-        """
-        rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-        #ic(rdata, len(rdata))
-        mode = 0
-        if len(data)== 1:
-            if "signature" in data.keys():
-                mode = 1
-            elif "title" in data.keys():
-                mode = 2
-        elif len(data) == 2:
-            mode = 3
-        else:
-            return None
-        ret = []
-        for book in rdata:
-            bookdata = load_pickle(book[1])
-            app_id = book[2]
-            prof_id = book[3]
-            if mode == 1:
-                if data["signature"] in bookdata.signature:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 2:
-                if data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 3:
-                if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-        #ic(ret)
-        return ret
-    def setAvailability(self, book_id:str, available:str):
-        """
-        Set the availability of a book in the database
-
-        Args:
-            book_id (str): The id of the book
-            available (str): The availability of the book
-        """
-        self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-    def getBookId(self, bookdata:BookData, app_id, prof_id)->int:
-        """
-        Get the id of a book based on the metadata of the book
-
-        Args:
-            bookdata (BookData): The wrapped metadata of the book
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-
-        Returns:
-            int: ID of the book
-        """
-        result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-        return result[0]
-    def getBook(self,book_id:int)->BookData:
-        """
-        Get the book based on the id in the database
-
-        Args:
-            book_id (int): The id of the book
-
-        Returns:
-            BookData: The metadata of the book wrapped in a BookData object
-        """
-        return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-    def getBooks(self, app_id, prof_id, deleted=0)->list[dict[int, BookData, int]]:
-        """
-        Get the Books based on the apparat id and the professor id
-
-        Args:
-            app_id (str): The ID of the apparat
-            prof_id (str): The ID of the professor
-            deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-        Returns:
-            list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-        """
-        qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-        ret_result = []
-        for result_a in qdata:
-            data = {"id": int, "bookdata": BookData, "available": int}
-            data["id"] = result_a[0]
-            data["bookdata"] = load_pickle(result_a[1])
-            data["available"] = result_a[2]
-            ret_result.append(data)
-        return ret_result
-
-    def updateBookdata(self, book_id, bookdata:BookData):
-        """
-        Update the bookdata in the database
-
-        Args:
-            book_id (str): The id of the book
-            bookdata (BookData): The new metadata of the book
-        """
-        self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-    def deleteBook(self, book_id):
-        """
-        Delete a book from the database
-
-        Args:
-            book_id (str): ID of the book
-        """
-        self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-    # File Interactions
-    def getBlob(self, filename, app_id):
-        """
-        Get a blob from the database
-
-        Args:
-            filename (str): The name of the file
-            app_id (str): ID of the apparat
-
-        Returns:
-            bytes: The file stored in 
-        """
-        return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-    def insertFile(self, file: list[dict], app_id: int, prof_id):
-        for f in file:
-            filename = f["name"]
-            path = f["path"]
-            filetyp = f["type"]
-            if path == "Database":
-                continue
-            blob = create_blob(path)
-            query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-            self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-    def recreateFile(self, filename, app_id,filetype):
-        blob = self.getBlob(filename, app_id)
-        tempdir = config.database.tempdir
-        tempdir = tempdir.replace("~", str(Path.home()))
-        tempdir_path = Path(tempdir)        
-        if not os.path.exists(tempdir_path):
-            os.mkdir(tempdir_path)
-        file = tempfile.NamedTemporaryFile(
-            delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-        )
-        file.write(blob)
-        print("file created")
-        return file.name
-    def getFiles(self, app_id:int, prof_id:int)->list[tuple]:
-        return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-    def getSemersters(self):
-        data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-        return [i[0] for i in data]
-
-    def getSubjects(self):
-        return self.query_db("SELECT * FROM subjects")
-
-    # Messages
-    def addMessage(self, message:dict,user, appnr):
-        def __getUserId(user):
-            return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-        user_id = __getUserId(user)
-        self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],appnr))
-    def getMessages(self, date:str):
-        def __get_user_name(user_id):
-            return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-        messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-        ret = [
-            {
-                "message": i[2],
-                "user": __get_user_name(i[4]),
-                "appnr": i[5],
-                "id": i[0]
-            }
-            for i in messages
-        ]
-        return ret
-    def deleteMessage(self, message_id):
-        self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-    # Prof data
-    def getProfNameById(self, prof_id:int,add_title:bool=False):
-        prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-        if add_title:
-            return f"{self.getTitleById(prof_id)}{prof[0]}"
-        else:
-            return prof[0]
-    def getTitleById(self, prof_id:int):
-        title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-        return f"{title} " if title is not None else ""
-    def getProfByName(self, prof_name:str):
-        return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-    def getProfId(self, prof_name:str):
-        """
-        getProfId _summary_
-
-        :param prof_name: _description_
-        :type prof_name: str
-        :return: _description_
-        :rtype: _type_
-        """
-        data = self.getProfByName(prof_name.replace(",", ""))
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def getSpecificProfData(self, prof_id:int, fields:List[str]):
-        """
-        getSpecificProfData _summary_
-        
-        
-        
-        Args:
-        ----
-            - prof_id (int): _description_
-            - fields (List[str]): _description_
-        
-        Returns:
-        -------
-            - _type_: _description_
-        """
-        query = "SELECT "
-        for field in fields:
-            query += f"{field},"
-        query = query[:-1]
-        query += " FROM prof WHERE id=?"
-        return self.query_db(query, (prof_id,), one=True)[0]
-    def getProfData(self, profname:str):
-        
-        data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-        return data
-    def createProf(self, prof_details:dict):
-        prof_title = prof_details["prof_title"]
-        prof_fname = prof_details["profname"].split(",")[1]
-        prof_fname = prof_fname.strip()
-        prof_lname = prof_details["profname"].split(",")[0]
-        prof_lname = prof_lname.strip()
-        prof_fullname = prof_details["profname"].replace(",", "")
-        prof_mail = prof_details["prof_mail"]
-        prof_tel = prof_details["prof_tel"]
-        params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-        query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-        self.insertInto(query=query, params=params)
-    def getProfs(self):
-        return self.query_db("SELECT * FROM prof")
-
-    # Apparat
-    def getAllAparats(self,deleted=0):
-        return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-    def getApparatData(self, appnr, appname)->ApparatData:
-
-        result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-        if result is None:
-            raise NoResultError("No result found")
-        apparat = ApparatData()
-        apparat.appname = result[1]
-        apparat.appnr = result[4]
-        apparat.dauerapp = True if result[7] == 1 else False
-        prof_data = self.getProfData(self.getProfNameById(result[2]))
-        apparat.profname = self.getProfNameById(result[2])
-        apparat.prof_mail = prof_data[0]
-        apparat.prof_tel = prof_data[1]
-        apparat.prof_title = prof_data[2]
-        apparat.app_fach = result[3]
-        apparat.erstellsemester = result[5]
-        apparat.semester = result[8]
-        apparat.deleted = result[9]
-        apparat.apparat_adis_id = result[11]
-        apparat.prof_adis_id = result[12]
-        return apparat
-    def getUnavailableApparatNumbers(self)->List[int]:
-        """
-        getUnavailableApparatNumbers returns a list of all currently used ApparatNumbers
-        
-        
-        
-        Returns:
-        -------
-            - number(List[int]): a list of all currently used apparat numbers
-        """
-        numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-        numbers = [i[0] for i in numbers]
-        logger.log_info(f"Currently used apparat numbers: {numbers}")
-        return numbers
-    def setNewSemesterDate(self, appnr, newDate, dauerapp=False):
-        date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-        if dauerapp:
-            self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,appnr))
-        else:
-            self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,appnr))
-    def getApparatId(self, apparat_name):
-        data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def createApparat(self, apparat:ApparatData)->Optional[AppPresentError]|int:
-        prof_id = self.getProfId(apparat.profname)
-        app_id = self.getApparatId(apparat.appname)
-        if app_id:
-            return AppPresentError(app_id)
-
-        self.createProf(apparat.get_prof_details())
-        prof_id = self.getProfId(apparat.profname)
-        #ic(prof_id)
-        query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-        logger.log_info(query)
-        self.query_db(query)
-        return self.getApparatId(apparat.appname)
-    def getApparatsByProf(self, prof_id:int)->list[tuple]:
-        return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-    def getApparatsBySemester(self, semester:str)->dict:
-        data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-        conn = self.connect()
-        cursor = conn.cursor()
-        c_tmp = []
-        for i in data:
-            c_tmp.append((i[0], self.getProfNameById(i[1])))
-        query = (
-            f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-        )
-        result = cursor.execute(query).fetchall()
-        d_tmp = []
-        for i in result:
-            d_tmp.append((i[0], self.getProfNameById(i[1])))
-        # group the apparats by prof
-        c_ret = {}
-        for i in c_tmp:
-            if i[1] not in c_ret.keys():
-                c_ret[i[1]] = [i[0]]
-            else:
-                c_ret[i[1]].append(i[0])
-        d_ret = {}
-        for i in d_tmp:
-            if i[1] not in d_ret.keys():
-                d_ret[i[1]] = [i[0]]
-            else:
-                d_ret[i[1]].append(i[0])
-        self.close_connection(conn)
-        return {"created": c_ret, "deleted": d_ret}
-    def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-        conn = self.connect()
-        cursor = conn.cursor()
-        semesters = self.getSemersters()
-        created = []
-        deleted = []
-        for semester in semesters:
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-            result = cursor.execute(query).fetchone()
-            created.append(result[0])
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-            result = cursor.execute(query).fetchone()
-            deleted.append(result[0])
-        # store data in a tuple
-        ret = []
-        e_tuple = ()
-        for sem in semesters:
-            e_tuple = (
-                sem,
-                created[semesters.index(sem)],
-                deleted[semesters.index(sem)],
-            )
-            ret.append(e_tuple)
-        self.close_connection(conn)
-        return ret 
-    def deleteApparat(self, appnr, semester):
-        self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,appnr)) 
-    def isEternal(self, id):
-        return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-    def getApparatName(self, app_id, prof_id):
-        return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-    def updateApparat(self, apparat_data:ApparatData):
-        query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-        params = (
-            apparat_data.appname,
-            apparat_data.app_fach,
-            apparat_data.dauerapp,
-            self.getProfId(apparat_data.profname),
-            apparat_data.appnr,        
-        )
-        self.query_db(query, params)
-    def checkApparatExists(self, apparat_name):
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-    def checkApparatExistsById(self, apparat_id):
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (apparat_id,), one=True) else False
-    # Statistics
-    def statistic_request(self, **kwargs: Any):
-        def __query(query):
-            conn = self.connect()
-            cursor = conn.cursor()
-            result = cursor.execute(query).fetchall()
-            for result_a in result:
-                orig_value = result_a
-                prof_name = self.getProfNameById(result_a[2])
-                # replace the prof_id with the prof_name
-                result_a = list(result_a)
-                result_a[2] = prof_name
-                result_a = tuple(result_a)
-                result[result.index(orig_value)] = result_a
-            self.close_connection(conn)
-            return result
-        if "deletable" in kwargs.keys():
-            query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-            return __query(query)
-        if "dauer" in kwargs.keys():
-            kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-        query = "SELECT * FROM semesterapparat WHERE "
-        for key, value in kwargs.items() if kwargs.items() is not None else {}:
-            print(key, value)
-            query += f"{key}='{value}' AND "
-            print(query)
-        # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-        if "deletesemester" in kwargs.keys():
-            query = query.replace(
-                f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-            )
-        if "endsemester" in kwargs.keys():
-            if "erstellsemester" in kwargs.keys():
-                query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-                query = query.replace(
-                    f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-                )
-            else:
-                query = query.replace(
-                    f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-                )
-                print("replaced")
-            query = query.replace(
-                "xyz",
-                f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-            )
-        # remove all x="" parts from the query where x is a key in kwargs
-        query = query[:-5]
-        print(query)
-        return __query(query)
-
-    # Admin data
-    def getUser(self):
-        return self.query_db("SELECT * FROM user", one=True)
-    def getUsers(self):
-        return self.query_db("SELECT * FROM user")
-
-    def login(self, user, hashed_password):
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        if salt is None:
-            return False
-        hashed_password = salt + hashed_password
-        password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-        if password == hashed_password:
-            return True
-        else:
-            return False
-    def changePassword(self, user, new_password):
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        new_password = salt + new_password
-        self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-    def getRole(self, user):
-        return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-    def getRoles(self):
-        return self.query_db("SELECT role FROM user")
-    def checkUsername(self, user):
-        data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-        return True if data is not None else False
-    def createUser(self, user, password, role, salt):
-        """Create a user based on passed data.
-
-        Args:
-        ----
-            - username (str): Username to be used
-            - password (str): the salted password
-            - role (str): Role of the user
-            - salt (str): a random salt for the user
-        """
-        self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-    def deleteUser(self, user):
-        self.query_db("DELETE FROM user WHERE username=?", (user,))
-    def updateUser(self, username, data:dict[str, str]):
-        conn = self.connect()
-        cursor = conn.cursor()
-        query = "UPDATE user SET "
-        for key,value in data.items():
-            if key == "username":
-                continue
-            query += f"{key}='{value}',"
-        query = query[:-1]
-        query += " WHERE username=?"
-        params = (username,)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getFacultyMember(self, name:str):
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-    def updateFacultyMember(self, data, oldlname, oldfname):
-        placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-        query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-        data["oldlname"] = oldlname
-        data["oldfname"] = oldfname
-        self.query_db(query, data)
-    def getFacultyMembers(self):
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class Database -(db_path: str = None) -
-
-

Initialize the database and create the tables if they do not exist.

-

Default constructor for the database class

-

Args

-
-
db_path : str, optional
-
Optional Path for testing / specific purposes. Defaults to None.
-
-
- -Expand source code - -
class Database:
-    """
-    Initialize the database and create the tables if they do not exist.
-    """
-    def __init__(self, db_path: str = None):
-        """
-        Default constructor for the database class
-
-        Args:
-            db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None.
-        """
-        if db_path is None:
-            self.db_path = config.database.path + config.database.name
-            self.db_path = self.db_path.replace("~", str(Path.home()))
-        else:
-            self.db_path = db_path
-        if self.get_db_contents() is None:
-            logger.log_critical("Database does not exist, creating tables")
-            self.create_tables()
-
-    def get_db_contents(self)->Union[List[Tuple], None]:
-        """
-        Get the contents of the 
-
-        Returns:
-            Union[List[Tuple], None]: _description_
-        """
-        try:
-            with sql.connect(self.db_path) as conn:
-                cursor = conn.cursor()
-                cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-                return cursor.fetchall()
-        except sql.OperationalError:
-            return None
-
-    def connect(self)->sql.Connection:
-        """
-        Connect to the database
-
-        Returns:
-            sql.Connection: The active connection to the database
-        """
-        return sql.connect(self.db_path)
-
-    def close_connection(self, conn: sql.Connection):
-        """
-        closes the connection to the database
-        
-        Args:
-        ----
-            - conn (sql.Connection): the connection to be closed
-        """
-        conn.close()
-
-    def create_tables(self):
-        """
-        Create the tables in the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        cursor.execute(CREATE_TABLE_APPARAT)
-        cursor.execute(CREATE_TABLE_MESSAGES)
-        cursor.execute(CREATE_TABLE_MEDIA)
-        cursor.execute(CREATE_TABLE_APPKONTOS)
-        cursor.execute(CREATE_TABLE_FILES)
-        cursor.execute(CREATE_TABLE_PROF)
-        cursor.execute(CREATE_TABLE_USER)
-        cursor.execute(CREATE_TABLE_SUBJECTS)
-        conn.commit()
-        self.close_connection(conn)
-
-    def insertInto(self, query:str, params:Tuple) -> None:
-        """
-        Insert sent data into the database
-
-        Args:
-            query (str): The query to be executed
-            params (Tuple): the parameters to be inserted into the database
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Inserting {params} into database with query {query}")
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-
-    def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-        """
-        Query the Database for the sent query.
-
-        Args:
-            query (str): The query to be executed
-            args (Tuple, optional): The arguments for the query. Defaults to ().
-            one (bool, optional): Return the first result only. Defaults to False.
-
-        Returns:
-            Union[Typle|List[Tuple]]: Returns the result of the query
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        logger.log_info(f"Querying database with query {query}, args: {args}")
-        cursor.execute(query, args)
-        rv = cursor.fetchall()
-        conn.commit()
-        self.close_connection(conn)
-        return (rv[0] if rv else None) if one else rv
-
-    # Books
-    def addBookToDatabase(self, bookdata:BookData,app_id:str, prof_id:str):
-        """
-        Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-        Args:
-            bookdata (BookData): The metadata of the book to be added
-            app_id (str): The apparat id where the book should be added to
-            prof_id (str): The id of the professor where the book should be added to.
-        """
-        conn = self.connect()
-        cursor = conn.cursor()
-        t_query = (
-            f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-        )
-        # print(t_query)
-        result = cursor.execute(t_query).fetchall()
-        result = [load_pickle(i[0]) for i in result]
-        if bookdata in result:
-            print("Bookdata already in database")
-            # check if the book was deleted in the apparat
-            query = (
-                "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-            )
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            result = cursor.execute(query, params).fetchone()
-            if result[0] == 1:
-                print("Book was deleted, updating bookdata")
-                query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-                params = (app_id, prof_id, dump_pickle(bookdata))
-                cursor.execute(query, params)
-                conn.commit()
-            return
-
-        query = (
-            "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-        )
-        converted = dump_pickle(bookdata)
-        params = (converted, app_id, prof_id, 0)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getBookIdBasedOnSignature(self, app_id:str, prof_id:str,signature:str)->int:
-        """
-        Get a book id based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            int: The id of the book
-        """
-        result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [(load_pickle(i[0]),i[1]) for i in result]
-        book = [i for i in books if i[0].signature == signature][0][1]
-        return book
-    def getBookBasedOnSignature(self, app_id:str, prof_id:str,signature:str)->BookData:
-        """
-        Get the book based on the signature of the book.
-
-        Args:
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-            signature (str): The signature of the book
-
-        Returns:
-            BookData: The total metadata of the book wrapped in a BookData object
-        """
-        result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-        books = [load_pickle(i[0]) for i in result]
-        book = [i for i in books if i.signature == signature][0]
-        return book
-    def getLastBookId(self)->int:
-        """
-        Get the last book id in the database
-
-        Returns:
-            int: ID of the last book in the database
-        """
-        return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-    def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-        """
-        Search a book in the database based on the sent data.
-
-        Args:
-            data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-            - signature: The signature of the book
-            - title: The title of the book
-
-        Returns:
-            list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-        """
-        rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-        #ic(rdata, len(rdata))
-        mode = 0
-        if len(data)== 1:
-            if "signature" in data.keys():
-                mode = 1
-            elif "title" in data.keys():
-                mode = 2
-        elif len(data) == 2:
-            mode = 3
-        else:
-            return None
-        ret = []
-        for book in rdata:
-            bookdata = load_pickle(book[1])
-            app_id = book[2]
-            prof_id = book[3]
-            if mode == 1:
-                if data["signature"] in bookdata.signature:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 2:
-                if data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-            elif mode == 3:
-                if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                    ret.append((bookdata,app_id,prof_id))
-        #ic(ret)
-        return ret
-    def setAvailability(self, book_id:str, available:str):
-        """
-        Set the availability of a book in the database
-
-        Args:
-            book_id (str): The id of the book
-            available (str): The availability of the book
-        """
-        self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-    def getBookId(self, bookdata:BookData, app_id, prof_id)->int:
-        """
-        Get the id of a book based on the metadata of the book
-
-        Args:
-            bookdata (BookData): The wrapped metadata of the book
-            app_id (str): The apparat id the book should be associated with
-            prof_id (str): The professor id the book should be associated with
-
-        Returns:
-            int: ID of the book
-        """
-        result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-        return result[0]
-    def getBook(self,book_id:int)->BookData:
-        """
-        Get the book based on the id in the database
-
-        Args:
-            book_id (int): The id of the book
-
-        Returns:
-            BookData: The metadata of the book wrapped in a BookData object
-        """
-        return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-    def getBooks(self, app_id, prof_id, deleted=0)->list[dict[int, BookData, int]]:
-        """
-        Get the Books based on the apparat id and the professor id
-
-        Args:
-            app_id (str): The ID of the apparat
-            prof_id (str): The ID of the professor
-            deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-        Returns:
-            list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-        """
-        qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-        ret_result = []
-        for result_a in qdata:
-            data = {"id": int, "bookdata": BookData, "available": int}
-            data["id"] = result_a[0]
-            data["bookdata"] = load_pickle(result_a[1])
-            data["available"] = result_a[2]
-            ret_result.append(data)
-        return ret_result
-
-    def updateBookdata(self, book_id, bookdata:BookData):
-        """
-        Update the bookdata in the database
-
-        Args:
-            book_id (str): The id of the book
-            bookdata (BookData): The new metadata of the book
-        """
-        self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-    def deleteBook(self, book_id):
-        """
-        Delete a book from the database
-
-        Args:
-            book_id (str): ID of the book
-        """
-        self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-    # File Interactions
-    def getBlob(self, filename, app_id):
-        """
-        Get a blob from the database
-
-        Args:
-            filename (str): The name of the file
-            app_id (str): ID of the apparat
-
-        Returns:
-            bytes: The file stored in 
-        """
-        return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-    def insertFile(self, file: list[dict], app_id: int, prof_id):
-        for f in file:
-            filename = f["name"]
-            path = f["path"]
-            filetyp = f["type"]
-            if path == "Database":
-                continue
-            blob = create_blob(path)
-            query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-            self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-    def recreateFile(self, filename, app_id,filetype):
-        blob = self.getBlob(filename, app_id)
-        tempdir = config.database.tempdir
-        tempdir = tempdir.replace("~", str(Path.home()))
-        tempdir_path = Path(tempdir)        
-        if not os.path.exists(tempdir_path):
-            os.mkdir(tempdir_path)
-        file = tempfile.NamedTemporaryFile(
-            delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-        )
-        file.write(blob)
-        print("file created")
-        return file.name
-    def getFiles(self, app_id:int, prof_id:int)->list[tuple]:
-        return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-    def getSemersters(self):
-        data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-        return [i[0] for i in data]
-
-    def getSubjects(self):
-        return self.query_db("SELECT * FROM subjects")
-
-    # Messages
-    def addMessage(self, message:dict,user, appnr):
-        def __getUserId(user):
-            return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-        user_id = __getUserId(user)
-        self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],appnr))
-    def getMessages(self, date:str):
-        def __get_user_name(user_id):
-            return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-        messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-        ret = [
-            {
-                "message": i[2],
-                "user": __get_user_name(i[4]),
-                "appnr": i[5],
-                "id": i[0]
-            }
-            for i in messages
-        ]
-        return ret
-    def deleteMessage(self, message_id):
-        self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-    # Prof data
-    def getProfNameById(self, prof_id:int,add_title:bool=False):
-        prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-        if add_title:
-            return f"{self.getTitleById(prof_id)}{prof[0]}"
-        else:
-            return prof[0]
-    def getTitleById(self, prof_id:int):
-        title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-        return f"{title} " if title is not None else ""
-    def getProfByName(self, prof_name:str):
-        return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-    def getProfId(self, prof_name:str):
-        """
-        getProfId _summary_
-
-        :param prof_name: _description_
-        :type prof_name: str
-        :return: _description_
-        :rtype: _type_
-        """
-        data = self.getProfByName(prof_name.replace(",", ""))
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def getSpecificProfData(self, prof_id:int, fields:List[str]):
-        """
-        getSpecificProfData _summary_
-        
-        
-        
-        Args:
-        ----
-            - prof_id (int): _description_
-            - fields (List[str]): _description_
-        
-        Returns:
-        -------
-            - _type_: _description_
-        """
-        query = "SELECT "
-        for field in fields:
-            query += f"{field},"
-        query = query[:-1]
-        query += " FROM prof WHERE id=?"
-        return self.query_db(query, (prof_id,), one=True)[0]
-    def getProfData(self, profname:str):
-        
-        data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-        return data
-    def createProf(self, prof_details:dict):
-        prof_title = prof_details["prof_title"]
-        prof_fname = prof_details["profname"].split(",")[1]
-        prof_fname = prof_fname.strip()
-        prof_lname = prof_details["profname"].split(",")[0]
-        prof_lname = prof_lname.strip()
-        prof_fullname = prof_details["profname"].replace(",", "")
-        prof_mail = prof_details["prof_mail"]
-        prof_tel = prof_details["prof_tel"]
-        params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-        query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-        self.insertInto(query=query, params=params)
-    def getProfs(self):
-        return self.query_db("SELECT * FROM prof")
-
-    # Apparat
-    def getAllAparats(self,deleted=0):
-        return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-    def getApparatData(self, appnr, appname)->ApparatData:
-
-        result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-        if result is None:
-            raise NoResultError("No result found")
-        apparat = ApparatData()
-        apparat.appname = result[1]
-        apparat.appnr = result[4]
-        apparat.dauerapp = True if result[7] == 1 else False
-        prof_data = self.getProfData(self.getProfNameById(result[2]))
-        apparat.profname = self.getProfNameById(result[2])
-        apparat.prof_mail = prof_data[0]
-        apparat.prof_tel = prof_data[1]
-        apparat.prof_title = prof_data[2]
-        apparat.app_fach = result[3]
-        apparat.erstellsemester = result[5]
-        apparat.semester = result[8]
-        apparat.deleted = result[9]
-        apparat.apparat_adis_id = result[11]
-        apparat.prof_adis_id = result[12]
-        return apparat
-    def getUnavailableApparatNumbers(self)->List[int]:
-        """
-        getUnavailableApparatNumbers returns a list of all currently used ApparatNumbers
-        
-        
-        
-        Returns:
-        -------
-            - number(List[int]): a list of all currently used apparat numbers
-        """
-        numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-        numbers = [i[0] for i in numbers]
-        logger.log_info(f"Currently used apparat numbers: {numbers}")
-        return numbers
-    def setNewSemesterDate(self, appnr, newDate, dauerapp=False):
-        date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-        if dauerapp:
-            self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,appnr))
-        else:
-            self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,appnr))
-    def getApparatId(self, apparat_name):
-        data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-        if data is None:
-            return None
-        else:
-            return data[0]
-    def createApparat(self, apparat:ApparatData)->Optional[AppPresentError]|int:
-        prof_id = self.getProfId(apparat.profname)
-        app_id = self.getApparatId(apparat.appname)
-        if app_id:
-            return AppPresentError(app_id)
-
-        self.createProf(apparat.get_prof_details())
-        prof_id = self.getProfId(apparat.profname)
-        #ic(prof_id)
-        query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-        logger.log_info(query)
-        self.query_db(query)
-        return self.getApparatId(apparat.appname)
-    def getApparatsByProf(self, prof_id:int)->list[tuple]:
-        return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-    def getApparatsBySemester(self, semester:str)->dict:
-        data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-        conn = self.connect()
-        cursor = conn.cursor()
-        c_tmp = []
-        for i in data:
-            c_tmp.append((i[0], self.getProfNameById(i[1])))
-        query = (
-            f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-        )
-        result = cursor.execute(query).fetchall()
-        d_tmp = []
-        for i in result:
-            d_tmp.append((i[0], self.getProfNameById(i[1])))
-        # group the apparats by prof
-        c_ret = {}
-        for i in c_tmp:
-            if i[1] not in c_ret.keys():
-                c_ret[i[1]] = [i[0]]
-            else:
-                c_ret[i[1]].append(i[0])
-        d_ret = {}
-        for i in d_tmp:
-            if i[1] not in d_ret.keys():
-                d_ret[i[1]] = [i[0]]
-            else:
-                d_ret[i[1]].append(i[0])
-        self.close_connection(conn)
-        return {"created": c_ret, "deleted": d_ret}
-    def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-        conn = self.connect()
-        cursor = conn.cursor()
-        semesters = self.getSemersters()
-        created = []
-        deleted = []
-        for semester in semesters:
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-            result = cursor.execute(query).fetchone()
-            created.append(result[0])
-            query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-            result = cursor.execute(query).fetchone()
-            deleted.append(result[0])
-        # store data in a tuple
-        ret = []
-        e_tuple = ()
-        for sem in semesters:
-            e_tuple = (
-                sem,
-                created[semesters.index(sem)],
-                deleted[semesters.index(sem)],
-            )
-            ret.append(e_tuple)
-        self.close_connection(conn)
-        return ret 
-    def deleteApparat(self, appnr, semester):
-        self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,appnr)) 
-    def isEternal(self, id):
-        return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-    def getApparatName(self, app_id, prof_id):
-        return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-    def updateApparat(self, apparat_data:ApparatData):
-        query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-        params = (
-            apparat_data.appname,
-            apparat_data.app_fach,
-            apparat_data.dauerapp,
-            self.getProfId(apparat_data.profname),
-            apparat_data.appnr,        
-        )
-        self.query_db(query, params)
-    def checkApparatExists(self, apparat_name):
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-    def checkApparatExistsById(self, apparat_id):
-        return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (apparat_id,), one=True) else False
-    # Statistics
-    def statistic_request(self, **kwargs: Any):
-        def __query(query):
-            conn = self.connect()
-            cursor = conn.cursor()
-            result = cursor.execute(query).fetchall()
-            for result_a in result:
-                orig_value = result_a
-                prof_name = self.getProfNameById(result_a[2])
-                # replace the prof_id with the prof_name
-                result_a = list(result_a)
-                result_a[2] = prof_name
-                result_a = tuple(result_a)
-                result[result.index(orig_value)] = result_a
-            self.close_connection(conn)
-            return result
-        if "deletable" in kwargs.keys():
-            query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-            return __query(query)
-        if "dauer" in kwargs.keys():
-            kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-        query = "SELECT * FROM semesterapparat WHERE "
-        for key, value in kwargs.items() if kwargs.items() is not None else {}:
-            print(key, value)
-            query += f"{key}='{value}' AND "
-            print(query)
-        # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-        if "deletesemester" in kwargs.keys():
-            query = query.replace(
-                f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-            )
-        if "endsemester" in kwargs.keys():
-            if "erstellsemester" in kwargs.keys():
-                query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-                query = query.replace(
-                    f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-                )
-            else:
-                query = query.replace(
-                    f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-                )
-                print("replaced")
-            query = query.replace(
-                "xyz",
-                f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-            )
-        # remove all x="" parts from the query where x is a key in kwargs
-        query = query[:-5]
-        print(query)
-        return __query(query)
-
-    # Admin data
-    def getUser(self):
-        return self.query_db("SELECT * FROM user", one=True)
-    def getUsers(self):
-        return self.query_db("SELECT * FROM user")
-
-    def login(self, user, hashed_password):
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        if salt is None:
-            return False
-        hashed_password = salt + hashed_password
-        password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-        if password == hashed_password:
-            return True
-        else:
-            return False
-    def changePassword(self, user, new_password):
-        salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-        new_password = salt + new_password
-        self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-    def getRole(self, user):
-        return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-    def getRoles(self):
-        return self.query_db("SELECT role FROM user")
-    def checkUsername(self, user):
-        data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-        return True if data is not None else False
-    def createUser(self, user, password, role, salt):
-        """Create a user based on passed data.
-
-        Args:
-        ----
-            - username (str): Username to be used
-            - password (str): the salted password
-            - role (str): Role of the user
-            - salt (str): a random salt for the user
-        """
-        self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-    def deleteUser(self, user):
-        self.query_db("DELETE FROM user WHERE username=?", (user,))
-    def updateUser(self, username, data:dict[str, str]):
-        conn = self.connect()
-        cursor = conn.cursor()
-        query = "UPDATE user SET "
-        for key,value in data.items():
-            if key == "username":
-                continue
-            query += f"{key}='{value}',"
-        query = query[:-1]
-        query += " WHERE username=?"
-        params = (username,)
-        cursor.execute(query, params)
-        conn.commit()
-        self.close_connection(conn)
-    def getFacultyMember(self, name:str):
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-    def updateFacultyMember(self, data, oldlname, oldfname):
-        placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-        query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-        data["oldlname"] = oldlname
-        data["oldfname"] = oldfname
-        self.query_db(query, data)
-    def getFacultyMembers(self):
-        return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-

Methods

-
-
-def addBookToDatabase(self, bookdata: src.logic.dataclass.BookData, app_id: str, prof_id: str) -
-
-

Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.

-

Args

-
-
bookdata : BookData
-
The metadata of the book to be added
-
app_id : str
-
The apparat id where the book should be added to
-
prof_id : str
-
The id of the professor where the book should be added to.
-
-
- -Expand source code - -
def addBookToDatabase(self, bookdata:BookData,app_id:str, prof_id:str):
-    """
-    Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on.
-
-    Args:
-        bookdata (BookData): The metadata of the book to be added
-        app_id (str): The apparat id where the book should be added to
-        prof_id (str): The id of the professor where the book should be added to.
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    t_query = (
-        f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}"
-    )
-    # print(t_query)
-    result = cursor.execute(t_query).fetchall()
-    result = [load_pickle(i[0]) for i in result]
-    if bookdata in result:
-        print("Bookdata already in database")
-        # check if the book was deleted in the apparat
-        query = (
-            "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?"
-        )
-        params = (app_id, prof_id, dump_pickle(bookdata))
-        result = cursor.execute(query, params).fetchone()
-        if result[0] == 1:
-            print("Book was deleted, updating bookdata")
-            query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?"
-            params = (app_id, prof_id, dump_pickle(bookdata))
-            cursor.execute(query, params)
-            conn.commit()
-        return
-
-    query = (
-        "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)"
-    )
-    converted = dump_pickle(bookdata)
-    params = (converted, app_id, prof_id, 0)
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def addMessage(self, message: dict, user, appnr) -
-
-
-
- -Expand source code - -
def addMessage(self, message:dict,user, appnr):
-    def __getUserId(user):
-        return self.query_db("SELECT id FROM user WHERE username=?", (user,), one=True)[0]
-    user_id = __getUserId(user)
-    self.query_db("INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", (message["message"],user_id,message["remind_at"],appnr))
-
-
-
-def changePassword(self, user, new_password) -
-
-
-
- -Expand source code - -
def changePassword(self, user, new_password):
-    salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-    new_password = salt + new_password
-    self.query_db("UPDATE user SET password=? WHERE username=?", (new_password,user))
-
-
-
-def checkApparatExists(self, apparat_name) -
-
-
-
- -Expand source code - -
def checkApparatExists(self, apparat_name):
-    return True if self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True) else False
-
-
-
-def checkApparatExistsById(self, apparat_id) -
-
-
-
- -Expand source code - -
def checkApparatExistsById(self, apparat_id):
-    return True if self.query_db("SELECT appnr FROM semesterapparat WHERE appnr=?", (apparat_id,), one=True) else False
-
-
-
-def checkUsername(self, user) -
-
-
-
- -Expand source code - -
def checkUsername(self, user):
-    data = self.query_db("SELECT username FROM user WHERE username=?", (user,), one=True)
-    return True if data is not None else False
-
-
-
-def close_connection(self, conn: sqlite3.Connection) -
-
-

closes the connection to the database

-

Args:

-
- conn (sql.Connection): the connection to be closed
-
-
- -Expand source code - -
def close_connection(self, conn: sql.Connection):
-    """
-    closes the connection to the database
-    
-    Args:
-    ----
-        - conn (sql.Connection): the connection to be closed
-    """
-    conn.close()
-
-
-
-def connect(self) ‑> sqlite3.Connection -
-
-

Connect to the database

-

Returns

-
-
sql.Connection
-
The active connection to the database
-
-
- -Expand source code - -
def connect(self)->sql.Connection:
-    """
-    Connect to the database
-
-    Returns:
-        sql.Connection: The active connection to the database
-    """
-    return sql.connect(self.db_path)
-
-
-
-def createApparat(self, apparat: src.logic.dataclass.ApparatData) ‑> Union[src.errors.DatabaseErrors.AppPresentError, ForwardRef(None), int] -
-
-
-
- -Expand source code - -
def createApparat(self, apparat:ApparatData)->Optional[AppPresentError]|int:
-    prof_id = self.getProfId(apparat.profname)
-    app_id = self.getApparatId(apparat.appname)
-    if app_id:
-        return AppPresentError(app_id)
-
-    self.createProf(apparat.get_prof_details())
-    prof_id = self.getProfId(apparat.profname)
-    #ic(prof_id)
-    query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')"
-    logger.log_info(query)
-    self.query_db(query)
-    return self.getApparatId(apparat.appname)
-
-
-
-def createProf(self, prof_details: dict) -
-
-
-
- -Expand source code - -
def createProf(self, prof_details:dict):
-    prof_title = prof_details["prof_title"]
-    prof_fname = prof_details["profname"].split(",")[1]
-    prof_fname = prof_fname.strip()
-    prof_lname = prof_details["profname"].split(",")[0]
-    prof_lname = prof_lname.strip()
-    prof_fullname = prof_details["profname"].replace(",", "")
-    prof_mail = prof_details["prof_mail"]
-    prof_tel = prof_details["prof_tel"]
-    params = (prof_title, prof_fname, prof_lname, prof_mail, prof_tel, prof_fullname)
-    query = "INSERT OR IGNORE INTO prof (titel, fname, lname, mail, telnr, fullname) VALUES (?, ?, ?, ?, ?, ?)"
-    self.insertInto(query=query, params=params)
-
-
-
-def createUser(self, user, password, role, salt) -
-
-

Create a user based on passed data.

-

Args:

-
- username (str): Username to be used
-- password (str): the salted password
-- role (str): Role of the user
-- salt (str): a random salt for the user
-
-
- -Expand source code - -
def createUser(self, user, password, role, salt):
-    """Create a user based on passed data.
-
-    Args:
-    ----
-        - username (str): Username to be used
-        - password (str): the salted password
-        - role (str): Role of the user
-        - salt (str): a random salt for the user
-    """
-    self.query_db("INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", (user,password,role,salt))
-
-
-
-def create_tables(self) -
-
-

Create the tables in the database

-
- -Expand source code - -
def create_tables(self):
-    """
-    Create the tables in the database
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    cursor.execute(CREATE_TABLE_APPARAT)
-    cursor.execute(CREATE_TABLE_MESSAGES)
-    cursor.execute(CREATE_TABLE_MEDIA)
-    cursor.execute(CREATE_TABLE_APPKONTOS)
-    cursor.execute(CREATE_TABLE_FILES)
-    cursor.execute(CREATE_TABLE_PROF)
-    cursor.execute(CREATE_TABLE_USER)
-    cursor.execute(CREATE_TABLE_SUBJECTS)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def deleteApparat(self, appnr, semester) -
-
-
-
- -Expand source code - -
def deleteApparat(self, appnr, semester):
-    self.query_db("UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", (semester,appnr)) 
-
-
-
-def deleteBook(self, book_id) -
-
-

Delete a book from the database

-

Args

-
-
book_id : str
-
ID of the book
-
-
- -Expand source code - -
def deleteBook(self, book_id):
-    """
-    Delete a book from the database
-
-    Args:
-        book_id (str): ID of the book
-    """
-    self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,))
-
-
-
-def deleteMessage(self, message_id) -
-
-
-
- -Expand source code - -
def deleteMessage(self, message_id):
-    self.query_db("DELETE FROM messages WHERE id=?", (message_id,))
-
-
-
-def deleteUser(self, user) -
-
-
-
- -Expand source code - -
def deleteUser(self, user):
-    self.query_db("DELETE FROM user WHERE username=?", (user,))
-
-
-
-def getAllAparats(self, deleted=0) -
-
-
-
- -Expand source code - -
def getAllAparats(self,deleted=0):
-    return self.query_db("SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,))
-
-
-
-def getApparatCountBySemester(self) ‑> tuple[list[str], list[int]] -
-
-
-
- -Expand source code - -
def getApparatCountBySemester(self)->tuple[list[str],list[int]]:
-    conn = self.connect()
-    cursor = conn.cursor()
-    semesters = self.getSemersters()
-    created = []
-    deleted = []
-    for semester in semesters:
-        query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'"
-        result = cursor.execute(query).fetchone()
-        created.append(result[0])
-        query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'"
-        result = cursor.execute(query).fetchone()
-        deleted.append(result[0])
-    # store data in a tuple
-    ret = []
-    e_tuple = ()
-    for sem in semesters:
-        e_tuple = (
-            sem,
-            created[semesters.index(sem)],
-            deleted[semesters.index(sem)],
-        )
-        ret.append(e_tuple)
-    self.close_connection(conn)
-    return ret 
-
-
-
-def getApparatData(self, appnr, appname) ‑> src.logic.dataclass.ApparatData -
-
-
-
- -Expand source code - -
def getApparatData(self, appnr, appname)->ApparatData:
-
-    result = self.query_db("SELECT * FROM semesterapparat WHERE appnr=? AND name=?", (appnr,appname), one=True)
-    if result is None:
-        raise NoResultError("No result found")
-    apparat = ApparatData()
-    apparat.appname = result[1]
-    apparat.appnr = result[4]
-    apparat.dauerapp = True if result[7] == 1 else False
-    prof_data = self.getProfData(self.getProfNameById(result[2]))
-    apparat.profname = self.getProfNameById(result[2])
-    apparat.prof_mail = prof_data[0]
-    apparat.prof_tel = prof_data[1]
-    apparat.prof_title = prof_data[2]
-    apparat.app_fach = result[3]
-    apparat.erstellsemester = result[5]
-    apparat.semester = result[8]
-    apparat.deleted = result[9]
-    apparat.apparat_adis_id = result[11]
-    apparat.prof_adis_id = result[12]
-    return apparat
-
-
-
-def getApparatId(self, apparat_name) -
-
-
-
- -Expand source code - -
def getApparatId(self, apparat_name):
-    data = self.query_db("SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True)
-    if data is None:
-        return None
-    else:
-        return data[0]
-
-
-
-def getApparatName(self, app_id, prof_id) -
-
-
-
- -Expand source code - -
def getApparatName(self, app_id, prof_id):
-    return self.query_db("SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", (app_id,prof_id), one=True)[0]
-
-
-
-def getApparatsByProf(self, prof_id: int) ‑> list[tuple] -
-
-
-
- -Expand source code - -
def getApparatsByProf(self, prof_id:int)->list[tuple]:
-    return self.query_db("SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,))
-
-
-
-def getApparatsBySemester(self, semester: str) ‑> dict -
-
-
-
- -Expand source code - -
def getApparatsBySemester(self, semester:str)->dict:
-    data = self.query_db("SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", (semester,))
-    conn = self.connect()
-    cursor = conn.cursor()
-    c_tmp = []
-    for i in data:
-        c_tmp.append((i[0], self.getProfNameById(i[1])))
-    query = (
-        f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'"
-    )
-    result = cursor.execute(query).fetchall()
-    d_tmp = []
-    for i in result:
-        d_tmp.append((i[0], self.getProfNameById(i[1])))
-    # group the apparats by prof
-    c_ret = {}
-    for i in c_tmp:
-        if i[1] not in c_ret.keys():
-            c_ret[i[1]] = [i[0]]
-        else:
-            c_ret[i[1]].append(i[0])
-    d_ret = {}
-    for i in d_tmp:
-        if i[1] not in d_ret.keys():
-            d_ret[i[1]] = [i[0]]
-        else:
-            d_ret[i[1]].append(i[0])
-    self.close_connection(conn)
-    return {"created": c_ret, "deleted": d_ret}
-
-
-
-def getBlob(self, filename, app_id) -
-
-

Get a blob from the database

-

Args

-
-
filename : str
-
The name of the file
-
app_id : str
-
ID of the apparat
-
-

Returns

-
-
bytes
-
The file stored in
-
-
- -Expand source code - -
def getBlob(self, filename, app_id):
-    """
-    Get a blob from the database
-
-    Args:
-        filename (str): The name of the file
-        app_id (str): ID of the apparat
-
-    Returns:
-        bytes: The file stored in 
-    """
-    return self.query_db("SELECT fileblob FROM files WHERE filename=? AND app_id=?", (filename,app_id), one=True)[0]
-
-
-
-def getBook(self, book_id: int) ‑> src.logic.dataclass.BookData -
-
-

Get the book based on the id in the database

-

Args

-
-
book_id : int
-
The id of the book
-
-

Returns

-
-
BookData
-
The metadata of the book wrapped in a BookData object
-
-
- -Expand source code - -
def getBook(self,book_id:int)->BookData:
-    """
-    Get the book based on the id in the database
-
-    Args:
-        book_id (int): The id of the book
-
-    Returns:
-        BookData: The metadata of the book wrapped in a BookData object
-    """
-    return load_pickle(self.query_db("SELECT bookdata FROM media WHERE id=?", (book_id,), one=True)[0])
-
-
-
-def getBookBasedOnSignature(self, app_id: str, prof_id: str, signature: str) ‑> src.logic.dataclass.BookData -
-
-

Get the book based on the signature of the book.

-

Args

-
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
signature : str
-
The signature of the book
-
-

Returns

-
-
BookData
-
The total metadata of the book wrapped in a BookData object
-
-
- -Expand source code - -
def getBookBasedOnSignature(self, app_id:str, prof_id:str,signature:str)->BookData:
-    """
-    Get the book based on the signature of the book.
-
-    Args:
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-        signature (str): The signature of the book
-
-    Returns:
-        BookData: The total metadata of the book wrapped in a BookData object
-    """
-    result = self.query_db("SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-    books = [load_pickle(i[0]) for i in result]
-    book = [i for i in books if i.signature == signature][0]
-    return book
-
-
-
-def getBookId(self, bookdata: src.logic.dataclass.BookData, app_id, prof_id) ‑> int -
-
-

Get the id of a book based on the metadata of the book

-

Args

-
-
bookdata : BookData
-
The wrapped metadata of the book
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
-

Returns

-
-
int
-
ID of the book
-
-
- -Expand source code - -
def getBookId(self, bookdata:BookData, app_id, prof_id)->int:
-    """
-    Get the id of a book based on the metadata of the book
-
-    Args:
-        bookdata (BookData): The wrapped metadata of the book
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-
-    Returns:
-        int: ID of the book
-    """
-    result = self.query_db("SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", (dump_pickle(bookdata),app_id,prof_id), one=True)
-    return result[0]
-
-
-
-def getBookIdBasedOnSignature(self, app_id: str, prof_id: str, signature: str) ‑> int -
-
-

Get a book id based on the signature of the book.

-

Args

-
-
app_id : str
-
The apparat id the book should be associated with
-
prof_id : str
-
The professor id the book should be associated with
-
signature : str
-
The signature of the book
-
-

Returns

-
-
int
-
The id of the book
-
-
- -Expand source code - -
def getBookIdBasedOnSignature(self, app_id:str, prof_id:str,signature:str)->int:
-    """
-    Get a book id based on the signature of the book.
-
-    Args:
-        app_id (str): The apparat id the book should be associated with
-        prof_id (str): The professor id the book should be associated with
-        signature (str): The signature of the book
-
-    Returns:
-        int: The id of the book
-    """
-    result = self.query_db("SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-    books = [(load_pickle(i[0]),i[1]) for i in result]
-    book = [i for i in books if i[0].signature == signature][0][1]
-    return book
-
-
-
-def getBooks(self, app_id, prof_id, deleted=0) ‑> list[dict[int, src.logic.dataclass.BookData, int]] -
-
-

Get the Books based on the apparat id and the professor id

-

Args

-
-
app_id : str
-
The ID of the apparat
-
prof_id : str
-
The ID of the professor
-
deleted : int, optional
-
The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-

Returns

-
-
list[dict[int, BookData, int]]
-
A list of dictionaries containing the id, the metadata of the book and the availability of the book
-
-
- -Expand source code - -
def getBooks(self, app_id, prof_id, deleted=0)->list[dict[int, BookData, int]]:
-    """
-    Get the Books based on the apparat id and the professor id
-
-    Args:
-        app_id (str): The ID of the apparat
-        prof_id (str): The ID of the professor
-        deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0.
-
-    Returns:
-        list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book
-    """
-    qdata = self.query_db(f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})")
-    ret_result = []
-    for result_a in qdata:
-        data = {"id": int, "bookdata": BookData, "available": int}
-        data["id"] = result_a[0]
-        data["bookdata"] = load_pickle(result_a[1])
-        data["available"] = result_a[2]
-        ret_result.append(data)
-    return ret_result
-
-
-
-def getFacultyMember(self, name: str) -
-
-
-
- -Expand source code - -
def getFacultyMember(self, name:str):
-    return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", (name,), one=True)
-
-
-
-def getFacultyMembers(self) -
-
-
-
- -Expand source code - -
def getFacultyMembers(self):
-    return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof")
-
-
-
-def getFiles(self, app_id: int, prof_id: int) ‑> list[tuple] -
-
-
-
- -Expand source code - -
def getFiles(self, app_id:int, prof_id:int)->list[tuple]:
-    return self.query_db("SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", (app_id,prof_id))
-
-
-
-def getLastBookId(self) ‑> int -
-
-

Get the last book id in the database

-

Returns

-
-
int
-
ID of the last book in the database
-
-
- -Expand source code - -
def getLastBookId(self)->int:
-    """
-    Get the last book id in the database
-
-    Returns:
-        int: ID of the last book in the database
-    """
-    return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0]
-
-
-
-def getMessages(self, date: str) -
-
-
-
- -Expand source code - -
def getMessages(self, date:str):
-    def __get_user_name(user_id):
-        return self.query_db("SELECT username FROM user WHERE id=?", (user_id,), one=True)[0]
-    messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,))
-    ret = [
-        {
-            "message": i[2],
-            "user": __get_user_name(i[4]),
-            "appnr": i[5],
-            "id": i[0]
-        }
-        for i in messages
-    ]
-    return ret
-
-
-
-def getProfByName(self, prof_name: str) -
-
-
-
- -Expand source code - -
def getProfByName(self, prof_name:str):
-    return self.query_db("SELECT * FROM prof WHERE fullname=?", (prof_name,), one=True)
-
-
-
-def getProfData(self, profname: str) -
-
-
-
- -Expand source code - -
def getProfData(self, profname:str):
-    
-    data = self.query_db("SELECT mail, telnr, titel FROM prof WHERE fullname=?", (profname.replace(",",""),), one=True)
-    return data
-
-
-
-def getProfId(self, prof_name: str) -
-
-

getProfId summary

-

:param prof_name: description -:type prof_name: str -:return: description -:rtype: type

-
- -Expand source code - -
def getProfId(self, prof_name:str):
-    """
-    getProfId _summary_
-
-    :param prof_name: _description_
-    :type prof_name: str
-    :return: _description_
-    :rtype: _type_
-    """
-    data = self.getProfByName(prof_name.replace(",", ""))
-    if data is None:
-        return None
-    else:
-        return data[0]
-
-
-
-def getProfNameById(self, prof_id: int, add_title: bool = False) -
-
-
-
- -Expand source code - -
def getProfNameById(self, prof_id:int,add_title:bool=False):
-    prof = self.query_db("SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True)
-    if add_title:
-        return f"{self.getTitleById(prof_id)}{prof[0]}"
-    else:
-        return prof[0]
-
-
-
-def getProfs(self) -
-
-
-
- -Expand source code - -
def getProfs(self):
-    return self.query_db("SELECT * FROM prof")
-
-
-
-def getRole(self, user) -
-
-
-
- -Expand source code - -
def getRole(self, user):
-    return self.query_db("SELECT role FROM user WHERE username=?", (user,), one=True)[0]
-
-
-
-def getRoles(self) -
-
-
-
- -Expand source code - -
def getRoles(self):
-    return self.query_db("SELECT role FROM user")
-
-
-
-def getSemersters(self) -
-
-
-
- -Expand source code - -
def getSemersters(self):
-    data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat")
-    return [i[0] for i in data]
-
-
-
-def getSpecificProfData(self, prof_id: int, fields: List[str]) -
-
-

getSpecificProfData summary

-

Args:

-
- prof_id (int): _description_
-- fields (List[str]): _description_
-
-

Returns:

-
- _type_: _description_
-
-
- -Expand source code - -
def getSpecificProfData(self, prof_id:int, fields:List[str]):
-    """
-    getSpecificProfData _summary_
-    
-    
-    
-    Args:
-    ----
-        - prof_id (int): _description_
-        - fields (List[str]): _description_
-    
-    Returns:
-    -------
-        - _type_: _description_
-    """
-    query = "SELECT "
-    for field in fields:
-        query += f"{field},"
-    query = query[:-1]
-    query += " FROM prof WHERE id=?"
-    return self.query_db(query, (prof_id,), one=True)[0]
-
-
-
-def getSubjects(self) -
-
-
-
- -Expand source code - -
def getSubjects(self):
-    return self.query_db("SELECT * FROM subjects")
-
-
-
-def getTitleById(self, prof_id: int) -
-
-
-
- -Expand source code - -
def getTitleById(self, prof_id:int):
-    title = self.query_db("SELECT titel FROM prof WHERE id=?", (prof_id,), one=True)[0]
-    return f"{title} " if title is not None else ""
-
-
-
-def getUnavailableApparatNumbers(self) ‑> List[int] -
-
-

getUnavailableApparatNumbers returns a list of all currently used ApparatNumbers

-

Returns:

-
- number(List[int]): a list of all currently used apparat numbers
-
-
- -Expand source code - -
def getUnavailableApparatNumbers(self)->List[int]:
-    """
-    getUnavailableApparatNumbers returns a list of all currently used ApparatNumbers
-    
-    
-    
-    Returns:
-    -------
-        - number(List[int]): a list of all currently used apparat numbers
-    """
-    numbers = self.query_db("SELECT appnr FROM semesterapparat WHERE deletion_status=0")    
-    numbers = [i[0] for i in numbers]
-    logger.log_info(f"Currently used apparat numbers: {numbers}")
-    return numbers
-
-
-
-def getUser(self) -
-
-
-
- -Expand source code - -
def getUser(self):
-    return self.query_db("SELECT * FROM user", one=True)
-
-
-
-def getUsers(self) -
-
-
-
- -Expand source code - -
def getUsers(self):
-    return self.query_db("SELECT * FROM user")
-
-
-
-def get_db_contents(self) ‑> Optional[List[Tuple]] -
-
-

Get the contents of the

-

Returns

-
-
Union[List[Tuple], None]
-
description
-
-
- -Expand source code - -
def get_db_contents(self)->Union[List[Tuple], None]:
-    """
-    Get the contents of the 
-
-    Returns:
-        Union[List[Tuple], None]: _description_
-    """
-    try:
-        with sql.connect(self.db_path) as conn:
-            cursor = conn.cursor()
-            cursor.execute("SELECT * FROM sqlite_master WHERE type='table'")
-            return cursor.fetchall()
-    except sql.OperationalError:
-        return None
-
-
-
-def insertFile(self, file: list[dict], app_id: int, prof_id) -
-
-
-
- -Expand source code - -
def insertFile(self, file: list[dict], app_id: int, prof_id):
-    for f in file:
-        filename = f["name"]
-        path = f["path"]
-        filetyp = f["type"]
-        if path == "Database":
-            continue
-        blob = create_blob(path)
-        query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)"
-        self.query_db(query, (filename, blob, app_id, filetyp,prof_id))
-
-
-
-def insertInto(self, query: str, params: Tuple) ‑> None -
-
-

Insert sent data into the database

-

Args

-
-
query : str
-
The query to be executed
-
params : Tuple
-
the parameters to be inserted into the database
-
-
- -Expand source code - -
def insertInto(self, query:str, params:Tuple) -> None:
-    """
-    Insert sent data into the database
-
-    Args:
-        query (str): The query to be executed
-        params (Tuple): the parameters to be inserted into the database
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    logger.log_info(f"Inserting {params} into database with query {query}")
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-def isEternal(self, id) -
-
-
-
- -Expand source code - -
def isEternal(self, id):
-    return self.query_db("SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True)
-
-
-
-def login(self, user, hashed_password) -
-
-
-
- -Expand source code - -
def login(self, user, hashed_password):
-    salt = self.query_db("SELECT salt FROM user WHERE username=?", (user,), one=True)[0]
-    if salt is None:
-        return False
-    hashed_password = salt + hashed_password
-    password = self.query_db("SELECT password FROM user WHERE username=?", (user,), one=True)[0]
-    if password == hashed_password:
-        return True
-    else:
-        return False
-
-
-
-def query_db(self, query: str, args: Tuple = (), one: bool = False) ‑> Union[Tuple, List[Tuple]] -
-
-

Query the Database for the sent query.

-

Args

-
-
query : str
-
The query to be executed
-
args : Tuple, optional
-
The arguments for the query. Defaults to ().
-
one : bool, optional
-
Return the first result only. Defaults to False.
-
-

Returns

-

Union[Typle|List[Tuple]]: Returns the result of the query

-
- -Expand source code - -
def query_db(self, query: str, args: Tuple = (), one: bool = False)->Union[Tuple, List[Tuple]]:
-    """
-    Query the Database for the sent query.
-
-    Args:
-        query (str): The query to be executed
-        args (Tuple, optional): The arguments for the query. Defaults to ().
-        one (bool, optional): Return the first result only. Defaults to False.
-
-    Returns:
-        Union[Typle|List[Tuple]]: Returns the result of the query
-    """
-    conn = self.connect()
-    cursor = conn.cursor()
-    logger.log_info(f"Querying database with query {query}, args: {args}")
-    cursor.execute(query, args)
-    rv = cursor.fetchall()
-    conn.commit()
-    self.close_connection(conn)
-    return (rv[0] if rv else None) if one else rv
-
-
-
-def recreateFile(self, filename, app_id, filetype) -
-
-
-
- -Expand source code - -
def recreateFile(self, filename, app_id,filetype):
-    blob = self.getBlob(filename, app_id)
-    tempdir = config.database.tempdir
-    tempdir = tempdir.replace("~", str(Path.home()))
-    tempdir_path = Path(tempdir)        
-    if not os.path.exists(tempdir_path):
-        os.mkdir(tempdir_path)
-    file = tempfile.NamedTemporaryFile(
-        delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}"
-    )
-    file.write(blob)
-    print("file created")
-    return file.name
-
-
-
-def searchBook(self, data: dict[str, str]) ‑> list[tuple[src.logic.dataclass.BookData, int]] -
-
-

Search a book in the database based on the sent data.

-

Args

-
-
data : dict[str, str]
-
A dictionary containing the data to be searched for. The dictionary can contain the following:
-
-
    -
  • signature: The signature of the book
  • -
  • title: The title of the book
  • -
-

Returns

-
-
list[tuple[BookData, int]]
-
A list of tuples containing the wrapped Metadata and the id of the book
-
-
- -Expand source code - -
def searchBook(self, data:dict[str, str])->list[tuple[BookData, int]]:
-    """
-    Search a book in the database based on the sent data.
-
-    Args:
-        data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: 
-        - signature: The signature of the book
-        - title: The title of the book
-
-    Returns:
-        list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book
-    """
-    rdata = self.query_db("SELECT * FROM media WHERE deleted=0")
-    #ic(rdata, len(rdata))
-    mode = 0
-    if len(data)== 1:
-        if "signature" in data.keys():
-            mode = 1
-        elif "title" in data.keys():
-            mode = 2
-    elif len(data) == 2:
-        mode = 3
-    else:
-        return None
-    ret = []
-    for book in rdata:
-        bookdata = load_pickle(book[1])
-        app_id = book[2]
-        prof_id = book[3]
-        if mode == 1:
-            if data["signature"] in bookdata.signature:
-                ret.append((bookdata,app_id,prof_id))
-        elif mode == 2:
-            if data["title"] in bookdata.title:
-                ret.append((bookdata,app_id,prof_id))
-        elif mode == 3:
-            if data["signature"] in bookdata.signature and data["title"] in bookdata.title:
-                ret.append((bookdata,app_id,prof_id))
-    #ic(ret)
-    return ret
-
-
-
-def setAvailability(self, book_id: str, available: str) -
-
-

Set the availability of a book in the database

-

Args

-
-
book_id : str
-
The id of the book
-
available : str
-
The availability of the book
-
-
- -Expand source code - -
def setAvailability(self, book_id:str, available:str):
-    """
-    Set the availability of a book in the database
-
-    Args:
-        book_id (str): The id of the book
-        available (str): The availability of the book
-    """
-    self.query_db("UPDATE media SET available=? WHERE id=?", (available,book_id))
-
-
-
-def setNewSemesterDate(self, appnr, newDate, dauerapp=False) -
-
-
-
- -Expand source code - -
def setNewSemesterDate(self, appnr, newDate, dauerapp=False):
-    date = datetime.datetime.strptime(newDate, "%d.%m.%Y").strftime("%Y-%m-%d")
-    if dauerapp:
-        self.query_db("UPDATE semesterapparat SET verlängerung_bis=?, dauerapp=? WHERE appnr=?", (date,dauerapp,appnr))
-    else:
-        self.query_db("UPDATE semesterapparat SET endsemester=? WHERE appnr=?", (date,appnr))
-
-
-
-def statistic_request(self, **kwargs: Any) -
-
-
-
- -Expand source code - -
def statistic_request(self, **kwargs: Any):
-    def __query(query):
-        conn = self.connect()
-        cursor = conn.cursor()
-        result = cursor.execute(query).fetchall()
-        for result_a in result:
-            orig_value = result_a
-            prof_name = self.getProfNameById(result_a[2])
-            # replace the prof_id with the prof_name
-            result_a = list(result_a)
-            result_a[2] = prof_name
-            result_a = tuple(result_a)
-            result[result.index(orig_value)] = result_a
-        self.close_connection(conn)
-        return result
-    if "deletable" in kwargs.keys():
-        query = f"SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND (erstellsemester!='{kwargs['deletesemester']}' OR verlängerung_bis!='{kwargs['deletesemester']}')"
-        return __query(query)
-    if "dauer" in kwargs.keys():
-        kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0")
-    query = "SELECT * FROM semesterapparat WHERE "
-    for key, value in kwargs.items() if kwargs.items() is not None else {}:
-        print(key, value)
-        query += f"{key}='{value}' AND "
-        print(query)
-    # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat
-    if "deletesemester" in kwargs.keys():
-        query = query.replace(
-            f"deletesemester='{kwargs['deletesemester']}' AND ", ""
-        )
-    if "endsemester" in kwargs.keys():
-        if "erstellsemester" in kwargs.keys():
-            query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "")
-            query = query.replace(
-                f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz"
-            )
-        else:
-            query = query.replace(
-                f"endsemester='{kwargs['endsemester']}' AND ", "xyz"
-            )
-            print("replaced")
-        query = query.replace(
-            "xyz",
-            f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ",
-        )
-    # remove all x="" parts from the query where x is a key in kwargs
-    query = query[:-5]
-    print(query)
-    return __query(query)
-
-
-
-def updateApparat(self, apparat_data: src.logic.dataclass.ApparatData) -
-
-
-
- -Expand source code - -
def updateApparat(self, apparat_data:ApparatData):
-    query = f"UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ? WHERE appnr = ?"
-    params = (
-        apparat_data.appname,
-        apparat_data.app_fach,
-        apparat_data.dauerapp,
-        self.getProfId(apparat_data.profname),
-        apparat_data.appnr,        
-    )
-    self.query_db(query, params)
-
-
-
-def updateBookdata(self, book_id, bookdata: src.logic.dataclass.BookData) -
-
-

Update the bookdata in the database

-

Args

-
-
book_id : str
-
The id of the book
-
bookdata : BookData
-
The new metadata of the book
-
-
- -Expand source code - -
def updateBookdata(self, book_id, bookdata:BookData):
-    """
-    Update the bookdata in the database
-
-    Args:
-        book_id (str): The id of the book
-        bookdata (BookData): The new metadata of the book
-    """
-    self.query_db("UPDATE media SET bookdata=? WHERE id=?", (dump_pickle(bookdata),book_id))
-
-
-
-def updateFacultyMember(self, data, oldlname, oldfname) -
-
-
-
- -Expand source code - -
def updateFacultyMember(self, data, oldlname, oldfname):
-    placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()])
-    query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname"
-    data["oldlname"] = oldlname
-    data["oldfname"] = oldfname
-    self.query_db(query, data)
-
-
-
-def updateUser(self, username, data: dict[str, str]) -
-
-
-
- -Expand source code - -
def updateUser(self, username, data:dict[str, str]):
-    conn = self.connect()
-    cursor = conn.cursor()
-    query = "UPDATE user SET "
-    for key,value in data.items():
-        if key == "username":
-            continue
-        query += f"{key}='{value}',"
-    query = query[:-1]
-    query += " WHERE username=?"
-    params = (username,)
-    cursor.execute(query, params)
-    conn.commit()
-    self.close_connection(conn)
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/database.md b/documentation/database.md deleted file mode 100644 index c518b09..0000000 --- a/documentation/database.md +++ /dev/null @@ -1,327 +0,0 @@ -Module database -=============== - -Classes -------- - -`Database(db_path: str = None)` -: Initialize the database and create the tables if they do not exist. - - Default constructor for the database class - - Args: - db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None. - - ### Methods - - `addBookToDatabase(self, bookdata: src.logic.dataclass.BookData, app_id: str, prof_id: str)` - : Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on. - - Args: - bookdata (BookData): The metadata of the book to be added - app_id (str): The apparat id where the book should be added to - prof_id (str): The id of the professor where the book should be added to. - - `addMessage(self, message: dict, user, appnr)` - : - - `changePassword(self, user, new_password)` - : - - `checkApparatExists(self, apparat_name)` - : - - `checkApparatExistsById(self, apparat_id)` - : - - `checkUsername(self, user)` - : - - `close_connection(self, conn: sqlite3.Connection)` - : closes the connection to the database - - Args: - ---- - - conn (sql.Connection): the connection to be closed - - `connect(self) ‑> sqlite3.Connection` - : Connect to the database - - Returns: - sql.Connection: The active connection to the database - - `createApparat(self, apparat: src.logic.dataclass.ApparatData) ‑> Union[src.errors.DatabaseErrors.AppPresentError, ForwardRef(None), int]` - : - - `createProf(self, prof_details: dict)` - : - - `createUser(self, user, password, role, salt)` - : Create a user based on passed data. - - Args: - ---- - - username (str): Username to be used - - password (str): the salted password - - role (str): Role of the user - - salt (str): a random salt for the user - - `create_tables(self)` - : Create the tables in the database - - `deleteApparat(self, appnr, semester)` - : - - `deleteBook(self, book_id)` - : Delete a book from the database - - Args: - book_id (str): ID of the book - - `deleteMessage(self, message_id)` - : - - `deleteUser(self, user)` - : - - `getAllAparats(self, deleted=0)` - : - - `getApparatCountBySemester(self) ‑> tuple[list[str], list[int]]` - : - - `getApparatData(self, appnr, appname) ‑> src.logic.dataclass.ApparatData` - : - - `getApparatId(self, apparat_name)` - : - - `getApparatName(self, app_id, prof_id)` - : - - `getApparatsByProf(self, prof_id: int) ‑> list[tuple]` - : - - `getApparatsBySemester(self, semester: str) ‑> dict` - : - - `getBlob(self, filename, app_id)` - : Get a blob from the database - - Args: - filename (str): The name of the file - app_id (str): ID of the apparat - - Returns: - bytes: The file stored in - - `getBook(self, book_id: int) ‑> src.logic.dataclass.BookData` - : Get the book based on the id in the database - - Args: - book_id (int): The id of the book - - Returns: - BookData: The metadata of the book wrapped in a BookData object - - `getBookBasedOnSignature(self, app_id: str, prof_id: str, signature: str) ‑> src.logic.dataclass.BookData` - : Get the book based on the signature of the book. - - Args: - app_id (str): The apparat id the book should be associated with - prof_id (str): The professor id the book should be associated with - signature (str): The signature of the book - - Returns: - BookData: The total metadata of the book wrapped in a BookData object - - `getBookId(self, bookdata: src.logic.dataclass.BookData, app_id, prof_id) ‑> int` - : Get the id of a book based on the metadata of the book - - Args: - bookdata (BookData): The wrapped metadata of the book - app_id (str): The apparat id the book should be associated with - prof_id (str): The professor id the book should be associated with - - Returns: - int: ID of the book - - `getBookIdBasedOnSignature(self, app_id: str, prof_id: str, signature: str) ‑> int` - : Get a book id based on the signature of the book. - - Args: - app_id (str): The apparat id the book should be associated with - prof_id (str): The professor id the book should be associated with - signature (str): The signature of the book - - Returns: - int: The id of the book - - `getBooks(self, app_id, prof_id, deleted=0) ‑> list[dict[int, src.logic.dataclass.BookData, int]]` - : Get the Books based on the apparat id and the professor id - - Args: - app_id (str): The ID of the apparat - prof_id (str): The ID of the professor - deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0. - - Returns: - list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book - - `getFacultyMember(self, name: str)` - : - - `getFacultyMembers(self)` - : - - `getFiles(self, app_id: int, prof_id: int) ‑> list[tuple]` - : - - `getLastBookId(self) ‑> int` - : Get the last book id in the database - - Returns: - int: ID of the last book in the database - - `getMessages(self, date: str)` - : - - `getProfByName(self, prof_name: str)` - : - - `getProfData(self, profname: str)` - : - - `getProfId(self, prof_name: str)` - : getProfId _summary_ - - :param prof_name: _description_ - :type prof_name: str - :return: _description_ - :rtype: _type_ - - `getProfNameById(self, prof_id: int, add_title: bool = False)` - : - - `getProfs(self)` - : - - `getRole(self, user)` - : - - `getRoles(self)` - : - - `getSemersters(self)` - : - - `getSpecificProfData(self, prof_id: int, fields: List[str])` - : getSpecificProfData _summary_ - - - - Args: - ---- - - prof_id (int): _description_ - - fields (List[str]): _description_ - - Returns: - ------- - - _type_: _description_ - - `getSubjects(self)` - : - - `getTitleById(self, prof_id: int)` - : - - `getUnavailableApparatNumbers(self) ‑> List[int]` - : getUnavailableApparatNumbers returns a list of all currently used ApparatNumbers - - - - Returns: - ------- - - number(List[int]): a list of all currently used apparat numbers - - `getUser(self)` - : - - `getUsers(self)` - : - - `get_db_contents(self) ‑> Optional[List[Tuple]]` - : Get the contents of the - - Returns: - Union[List[Tuple], None]: _description_ - - `insertFile(self, file: list[dict], app_id: int, prof_id)` - : - - `insertInto(self, query: str, params: Tuple) ‑> None` - : Insert sent data into the database - - Args: - query (str): The query to be executed - params (Tuple): the parameters to be inserted into the database - - `isEternal(self, id)` - : - - `login(self, user, hashed_password)` - : - - `query_db(self, query: str, args: Tuple = (), one: bool = False) ‑> Union[Tuple, List[Tuple]]` - : Query the Database for the sent query. - - Args: - query (str): The query to be executed - args (Tuple, optional): The arguments for the query. Defaults to (). - one (bool, optional): Return the first result only. Defaults to False. - - Returns: - Union[Typle|List[Tuple]]: Returns the result of the query - - `recreateFile(self, filename, app_id, filetype)` - : - - `searchBook(self, data: dict[str, str]) ‑> list[tuple[src.logic.dataclass.BookData, int]]` - : Search a book in the database based on the sent data. - - Args: - data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: - - signature: The signature of the book - - title: The title of the book - - Returns: - list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book - - `setAvailability(self, book_id: str, available: str)` - : Set the availability of a book in the database - - Args: - book_id (str): The id of the book - available (str): The availability of the book - - `setNewSemesterDate(self, appnr, newDate, dauerapp=False)` - : - - `statistic_request(self, **kwargs: Any)` - : - - `updateApparat(self, apparat_data: src.logic.dataclass.ApparatData)` - : - - `updateBookdata(self, book_id, bookdata: src.logic.dataclass.BookData)` - : Update the bookdata in the database - - Args: - book_id (str): The id of the book - bookdata (BookData): The new metadata of the book - - `updateFacultyMember(self, data, oldlname, oldfname)` - : - - `updateUser(self, username, data: dict[str, str])` - : \ No newline at end of file diff --git a/resources.qrc b/resources.qrc deleted file mode 100644 index 7be4647..0000000 --- a/resources.qrc +++ /dev/null @@ -1,16 +0,0 @@ - - - resources/logo_SAP.ico - resources/logo_SAP.png - resources/1f4e7.svg - resources/1f510.svg - resources/2795.svg - resources/VZjRNn1k.ico - - - resources/critical.png - resources/information.png - resources/question.png - resources/warning.png - - diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 747a691..0000000 --- a/ruff.toml +++ /dev/null @@ -1,77 +0,0 @@ -# Exclude a variety of commonly ignored directories. -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "site-packages", - "venv", -] - -# Same as Black. -line-length = 88 -indent-width = 4 - -# Assume Python 3.8 -target-version = "py312" - -[lint] -# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. -# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or -# McCabe complexity (`C901`) by default. -select = ["E4", "E7", "E9", "F"] -ignore = [] - -# Allow fix for all enabled rules (when `--fix`) is provided. -fixable = ["ALL"] -unfixable = [] - -# Allow unused variables when underscore-prefixed. -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" - -[format] -# Like Black, use double quotes for strings. -quote-style = "double" - -# Like Black, indent with spaces, rather than tabs. -indent-style = "space" - -# Like Black, respect magic trailing commas. -skip-magic-trailing-comma = false - -# Like Black, automatically detect the appropriate line ending. -line-ending = "auto" - -# Enable auto-formatting of code examples in docstrings. Markdown, -# reStructuredText code/literal blocks and doctests are all supported. -# -# This is currently disabled by default, but it is planned for this -# to be opt-out in the future. -docstring-code-format = false - -# Set the line length limit used when formatting code snippets in -# docstrings. -# -# This only has an effect when the `docstring-code-format` setting is -# enabled. -docstring-code-line-length = "dynamic" diff --git a/sap.db b/sap.db deleted file mode 100644 index adc66bafab03827e30c6e1a67bf9be526bd41a83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 647168 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
  • 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%0A^CO(a2mV+z8 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 diff --git a/semap.db b/semap.db deleted file mode 100644 index e69de29..0000000 diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/bookgrabbertest.py b/test/bookgrabbertest.py deleted file mode 100644 index a744df6..0000000 --- a/test/bookgrabbertest.py +++ /dev/null @@ -1,66 +0,0 @@ -from src.backend.database import Database -from src.logic.webrequest import BibTextTransformer, WebRequest -import sqlite3 - -class BookGrabber: - - def __init__( - self, - mode: str = None, - data: list = None, - app_id: int = None, - prof_id: int = None, - ): - self.app_id = app_id - self.prof_id = prof_id - self.mode = mode - self.data = data - self.book_id = None - - def run(self): - self.db = Database() - item = 0 - for entry in self.data: - signature = str(entry) - self.logger.log_info("Processing entry: " + signature) - - webdata = WebRequest().get_ppn(entry).get_data() - if webdata == "error": - continue - bd = BibTextTransformer(self.mode).get_data(webdata).return_data() - transformer = BibTextTransformer("RDS") - rds = transformer.get_data(webdata).return_data("rds_availability") - bd.signature = entry - # confirm lock is acquired - print("lock acquired, adding book to database") - self.db.addBookToDatabase(bd, self.app_id, self.prof_id) - # get latest book id - self.book_id = self.db.getLastBookId() - self.logger.log_info("Added book to database") - state = 0 - for rds_item in rds.items: - sign = rds_item.superlocation - loc = rds_item.location - # print(item.location) - if self.app_id in sign or self.app_id in loc: - state = 1 - book_id = None - # for book in self.books: - # if book["bookdata"].signature == entry: - # book_id = book["id"] - # break - self.logger.log_info(f"State of {signature}: {state}") - print( - "lock acquired, updating availability of " - + str(book_id) - + " to " - + str(state) - ) - try: - self.db.setAvailability(self.book_id, state) - except sqlite3.OperationalError as e: - self.logger.log_error(f"Failed to update availability: {e}") - break - - # time.sleep(5) - item += 1 diff --git a/test/database_test.py b/test/database_test.py deleted file mode 100644 index a4d091d..0000000 --- a/test/database_test.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest - -from src.backend.database import Database - - -def test_prof(): - assert Database().getProfNameById(1, add_title=True) == "Karoß Sabine" - assert Database().getProfNameById(1, add_title=False) == "Karoß Sabine" - assert Database().getProfId("Karoß Sabine") == 1 - - -def test_apparat(): - assert Database().getApparatName(3, 1) == "Theorie und Praxis Gymnastik" - - -def test_admin(): - assert len(Database().getUser()) >= 1 and "admin" in Database().getUser() diff --git a/test/many_webrequest_test.py b/test/many_webrequest_test.py deleted file mode 100644 index 2cdd479..0000000 --- a/test/many_webrequest_test.py +++ /dev/null @@ -1,10 +0,0 @@ -from test.webrequest_test import test_webdata_bibtexttransform - - -def many_test_webdata(): - test_webdata_bibtexttransform("RIS") - test_webdata_bibtexttransform("BibTeX") - test_webdata_bibtexttransform("COinS") - test_webdata_bibtexttransform("ARRAY") - test_webdata_bibtexttransform("RDS") - assert True is True diff --git a/test/rds_test.py b/test/rds_test.py deleted file mode 100644 index a7e6b6c..0000000 --- a/test/rds_test.py +++ /dev/null @@ -1,103 +0,0 @@ -def contact_prof(self): - dialog = QtWidgets.QDialog() - mail_prev = Mail_Dialog() - mail_prev.setupUi(dialog) - mail_prevs = os.listdir("mail_vorlagen") - if self.app_name.text() == "": - mail_prevs.remove("Information zum Semesterapparat.eml") - mail_prev.comboBox.addItems(mail_prevs) - active_apparat_id = self.tableWidget_apparate.item( - self.tableWidget_apparate.currentRow(), 0 - ).text() - general_data = { - "Appname": self.app_name.text(), - "AppSubject": self.app_fach.currentText(), - "appnr": self.active_apparat, - } - print(active_apparat_id) - mail_prev.appid = active_apparat_id - base_data = self.db.getProfData(id=active_apparat_id) - profname = self.db.getProfNameById(base_data["id"]) - profname = profname.split(" ") - profname = f"{profname[1]} {profname[0]}" - pass_data = { - "prof_name": profname, - "mail_name": base_data["prof_mail"], - "general": general_data, - } - - mail_prev.set_data(pass_data) - mail_prev.set_mail() - dialog.exec() - - -import subprocess -import tempfile - -from PyQt6 import QtCore, QtGui, QtWidgets - -from src.ui.dialogs.mail_preview import Ui_Dialog - - -class Mail_Dialog(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, parent=None): - super().__init__(parent) - self.setupUi(self) - self.appid = "" - self.mail_data = "" - self.data = None - self.buttonBox.accepted.connect(self.save_mail) - - def set_data(self, data: dict): - print(data) - self.prof_name.setText(data["prof_name"]) - self.mail_name.setText(data["mail_name"]) - # assign data["general"] to self.data - self.data = data["general"] - - def set_mail(self): - email_template = self.comboBox.currentText() - with open(f"mail_vorlagen/{email_template}", "r", encoding="utf-8") as f: - mail_template = f.read() - email_header = email_template.split(".eml")[0] - if "{AppNr}" in email_template: - email_header = email_template.split(".eml")[0].format(AppNr=self.appid) - self.mail_header.setText(email_header) - self.mail_data = mail_template.split("")[0] - mail_html = mail_template.split("")[1] - mail_html = "" + mail_html - print(self.data) - Appname = self.data["Appname"] - mail_html = mail_html.format( - Profname=self.prof_name.text().split(" ")[-1], Appname=Appname - ) - - self.mail_body.setHtml(mail_html) - - def save_mail(self): - # create a temporary file - mail_header = self.mail_header.text() - mail_body = self.mail_body.toHtml() - mail = self.mail_data + mail_body - mail = mail.replace("Subject:", f"Subject: {mail_header}") - with tempfile.NamedTemporaryFile( - mode="w", delete=False, suffix=".eml", encoding="utf-8", dir="mails" - ) as f: - f.write(mail) - self.mail_path = f.name - print(self.mail_path) - # open the file using thunderbird - subprocess.Popen(["thunderbird", f"{self.mail_path}"]) - # delete the file - # os.remove(self.mail_path) - - -if __name__ == "__main__": - import sys - - app = QtWidgets.QApplication(sys.argv) - Dialog = QtWidgets.QDialog() - ui = Mail_Dialog() - ui.setupUi(Dialog) - Dialog.show() - sys.exit(app.exec()) diff --git a/test/semestergen_test.py b/test/semestergen_test.py deleted file mode 100644 index 8c398e0..0000000 --- a/test/semestergen_test.py +++ /dev/null @@ -1,7 +0,0 @@ -import pytest - -from src.backend.semester import generateSemesterByDate - - -def test_generateSemesterByDate(): - assert generateSemesterByDate() == "WiSe 23/24" diff --git a/test/test_database.py b/test/test_database.py deleted file mode 100644 index 6008df2..0000000 --- a/test/test_database.py +++ /dev/null @@ -1,4 +0,0 @@ -from src.backend.database import Database - -db = Database("semap.db") -# print(db.query_db("SELECT * FROM subjects WHERE id=1")) diff --git a/test/webrequest_test.py b/test/webrequest_test.py deleted file mode 100644 index 9de4d7c..0000000 --- a/test/webrequest_test.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -from src.logic.dataclass import BookData -from src.logic.webrequest import BibTextTransformer, WebRequest - - -def test_webdata_bibtexttransform(source_data: str = "RIS"): - request = WebRequest().get_ppn("ST 250 U42 (15) ").get_data() - assert isinstance(request, list) is True - assert len(request) > 0 - model: BookData = BibTextTransformer(source_data).get_data(request).return_data() - assert model is not None - assert model.signature == "ST 250 U42 (15)" - assert model.ppn == "1693321114" - assert model.author == "Ullenboom, Christian" - assert model.link == "https://rds.ibs-bw.de/phfreiburg/link?kid=1693321114" - assert model.pages == "1246" - assert model.publisher == "Rheinwerk Computing"