Added: Postgres Support
This commit is contained in:
parent
2d25d5105a
commit
4b82b11072
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
# ---> Custom
|
||||
state
|
||||
pickles/*
|
||||
assets/documents/*
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
@ -9,7 +9,7 @@ Nova,Eib,Female,MeWi4,30%,Medienwissenschaften,31,15,15,34,20,20,21,27,19,21
|
||||
Lena,Fricke,Female,MeWi4,30%,Medienwissenschaften,13,14,15,21,15,17,17,18,19,11
|
||||
Nele,Grundke,Female,MeWi6,30%,Medienwissenschaften,23.5,13,16,28,20,17,21,18,22,11
|
||||
Anna,Grünewald,Female,MeWi3,30%,Medienwissenschaften,12,14,16,29,16,15,19,9,16,12
|
||||
Yannik,Haupt,Male,NoGroup,30%,Unknown,18,6,14,21,13,2,9,0,0,0
|
||||
Yannik,Haupt,Male,NoGroup,30%,Student,18,6,14,21,13,2,9,0,0,0
|
||||
Janna,Heiny,Female,MeWi1,30%,Technologie Orientiertes Managment,30,15,18,33,18,20,22,25,24,30
|
||||
Milena,Krieger,Female,MeWi1,30%,Technologie Orientiertes Managment,30,15,18,33,20,20,21.5,26,20,22
|
||||
Julia,Limbach,Female,MeWi6,30%,Medienwissenschaften,27.5,12,18,29,11,19,17.5,26,24,28
|
||||
@ -32,7 +32,5 @@ Inga-Brit,Turschner,Female,MeWi2,30%,Medienwissenschaften,25.5,14,18,34,20,16,19
|
||||
Alea,Unger,Female,MeWi5,30%,Medienwissenschaften,30,12,18,31,20,20,21,22,15,21.5
|
||||
Marie,Wallbaum,Female,MeWi5,30%,Medienwissenschaften,28.5,14,18,34,17,20,19,24,12,22
|
||||
Katharina,Walz,Female,MeWi4,30%,Medienwissenschaften,31,15,18,31,19,19,17,24,17,14.5
|
||||
Xiaowei,Wang,Male,NoGroup,30%,Unknown,30.5,14,18,26,19,17,0,0,0,0
|
||||
Xiaowei,Wang,Male,NoGroup,30%,Student,30.5,14,18,26,19,17,0,0,0,0
|
||||
Lilly-Lu,Warnken,Female,DiKum,30%,Medienwissenschaften,30,15,18,30,14,17,19,14,16,24
|
||||
,,,,,,,,,,,,,,,
|
||||
|
||||
|
|
Binary file not shown.
@ -31,12 +31,14 @@ groups = {
|
||||
}
|
||||
|
||||
print(df)
|
||||
init_db('WiSe_24_25.db')
|
||||
|
||||
|
||||
#init_db('WiSe_24_25.db')
|
||||
init_postgres('postgresql://admin:admin@127.0.0.1/learnlytics')
|
||||
db.drop_tables(tables)
|
||||
db.create_tables(tables)
|
||||
# Create Class
|
||||
clas = Class.create(name='WiSe 24/25')
|
||||
#print(clas.id)
|
||||
|
||||
Study.create(name='Student')
|
||||
|
||||
# Create Courses
|
||||
for k, v in courses.items():
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,7 +1,8 @@
|
||||
from .utils import (
|
||||
tables,
|
||||
table_labels,
|
||||
init_db,
|
||||
init_local,
|
||||
init_postgres,
|
||||
save_as_json,
|
||||
create_from_json
|
||||
)
|
||||
|
@ -9,17 +9,18 @@ Online Viewer: https://dbdiagram.io
|
||||
|
||||
|
||||
from peewee import *
|
||||
from playhouse.db_url import connect
|
||||
from datetime import datetime
|
||||
|
||||
db = DatabaseProxy()
|
||||
|
||||
# WIP: Add Switch Function
|
||||
if True:
|
||||
database = SqliteDatabase(None, autoconnect=False)
|
||||
else:
|
||||
database = PostgresqlDatabase(None, autoconnect=False)
|
||||
|
||||
db.initialize(database)
|
||||
#if False:
|
||||
# database = SqliteDatabase(None, autoconnect=False)
|
||||
#else:
|
||||
# database = connect('postgresql://admin:admin@127.0.0.1:5432/learnlytics')
|
||||
|
||||
#db.initialize(database)
|
||||
|
||||
class BaseModel(Model):
|
||||
'''
|
||||
@ -94,4 +95,3 @@ class Submission(BaseModel):
|
||||
points = FloatField()
|
||||
created_at = DateTimeField(default=datetime.now)
|
||||
|
||||
|
||||
|
@ -12,6 +12,7 @@ import sys, inspect, json
|
||||
from datetime import datetime, date
|
||||
from pathlib import Path
|
||||
from playhouse.shortcuts import model_to_dict, dict_to_model
|
||||
from playhouse.db_url import connect
|
||||
from .model import *
|
||||
|
||||
class DateTimeEncoder(json.JSONEncoder):
|
||||
@ -59,7 +60,7 @@ def get_module_cls(module: str) -> tuple[tuple[str, object]]:
|
||||
tables = tuple(table[1] for table in get_module_cls('dbmodel.model') if not table[1] is BaseModel)
|
||||
table_labels = tuple(table[0] for table in get_module_cls('dbmodel.model') if not table[1] is BaseModel)
|
||||
|
||||
def init_db(name: Path | str) -> None:
|
||||
def init_local(name: Path | str) -> None:
|
||||
'''
|
||||
(Creates,) Connects and Initializes the db descriptor from a given *.db file
|
||||
'''
|
||||
@ -71,14 +72,23 @@ def init_db(name: Path | str) -> None:
|
||||
if type(name) is Path:
|
||||
name = str(name)
|
||||
|
||||
# Switch Database; Initialize if needed
|
||||
if not db.is_closed():
|
||||
db.close()
|
||||
database = SqliteDatabase(None, autoconnect=False)
|
||||
db.initialize(database)
|
||||
db.init(name)
|
||||
db.connect()
|
||||
db.create_tables(tables) # Ensure tables exist
|
||||
|
||||
|
||||
def init_postgres(url: str) -> None:
|
||||
|
||||
assert isinstance(url, str), "Provided url isnt a String"
|
||||
|
||||
database = connect(url)
|
||||
db.initialize(database)
|
||||
db.connect()
|
||||
db.create_tables(tables)
|
||||
|
||||
|
||||
def save_as_json(filename: str, path: Path | str = Path('.')) -> Path:
|
||||
'''
|
||||
Saves the current loaded Database as <filename>.json to given <path>
|
||||
|
@ -7,9 +7,14 @@ class AnalyzerState:
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self.class_id = 1
|
||||
self.group_id = 1
|
||||
self.student_id = 1
|
||||
self.class_id = -1
|
||||
self.group_id = -1
|
||||
self.student_id = -1
|
||||
|
||||
def reset(self):
|
||||
self.class_id = -1
|
||||
self.group_id = -1
|
||||
self.student_id = -1
|
||||
|
||||
def __str__(self):
|
||||
return f'''
|
||||
|
@ -167,7 +167,6 @@ def add_lecture(class_id: int) -> bool:
|
||||
@immapp.static(inited=False)
|
||||
def group_list(class_id: int) -> None:
|
||||
imgui_md.render_unindented('# Groups')
|
||||
|
||||
if class_id < 1:
|
||||
return
|
||||
|
||||
@ -340,7 +339,7 @@ def add_student(class_id: int) -> bool:
|
||||
statics.prename = str()
|
||||
statics.surname = str()
|
||||
|
||||
statics.genders = ["Male", "Female"]
|
||||
statics.genders = ["Male", "Female", "Diverse"]
|
||||
statics.gender_select = 0
|
||||
|
||||
statics.studys = list(Study.select())
|
||||
@ -367,14 +366,22 @@ def add_student(class_id: int) -> bool:
|
||||
|
||||
_, statics.prename = imgui.input_text("First Name", statics.prename)
|
||||
_, statics.surname = imgui.input_text("Last Name", statics.surname)
|
||||
|
||||
_, statics.gender_select = imgui.combo("Gender", statics.gender_select, statics.genders)
|
||||
|
||||
imgui.text("Gender")
|
||||
for n, gender in enumerate(statics.genders):
|
||||
changed = imgui.radio_button(gender, statics.gender_select == n)
|
||||
if changed:
|
||||
statics.gender_select = n
|
||||
|
||||
_, statics.study_select = imgui.combo("Study Progamm", statics.study_select, statics.study_labels)
|
||||
|
||||
_, statics.group_select = imgui.combo("Group", statics.group_select, statics.group_labels)
|
||||
|
||||
_, statics.grader_select = imgui.combo("Grader", statics.grader_select, statics.graders)
|
||||
|
||||
imgui.text("Grader")
|
||||
for n, grader in enumerate(statics.graders):
|
||||
changed = imgui.radio_button(grader, statics.grader_select == n)
|
||||
if changed:
|
||||
statics.grader_select = n
|
||||
|
||||
if imgui.button("Add"):
|
||||
s = Student.create(
|
||||
@ -408,6 +415,49 @@ def add_student(class_id: int) -> bool:
|
||||
|
||||
return False
|
||||
|
||||
@immapp.static(inited=False)
|
||||
def study_list() -> None:
|
||||
statics = study_list
|
||||
|
||||
if not statics.inited:
|
||||
statics.studys = Study.select().order_by(Study.id)
|
||||
statics.new_study = str()
|
||||
statics.inited = True
|
||||
|
||||
imgui_md.render_unindented("# Study Programms")
|
||||
|
||||
if imgui.button("Add Study"):
|
||||
Study.create(name=statics.new_study)
|
||||
statics.inited = False
|
||||
|
||||
imgui.same_line()
|
||||
_, statics.new_study = imgui.input_text("##study", statics.new_study)
|
||||
|
||||
if imgui.begin_table("Studys", 2, imgui.TableFlags_.sizing_fixed_fit.value):
|
||||
for study in statics.studys:
|
||||
if study.name == 'Student':
|
||||
continue
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
|
||||
if imgui.button(f"X##{study.id}"):
|
||||
students = Student.select().where(Student.study_id == study.id)
|
||||
nostudy = Study.select().where(Study.name == "Student").get()
|
||||
for student in students:
|
||||
student.study_id = nostudy.id
|
||||
with db.atomic():
|
||||
Student.bulk_update(students, fields=[Student.study_id], batch_size=50)
|
||||
study.delete_instance()
|
||||
statics.inited = False
|
||||
imgui.end_table()
|
||||
return
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text(study.name)
|
||||
#imgui.table_next_column()
|
||||
#imgui.text(f"{group.project}")
|
||||
imgui.end_table()
|
||||
|
||||
|
||||
def class_graph() -> None:
|
||||
if db.is_closed():
|
||||
@ -440,4 +490,8 @@ def class_graph() -> None:
|
||||
if imgui.begin_child("Ranking", ImVec2(w1/3, h1), imgui.ChildFlags_.borders.value):
|
||||
ranking(state.class_id)
|
||||
imgui.end_child()
|
||||
imgui.same_line()
|
||||
if imgui.begin_child("Studys", ImVec2(w1/3, h1), imgui.ChildFlags_.borders.value):
|
||||
study_list()
|
||||
imgui.end_child()
|
||||
imgui.end_child()
|
||||
|
@ -52,6 +52,19 @@ def load_properties() -> DocumentProperties:
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
file = Path('./pickles/document_properties.pkl')
|
||||
try:
|
||||
file.resolve(strict=True)
|
||||
|
||||
except FileNotFoundError:
|
||||
dump_properties(DocumentProperties(
|
||||
logo = Path("./assets/learnlytics.svg"),
|
||||
save_dir = Path.home(),
|
||||
file_name = "document",
|
||||
author = "Learnlytics"
|
||||
))
|
||||
|
||||
|
||||
@immapp.static(inited=False)
|
||||
def document_properties(class_id: int) -> None:
|
||||
# TO DO: Store Properties persistent
|
||||
@ -137,14 +150,21 @@ def document_properties(class_id: int) -> None:
|
||||
return statics.properties
|
||||
|
||||
@immapp.static(inited=False)
|
||||
def filters() -> list[Study]:
|
||||
def filters(class_id: int) -> list[Study]:
|
||||
statics = filters
|
||||
|
||||
if not statics.inited:
|
||||
statics.class_id = class_id
|
||||
statics.studys = list(Study.select())
|
||||
statics.checked = [True] * len(statics.studys)
|
||||
statics.inited = True
|
||||
|
||||
if statics.class_id != class_id:
|
||||
statics.inited = False
|
||||
|
||||
if Study.select().count() != len(statics.studys):
|
||||
statics.inited = False
|
||||
|
||||
imgui_md.render_unindented("### Study Filters")
|
||||
imgui.text("")
|
||||
|
||||
@ -219,7 +239,7 @@ def create_document(class_id: int) -> None:
|
||||
if imgui.begin_child("Filters", ImVec2(w*0.54, h), imgui.ChildFlags_.borders.value):
|
||||
w1, h1 = imgui.get_content_region_avail()
|
||||
if imgui.begin_child("Study Filters", ImVec2(w1, h1*0.2)):
|
||||
study_filters = filters()
|
||||
study_filters = filters(statics.class_id)
|
||||
imgui.end_child()
|
||||
|
||||
if imgui.begin_child("Order By", ImVec2(w1, h1*0.2)):
|
||||
@ -230,17 +250,17 @@ def create_document(class_id: int) -> None:
|
||||
properties = document_properties(statics.class_id)
|
||||
if imgui.button("Create"):
|
||||
pdf, css = get_pdf(statics.class_id)
|
||||
header = get_pdf_header(state.class_id, [study.name for study in study_filters], properties.author, properties.logo)
|
||||
header = get_pdf_header_creator(state.class_id, [study.name for study in study_filters], properties.author, properties.logo)
|
||||
|
||||
match statics.order_by:
|
||||
case "Group":
|
||||
sections = create_group_pdf(study_filters)
|
||||
sections = create_group_pdf(statics.class_id, study_filters)
|
||||
|
||||
case "Study Program":
|
||||
sections = create_study_pdf(study_filters)
|
||||
sections = create_study_pdf(statics.class_id, study_filters)
|
||||
|
||||
case 'Ranking':
|
||||
sections = create_ranking_pdf(study_filters)
|
||||
sections = create_ranking_pdf(statics.class_id, study_filters)
|
||||
|
||||
pdf.add_section(header, user_css=css)
|
||||
for section in sections:
|
||||
|
@ -2,18 +2,21 @@ from imgui_bundle import (
|
||||
imgui,
|
||||
imgui_md,
|
||||
immapp,
|
||||
im_file_dialog,
|
||||
ImVec2,
|
||||
ImVec4
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
from peewee import fn
|
||||
from pathlib import Path
|
||||
|
||||
from dbmodel import *
|
||||
from grader import get_gradings, get_grader
|
||||
|
||||
from pdf import *
|
||||
from .analyzer_state import AnalyzerState
|
||||
from .plotter import plot_bar_line_percentage
|
||||
from .document_creator import load_properties
|
||||
|
||||
state = AnalyzerState()
|
||||
PROGRESS_BAR_COLOR = ImVec4(190, 190, 40, 255)/255
|
||||
@ -43,7 +46,15 @@ def header(student_id: int, reset: bool) -> None:
|
||||
|
||||
imgui_md.render_unindented(f"# {statics.student.prename} {statics.student.surname} - {statics.has_passed}")
|
||||
imgui_md.render_unindented(f"Degree Program: **{statics.study.name}**")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("Create Summary"):
|
||||
properties = load_properties()
|
||||
pdf, css = get_pdf(statics.student.class_id)
|
||||
sections = get_pdf_student(statics.student.id, properties.author, properties.logo)
|
||||
for section in sections:
|
||||
pdf.add_section(section, user_css = css)
|
||||
save = properties.save_dir / (properties.file_name + '.pdf')
|
||||
pdf.save(save)
|
||||
|
||||
@immapp.static(inited=False)
|
||||
def plot(student_id: int, reset: bool) -> None:
|
||||
@ -96,7 +107,9 @@ def dialog(student_id: int) -> None:
|
||||
|
||||
statics.prename = statics.student.prename
|
||||
statics.surname = statics.student.surname
|
||||
statics.sex = 0 if statics.student.sex == "Male" else 1
|
||||
|
||||
statics.genders = ["Male", "Female", "Diverse"]
|
||||
statics.sex = statics.genders.index(statics.student.sex)
|
||||
|
||||
statics.studys = list(Study.select())
|
||||
s = Study.get_by_id(statics.student.study_id)
|
||||
@ -131,7 +144,7 @@ def dialog(student_id: int) -> None:
|
||||
imgui.table_next_column()
|
||||
imgui.text("Sex")
|
||||
imgui.table_next_column()
|
||||
_, statics.sex = imgui.combo("##Sex", statics.sex, ["Male", "Female"])
|
||||
_, statics.sex = imgui.combo("##Sex", statics.sex, statics.genders)
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
@ -156,7 +169,7 @@ def dialog(student_id: int) -> None:
|
||||
if imgui.button("Save"):
|
||||
statics.student.prename = statics.prename
|
||||
statics.student.surname = statics.surname
|
||||
statics.student.sex = "Male" if statics.sex == 0 else "Female"
|
||||
statics.student.sex = statics.genders[statics.sex]
|
||||
statics.student.study_id = statics.studys[statics.study].id
|
||||
statics.student.group_id = statics.groups[statics.group].id
|
||||
statics.student.grader = get_grader(statics.graders[statics.grader]).alt_name
|
||||
|
@ -18,15 +18,18 @@ def class_selector() -> int:
|
||||
if not statics.inited:
|
||||
statics.selector = 0
|
||||
statics.inited = True
|
||||
|
||||
labels = (c.name for c in Class.select())
|
||||
_, statics.selector = imgui.combo("##Classes", statics.selector, list(labels))
|
||||
|
||||
current = Class.select()[statics.selector].id
|
||||
|
||||
if imgui.button("New Class"):
|
||||
imgui.open_popup("NewC")
|
||||
new_class()
|
||||
|
||||
classes = Class.select()
|
||||
if classes.count() < 1:
|
||||
return -1
|
||||
|
||||
labels = (c.name for c in classes)
|
||||
_, statics.selector = imgui.combo("##Classes", statics.selector, list(labels))
|
||||
|
||||
current = classes[statics.selector].id
|
||||
|
||||
imgui.same_line()
|
||||
|
||||
@ -56,6 +59,7 @@ def new_class() -> None:
|
||||
|
||||
if imgui.button("Add"):
|
||||
clas = Class.create(name=statics.name)
|
||||
Study.get_or_create(name='Student')
|
||||
Group.create(
|
||||
name="NoGroup",
|
||||
project="NoProject",
|
||||
|
@ -4,8 +4,6 @@ from typing import List
|
||||
|
||||
from .database import *
|
||||
|
||||
from .editor import editor
|
||||
|
||||
def database_docking_splits() -> List[hello_imgui.DockingSplit]:
|
||||
"""
|
||||
Defines the docking layout for the database editor.
|
||||
@ -54,17 +52,7 @@ def set_database_editor_layout() -> List[hello_imgui.DockableWindow]:
|
||||
log.dock_space_name = "CommandSpace2"
|
||||
log.gui_function = hello_imgui.log_gui
|
||||
|
||||
table_view = hello_imgui.DockableWindow()
|
||||
table_view.label = "Table"
|
||||
table_view.dock_space_name = "MainDockSpace"
|
||||
table_view.gui_function = lambda: table()
|
||||
|
||||
eeditor = hello_imgui.DockableWindow()
|
||||
eeditor.label = "Editor"
|
||||
eeditor.dock_space_name = "CommandSpace"
|
||||
eeditor.gui_function = lambda: editor()
|
||||
|
||||
return [file_dialog, log, table_view, eeditor]
|
||||
return [file_dialog, log]
|
||||
|
||||
def database_editor_layout() -> hello_imgui.DockingParams:
|
||||
"""
|
||||
|
@ -9,6 +9,7 @@ to set up the database editing environment.
|
||||
# Custom
|
||||
from dbmodel import *
|
||||
from gui import *
|
||||
from gui.analyzer.analyzer_state import AnalyzerState
|
||||
from grader import get_gradings
|
||||
|
||||
# External
|
||||
@ -57,7 +58,7 @@ def file_info(path: Path) -> None:
|
||||
}
|
||||
|
||||
# Create ImGui table to display file information
|
||||
if imgui.begin_table("File Info", 2):
|
||||
if imgui.begin_table("File Info", 2, imgui.TableFlags_.sizing_fixed_fit.value):
|
||||
imgui.table_setup_column(" ", 0)
|
||||
imgui.table_setup_column(" ")
|
||||
|
||||
@ -73,207 +74,90 @@ def file_info(path: Path) -> None:
|
||||
|
||||
imgui.end_table()
|
||||
|
||||
@immapp.static(inited=False, res=False)
|
||||
|
||||
def connect_postgres() -> None:
|
||||
pass
|
||||
|
||||
@immapp.static(inited=False)
|
||||
def select_file() -> None:
|
||||
"""
|
||||
Handles the selection and loading of a database file (JSON or SQLite).
|
||||
It initializes necessary state, allows users to open a file dialog,
|
||||
and processes the selected file by either loading or converting it.
|
||||
"""
|
||||
statics = select_file # Access static variables within the function
|
||||
|
||||
# Initialize static variables on the first function call
|
||||
statics = select_file
|
||||
if not statics.inited:
|
||||
statics.res = None # Stores the selected file result
|
||||
statics.current = None # Stores the currently loaded database file path
|
||||
|
||||
# Retrieve the last used database file from persistent storage
|
||||
with shelve.open("state") as state:
|
||||
statics.current = Path(state.get("DB") or "")
|
||||
try:
|
||||
with open("./pickles/database_location.txt", "r") as f:
|
||||
statics.file = Path(f.read())
|
||||
except FileNotFoundError:
|
||||
statics.file = None
|
||||
|
||||
statics.inited = True
|
||||
|
||||
imgui_md.render_unindented('# Database Manager')
|
||||
|
||||
# Render UI title and display file information
|
||||
imgui_md.render("# Database Manager")
|
||||
file_info(statics.current)
|
||||
if imgui.button("Create DB"):
|
||||
im_file_dialog.FileDialog.instance().save(
|
||||
"CreateDatabase", "Create Database", "Database File (*.db){.db}", str(Path.home())
|
||||
)
|
||||
|
||||
# Button to open the file selection dialog
|
||||
if imgui.button("Open File"):
|
||||
# Handle the file dialog result
|
||||
if im_file_dialog.FileDialog.instance().is_done("CreateDatabase"):
|
||||
if im_file_dialog.FileDialog.instance().has_result():
|
||||
file = im_file_dialog.FileDialog.instance().get_result()
|
||||
init_db(str(file.path()))
|
||||
with open("./pickles/database_location.txt", "w") as f:
|
||||
f.write(str(file.path()))
|
||||
statics.file = Path(str(file))
|
||||
im_file_dialog.FileDialog.instance().close()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("Load DB File"):
|
||||
im_file_dialog.FileDialog.instance().open(
|
||||
"SelectDatabase", "Open Database", "Database File (*.json;*.db){.json,.db}"
|
||||
)
|
||||
"SelectDatabase", "Open Database", "Database File (*.db){.db}", False, str(Path.home())
|
||||
)
|
||||
|
||||
# Handle the file dialog result
|
||||
if im_file_dialog.FileDialog.instance().is_done("SelectDatabase"):
|
||||
if im_file_dialog.FileDialog.instance().has_result():
|
||||
statics.res = im_file_dialog.FileDialog.instance().get_result()
|
||||
file = im_file_dialog.FileDialog.instance().get_result()
|
||||
init_db(str(file.path()))
|
||||
with open("./pickles/database_location.txt", "w") as f:
|
||||
f.write(str(file.path()))
|
||||
statics.file = Path(str(file))
|
||||
AnalyzerState().reset()
|
||||
im_file_dialog.FileDialog.instance().close()
|
||||
|
||||
# Process the selected file if available
|
||||
if statics.res:
|
||||
filename = statics.res.filename()
|
||||
info = Path(statics.res.path())
|
||||
|
||||
imgui.separator() # UI separator for clarity
|
||||
file_info(info) # Display information about the selected file
|
||||
|
||||
file = None
|
||||
|
||||
# Load the selected database file
|
||||
if imgui.button("Load"):
|
||||
# Ensure any currently open database is closed before loading a new one
|
||||
if not db.is_closed():
|
||||
db.close()
|
||||
|
||||
# Handle JSON files by converting them to SQLite databases
|
||||
if statics.res.extension() == '.json':
|
||||
file = filename.removesuffix('.json') + '.db' # Convert JSON filename to SQLite filename
|
||||
init_db(file)
|
||||
load_from_json(str(info)) # Convert and load JSON data into the database
|
||||
|
||||
# Handle SQLite database files directly
|
||||
if statics.res.extension() == '.db':
|
||||
file = str(statics.res.path())
|
||||
init_db(file)
|
||||
|
||||
# Save the selected database path to persistent storage
|
||||
with shelve.open("state") as state:
|
||||
state["DB"] = file
|
||||
|
||||
# Update application state and reset selection result
|
||||
statics.res = None
|
||||
|
||||
@immapp.static(inited=False)
|
||||
def editor() -> None:
|
||||
"""
|
||||
Class Editor UI Component.
|
||||
|
||||
This function initializes and renders the class selection interface within the database editor.
|
||||
It maintains a static state to keep track of the selected class and fetches available classes.
|
||||
"""
|
||||
statics = class_editor
|
||||
if db.is_closed():
|
||||
imgui.text("DB")
|
||||
return
|
||||
statics.classes = Class.select()
|
||||
if not statics.inited:
|
||||
statics.selected = 0
|
||||
statics.value = statics.classes[statics.selected].name if statics.classes else str()
|
||||
statics.inited = True
|
||||
|
||||
# Fetch available classes from the database
|
||||
|
||||
# Render the UI for class selection
|
||||
imgui_md.render("# Edit Classes")
|
||||
changed, statics.selected = imgui.combo("##Classes", statics.selected, [c.name for c in statics.classes])
|
||||
if changed:
|
||||
statics.value = statics.classes[statics.selected].name
|
||||
_, statics.value = imgui.input_text("##input_class", statics.value)
|
||||
|
||||
if imgui.button("Update"):
|
||||
clas = statics.classes[statics.selected]
|
||||
clas.name, old_name = statics.value, clas.name
|
||||
clas.save()
|
||||
|
||||
imgui.same_line()
|
||||
|
||||
if imgui.button("New"):
|
||||
Class.create(name=statics.value)
|
||||
|
||||
imgui.same_line()
|
||||
|
||||
if imgui.button("Delete"):
|
||||
clas = statics.classes[statics.selected]
|
||||
clas.delete_instance()
|
||||
statics.selected -= 1
|
||||
statics.value = statics.classes[statics.selected].name
|
||||
|
||||
|
||||
def create_editor_popup(table, action: str, selectors: dict) -> None:
|
||||
table_flags = (
|
||||
imgui.TableFlags_.row_bg.value
|
||||
if imgui.button("Export DB"):
|
||||
im_file_dialog.FileDialog.instance().save(
|
||||
"ExportJSON", "Export JSON", "JSON Export (*.json){.json}", str(Path.home())
|
||||
)
|
||||
cols = 2
|
||||
rows = len(table._meta.fields)
|
||||
|
||||
if imgui.begin_table("Editor Grid", cols, table_flags):
|
||||
# Setup Header
|
||||
for header in ["Attribute", "Value"]:
|
||||
imgui.table_setup_column(header, imgui.TableColumnFlags_.width_stretch.value, 1/len(header))
|
||||
imgui.table_headers_row()
|
||||
|
||||
id = 0
|
||||
for k, v in table._meta.fields.items():
|
||||
# Don't show Fields
|
||||
match type(v):
|
||||
case peewee.AutoField:
|
||||
continue
|
||||
case peewee.DateTimeField:
|
||||
continue
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
label = str(k).removesuffix('_id')
|
||||
imgui.text(label.title())
|
||||
imgui.table_set_column_index(1)
|
||||
|
||||
# Generate Input for Type
|
||||
match type(v):
|
||||
case peewee.IntegerField:
|
||||
if id not in selectors:
|
||||
selectors[id] = int()
|
||||
_, selectors[id] = imgui.input_int(f"##{id}", selectors[id], 1)
|
||||
case peewee.CharField:
|
||||
if id not in selectors:
|
||||
if k == 'grader':
|
||||
selectors[id] = int()
|
||||
else:
|
||||
selectors[id] = str()
|
||||
|
||||
if k == 'grader':
|
||||
graders = [g.alt_name for g in get_gradings()]
|
||||
_, selectors[id] = imgui.combo(f"##{id}", selectors[id], graders)
|
||||
else:
|
||||
_, selectors[id] = imgui.input_text(f"##{id}", selectors[id])
|
||||
case peewee.FloatField:
|
||||
if id not in selectors:
|
||||
selectors[id] = float()
|
||||
_, selectors[id] = imgui.input_float(f"##{id}", selectors[id])
|
||||
case peewee.ForeignKeyField:
|
||||
if id not in selectors:
|
||||
selectors[id] = int()
|
||||
|
||||
labels = list()
|
||||
match k:
|
||||
case 'class_id':
|
||||
labels = [clas.name for clas in Class.select()]
|
||||
case 'lecture_id':
|
||||
labels = [lecture.title for lecture in Lecture.select()]
|
||||
|
||||
if not labels:
|
||||
imgui.text("No Element for this Attribute")
|
||||
else:
|
||||
_, selectors[id] = imgui.combo(f"##{id}", selectors[id], labels)
|
||||
case _:
|
||||
imgui.text(f"Unknown Field {k}")
|
||||
|
||||
id += 1
|
||||
imgui.end_table()
|
||||
|
||||
if imgui.button(action):
|
||||
match action:
|
||||
case "Create":
|
||||
print("Create")
|
||||
case "Update":
|
||||
print("Update")
|
||||
case "Delete":
|
||||
print("Delete")
|
||||
case _:
|
||||
print("Unknown Case")
|
||||
|
||||
# Clear & Close Popup
|
||||
selectors.clear()
|
||||
imgui.close_current_popup()
|
||||
|
||||
|
||||
|
||||
# Handle the file dialog result
|
||||
if im_file_dialog.FileDialog.instance().is_done("ExportJSON"):
|
||||
if im_file_dialog.FileDialog.instance().has_result():
|
||||
file = im_file_dialog.FileDialog.instance().get_result()
|
||||
filename = str(file).removesuffix('.json') + '.db'
|
||||
save_as_json(filename, Path(str(file.path())))
|
||||
im_file_dialog.FileDialog.instance().close()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("Load JSON"):
|
||||
im_file_dialog.FileDialog.instance().open(
|
||||
"SelectJSON", "Open JSON Export", "JSON Export (*.json){.json}", False, str(Path.home())
|
||||
)
|
||||
|
||||
if im_file_dialog.FileDialog.instance().is_done("SelectJSON"):
|
||||
if im_file_dialog.FileDialog.instance().has_result():
|
||||
file = im_file_dialog.FileDialog.instance().get_result()
|
||||
filename = str(file).removesuffix('.json') + '.db'
|
||||
create_from_json(filename, Path(str(file.path())))
|
||||
with open("./pickles/database_location.txt", "w") as f:
|
||||
f.write(str(file.path()))
|
||||
statics.file = Path(filename)
|
||||
AnalyzerState().reset()
|
||||
im_file_dialog.FileDialog.instance().close()
|
||||
|
||||
if imgui.button("Connect Postgres (WIP)"):
|
||||
connect_postgres()
|
||||
|
||||
imgui.separator()
|
||||
if statics.file:
|
||||
file_info(statics.file)
|
||||
|
@ -1,141 +0,0 @@
|
||||
from imgui_bundle import (
|
||||
imgui,
|
||||
immapp,
|
||||
imgui_md,
|
||||
hello_imgui
|
||||
)
|
||||
|
||||
from grader import get_gradings, get_grader
|
||||
|
||||
from dbmodel import *
|
||||
|
||||
@immapp.static(inited=False)
|
||||
def editor() -> None:
|
||||
"""
|
||||
Database Editor UI Function.
|
||||
|
||||
Calls the class editor function to render its UI component.
|
||||
"""
|
||||
if db.is_closed():
|
||||
imgui.text("Please open a Database")
|
||||
return
|
||||
|
||||
statics = editor
|
||||
if not statics.inited:
|
||||
statics.selected = 0
|
||||
statics.actions = ["Create", "Update", "Delete"]
|
||||
statics.action = str()
|
||||
statics.inited = True
|
||||
statics.selectors = dict()
|
||||
|
||||
imgui.text("Select what you want to Edit:")
|
||||
changed, statics.selected = imgui.combo('##DBSelector', statics.selected, table_labels)
|
||||
for action in statics.actions:
|
||||
if imgui.button(action):
|
||||
imgui.open_popup(table_labels[statics.selected])
|
||||
statics.action = action
|
||||
imgui.same_line()
|
||||
|
||||
if imgui.begin_popup_modal(table_labels[statics.selected])[0]:
|
||||
table = tables[statics.selected]
|
||||
imgui_md.render(f"# {statics.action} {table_labels[statics.selected]}")
|
||||
|
||||
if imgui.begin_table("Editor Grid", 2, imgui.TableFlags_.row_bg.value):
|
||||
# Setup Header
|
||||
for header in ["Attribute", "Value"]:
|
||||
imgui.table_setup_column(header, imgui.TableColumnFlags_.width_stretch.value, 1/len(header))
|
||||
imgui.table_headers_row()
|
||||
|
||||
|
||||
student_editor(statics.action)
|
||||
|
||||
imgui.end_table()
|
||||
|
||||
|
||||
imgui.end_popup()
|
||||
|
||||
@immapp.static(inited=False)
|
||||
def student_editor(action: str) -> dict:
|
||||
'''
|
||||
|
||||
'''
|
||||
statics = student_editor
|
||||
if not statics.inited:
|
||||
statics.classes = tuple(Class.select())
|
||||
statics.residences = tuple(Residence.select())
|
||||
statics.classes_labels = tuple(clas.name for clas in statics.classes)
|
||||
statics.graders = tuple(grader.alt_name for grader in get_gradings())
|
||||
statics.buffer = {
|
||||
'prename': "",
|
||||
'surname': "",
|
||||
'sex': 0,
|
||||
'class_id': 0,
|
||||
'group_id': 0,
|
||||
'grader': 0,
|
||||
'residence': 0,
|
||||
|
||||
}
|
||||
statics.inited = True
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.text("First Name")
|
||||
imgui.table_set_column_index(1)
|
||||
_, statics.buffer['prename'] = imgui.input_text("##prename", statics.buffer['prename'])
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.text("Last Name")
|
||||
imgui.table_set_column_index(1)
|
||||
_, statics.buffer['surname'] = imgui.input_text("##surname", statics.buffer['surname'])
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.text("Gender")
|
||||
imgui.table_set_column_index(1)
|
||||
_, statics.buffer['sex'] = imgui.combo("##sex", statics.buffer['sex'], ['Male', 'Female'])
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.text("Class")
|
||||
imgui.table_set_column_index(1)
|
||||
_, statics.buffer['class_id'] = imgui.combo("##class_id", statics.buffer['class_id'], statics.classes_labels)
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.text("Project Group")
|
||||
imgui.table_set_column_index(1)
|
||||
groups = tuple(Group.select().where(Group.class_id == statics.classes[statics.buffer['class_id']].id))
|
||||
_, statics.buffer['group_id'] = imgui.combo("##group_id", statics.buffer['group_id'], tuple(group.name for group in groups))
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.text("Grader")
|
||||
imgui.table_set_column_index(1)
|
||||
_, statics.buffer['grader'] = imgui.combo("##grader", statics.buffer['grader'], statics.graders)
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.text("Residence")
|
||||
imgui.table_set_column_index(1)
|
||||
_, statics.buffer['residence'] = imgui.combo("##residence", statics.buffer['residence'], [str(residence.id) for residence in statics.residences])
|
||||
|
||||
if imgui.button(action):
|
||||
match action:
|
||||
case "Create":
|
||||
Student.create(
|
||||
prename = statics.buffer['prename'],
|
||||
surname = statics.buffer['surname'],
|
||||
sex = 'Female' if statics.buffer['sex'] else 'Male',
|
||||
class_id = statics.classes[statics.buffer['class_id']].id,
|
||||
group_id = groups[statics.buffer['group_id']].id,
|
||||
residence = statics.residences[statics.buffer['residence']]
|
||||
)
|
||||
case "Update":
|
||||
pass
|
||||
case "Delete":
|
||||
pass
|
||||
statics.inited = False
|
||||
imgui.close_current_popup()
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import shelve
|
||||
|
||||
from imgui_bundle import (
|
||||
hello_imgui,
|
||||
immapp
|
||||
@ -12,18 +10,22 @@ from gui import (
|
||||
status_bar
|
||||
)
|
||||
|
||||
from dbmodel import init_db
|
||||
from dbmodel import init_postgres
|
||||
from pathlib import Path
|
||||
|
||||
def main() -> None:
|
||||
"""Main function to initialize and run the application."""
|
||||
|
||||
# Load Database
|
||||
try:
|
||||
with shelve.open("state") as state:
|
||||
v = state.get("DB") # Retrieve stored database connection info
|
||||
init_db(v)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
with open("./pickles/database_location.txt", "r") as f:
|
||||
file = f.read()
|
||||
except FileNotFoundError:
|
||||
file = str(Path.home() / "learnlytics.db")
|
||||
with open("./pickles/database_location.txt", "w") as f:
|
||||
f.write(file)
|
||||
|
||||
init_postgres('postgres://admin:admin@127.0.0.1:5432/learnlytics')
|
||||
|
||||
# Set Window Parameters
|
||||
runner_params = hello_imgui.RunnerParams()
|
||||
|
@ -1,4 +1,4 @@
|
||||
from .utils import get_pdf, get_pdf_header
|
||||
from .utils import get_pdf, get_pdf_header_creator, get_pdf_student
|
||||
from .ranking_pdf import create_ranking_pdf
|
||||
from .study_pdf import create_study_pdf
|
||||
from .group_pdf import create_group_pdf
|
||||
|
@ -70,6 +70,11 @@ hr {
|
||||
border: 3px solid #be1e3c;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 14px;
|
||||
font-family: 'NexusSansPro-Bold';
|
||||
}
|
||||
|
||||
/* Table */
|
||||
table {
|
||||
margin: 0 auto;
|
||||
|
@ -1,27 +1,34 @@
|
||||
from dbmodel import *
|
||||
from grader import get_grader
|
||||
from .utils import error
|
||||
|
||||
from markdown_pdf import Section
|
||||
|
||||
from functools import reduce
|
||||
import operator
|
||||
|
||||
def create_group_pdf(filter: list[Study]) -> list[Section]:
|
||||
def create_group_pdf(class_id: int, filter: list[Study]) -> list[Section]:
|
||||
if not filter:
|
||||
return error()
|
||||
|
||||
sections = list()
|
||||
text = ''
|
||||
|
||||
filter = [(Student.study_id == study.id) for study in filter]
|
||||
expr = reduce(operator.or_, filter)
|
||||
students = list(Student.select().where(expr))
|
||||
students = list(Student.select().where(expr).where(Student.class_id == class_id))
|
||||
|
||||
group_ids = set([student.group_id for student in students])
|
||||
group_filter = [(Group.id == id) for id in group_ids]
|
||||
if not group_filter:
|
||||
return [Section('# No Student found')]
|
||||
group_expr = reduce(operator.or_, group_filter)
|
||||
|
||||
data = {
|
||||
group: [student for student in Student.select().where(Student.group_id == group.id) if student.study in filter]
|
||||
group: [student for student in Student.select().where(Student.group_id == group.id).where(expr)]
|
||||
for group in Group.select().where(group_expr)
|
||||
}
|
||||
|
||||
|
||||
max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == next(iter(data.keys())).class_id)]
|
||||
|
||||
n = 0
|
||||
|
@ -6,7 +6,7 @@ from itertools import compress
|
||||
import operator
|
||||
from grader import get_grader
|
||||
from collections import Counter
|
||||
|
||||
from .utils import error
|
||||
|
||||
def create_header(number_of_students: int) -> str:
|
||||
text = '## Ranking\n'
|
||||
@ -16,12 +16,15 @@ def create_header(number_of_students: int) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def create_ranking_pdf(filter: list[int]) -> list[Section]:
|
||||
filter = [(Student.study_id == id) for id in filter]
|
||||
def create_ranking_pdf(class_id: int, filter: list[Study]) -> list[Section]:
|
||||
if not filter:
|
||||
return error()
|
||||
|
||||
filter = [(Student.study_id == study.id) for study in filter]
|
||||
expr = reduce(operator.or_, filter)
|
||||
students = [
|
||||
(student, Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == student.id).scalar())
|
||||
for student in Student.select().where(expr)
|
||||
for student in Student.select().where(expr).where(Student.class_id == class_id)
|
||||
]
|
||||
students.sort(key = lambda tup: tup[1], reverse=True)
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
from dbmodel import *
|
||||
from grader import get_grader
|
||||
from markdown_pdf import Section
|
||||
from .utils import error
|
||||
|
||||
def create_study_pdf(class_id: int, filter: list[Study]) -> list[Section]:
|
||||
if not filter:
|
||||
return error()
|
||||
|
||||
def create_study_pdf(filter: list[int]) -> list[Section]:
|
||||
sections = list()
|
||||
text = ''
|
||||
|
||||
data = {
|
||||
study: list(Student.select().where(Student.study_id == study.id))
|
||||
study: list(Student.select().where(Student.study_id == study.id).where(Student.class_id == class_id))
|
||||
for study in filter
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,13 @@
|
||||
from dbmodel import Class
|
||||
from dbmodel import *
|
||||
from markdown_pdf import MarkdownPdf, Section
|
||||
from datetime import datetime
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from grader import get_grader
|
||||
|
||||
def error() -> list[Section]:
|
||||
return [Section('# No Filter selected')]
|
||||
|
||||
|
||||
def get_pdf(class_id: int) -> tuple[MarkdownPdf, str]:
|
||||
clas = Class.get_by_id(class_id)
|
||||
@ -21,7 +26,73 @@ def get_pdf(class_id: int) -> tuple[MarkdownPdf, str]:
|
||||
|
||||
return (pdf, css)
|
||||
|
||||
def get_pdf_header(class_id: int, filter: Sequence[str], author: str, logo_file: Path = None) -> Section:
|
||||
|
||||
def get_metadata(author: str) -> str:
|
||||
text = f'Author: {author}\n\n'
|
||||
|
||||
date = datetime.now()
|
||||
text += f'Created: {date.strftime("%A %d.%m.%Y %H:%M")}\n\n'
|
||||
|
||||
text += 'This document is system-generated and may not require manual signatures.\n'
|
||||
return text
|
||||
|
||||
def get_pdf_student(student_id: int, author: str, logo_file: Path = None) -> list[Section]:
|
||||
if student_id < 1:
|
||||
return Section('# No Student selected')
|
||||
sections = list()
|
||||
student = Student.get_by_id(student_id)
|
||||
study = Study.get_by_id(student.study_id)
|
||||
subs = list(Submission.select().where(Submission.student_id == student.id))
|
||||
|
||||
grader = get_grader(student.grader)
|
||||
points = [sub.points for sub in subs]
|
||||
maxs = [sub.lecture_id.points for sub in subs]
|
||||
|
||||
name = f'{student.prename} {student.surname}'
|
||||
passed = f'{name} has passed the course successfully.' if grader.has_passed_course(points, maxs) else f'{name} failed the course.'
|
||||
|
||||
text = ''
|
||||
|
||||
# Append Logo
|
||||
if logo_file:
|
||||
text += f'\n'
|
||||
|
||||
text += f'# {student.prename} {student.surname} - {student.class_id.name}\n'
|
||||
text += f'#### {study.name}\n\n'
|
||||
text += f'Gender: **{student.sex}**\n\n'
|
||||
s = sum(points)
|
||||
text += f'Score: **{int(s) if s.is_integer() else s}/{sum(maxs)}**\n\n'
|
||||
text += f'Percent: **{s/sum(maxs):.1%}**\n\n'
|
||||
text += f'Grade: {grader.get_final_grade(points, maxs)}\n\n'
|
||||
text += passed + '\n\n'
|
||||
|
||||
text += '---\n\n'
|
||||
text += get_metadata(author)
|
||||
sections.append(Section(text))
|
||||
|
||||
text = '# Lectures\n'
|
||||
text += '|Lecture|Submission|Passed?|\n'
|
||||
text += '|-|-|-|\n'
|
||||
n = 0
|
||||
for sub in subs:
|
||||
text += f'|{sub.lecture_id.title}|{int(sub.points) if sub.points.is_integer() else sub.points}/{sub.lecture_id.points}|{grader.get_grade(sub.points, sub.lecture_id.points)}|\n'
|
||||
|
||||
n += 1
|
||||
if n == 30:
|
||||
text += '---\n\n'
|
||||
sections.append(Section(text))
|
||||
n = 0
|
||||
text = '# Lectures\n'
|
||||
text += '|Lecture|Submission|Passed?|\n'
|
||||
text += '|-|-|-|\n'
|
||||
|
||||
if text and n != 0:
|
||||
text += '---\n\n'
|
||||
sections.append(Section(text))
|
||||
|
||||
return sections
|
||||
|
||||
def get_pdf_header_creator(class_id: int, filter: Sequence[str], author: str, logo_file: Path = None) -> Section:
|
||||
text = ''
|
||||
# Append Logo
|
||||
if logo_file:
|
||||
@ -37,11 +108,5 @@ def get_pdf_header(class_id: int, filter: Sequence[str], author: str, logo_file:
|
||||
text += '---\n\n'
|
||||
|
||||
# Metadata
|
||||
text += f'Author: {author}\n\n'
|
||||
|
||||
date = datetime.now()
|
||||
text += f'Created: {date.strftime("%A %d.%m.%Y %H:%M")}\n\n'
|
||||
|
||||
text += 'This document is system-generated and may not require manual signatures.'
|
||||
|
||||
text += get_metadata(author)
|
||||
return Section(text)
|
||||
|
Binary file not shown.
@ -1 +1 @@
|
||||
/storage/programming/Learnlytics/assets/documents/wise-23-24-fri-07-03-2025-01-15a.pdf
|
||||
/home/phil/wise-24-25-sat-08-03-2025-01-53.pdf
|
79
poetry.lock
generated
79
poetry.lock
generated
@ -710,6 +710,83 @@ files = [
|
||||
greenlet = ">=3.1.1,<4.0.0"
|
||||
pyee = ">=12,<13"
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.10"
|
||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"},
|
||||
{file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"},
|
||||
{file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"},
|
||||
{file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"},
|
||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"},
|
||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"},
|
||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
@ -1178,4 +1255,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "0f368a19ff46c53164f16b88dee00580f42b9559212a4fa7fa71b9aad0721824"
|
||||
content-hash = "799a180a55fe1eac994203a47c03b390b1abbd444d0fd7d5d92045b310947319"
|
||||
|
@ -29,6 +29,7 @@ markdown-pdf = "^1.3.3"
|
||||
linkify-it-py = "^2.0.3"
|
||||
python-slugify = "^8.0.4"
|
||||
cairosvg = "^2.7.1"
|
||||
psycopg2-binary = "^2.9.10"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
29
tests/postgres/compose.yml
Normal file
29
tests/postgres/compose.yml
Normal file
@ -0,0 +1,29 @@
|
||||
services:
|
||||
postgres:
|
||||
container_name: learnlytics-pg
|
||||
image: postgres
|
||||
hostname: localhost
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: admin
|
||||
POSTGRES_PASSWORD: admin
|
||||
POSTGRES_DB: learnlytics
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
pgadmin:
|
||||
container_name: learnlytics-pgadmin
|
||||
image: dpage/pgadmin4
|
||||
depends_on:
|
||||
- postgres
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@learnlytics.de
|
||||
PGADMIN_DEFAULT_PASSWORD: admin
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
Loading…
Reference in New Issue
Block a user