Merge pull request 'Update Main to reflect latest dev changes' (#5) from dev into main

Reviewed-on: WorldTeacher/LibrarySystem#5
This commit is contained in:
2024-10-08 10:22:29 +02:00
119 changed files with 4880 additions and 393 deletions

2
.gitignore vendored
View File

@@ -218,3 +218,5 @@ compile_commands.json
.history/*
**/tempCodeRunnerFile.py
output/**

111
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,111 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build LibrarySystem (Debug)",
"type": "shell",
"command": "pyinstaller",
"args": [
"--noconfirm",
"--onedir",
"--console",
"--icon",
"'${config:ApplicationIconPath}'",
"--name",
"LibrarySystem-debug",
"--contents-directory",
".",
"--clean",
"--add-data",
"'${config:configPath};config/'",
"--add-data",
"'${config:iconsPath};icons/'",
"'${workspaceFolder}/main_dev.py'"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new",
"focus": false
},
"dependsOn": "Compress dist Folder Debug",
"problemMatcher": "$pyinstaller"
},
{
"label": "Build LibrarySystem (Release)",
"type": "shell",
"command": "pyinstaller",
"args": [
"--noconfirm",
"--onedir",
"--windowed",
"--name",
"LibrarySystem",
"--contents-directory",
".",
"--clean",
"--add-data",
"'${config:configPath};config/'",
"--add-data",
"'${config:iconsPath};icons/'",
"--icon",
"'${config:ApplicationIconPath}'",
"'${workspaceFolder}/main.py'"
],
"group": {
"kind": "build",
"isDefault": false
},
"presentation": {
"reveal": "always",
"panel": "new",
"focus": false
},
"dependsOn": "Compress dist Folder Release",
"problemMatcher": "$pyinstaller"
},
{
"label": "Run LibrarySystem (live)",
"type": "shell",
"command": "c:/Users/aky547/GitHub/LibrarySystem/.venv/Scripts/python.exe",
"args": [
"'c:/Users/aky547/GitHub/LibrarySystem/main_dev.py'",
"--ic-logging"
],
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": "$python"
},
{
"label": "Compress dist Folder Debug",
"type": "shell",
"command": "Compress-Archive",
"args": [
"-Path",
"${workspaceFolder}/dist/LibrarySystem-debug/",
"-DestinationPath",
"${workspaceFolder}/output/LibrarySystem-debug.zip",
],
"group": "build",
"presentation": "panel"
},
{
"label": "Compress dist Folder Release",
"type": "shell",
"command": "Compress-Archive",
"args": [
"-Path",
"${workspaceFolder}/dist/LibrarySystem/",
"-DestinationPath",
"${workspaceFolder}/output/LibrarySystem.zip",
],
"group": "build",
"presentation": "panel"
}
]
}

5
ATTRIBUTIONS.MD Normal file
View File

@@ -0,0 +1,5 @@
## Icons
if not stated elsewhere, all Icons are part of the Material Symbols Package provided by Google
### Specific Icons
- borrow_boks.svg Credits to Maxicons, Icon downloaded frmo thenounproject.com [Link](https://thenounproject.com/icon/borrow-book-3317975/)
- borrow

View File

@@ -1,3 +1,30 @@
# LibrarySystem
universal library system for facilities in the university
universal library system for facilities in the university
## What is this?
This is a library system for the different facilities in the university. Because some facilities lend their media, some wanted a software that allows them to keep track of who had what when.
### Installation
either clone the Repo
```
git clone https://git.theprivateserver.de/WorldTeacher/LibrarySystem.git
cd LibrarySystem
./build(.exe)
```
or head over to [releases](https://git.theprivateserver.de/WorldTeacher/LibrarySystem/releases)
and download the latest version
### Configuration
the software contains a config file in configs, as well as a config for icons, located in icons.
#### config
tbd
#### icons
- color is a value to re-paint the icons
- icons/* a dict of the icons structured like: icon_Name_in_Application : icon_File_name

77
build.app.json Normal file
View File

@@ -0,0 +1,77 @@
{
"version": "auto-py-to-exe-configuration_v1",
"pyinstallerOptions": [
{
"optionDest": "noconfirm",
"value": true
},
{
"optionDest": "filenames",
"value": "C:/Users/aky547/GitHub/LibrarySystem/main.py"
},
{
"optionDest": "onefile",
"value": false
},
{
"optionDest": "console",
"value": false
},
{
"optionDest": "icon_file",
"value": "C:/Users/aky547/Downloads/1490971308-map-icons-7_82746.ico"
},
{
"optionDest": "name",
"value": "LibrarySystem"
},
{
"optionDest": "contents_directory",
"value": "."
},
{
"optionDest": "clean_build",
"value": true
},
{
"optionDest": "strip",
"value": false
},
{
"optionDest": "noupx",
"value": false
},
{
"optionDest": "disable_windowed_traceback",
"value": false
},
{
"optionDest": "uac_admin",
"value": false
},
{
"optionDest": "uac_uiaccess",
"value": false
},
{
"optionDest": "argv_emulation",
"value": false
},
{
"optionDest": "bootloader_ignore_signals",
"value": false
},
{
"optionDest": "datas",
"value": "C:/Users/aky547/GitHub/LibrarySystem/config;config/"
},
{
"optionDest": "datas",
"value": "C:/Users/aky547/GitHub/LibrarySystem/icons;icons/"
}
],
"nonPyinstallerOptions": {
"increaseRecursionLimit": true,
"manualArguments": ""
}
}

77
build.debug.app.json Normal file
View File

@@ -0,0 +1,77 @@
{
"version": "auto-py-to-exe-configuration_v1",
"pyinstallerOptions": [
{
"optionDest": "noconfirm",
"value": true
},
{
"optionDest": "filenames",
"value": "C:/Users/aky547/GitHub/LibrarySystem/main_dev.py"
},
{
"optionDest": "onefile",
"value": false
},
{
"optionDest": "console",
"value": true
},
{
"optionDest": "icon_file",
"value": "C:/Users/aky547/Downloads/1490971308-map-icons-7_82746.ico"
},
{
"optionDest": "name",
"value": "LibrarySystem-debug"
},
{
"optionDest": "contents_directory",
"value": "."
},
{
"optionDest": "clean_build",
"value": true
},
{
"optionDest": "strip",
"value": false
},
{
"optionDest": "noupx",
"value": false
},
{
"optionDest": "disable_windowed_traceback",
"value": false
},
{
"optionDest": "uac_admin",
"value": false
},
{
"optionDest": "uac_uiaccess",
"value": false
},
{
"optionDest": "argv_emulation",
"value": false
},
{
"optionDest": "bootloader_ignore_signals",
"value": false
},
{
"optionDest": "datas",
"value": "C:/Users/aky547/GitHub/LibrarySystem/config;config/"
},
{
"optionDest": "datas",
"value": "C:/Users/aky547/GitHub/LibrarySystem/icons;icons/"
}
],
"nonPyinstallerOptions": {
"increaseRecursionLimit": true,
"manualArguments": ""
}
}

77
build.release.app.json Normal file
View File

@@ -0,0 +1,77 @@
{
"version": "auto-py-to-exe-configuration_v1",
"pyinstallerOptions": [
{
"optionDest": "noconfirm",
"value": true
},
{
"optionDest": "filenames",
"value": "C:/Users/aky547/GitHub/LibrarySystem/main.py"
},
{
"optionDest": "onefile",
"value": false
},
{
"optionDest": "console",
"value": false
},
{
"optionDest": "icon_file",
"value": "C:/Users/aky547/Downloads/1490971308-map-icons-7_82746.ico"
},
{
"optionDest": "name",
"value": "LibrarySystem"
},
{
"optionDest": "contents_directory",
"value": "."
},
{
"optionDest": "clean_build",
"value": true
},
{
"optionDest": "strip",
"value": false
},
{
"optionDest": "noupx",
"value": false
},
{
"optionDest": "disable_windowed_traceback",
"value": false
},
{
"optionDest": "uac_admin",
"value": false
},
{
"optionDest": "uac_uiaccess",
"value": false
},
{
"optionDest": "argv_emulation",
"value": false
},
{
"optionDest": "bootloader_ignore_signals",
"value": false
},
{
"optionDest": "datas",
"value": "C:/Users/aky547/GitHub/LibrarySystem/config;config/"
},
{
"optionDest": "datas",
"value": "C:/Users/aky547/GitHub/LibrarySystem/icons;icons/"
}
],
"nonPyinstallerOptions": {
"increaseRecursionLimit": true,
"manualArguments": ""
}
}

1
config/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .config import Config

176
config/config.py Normal file
View File

@@ -0,0 +1,176 @@
from typing import Optional
import omegaconf
class Config:
_config: Optional[omegaconf.DictConfig] = None
def __init__(self, config_path: str):
"""
Loads the configuration file and stores it for future access.
Args:
config_path (str): Path to the YAML configuration file.
"""
self._config = omegaconf.OmegaConf.load(config_path)
@property
def institution_name(self)->str:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.institution_name
@institution_name.setter
def institution_name(self, value: str):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.institution_name = value
@property
def loan_duration(self)->int:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.default_loan_duration
@loan_duration.setter
def loan_duration(self, value: int):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.default_loan_duration = value
@property
def delete_inactive_user_duration(self)->int:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.inactive_user_deletion
@delete_inactive_user_duration.setter
def delete_inactive_user_duration(self, value: int):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.inactive_user_deletion = value
@property
def catalogue(self)->bool:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.catalogue
@catalogue.setter
def catalogue(self, value: bool):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.catalogue = value
@property
def shortcuts(self)->omegaconf.DictConfig:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.shortcuts
@shortcuts.setter
def shortcuts(self, value: omegaconf.DictConfig):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.shortcuts = value
@property
def advanced_refresh(self)->omegaconf.DictConfig:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.advanced_refresh
@advanced_refresh.setter
def advanced_refresh(self, value: omegaconf.DictConfig):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.advanced_refresh = value
@property
def database(self)->omegaconf.DictConfig:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.database
@database.setter
def database(self, value: omegaconf.DictConfig):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.database = value
@property
def report(self)->omegaconf.DictConfig:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.report
@report.setter
def report(self, value: omegaconf.DictConfig):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.report = value
@property
def debug(self)->bool:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.debug
@debug.setter
def debug(self, value: bool):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.debug = value
@property
def log_debug(self)->bool:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.log_debug
@log_debug.setter
def log_debug(self, value: bool):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.log_debug = value
@property
def ic_logging(self)->bool:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.ic_logging
@ic_logging.setter
def ic_logging(self, value:bool):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.ic_logging = value
def save(self):
if self._config is None:
raise RuntimeError("Configuration not loaded")
omegaconf.OmegaConf.save(self._config, "config/settings.yaml")
def apply_options(self, options:list):
if self._config is None:
raise RuntimeError("Configuration not loaded")
for option in options:
if option in self._config:
self._config[option] = True
else:
raise KeyError(f"Option {option} not found in configuration")
@property
def documentation(self)->bool:
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.documentation
@documentation.setter
def documentation(self, value: bool):
if self._config is None:
raise RuntimeError("Configuration not loaded")
self._config.documentation = value
def to_Omegaconf(self):
return omegaconf.OmegaConf.create(self._config)
def updateValue(self, key:str, value):
if self._config is None:
raise RuntimeError("Configuration not loaded")
if "." in key:
keys = key.split(".")
if keys[0] in self._config:
self._config[keys[0]][keys[1]] = value
else:
raise KeyError(f"Option {keys[0]} not found in configuration")
else:
self._config[key] = value
if __name__ == "__main__":
cfg = Config("config/settings.yaml")
#print(cfg.database.path)
cfg.database.path = "nopathhere"
#print(cfg.database.path)
cfg.save()
#cfg.updateValue("database.path", "Test")

View File

@@ -1,8 +1,34 @@
institution_name: HB Testbibliothek Psychologie
institution_name: Test
default_loan_duration: 7
inactive_user_deletion: 365
database:
path: C:/db
path: ./database
name: library.db
backupLocation: ./backup
library_id: 20735
do_backup: true
report:
generate_report: true
path: ./report
report_day: 0
shortcuts:
- name: Rueckgabemodus
default: F5
current: F5
- name: Nutzer
default: F6
current: F6
- name: Hilfe
default: F1
current: F1
- name: Bericht_erstellen
default: F7
current: F7
- name: Ausleihhistorie
default: F8
current: F8
advanced_refresh: false
catalogue: true
debug: false
log_debug: false
ic_logging: false
documentation: true

View File

@@ -0,0 +1,16 @@
# Sonderfälle bei der Ausleihe
In einigen Fällen kann es bei der Ausleihe zu Problemen kommen. Diese werden hier aufgeführt.
## Medium bereits entliehen
Diese Meldung erscheint, wenn ein Medium bereits entliehen ist und erneut ausgeliehen werden soll.
Es erscheint folgende Meldung:
![FehlerAusleihe](images/book_loaned.png)
Wenn "Yes" geklickt wird, erscheint ein neues Fenster zum anlegen eines neuen Exemplars.
![AddNewBook](images/book_addNew.png)
Über das + Symbol können neue Exemplare hinzugefügt werden. Es ist empfohlen, die Signatur jeweils um +1 zu erhöhen, um eine eindeutige Zuordnung zu gewährleisten.

15
docs/Ausleihhistorie.md Normal file
View File

@@ -0,0 +1,15 @@
# Ausleihhistorie
![Oberfläche](images/loanhistory.png)
!!! info
Die Ausleihhistorie kann vom Ausleihsystem immer aufgerufen werden. Dazu kann man entweder das Menu öffnen (Fenster -> Ausleihhistorie) oder den festgelegten Shortcut verwenden.
# Bedienung
Hier werden alle Medien angezeigt.
Über die Filter kann man gezielt
- alle Ausleihen
- aktuell entliehene Medien
- überzogene Medien
anzeigen.
Zudem kann man mithilfe der Eingabezeile unter den Filteroptionen gezielt nach einem Titel, Nutzer oder einer Signatur gesucht werden.

83
docs/Ausleihsystem.md Normal file
View File

@@ -0,0 +1,83 @@
# Ausleihsystem
## Oberfläche
![Nutzeroberfläche ohne Bewerkungen](images/main_marked%20areas.png)
Die Oberfläche kann generell in drei Bereiche unterteilt werden:
- Konto und Ausleihe
- Nutzerdaten
- Historie
### Konto und Ausleihe
Hier werden die Kontodaten und die Ausleihen angezeigt.
Der Bereich beschränkt sich auf folgende Inhalte:
![Kontobereich](images/main_ktoarea.png)
Hier werden folgende Daten angezeigt
- Modus: entweder "Ausleihe" oder "Rückgabe" (1) Kann entweder durch anklicken des Knopfes oder mit dem Shortcut (Standard: `F5`) geändert werden
- Matrikelnummer: die Matrikelnummer des Nutzers, um das Konto zu öffnen (3)
- Benutzername: der Benutzername des Nutzers (3)
- Signatur: die Signatur des Mediums, welches entliehen oder zurückgegeben wird (4)
- Ausleihe bis: bis wann das Medium ausgeliehen wird, Zeitraum wird in den Einstellungen angepasst
- Nutzer anlegen: ein Knopf, um einen neuen Nutzer anzulegen (2)
### Nutzerdaten
Hier werden die Nutzerdaten angezeigt. Vorraussetzung ist, dass ein Nutzer angelegt und geöffnet wurde, oder dass ein Medium zurückgegeben wurde.
Dieser Bereich beschränkt sich auf folgende Inhalte:
![Nutzerdaten](images/main_userdata.png)
- Das Feld Nutzerdaten (6)beinhaltet
- Namen
- Matrikelnummer
- E-Mail
- Das Feld Ausleihdaten beinhaltet:
- Anzahl Ausleihen (5) : ein klickbares Feld, welches die Anzahl der Ausleihen anzeigt. Beim Klick wird die Übersicht des aktiven Nutzers angezeigt.
- Nächstes Rückgabedatum (wird angezeigt wenn ein Nutzer (mehr als) eine Ausleihe hat und ein Medium zurückgegeben wurde oder ein Nutzer geöffnet wird)
### Historie (7)
Das Feld der Historie listet alle Medien auf, die im aktiven Prozess ausgeliehen oder zurückgegeben wurden.
## Ausleihen
Um ein Medium auszuleihen, muss zuerst ein Nutzer geöffnet sein. Dazu entweder den Knopf neben Modus drücken, oder den Shortcut (Standard: `F5`) verwenden.
Die Oberfläche sieht dann wie folgt aus:
![Ausleihe](images/main_loan_active.png)
Der Cursor wird automatisch auf die Matrikelnummer gesetzt. Hier kann entweder die vollständige Nummer, oder ein Teil eingegeben werden. Sollten mehrere Nummern dem Teilfilter ensprechen, wird eine Auswahl angezeigt. (s. [MultiUser](MultiUser.md))
Nach der Auswahl wird das entsprechende Konto geöffnet und die Ausleihe kann durchgeführt werden.
![Ausleihe](images/activeLoan.png)
Sollte der aktive Nutzer aktive Ausleihen haben, so wird die Anzahl der Medien neben "Anzahl Ausleihen" angezeigt. Das nächste Rückgabedatum wird ebenfalls angezeigt. Über einen Klick auf die Zahl der Ausleihen gelangen Sie zur [Übersicht der Ausleihen des Nutzers](Nutzeroberfläche.md).
Um ein Medium auszuleihen, muss die Signatur in die entsprechende Zeile eingegeben werden. Sollte die Signatur nicht in der Datenbank existieren, wird der definierte Katalog geprüft. Wird das Medium gefunden, wird es in die Datenbank übernommen und die Ausleihe wird durchgeführt. Sollte das Medium nicht gefunden werden, wird eine Fehlermeldung angezeigt.
![FehlerAusleihe](images/err_noBook.png)
!!! info
Die Signatur muss mindestens ein Leerzeichen enthalten, ansonsten wird eine Fehlermeldung angezeigt.
Sonderfälle siehe [AusleiheSonderfälle](Ausleihe_Sonderfälle.md)
## Rückgaben
!!! info
Rückgaben funktionieren nur, wenn kein Nutzer offen ist. Vor einer Rückgabe bitte sicherstellen, dass der Modus auf "Rückgabe" gesetzt ist.
Um ein Medium zurückzugeben, muss die Signatur in die entsprechende Zeile eingegeben werden. Sollte die Signatur nicht existieren, wird eine Fehlermeldung angezeigt.
![FehlerRückgabe](images/err_return.png)
Existiert das Medium in der Datenbank und ist entliehen, wird die Rückgabe durchgeführt. Die Rückgabe wird in der Historie gespeichert und das Medium wird wieder als verfügbar markiert. Die Daten des Nutzers werden in der Oberfläche angezeigt.

27
docs/Bericht erstellen.md Normal file
View File

@@ -0,0 +1,27 @@
# Bericht erstellen
![Bericht erstellen](images/generateReport.png)
## Information
Diese Oberfläche kann immer von der [Hauptoberfläche](Ausleihsystem.md) geöffnet werden. Hierzu entweder
`Fenster -> Bericht erstellen` oder den Shortcut F7 verwenden
## Bericht generieren
Um einen Bericht zu erstellen, müssen folgende Kriterien erfüllt sein:
- Zeitspanne festgelegt (Entweder über den Slider, oder über die Woche / Monat / Jahr Knöpfe)
- Datenformat ausgewählt
Nachdem diese Kriterien erfüllt sind, kann der Bericht über den Knopf `Bericht erstellen` erstellt werden. Der Bericht wird erstellt, bei größeren Datensätzen kann es länger dauern, eine Fortschrittsanzeige gibt an, wie weit der Prozess ist.
Wurde der Bericht erfolgreich erstellt, sieht die Oberfläche wie folgt aus:
![Bericht erstellt](images/generatedReport.png)
Über einen Klick auf `Report` wird die entsprechende Datei geöffnet.
!!! info
Text öffnet das Notepad, Excel öffnet Excel

21
docs/Einstellungen.md Normal file
View File

@@ -0,0 +1,21 @@
# Einstellungen
![Einstellungen](images/settings.png)
## Bedienung
Hier werden die Einstellungen geändert. Sobald ein Wert geändert wird, ist es möglich die Einstellungen rückgängig zu machen, oder die Änderungen zu speichern.
## Besonderheiten
Der Knopf neben Speicherort, Datenbankname, Sicherungspfad und Speicherpfad kann verwendet werden, um den Pfad gezielt zu setzen.
Beim Datenbanknamen wird allerdings nur der Name der Datenbank übernommen
## Hinweis
Einige Aktionen (bspw. Änderungen der Shortcuts) erfordern einen Neustart der Anwendung. Dies wird beim Speichern der Einstellungen mit folgendem Dialog dargestelt:
![restart](images/restart.png)
Im Anschluss an diesen Dialog erscheint ein neuer Dialog:
![restart Application](images/settings_changed_restart.png)

5
docs/MultiUser.md Normal file
View File

@@ -0,0 +1,5 @@
# Mehrere Nutzer gefunden
![Mehrere Nutzer gefunden](images/multiUser.png)
!!! info
Es wurden mehrere Nutzer gefunden, die auf die Suchanfrage passen. Bitte wählen Sie einen Nutzer aus der Liste aus, und bestätigen Sie mit `OK`.

18
docs/Nutzer anlegen.md Normal file
View File

@@ -0,0 +1,18 @@
# Nutzer anlegen
![Nutzer erstellen](images/add_user.png)
## Information
Diese Oberfläche kann nur geöffnet werden, wenn die [Hauptoberfläche](Ausleihsystem.md) geöffnet ist. Hierzu muss bei dieser auf folgenden Knopf gedrückt werden:
![create user](images/createUser.png)
## Bedienung
Um einen Nutzer anzulegen, müssen alle Angaben korrekt ausgefüllt sein.
### Limitierungen
Folgende Regeln sind zwingend einzuhalten:
- Nachname, Vorname muss mit `, ` getrennt sein
- Matrikelnummer darf nicht länger als 20 Zeichen sein und nur Nummern enthalten
- Mail muss einem validen Schema entsprechen (s. Bild)
Nachdem alle Kriterien erfüllt sind, kann der Knopf `Save` angeklickt werden. Der Nutzer wird gespeichert und in der [Hauptoberfläche](Ausleihsystem.md) geöffnet.

31
docs/Nutzeroberfläche.md Normal file
View File

@@ -0,0 +1,31 @@
# Nutzerdatenfenster
![alt text](images/user_main.png)
!!! info
Die Nutzeroberfläche kann nur geöffnet werden, wenn ein Nutzer offen ist. Ansonsten wird ein Fehler angezeigt. (s. unten)
![Error](images/err_nouser.png)
# Bedeutung der Felder
- (1) Nutzerdaten:
Name des Nutzers, Matrikelnummer, E-Mail
Wird eine Angabe geändert, erscheint Feld (3) um diese Angaben entweder zu speichern oder um die Änderungen rückgängig zu machen.
- (2) Nutzer Löschen:
Mit dem Klick auf den Mülleimer wird der Nutzer gelöscht. Alle zugewiesenen Ausleihen werden in der [Ausleihhistorie](Ausleihhistorie.md) mit den Nutzerkonto `gelöscht` angezeigt. Diese Option ist nur vorhanden, wenn keine Ausleihen aktiv sind.
- (4) Medien:
Umfasst unter anderem Feld (5), welches ein neues Fenster zum [verlängern](ausleihe verlängern.md) der Ausleihe öffnet.
Mit einem Klick auf
- Alle Ausleihen
- Aktuell entliehen
- Überzogen
werden die Einträge der Tabelle gefiltert. Zusätzlich kann mithilfe der Eingabezeile unter den Filteroptionen gezielt nach einem Titel oder einer Signatur gesucht werden.
- (5) Verlängern:
Um Medien zu verlängern, müssen diese in der Tabelle angeklickt werden. Mit Strg können mehrere Medien gleichzeitig ausgewählt und verlängert werden. Hierzu wird ein neues Fenster geöffnet, siehe [Ausleihe verlängern](ausleihe verlängern.md).

View File

@@ -0,0 +1,9 @@
# Medien verlängern
![Oberfläche](images/extend.png)
## Information
Die Ausleihe verlängern kann nur geöffnet werden, wenn ein Nutzer offen ist.
Diese Oberfläche erlaubt es, eine oder mehrere Medien zu verlängern. Hierzu muss ein neues, in der Zukunft liegendes Datum ausgewählt und mit OK bestätigt werden.
In der Datenbank wird nun das neue Datum gespeichert und die Einträge in der Tabelle werden aktualisiert.

BIN
docs/images/activeLoan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/images/add_user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
docs/images/book_addNew.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
docs/images/book_loaned.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
docs/images/createUser.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

BIN
docs/images/err_noBook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
docs/images/err_nouser.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
docs/images/err_return.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
docs/images/extend.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
docs/images/loanhistory.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
docs/images/multiUser.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
docs/images/restart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
docs/images/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
docs/images/user_main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

21
docs/index.md Normal file
View File

@@ -0,0 +1,21 @@
# Übersicht
!!! info
Einige Knöpfe sind auf Englisch. Diese Einstellung kann ich leider nicht ändern, da diese Übersetzung von der Anwendung automatisch ausgeführt werden
## [Hauptoberfläche](Ausleihsystem.md)
Die Hauptoberfläche wird angezeigt, wenn die Anwendung gestartet wird.
Von hier aus können Nutzer angelegt und bearbeitet werden, Medien ausgeliehen und zurückgegeben werden, sowie verschiedene Unterbereiche anzeigen.
Unterbereiche umfassen:
- [Hauptoberfläche](Ausleihsystem.md)
- [Nutzerdaten](Nutzeroberfläche.md) - Daten für den aktuellen Nutzer anzeigen und bearbeiten
- [Nutzer anlegen](Nutzer anlegen.md) - Neuen Nutzer anlegen
- [Ausleihhistorie](Ausleihhistorie.md) - Historie der Ausleihen
- [Bericht erstellen](Bericht erstellen.md) - Bericht für einen festgelegten Zeitrahmen erstellen
- [Einstellungen](Einstellungen.md) - Einstellungen der Anwendung ändern

16
docs/shortcuts.md Normal file
View File

@@ -0,0 +1,16 @@
# Shortcuts
!!! info
Die Shortcuts können für die Anwendung manuell festgelegt werden.
Dafür in den [Einstellungen](Einstellungen.md) unter Shortcuts den neuen Shortcut eingeben und speichern
Standardmäßig sind die Shortcuts wie folgt festgelegt:
| Shortcut | Standard |
| ------- | -------- |
| Ausleihhistorie | F7 |
| Bericht erstellen | F6 |
| Hilfe | F1 |
| Nutzer | F5 |
| Rückgabemodus | F8 |

View File

@@ -0,0 +1,5 @@
.md-typeset .admonition,
.md-typeset details {
border-width: 0;
border-left-width: 4px;
}

103
filehandle.py Normal file
View File

@@ -0,0 +1,103 @@
import os
import shutil
from pathlib import Path
ORIGIN = "C:/testing/source_2"
BACKUP = "C:/Databasebackup"
FILE = "database.db"
def handle_file():
"""
Handles file creation, movement, and backup based on path and backup existence.
Args:
path (str): The desired path for the file.
filename (str): The name of the file.
backup_location (str): The location for backups.
Returns:
str: The final path of the file.
"""
full = Path(ORIGIN) / FILE
full_path = str(full)
backup = Path(BACKUP) / FILE
backup_path = str(backup)
origin_reachable = os.path.exists(full_path)
backup_reachable = os.path.exists(backup_path)
print(origin_reachable, backup_reachable)
if not origin_reachable and not backup_reachable:
make_dirs(ORIGIN, BACKUP)
# if not os.path.exists(full_path):
# print("File does not exist, creating file")
# create_file(full_path)
if os.path.exists(backup_path) and ".backup" in os.listdir(BACKUP):
print("Used backup previously, overwriting file")
#overwrite file at source with backup
shutil.copy(backup_path, full_path)
os.remove(BACKUP + "/.backup")
def make_dirs(full_path, backup_path):
"""
Creates directories if they do not exist and moves the file to the desired location.
Args:
full_path (str): The full path of the file.
backup_path (str): The backup path of the file.
"""
if not os.path.exists(full_path):
os.makedirs(full_path)
if not os.path.exists(backup_path):
os.makedirs(backup_path)
def create_file(full_path):
"""
Creates a file at the desired location.
Args:
full_path (str): The full path of the file.
"""
with open(full_path, "w") as f:
f.write("")
def handle_folder_reachability(original_path, backup_path):
"""
Checks if the original folder is reachable. If not, creates a backup.
If the original folder becomes reachable again, restores the backup.
Args:
original_path (str): Path to the original folder.
backup_path (str): Path to the backup folder.
Returns:
str: Path to the current accessible folder.
"""
backup_file = os.path.join(backup_path, ".backup")
try:
os.makedirs(original_path)
except FileExistsError:
pass
if not os.path.exists(original_path):
#original folder not reachable, use backup path and create .backup file
if not os.path.exists(backup_path):
os.makedirs(backup_path)
with open(backup_file, "w") as f:
f.write("")
# Create an empty backup file as a marker
return backup_path +"/" + FILE
else:
# Original folder is reachable, check for backup
if os.path.exists(backup_file):
# Restore backup
shutil.rmtree(original_path) # Remove original folder to avoid conflicts
shutil.move(backup_path, original_path)
#os.remove(backup_file)
#remove backup file from original path
os.remove(original_path + "/.backup")
os.makedirs(backup_path)
return original_path +"/" + FILE
if __name__=="__main__":
print(handle_folder_reachability(ORIGIN, BACKUP)) # should create a new file at C:/newdatabase/database.db)

4
icons/add_book.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" fill="#000000">
<path d="m17,10c-3.859,0-7,3.141-7,7s3.141,7,7,7,7-3.141,7-7-3.141-7-7-7Zm0,12c-2.757,0-5-2.243-5-5s2.243-5,5-5,5,2.243,5,5-2.243,5-5,5Zm1-6h2v2h-2v2h-2v-2h-2v-2h2v-2h2v2Zm-8.482,6H3c-.552,0-1-.448-1-1s.448-1,1-1h5.523c-.226-.638-.388-1.306-.464-2h-2.059V2h12v6.058c.695.077,1.362.239,2,.464V2c0-1.103-.898-2-2-2H3C1.346,0,0,1.346,0,3v18c0,1.654,1.346,3,3,3h8.349c-.706-.571-1.325-1.244-1.831-2ZM2,3c0-.552.448-1,1-1h1v16h-1c-.352,0-.686.072-1,.184V3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 612 B

1
icons/add_user.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M720-400v-120H600v-80h120v-120h80v120h120v80H800v120h-80Zm-360-80q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm80-80h480v-32q0-11-5.5-20T580-306q-54-27-109-40.5T360-360q-56 0-111 13.5T140-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T440-640q0-33-23.5-56.5T360-720q-33 0-56.5 23.5T280-640q0 33 23.5 56.5T360-560Zm0-80Zm0 400Z"/></svg>

After

Width:  |  Height:  |  Size: 603 B

1
icons/backup.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M480-400q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0 280q-139 0-241-91.5T122-440h82q14 104 92.5 172T480-200q117 0 198.5-81.5T760-480q0-117-81.5-198.5T480-760q-69 0-129 32t-101 88h110v80H120v-240h80v94q51-64 124.5-99T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Z"/></svg>

After

Width:  |  Height:  |  Size: 499 B

1
icons/book.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#c0c0c0"><path d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h480q33 0 56.5 23.5T800-800v640q0 33-23.5 56.5T720-80H240Zm0-80h480v-640h-80v280l-100-60-100 60v-280H240v640Zm0 0v-640 640Zm200-360 100-60 100 60-100-60-100 60Z"/></svg>

After

Width:  |  Height:  |  Size: 344 B

1
icons/calendar_event.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#c0c0c0"><path d="M580-240q-42 0-71-29t-29-71q0-42 29-71t71-29q42 0 71 29t29 71q0 42-29 71t-71 29ZM200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

4
icons/db_backup.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" fill="#000000">
<path d="m16,14c-.795,0-.951.69-.956.707-.295.961-2.733,2.293-6.544,2.293-3.968,0-6.5-1.48-6.5-2.5v-1.908c1.64.929,4.077,1.408,6.5,1.408.177,0,.353-.003.527-.007.552-.015.987-.475.973-1.027-.015-.552-.454-1.012-1.026-.973-.156.004-.314.006-.473.006-4.206,0-6.5-1.463-6.5-2.214v-2.193c1.64.929,4.077,1.408,6.5,1.408.177,0,.353-.002.527-.007.552-.015.987-.474.973-1.026-.015-.552-.454-1.012-1.026-.973-.156.004-.314.006-.473.006-4.206,0-6.5-1.463-6.5-2.214v-.571c0-.751,2.294-2.214,6.5-2.214.642,0,1.275.037,1.882.109.547.056,1.046-.327,1.111-.875.065-.548-.327-1.046-.875-1.111C5.912-.437,0,.976,0,4.214v15.572c0,2.767,4.276,4.214,8.5,4.214s8.5-1.447,8.5-4.214v-4.786c0-.018-.051-1-1-1Zm-7.5,8c-4.206,0-6.5-1.463-6.5-2.214v-2.327c1.535.954,3.835,1.541,6.5,1.541,2.626,0,4.939-.568,6.5-1.505v2.291c0,.751-2.294,2.214-6.5,2.214ZM24,1.22v3.06c0,.398-.322.72-.72.72h-.693s-.002,0-.002,0c-.001,0-.002,0-.003,0h-2.361c-.291,0-.554-.175-.665-.444-.112-.269-.05-.579.156-.785l.842-.842c-.705-.585-1.6-.929-2.553-.929-1.591,0-3.03.942-3.668,2.4-.22.505-.805.738-1.316.516-.506-.221-.737-.811-.516-1.317.956-2.187,3.115-3.6,5.5-3.6,1.494,0,2.899.555,3.976,1.506l.795-.796c.206-.206.515-.267.785-.156.269.111.444.374.444.665Zm-.5,7.181c-.955,2.187-3.114,3.6-5.5,3.6-1.494,0-2.899-.555-3.976-1.506l-.795.795c-.206.206-.515.267-.785.156-.269-.111-.444-.374-.444-.665v-3.06c0-.398.322-.72.72-.72h3.06c.291,0,.554.175.665.444.112.269.05.579-.156.785l-.842.842c.705.585,1.6.929,2.553.929,1.591,0,3.03-.942,3.668-2.4.221-.504.806-.735,1.316-.516.506.221.737.811.516,1.317Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

1
icons/delete.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="m376-300 104-104 104 104 56-56-104-104 104-104-56-56-104 104-104-104-56 56 104 104-104 104 56 56Zm-96 180q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520Zm-400 0v520-520Z"/></svg>

After

Width:  |  Height:  |  Size: 367 B

1
icons/duplicate.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#c0c0c0"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z"/></svg>

After

Width:  |  Height:  |  Size: 340 B

1
icons/error.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#c0c0c0"><path d="M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm-40-160h80v-240h-80v240Zm40 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>

After

Width:  |  Height:  |  Size: 537 B

1
icons/group.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm720 0v-120q0-44-24.5-84.5T666-434q51 6 96 20.5t84 35.5q36 20 55 44.5t19 53.5v120H760ZM360-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm400-160q0 66-47 113t-113 47q-11 0-28-2.5t-28-5.5q27-32 41.5-71t14.5-81q0-42-14.5-81T544-792q14-5 28-6.5t28-1.5q66 0 113 47t47 113ZM120-240h480v-32q0-11-5.5-20T580-306q-54-27-109-40.5T360-360q-56 0-111 13.5T140-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T440-640q0-33-23.5-56.5T360-720q-33 0-56.5 23.5T280-640q0 33 23.5 56.5T360-560Zm0 320Zm0-400Z"/></svg>

After

Width:  |  Height:  |  Size: 766 B

BIN
icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

18
icons/icons.yaml Normal file
View File

@@ -0,0 +1,18 @@
color: '#B89230' #Hex code of the color
icons:
addBook: add_book.svg
add_user: add_user.svg
backup: db_backup.svg
borrow: book.svg
duplicate: duplicate.svg
error: error.svg
loan_extend: calendar_event.svg
main: library.svg
multiuser: multiple_user.svg
newentry: library_add.svg
report: report.svg
settings: settings.svg
user: user.svg
warning: warning.svg
delete: delete.svg
restart: restart.svg

1
icons/library.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-library-big"><rect width="8" height="18" x="3" y="3" rx="1"/><path d="M7 3v18"/><path d="M20.4 18.9c.2.5-.1 1.1-.6 1.3l-1.9.7c-.5.2-1.1-.1-1.3-.6L11.1 5.1c-.2-.5.1-1.1.6-1.3l1.9-.7c.5-.2 1.1.1 1.3.6Z"/></svg>

After

Width:  |  Height:  |  Size: 410 B

1
icons/library_add.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#c0c0c0"><path d="M520-400h80v-120h120v-80H600v-120h-80v120H400v80h120v120ZM320-240q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"/></svg>

After

Width:  |  Height:  |  Size: 395 B

BIN
icons/library_shelf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

1
icons/multiple_user.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M0-240v-63q0-43 44-70t116-27q13 0 25 .5t23 2.5q-14 21-21 44t-7 48v65H0Zm240 0v-65q0-32 17.5-58.5T307-410q32-20 76.5-30t96.5-10q53 0 97.5 10t76.5 30q32 20 49 46.5t17 58.5v65H240Zm540 0v-65q0-26-6.5-49T754-397q11-2 22.5-2.5t23.5-.5q72 0 116 26.5t44 70.5v63H780Zm-455-80h311q-10-20-55.5-35T480-370q-55 0-100.5 15T325-320ZM160-440q-33 0-56.5-23.5T80-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T160-440Zm640 0q-33 0-56.5-23.5T720-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T800-440Zm-320-40q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm0-80q17 0 28.5-11.5T520-600q0-17-11.5-28.5T480-640q-17 0-28.5 11.5T440-600q0 17 11.5 28.5T480-560Zm1 240Zm-1-280Z"/></svg>

After

Width:  |  Height:  |  Size: 831 B

1
icons/report.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#c0c0c0"><path d="M320-600q17 0 28.5-11.5T360-640q0-17-11.5-28.5T320-680q-17 0-28.5 11.5T280-640q0 17 11.5 28.5T320-600Zm0 160q17 0 28.5-11.5T360-480q0-17-11.5-28.5T320-520q-17 0-28.5 11.5T280-480q0 17 11.5 28.5T320-440Zm0 160q17 0 28.5-11.5T360-320q0-17-11.5-28.5T320-360q-17 0-28.5 11.5T280-320q0 17 11.5 28.5T320-280ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h440l200 200v440q0 33-23.5 56.5T760-120H200Zm0-80h560v-400H600v-160H200v560Zm0-560v160-160 560-560Z"/></svg>

After

Width:  |  Height:  |  Size: 586 B

1
icons/restart.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M440-122q-121-15-200.5-105.5T160-440q0-66 26-126.5T260-672l57 57q-38 34-57.5 79T240-440q0 88 56 155.5T440-202v80Zm80 0v-80q87-16 143.5-83T720-440q0-100-70-170t-170-70h-3l44 44-56 56-140-140 140-140 56 56-44 44h3q134 0 227 93t93 227q0 121-79.5 211.5T520-122Z"/></svg>

After

Width:  |  Height:  |  Size: 382 B

1
icons/settings.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>

After

Width:  |  Height:  |  Size: 771 B

1
icons/user.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"/></svg>

After

Width:  |  Height:  |  Size: 751 B

1
icons/warning.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm-40-160h80v-240h-80v240Zm40 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>

After

Width:  |  Height:  |  Size: 537 B

View File

@@ -1,3 +1,5 @@
hello_world = lambda: "Hello, World!"
from src.ui.main_ui import launch
print(hello_world())
if __name__ == "__main__":
launch()

5
main_dev.py Normal file
View File

@@ -0,0 +1,5 @@
from src.ui.main_ui import launch
if __name__ == "__main__":
launch("--debug")

30
mkdocs.yml Normal file
View File

@@ -0,0 +1,30 @@
site_name: LibrarySystem
theme:
features:
- search.suggest
- search.highlight
name: material
icon:
admonition:
note: fontawesome/solid/note-sticky
abstract: fontawesome/solid/book
info: fontawesome/solid/circle-info
tip: fontawesome/solid/bullhorn
success: fontawesome/solid/check
question: fontawesome/solid/circle-question
warning: fontawesome/solid/triangle-exclamation
failure: fontawesome/solid/bomb
danger: fontawesome/solid/skull
bug: fontawesome/solid/robot
example: fontawesome/solid/flask
quote: fontawesome/solid/quote-left
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- tables
extra_css:
- stylesheets/extra.css
plugins:
- search

BIN
requirements.txt Normal file

Binary file not shown.

View File

@@ -1,2 +1,61 @@
import omegaconf
config = omegaconf.OmegaConf.load("config/settings.yaml")
import sys
from config import Config
__version__ = "0.1.0"
__author__ = "Alexander Kirchner"
__email__ = "alexander.kirchner@ph-freiburg.de"
__license__ = "MIT"
docport = 6543
config = Config("config/settings.yaml")
valid_args = ["--debug", "--log", "--no-backup", "--ic-logging", "--version", "-h","--no-documentation"]
args_description = {
"--debug": "Enable debug mode",
"--log": "Enable logging",
"--no-backup": "Disable database backup",
"--ic-logging": "Enable icecream logging (not available in production)",
"--version": "Show version",
"-h": "Show help message and exit",
"--no-documentation": "Disable documentation server and shortcut"
}
args = sys.argv[1:]
if any(arg not in valid_args for arg in args):
print("Invalid argument present")
print([arg for arg in args if arg not in valid_args])
sys.exit()
def help():
print("Ausleihsystem")
print("Ein Ausleihsystem für Handbibliotheken")
print("Version: {}".format(__version__))
print("Valide Argumente:")
print("args")
print("--------")
print("usage: main.py [-h] [--debug] [--log] [--no-backup] [--ic-logging] [--version] [--no-documentation]")
print("options:")
for arg in valid_args:
print(f"{arg} : {args_description[arg]}")
# based on the arguments, set the config values
if"-h" in args:
help()
sys.exit()
if "--debug" in args:
config.debug = True
if "--log" in args:
config.log_debug = True
if "--no-backup" in args:
config.no_backup = True
if "--ic-logging" in args:
config.ic_logging = True
if "--no-documentation" in args:
config.documentation = False
if "--version" in args:
print(__version__)
sys.exit()

View File

@@ -1,3 +1,4 @@
__help__ = "This package contains the logic of the application."
from .database import Database
from .catalogue import Catalogue
from .catalogue import Catalogue
from .backup import Backup

29
src/logic/backup.py Normal file
View File

@@ -0,0 +1,29 @@
import os
import sys
import shutil
from src import config
class Backup:
def __init__(self):
self.source_path = config.database.path + "/" + config.database.name
self.backup_path = config.database.backupLocation + "/" + config.database.name
self.backup = False
if not os.path.exists(config.database.backupLocation):
os.makedirs(config.database.backupLocation)
if config.database.do_backup == True:
self.checkpaths()
config.database.do_backup = self.backup
def checkpaths(self):
if os.path.exists(config.database.backupLocation):
self.backup = True
def createBackup(self):
if self.backup:
if os.path.exists(self.source_path):
if os.path.exists(self.backup_path):
os.remove(self.backup_path)
shutil.copy(self.source_path, self.backup_path)
return True
return False

View File

@@ -2,15 +2,28 @@ import requests
from bs4 import BeautifulSoup
from src import config
from src.schemas import Book
URL = "https://rds.ibs-bw.de/phfreiburg/opac/RDSIndex/Search?lookfor={}+&type=AllFields&limit=10&sort=py+desc%2C+title"
from src.utils import Log
URL = "https://rds.ibs-bw.de/phfreiburg/opac/RDSIndex/Search?type0%5B%5D=allfields&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=au&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ti&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ct&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=isn&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ta&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=co&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=py&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=pp&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=pu&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=si&lookfor0%5B%5D={}&join=AND&bool0%5B%5D=AND&type0%5B%5D=zr&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=cc&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND"
BASE = "https://rds.ibs-bw.de"
log = Log("Catalogue")
class Catalogue:
def __init__(self, timeout=5):
self.timeout = timeout
reachable = self.check_connection()
if reachable:
config.catalogue = True
else:
config.catalogue = False
def check_connection(self):
try:
response = requests.get("https://www.google.com", timeout=self.timeout)
if response.status_code == 200:
return True
except requests.exceptions.RequestException as e:
log.error(f"Could not connect to google.com: {e}")
def search_book(self, searchterm: str):
response = requests.get(URL.format(searchterm), timeout=self.timeout)
return response.text
@@ -29,6 +42,8 @@ class Catalogue:
return res
def get_book(self, searchterm: str):
log.info(f"Searching for term: {searchterm}")
links = self.get_book_links(searchterm)
for link in links:
result = self.search(link)

View File

@@ -1,72 +1,161 @@
import sqlite3 as sql
import os
import shutil
from src import config
from pathlib import Path
from src.schemas import USERS, MEDIA, LOANS, User, Book
from src.schemas import USERS, MEDIA, LOANS, User, Book, Loan
from src.utils import stringToDate, Log, debugMessage as dbg
from PyQt6 import QtCore
log = Log("Database")
FILE = config.database.name
class Database:
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.handle_folder_reachability(config.database.path, config.database.backupLocation)
else:
self.db_path = db_path
if not os.path.exists(config.database.path):
os.makedirs(config.database.path)
try:
os.makedirs(config.database.path)
except FileNotFoundError:
dbg(self.db_path)
if not os.path.exists(config.database.backupLocation):
os.makedirs(config.database.backupLocation)
#if main path does not exist, try to create it. if that fails, use the backuplocation
dbg(self.db_path)
self.checkDatabaseStatus()
def handle_folder_reachability(self, original_path, backup_path):
"""
Checks if the original folder is reachable. If not, creates a backup.
If the original folder becomes reachable again, restores the backup.
Args:
original_path (str): Path to the original folder.
backup_path (str): Path to the backup folder.
Returns:
str: Path to the current accessible folder.
"""
backup_file = os.path.join(backup_path, ".backup")
if not os.path.exists(original_path):
#original folder not reachable, use backup path and create .backup file
if not os.path.exists(backup_path):
os.makedirs(backup_path)
with open(backup_file, "w") as f:
f.write("")
# Create an empty backup file as a marker
return backup_path +"/" + FILE
else:
dbg("Original Path Exists, using this path")
# Original folder is reachable, check for backup
if os.path.exists(backup_file):
# Restore backup
shutil.rmtree(original_path) # Remove original folder to avoid conflicts
os.rename(backup_path, original_path)
# (backup_path, original_path)
#os.remove(backup_file)
#remove backup file from original path
os.remove(original_path + "/.backup")
os.makedirs(backup_path)
return original_path +"/" + FILE
def checkDatabasePath(self):
self.db_path = config.database.path + "/" + config.database.name
#if backup file in backup location, move database to main location, delete backup file
if os.path.exists(config.database.backupLocation + "/backup"):
if os.path.exists(self.db_path):
os.remove(self.db_path)
os.rename(f"{config.database.backupLocation}/{config.database.name}", self.db_path)
#remove backup file
os.remove(config.database.backupLocation + "/backup")
return self.db_path
else:
#keep using backup file
self.db_path = config.database.backupLocation + "/" + config.database.name
if not os.path.exists(config.database.path):
try:
os.makedirs(config.database.path)
except:
self.db_path = config.database.backupLocation + "/" + config.database.name
if not os.path.exists(config.database.backupLocation):
os.makedirs(config.database.backupLocation)
#create a backup file in the backup location
with open(f"{config.database.backupLocation}/backup.txt", "w") as f:
f.write("Backup File")
return self.db_path
def checkDatabaseStatus(self):
log.info("Checking Database Status")
if self.tableCheck() == []:
# self.logger.log_critical("Database does not exist, creating tables")
self.createDatabase()
# self.insertSubjects()
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 createDatabase(self):
print("Creating Database")
if not os.path.exists(config.database.path):
os.makedirs(config.database.path)
log.info("Creating Database")
#print("Creating Database")
if not os.path.exists(self.db_path):
os.makedirs(self.db_path)
conn = self.connect()
cursor = conn.cursor()
cursor.execute(USERS)
log.debug("Creating Users Table")
cursor.execute(MEDIA)
log.debug("Creating Media Table")
cursor.execute(LOANS)
log.debug("Creating Loans Table")
conn.commit()
self.close_connection(conn)
def getLoanCount(self):
query = "SELECT COUNT(*) FROM loans"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
result = cursor.fetchone()
self.close_connection(conn)
return result[0]
def tableCheck(self):
# check if database has tables
"""
'''
Get the contents of the
Returns:
Union[List[Tuple], None]: _description_
"""
Union[List[Tuple], None]: Returns a list of tuples containing the table names or None if no tables are present
'''
try:
with sql.connect(self.db_path) as conn:
cursor = conn.cursor()
@@ -79,7 +168,9 @@ class Database:
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
result = cursor.fetchall()
self.close_connection(conn)
return result
def checkUserExists(self, key, value) -> list[User] | bool:
query = f"SELECT * FROM users WHERE {key} like '%{value}%'"
conn = self.connect()
@@ -91,29 +182,120 @@ class Database:
users = cursor.fetchall()
for index, user in enumerate(users):
users[index] = User(id=user[0], username=user[1], email=user[2])
users[index] = User(userid=user[1], username=user[2], email=user[3], id=user[0])
self.close_connection(conn)
return users
def insertUser(self, username, userno, usermail):
log.debug(f"Inserting User {userno}, {username}, {usermail}")
conn = self.connect()
cursor = conn.cursor()
try:
cursor.execute(
f"INSERT INTO users (user_id,username, usermail) VALUES ('{userno}', '{username}', '{usermail}' )"
)
conn.commit()
except sql.IntegrityError:
return False
self.close_connection(conn)
return True
def getUserBy(self, key, value) -> User:
conn = self.connect()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE {key} = '{value}'")
result = cursor.fetchone()
self.close_connection(conn)
user = User(userid=result[1], username=result[2], email=result[3], id=result[0])
return user
def getUser(self, user_id) -> User:
conn = self.connect()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users")
result = cursor.fetchall()
self.close_connection(conn)
if len(str(user_id)) == 1:
for res in result:
if res[0] == user_id:
user = User(userid=res[1], username=res[2], email=res[3], id=res[0])
dbg(f"Returning User {user}")
log.info(f"Returning User {user}")
return user
else:
for res in result:
if res[1] == user_id:
user = User(userid=res[1], username=res[2], email=res[3], id=res[0])
dbg(f"Returning User {user}")
log.info(f"Returning User {user}")
return user
raise ValueError(f"User {user_id} not found")
#return User(userid="gelöscht", username="gelöscht", email="gelöscht", id="gelöscht")
# user = User(userid=result[1], username=result[2], email=result[3],id = result[0])
# return user
def getUserId(self, username) -> User:
conn = self.connect()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")
result = cursor.fetchone()
self.close_connection(conn)
user = User(userid=result[1], username=result[2], email=result[3],id = result[0])
log.info(f"Returning User {user}")
return user
def updateUser(self, username, user_id, usermail):
log.debug(f"Updating User {userno}, {username}, {usermail}")
conn = self.connect()
cursor = conn.cursor()
cursor.execute(
f"INSERT INTO users (id,username, usermail) VALUES ('{userno}', '{username}', '{usermail}' )"
f"UPDATE users SET username = '{username}', usermail = '{usermail}' WHERE user_id = '{user_id}'"
)
conn.commit()
self.close_connection(conn)
def getUser(self, userid) -> User:
def setUserActiveDate(self, userid,date):
query = f"UPDATE users SET lastActive = '{date}' WHERE user_id = '{userid}'"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE id = '{userid}'")
result = cursor.fetchone()
cursor.execute(query)
conn.commit()
dbg(f"Setting User {userid} to active on {date}")
def renameInactiveUsers(self):
lastYear = QtCore.QDate.currentDate().addDays(int(f"-{config.delete_inactive_user_duration}")).toString("yyyy-MM-dd")
query = f"SELECT id FROM users WHERE lastActive < '{lastYear}'"
conn = self.connect()
cursor = conn.cursor()
result = cursor.execute(query).fetchall()
self.close_connection(conn)
user = User(id=result[0], username=result[1], email=result[2])
return user
if len(result) == 0:
log.info(f"Deleting {len(result)} inactive users")
for user in result:
hasLoans = self.hasLoans(user[0])
if not hasLoans:
self.deleteUser(user)
def hasLoans(self, userid)->bool:
query = f"SELECT * FROM loans WHERE user_id = '{userid}' AND returned = 0"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
result = cursor.fetchall()
self.close_connection(conn)
return False if len(result) == 0 else True
def deleteUser(self, userid):
log.debug(f"Deleting User {userid}")
conn = self.connect()
cursor = conn.cursor()
cursor.execute(f"UPDATE users SET username='gelöscht', usermail = 'gelöscht', user_id='gelöscht' WHERE id = '{userid}'")
conn.commit()
self.close_connection(conn)
def getActiveLoans(self, userid):
dbg("id", str(userid))
conn = self.connect()
cursor = conn.cursor()
try:
@@ -124,9 +306,37 @@ class Database:
except sql.OperationalError:
result = []
self.close_connection(conn)
log.info(f"Returning Active Loans {result}")
return str(len(result))
def getMediaList(self):
query = "SELECT signature FROM media"
result = self.query(query)
return [res[0] for res in result]
def getAllLoans(self):
loan_data = []
query = "SELECT * FROM loans"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
loans = cursor.fetchall()
for loan in loans:
l = Loan(
loan[0],
loan[1],
loan[2],
stringToDate(loan[3]),
stringToDate(loan[4]),
loan[5],
stringToDate(loan[6]),
self.getMedia(loan[2]),
user_name=self.getUser(loan[1]).username,
)
loan_data.append(l)
return loan_data
def insertLoan(self, userid, mediaid, loandate, duedate):
log.debug(f"Inserting Loan {userid}, {mediaid}, {loandate}, {duedate}")
query = f"INSERT INTO loans (user_id, media_id, loan_date, return_date) Values ('{userid}', '{mediaid}', '{loandate}', '{duedate}')"
conn = self.connect()
cursor = conn.cursor()
@@ -135,15 +345,51 @@ class Database:
self.close_connection(conn)
def insertMedia(self, media):
query = f"INSERT OR IGNORE INTO media (ppn, title, signature, isbn) VALUES ('{media.ppn}', '{media.title}', '{media.signature}', '{media.isbn}')" # , '{media.link}'
log.debug(f"Inserting Media {media}")
query = f"INSERT OR IGNORE INTO media (ppn, title, signature, isbn,link) VALUES ('{media.ppn}', '{media.title}', '{media.signature}', '{media.isbn}','{media.link}')" # , '{media.link}'
log.info(f"Query: |{query}|")
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
conn.commit()
self.close_connection(conn)
return cursor.lastrowid
def getLoansBy(self, field, value):
# query all loans, sort by date descending and return
pass
def getMediaSimilarSignatureByID(self, media_id) -> list[Book]:
log.info(f"Getting Media Similar to {media_id}")
query = f"SELECT * FROM media WHERE id = '{media_id}'"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
result = cursor.fetchone()
signature = result[1]
#print(signature)
query = f"SELECT * FROM media WHERE signature LIKE '%{signature}%'"
cursor.execute(query)
result = cursor.fetchall()
self.close_connection(conn)
data = []
for res in result:
data.append(
Book(
signature=res[1],
isbn=res[2],
ppn=res[3],
title=res[4],
database_id=res[0],
)
)
log.debug(f"Returning Similar Media {data}")
return data
def getMedia(self, media_id):
# log.info(f"Getting Media {media_id}")
query = f"SELECT * FROM media WHERE id = '{media_id}'"
conn = self.connect()
cursor = conn.cursor()
@@ -159,7 +405,27 @@ class Database:
)
return res
def getAllMedia(self, user_id):
# get all books that have the user id in the loans table
query = f"SELECT * FROM loans WHERE user_id = '{user_id}'"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
result = cursor.fetchall()
self.close_connection(conn)
books = []
for res in result:
book = self.getMedia(res[2])
book.loan_from = res[3]
book.loan_to = res[4]
book.returned = res[5]
book.returned_date = res[6]
books.append(book)
log.info(f"Returning All Media entries ({len(books)}) for user {user_id}")
return books
def checkMediaExists(self, media):
log.info(f"Checking Media {media}")
conn = self.connect()
cursor = conn.cursor()
query = f"SELECT id, signature FROM media WHERE ppn = '{media.ppn}' OR title = '{media.title}' OR isbn = '{media.isbn}' OR signature = '{media.signature}'"
@@ -174,6 +440,7 @@ class Database:
return False
def checkLoanState(self, book_id):
log.info(f"Checking Loan State for {book_id}")
query = f"SELECT * FROM loans WHERE media_id = '{book_id}' AND returned = 0"
conn = self.connect()
cursor = conn.cursor()
@@ -182,8 +449,9 @@ class Database:
self.close_connection(conn)
return result
def returnMedia(self, media_id):
query = f"UPDATE loans SET returned = 1 WHERE media_id = '{media_id}'"
def returnMedia(self, media_id, returndate):
log.info(f"Returning Media {media_id}")
query = f"UPDATE loans SET returned = 1, returned_date = '{returndate}' WHERE media_id = '{media_id}' AND returned = 0"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
@@ -214,11 +482,23 @@ class Database:
#
def selectClosestReturnDate(self, user_id):
log.info(f"Selecting Closest Return Date for {user_id}")
query = f"SELECT return_date FROM loans WHERE user_id = '{user_id}' AND returned = 0 ORDER BY return_date ASC LIMIT 1"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
result = cursor.fetchone()
dbg("Result", response=result)
self.close_connection(conn)
if result is not None:
return result[0]
def extendLoanDuration(self, signature, newDate):
log.info(f"Extending Loan Duration for {signature} to {newDate}")
book_id = self.checkMediaExists(Book(signature=signature))
query = f"UPDATE loans SET return_date = '{newDate}' WHERE media_id = '{book_id[0]}' AND returned = 0"
conn = self.connect()
cursor = conn.cursor()
cursor.execute(query)
conn.commit()
self.close_connection(conn)

View File

@@ -0,0 +1,9 @@
from PyQt6.QtCore import QThread, pyqtSignal
from src.utils import launch_documentation
class DocumentationThread(QThread):
def __init__(self):
super().__init__()
def run(self):
launch_documentation()

View File

@@ -1,3 +1,4 @@
from .database import LOANS, MEDIA, USERS
from .user import User
from .book import Book
from .book import Book
from .loan import Loan

View File

@@ -3,10 +3,14 @@ from dataclasses import dataclass
@dataclass
class Book:
title: str | None = None
ppn: int | None = None
signature: str | None = None
isbn: str | None = None
link: str | None = None
database_id: int | None = None
link: str | None = None
title: str = ""
ppn: int = ""
signature: str = ""
isbn: str = ""
link: str = ""
database_id: int = ""
link: str = ""
loan_from: str = ""
loan_to: str = ""
returned: int = ""
returned_date: str = ""

View File

@@ -1,15 +1,18 @@
USERS = """CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
username TEXT NOT NULL,
usermail TEXT NOT NULL);
usermail TEXT NOT NULL,
lastActive TEXT);
""" # id == matrikelnr,
#matrikelnr TEXT NOT NULL,
MEDIA = """CREATE TABLE IF NOT EXISTS media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
signature TEXT NOT NULL,
isbn TEXT NOT NULL,
ppn TEXT NOT NULL,
isbn TEXT,
ppn TEXT,
title TEXT NOT NULL,
link TEXT NOT NULL,);
link TEXT);
"""
LOANS = """CREATE TABLE IF NOT EXISTS loans (
@@ -19,6 +22,7 @@ media_id INTEGER NOT NULL,
loan_date TEXT NOT NULL,
return_date TEXT NOT NULL,
returned INTEGER DEFAULT 0,
returned_date TEXT,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (media_id) REFERENCES media(id));
"""

15
src/schemas/loan.py Normal file
View File

@@ -0,0 +1,15 @@
from .book import Book
from dataclasses import dataclass
@dataclass
class Loan:
id: int
user_id: int
media_id: int
loan_date: str
return_date: str
returned: int
returned_date: str
book: Book
user_name: str

View File

@@ -1,11 +1,12 @@
from dataclasses import dataclass
from typing import Any
@dataclass
class User:
username: str
id: int
userid: Any
email: str
id : int = None
def __repr__(self):
return f"Name: {self.username}\nMatrikelnr.: {self.id}\neMail: {self.email}"
return f"Name: {self.username}\nMatrikelnr.: {self.userid}\neMail: {self.email}"

View File

@@ -1,13 +1,15 @@
from .sources.Ui_dialog_createUser import Ui_Dialog
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtGui import QRegularExpressionValidator
from src.logic import Database
from src.schemas import User
from src.utils import Icon, Log
class CreateUser(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, fieldname, data):
super(CreateUser, self).__init__()
self.setupUi(self)
self.setWindowTitle("Benutzer erstellen")
self.setWindowIcon(Icon("user").icon)
# disable buttonbox save
self.db = Database()
self.buttonBox.button(
@@ -25,12 +27,22 @@ class CreateUser(QtWidgets.QDialog, Ui_Dialog):
QtWidgets.QDialogButtonBox.StandardButton.Save
).clicked.connect(self.saveUser)
self.userid = None
self.userno.setMaxLength(20)
self.user_mail.setValidator(
QRegularExpressionValidator(
QtCore.QRegularExpression(
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}"
)
)
)
self.userno.textChanged.connect(
lambda: self.validateInputUserno(self.userno.text(), "int")
)
def checkFields(self):
if (
self.username.text() != ""
and self.userno.text() != ""
and self.user_mail.text() != ""
self.username.hasAcceptableInput()
and self.userno.hasAcceptableInput()
and self.user_mail.hasAcceptableInput()
):
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Save
@@ -44,5 +56,33 @@ class CreateUser(QtWidgets.QDialog, Ui_Dialog):
username = self.username.text()
userno = int(self.userno.text())
usermail = self.user_mail.text()
self.db.insertUser(username, userno, usermail)
self.userid = userno
if self.db.insertUser(username, userno, usermail):
self.userid = userno
else:
self.setStatusTipMessage(
"Benutzer konnte nicht erstellt werden, bitte überprüfen Sie die Eingaben"
)
def setStatusTipMessage(self, message):
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Information")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Information)
dialog.setWindowIcon(Icon("error").overwriteColor("#EA3323"))
dialog.setText(message)
dialog.exec()
def validateInputUserno(self, value, type):
lastchar = value[-1] if value else ""
# if lastchar is not of the type, remove it
if type == "int":
if not lastchar.isdigit():
self.userno.setText(value[:-1])
def validateInputMail(self, value):
pass
def launch():
app = QtWidgets.QApplication([])
window = CreateUser("id", "123456")
window.exec()

32
src/ui/extendLoan.py Normal file
View File

@@ -0,0 +1,32 @@
from .sources.Ui_dialog_extendLoanDuration import Ui_Dialog
from PyQt6 import QtWidgets, QtCore
from src.utils import Icon
class ExtendLoan(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, user, media):
super(ExtendLoan, self).__init__()
self.setupUi(self)
self.setWindowTitle("Leihfrist verlängern")
self.setWindowIcon(Icon("loan_extend").icon)
self.user = user
self.media = media
self.currentDate = QtCore.QDate.currentDate().addDays(1)
self.extendDate = ""
self.extenduntil.setMinimumDate(self.currentDate)
self.show()
self.buttonBox.accepted.connect(self.extendLoan)
def extendLoan(self):
#print("Extend Loan")
selectedDate = self.extenduntil.selectedDate()
#print(selectedDate)
self.extendDate = selectedDate
self.close()
def launch(user, media):
import sys
app = QtWidgets.QApplication(sys.argv)
window = ExtendLoan(user, media)
sys.exit(app.exec())

138
src/ui/loans.py Normal file
View File

@@ -0,0 +1,138 @@
from .sources.Ui_main_Loans import Ui_MainWindow
from PyQt6 import QtCore, QtGui, QtWidgets
from .user import UserUI
from src.logic import Database
from src.utils import stringToDate, Icon
from src.utils import debugMessage as dbg
from icecream import ic
TABLETOFIELDTRANSLATE = {
"Titel": "book.title",
"Signatur": "book.signature",
"Nutzer": "user_name",
}
class LoanWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self):
super(LoanWindow, self).__init__()
self.setupUi(self)
self.setWindowTitle("Ausleihhistorie")
self.setWindowIcon(Icon("borrow").icon)
self.loanTable.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeMode.Stretch
)
self.db = Database()
self.loans = []
self.loadLoans()
# lineedits
self.searchbar.textChanged.connect(self.limitResults)
self.searchbar.returnPressed.connect(self.selfpass)
# radio buttons
self.radio_all.clicked.connect(self.filterResults)
self.radio_current.clicked.connect(self.filterResults)
self.radio_overdue.clicked.connect(self.filterResults)
# table
self.loanTable.doubleClicked.connect(self.showUser)
self.show()
def selfpass(self):
pass
def insertRow(self, data):
dbg(contents=data)
retdate = ""
if data.returned_date != "":
retdate = stringToDate(data.returned_date).toString("dd.MM.yyyy")
ic(retdate)
self.loanTable.insertRow(0)
self.loanTable.setItem(0, 0, QtWidgets.QTableWidgetItem(data.book.isbn))
self.loanTable.setItem(0, 1, QtWidgets.QTableWidgetItem(data.book.signature))
self.loanTable.setItem(0, 2, QtWidgets.QTableWidgetItem(data.book.title))
self.loanTable.setItem(
0,
3,
QtWidgets.QTableWidgetItem(str(self.db.getUser(data.user_id).username)),
)
self.loanTable.setItem(
0,
4,
QtWidgets.QTableWidgetItem(
stringToDate(data.loan_date).toString("dd.MM.yyyy")
),
)
self.loanTable.setItem(
0,
5,
QtWidgets.QTableWidgetItem(
stringToDate(data.return_date).toString("dd.MM.yyyy")
),
)
self.loanTable.setItem(0, 6, QtWidgets.QTableWidgetItem(retdate))
def loadLoans(self):
loans = self.db.getAllLoans()
for loan in loans:
self.insertRow(loan)
self.loans = loans
def filterResults(self):
mode = (
"all"
if self.radio_all.isChecked()
else "current"
if self.radio_current.isChecked()
else "overdue"
)
self.loanTable.setRowCount(0)
today = QtCore.QDate.currentDate()
for loan in self.loans:
# convert string to Qdate
date = loan.return_date
qdate = stringToDate(date)
# current mode
if mode == "current":
if loan.returned == 1:
continue
elif mode == "overdue":
if loan.returned_date != "":
continue
else:
if qdate > today:
continue
self.insertRow(loan)
def limitResults(self):
limiter = self.searchbar.text().lower()
limiter = str(limiter)
searchfield = self.searchFields.currentText()
searchfield = TABLETOFIELDTRANSLATE[searchfield]
dbg(limiter=limiter, search=searchfield)
self.loanTable.setRowCount(0)
for loan in self.loans:
fielddata = eval(f"loan.{searchfield}")
if isinstance(fielddata, str):
fielddata = fielddata.lower()
if limiter in fielddata:
self.insertRow(loan)
def showUser(self):
row = self.loanTable.currentRow()
user_name = self.loanTable.item(row, 3).text()
user = self.db.getUserId(user_name)
self.user = UserUI(user.username, user.id, user.email)
self.user.show()
def launch():
import sys
app = QtWidgets.QApplication(sys.argv)
main_ui = LoanWindow()
# atexit.register(exit_handler)
sys.exit(app.exec())

View File

@@ -1,224 +1,498 @@
import ast
import sys
import atexit
import datetime
import webbrowser
from src import config, __email__, docport
from src.logic import Database, Catalogue, Backup
from src.utils import stringToDate, Icon, Log
from src.utils import debugMessage as dbg
from src.utils.createReport import generate_report
from src.schemas import Book
from .sources.Ui_main_UserInterface import Ui_MainWindow
from .user import UserUI
from .createUser import CreateUser
from .multiUserInfo import MultiUserFound
from .newentry import NewEntry
from src import config
from src.logic import Database, Catalogue
from src.utils.stringtodate import stringToDate
from src.schemas import User, Book
from PyQt6 import QtCore, QtGui, QtWidgets
import sys
from .settings import Settings
from .newBook import NewBook
from .loans import LoanWindow
from .reportUi import ReportUi
from PyQt6 import QtCore, QtWidgets
from omegaconf import OmegaConf
from src.logic.documentation_thread import DocumentationThread
backup = Backup()
cat = Catalogue()
log = Log("main")
dbg(backup=config.database.do_backup, catalogue=config.catalogue)
def getShortcut(shortcuts, name):
shortcut = [cut for cut in shortcuts if cut["name"] == name][0]
return shortcut["current"]
class MainUI(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self):
super(MainUI, self).__init__()
self.setupUi(self)
self.db = Database()
self.currentDate = QtCore.QDate.currentDate()
self.setWindowTitle(f"Handbibliotheksleihsystem {config.institution_name}")
self.setWindowIcon(Icon("main").icon)
self.label_7.hide()
self.nextReturnDate.hide()
self.activeUser = None
# add default loan duration to current date
loanDate = self.currentDate.addDays(config.default_loan_duration)
self.duedate.setDate(loanDate)
# hotkeys
self.actionRueckgabemodus.triggered.connect(self.changeMode)
self.actionNutzer.triggered.connect(self.showUser)
#Buttons
self.actionEinstellungen.triggered.connect(self.showSettings)
self.actionAusleihhistorie.triggered.connect(self.showLoanHistory)
self.actionBericht_erstellen.triggered.connect(self.generateReport)
self.actionDokumentation_ffnen.triggered.connect(self.openDocumentation)
self.actionBeenden.triggered.connect(self.shutdown)
def __mail():
webbrowser.open(f"mailto:{__email__}")
self.actionProblem_melden.triggered.connect(__mail)
#if close button is pressed call shutdown
self.closeEvent = self.shutdown
# Buttons
self.btn_show_lentmedia.clicked.connect(self.showUser)
self.btn_createNewUser.clicked.connect(self.createUser)
self.btn_createNewUser.setText("")
self.btn_createNewUser.setIcon(Icon("add_user").overwriteColor("#1E90FF"))
self.mode.clicked.connect(self.changeMode)
# LineEdits
self.input_userno.returnPressed.connect(
lambda: self.checkUser("id", self.input_userno.text())
lambda: self.checkUser("user_id", self.input_userno.text())
)
self.input_username.returnPressed.connect(
lambda: self.checkUser("username", self.input_username.text())
)
self.input_file_ident.returnPressed.connect(self.addMedia)
self.input_userno.setValidator(QtGui.QIntValidator())
self.input_file_ident.returnPressed.connect(self.handleLineInput)
self.input_userno.setMaxLength(40)
self.input_userno.textChanged.connect(
lambda: self.validateInput(self.input_userno.text(), "int")
)
self.input_username.setEnabled(False)
self.input_userno.setEnabled(False)
self.duedate.setEnabled(False)
# TableWidget
# set header size to be width/number of columns
self.mediaOverview.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeMode.Stretch
)
# self.input_userno.
self.input_file_ident.setFocus()
self.assignShortcuts()
# variables
self.db = Database()
self.currentDate = QtCore.QDate.currentDate()
loanDate = self.currentDate.addDays(config.loan_duration)
self.activeUser = None
self.activeState = "Rückgabe"
self.docu = DocumentationThread()
if config.documentation:
self.docu.start()
self.duedate.setDate(loanDate)
# functions
self.activateReturnMode()
if backup.backup:
log.info("Backup enabled")
else:
log.warning("Backup disabled")
self.show()
def shutdown(self, *args):
#kill documentation thread
log.info("Shutting down")
if config.documentation:
self.docu.terminate()
sys.exit()
def assignShortcuts(self):
shortcuts = config.shortcuts
shortcuts = OmegaConf.to_container(shortcuts)
#convert to dictconfig
self.actionDokumentation_ffnen.setShortcut(getShortcut(shortcuts, "Hilfe"))
self.actionAusleihhistorie.setShortcut(getShortcut(shortcuts, "Ausleihhistorie"))
self.actionBericht_erstellen.setShortcut(getShortcut(shortcuts, "Bericht_erstellen"))
self.actionNutzer.setShortcut(getShortcut(shortcuts, "Nutzer"))
self.actionRueckgabemodus.setShortcut(getShortcut(shortcuts, "Rueckgabemodus"))
def generateReport(self):
log.info("Generating Report")
report = ReportUi()
report.exec()
def showLoanHistory(self):
log.info("Showing Loan History")
self.loan = LoanWindow()
self.loan.show()
def validateInput(self, value, type):
lastchar = value[-1] if value else ""
# if lastchar is not of the type, remove it
if type == "int":
if not lastchar.isdigit():
self.input_userno.setText(value[:-1])
def showSettings(self):
log.info("Showing Settings")
settings = Settings()
settings.exec()
result = settings.result()
print(settings.settingschanged, settings.restart_required)
if result == 1:
#dialog to ask if program should be restarted
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Einstellungen geändert")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Information)
dialog.setWindowIcon(Icon("settings").icon)
dialog.setText("Einstellungen wurden geändert\nProgramm neu starten?")
dialog.setStandardButtons(
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No
)
dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.No)
#translate buttons
yes = dialog.button(QtWidgets.QMessageBox.StandardButton.Yes)
yes.setText("Ja")
no = dialog.button(QtWidgets.QMessageBox.StandardButton.No)
no.setText("Nein")
dialog.exec()
result = dialog.result()
if result == QtWidgets.QMessageBox.StandardButton.Yes:
self.restart()
# reload settings
#print(config)
def openDocumentation(self):
log.info("Opening Documentation")
if config.documentation:
webbrowser.open("http://localhost:{}/".format(docport))
else:
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Dokumentation nicht verfügbar")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
dialog.setWindowIcon(Icon("warning").icon)
dialog.setText("Dokumentation nicht verfügbar")
dialog.exec()
def restart(self):
#log restart
dbg("Restarting Program")
import os
python_executable = sys.executable
args = sys.argv[:]
args.insert(0, sys.executable)
os.execvp(python_executable, args)
def changeMode(self):
current = self.mode.text()
if current == "Ausleihe":
self.mode.setText("Rückgabe")
self.input_username.clear()
self.input_userno.clear()
self.userdata.clear()
self.btn_show_lentmedia.setText("")
self.label_7.hide()
self.nextReturnDate.hide()
if current == "Rückgabe":
self.input_username.clear()
self.input_userno.clear()
self.userdata.clear()
self.mediaOverview.clearContents()
self.btn_show_lentmedia.setText("")
self.input_file_ident.clear()
self.nextReturnDate.hide()
self.label_7.hide()
log.info("Changing Mode")
dbg(f"Current mode: {self.activeState}")
self.input_username.clear()
stayReturn = False
if config.advanced_refresh and self.userdata.toPlainText() != "":
stayReturn = True
self.userdata.clear()
self.input_userno.clear()
self.btn_show_lentmedia.setText("")
self.input_file_ident.clear()
self.label_7.hide()
self.nextReturnDate.hide()
self.mediaOverview.setRowCount(0)
self.activeUser = None #! remove if last user should be kept
if self.activeState == "Rückgabe":
if stayReturn:
self.activateReturnMode()
else: self.activateLoanMode()
else:
self.activateReturnMode()
def activateLoanMode(self):
dbg("Activating Loan Mode")
self.input_username.setEnabled(True)
self.input_userno.setEnabled(True)
self.duedate.setEnabled(True)
self.input_username.setPlaceholderText("")
self.input_userno.setPlaceholderText("")
self.input_userno.setFocus()
# set mode background color to blue with rounded edges
self.mode.setStyleSheet("background-color: #1E90FF")
self.mode.setText("Ausleihe")
self.activeState = "Ausleihe"
if self.input_userno.text() == "" or self.input_username.text() == "":
self.input_file_ident.setEnabled(False)
self.input_file_ident.setPlaceholderText("Bitte zuerst Nutzerdaten eingeben")
else:
self.input_file_ident.setEnabled(True)
def activateReturnMode(self):
dbg("Activating Return Mode")
self.input_username.setEnabled(False)
self.input_userno.setEnabled(False)
# set mode background color to orange
self.mode.setStyleSheet("background-color: #FFA500")
self.duedate.setEnabled(False)
self.activeState = "Rückgabe"
self.mode.setText("Rückgabe")
self.input_file_ident.setEnabled(True)
self.input_file_ident.setPlaceholderText("Buchidentifikation eingeben")
self.input_username.setPlaceholderText("Bitte erst in den Ausleihmodus wechseln")
self.input_userno.setPlaceholderText("Bitte erst in den Ausleihmodus wechseln")
def showUser(self):
log.info(f"Showing User {self.activeUser}")
if self.activeUser is None:
# create warning dialog
log.info("Showing no user selected warning")
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Kein Nutzer ausgewählt")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
dialog.setWindowIcon(Icon("warning").overwriteColor("#EA3323"))
dialog.setText("Kein Nutzer ausgewählt")
dialog.exec()
return
self.user_ui = UserUI(
self.activeUser.username, self.activeUser.id, self.activeUser.email
self.activeUser
)
# self.user_ui.setFields("John Doe", "123456789", "test@mail.com")
self.user_ui.show()
def setUserData(self):
self.input_username.setText(self.activeUser.username)
self.input_userno.setText(str(self.activeUser.id))
log.info("Setting User Data")
self.input_username.setText(str(self.activeUser.username))
self.input_userno.setText(str(self.activeUser.userid))
self.userdata.setText(self.activeUser.__repr__())
today = QtCore.QDate.currentDate().toString("yyyy-MM-dd")
self.db.setUserActiveDate(self.activeUser.userid, today)
def createUser(self):
log.info("Creating User")
user = CreateUser(fieldname="id", data="")
user.exec()
userid = user.userid
print(userid)
if userid:
log.info(f"User created {userid}")
data = self.db.getUserBy("user_id", userid)
self.activeUser = data
# set user to active user
self.setUserData()
self.activateLoanMode()
self.input_file_ident.setPlaceholderText("Buchidentifikation eingeben")
self.input_file_ident.setFocus()
return
def checkUser(self, fieldname, data):
print("Checking User", fieldname, data)
log.info(f"Checking User {fieldname}, {data}")
#print("Checking User", fieldname, data)
# set fieldname as key and data as variable
user = self.db.checkUserExists(fieldname, data)
if not user:
user = CreateUser(fieldname, data)
user.exec()
userid = user.userid
if userid:
data = self.db.getUser(userid)
self.activeUser = data
warning = QtWidgets.QMessageBox()
warning.setWindowTitle("Nutzer nicht gefunden")
warning.setIcon(QtWidgets.QMessageBox.Icon.Warning)
warning.setWindowIcon(Icon("warning").overwriteColor("#EA3323"))
warning.setText("Nutzer nicht gefunden, bitte erst anlegen")
warning.exec()
self.input_username.clear()
self.input_userno.clear()
return
else:
if len(user) > 1:
log.info("Multiple Users found")
multi = MultiUserFound(user)
multi.exec()
self.activeUser = multi.userdata
else:
#print("User found", user[0])
self.activeUser = user[0]
if self.activeUser is not None:
log.info(f"User found {self.activeUser}")
#print("User found", self.activeUser)
self.setUserData()
self.input_file_ident.setFocus()
self.mode.setText("Ausleihe")
self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id))
#print(self.activeUser.__dict__)
loans = self.db.getActiveLoans(self.activeUser.id)
dbg(loans=loans)
self.btn_show_lentmedia.setText(loans)
retdate = self.db.selectClosestReturnDate(self.activeUser.id)
date = stringToDate(retdate)
self.nextReturnDate.setText(date)
self.nextReturnDate.show()
self.label_7.show()
if retdate:
date = stringToDate(retdate).toString("dd.MM.yyyy")
self.nextReturnDate.setText(date)
self.nextReturnDate.show()
self.label_7.show()
self.input_file_ident.setEnabled(True)
self.input_file_ident.setPlaceholderText("Buchidentifikation eingeben")
self.input_file_ident.setFocus()
def moveToLine(self, line):
log.debug("Moving to Line", line)
line.setFocus()
def handleLineInput(self):
value = self.input_file_ident.text().strip()
log.debug(f"Handling Line Input {value}")
if self.mode.text() == "Rückgabe":
if value in self.db.getMediaList():
self.returnMedia(value)
else:
# create warning dialog
log.info("Invalid Input")
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Ungültige Eingabe")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
dialog.setWindowIcon(Icon("warning").overwriteColor("#EA3323"))
dialog.setText("Eingabe ist nicht in der Datenbank\nBitte prüfen und erneut eingeben")
dialog.exec()
self.input_file_ident.setFocus()
self.input_file_ident.clear()
return
else:
if not " " in value:
# create warning dialog
log.info("Invalid Input")
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Ungültige Eingabe")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
dialog.setWindowIcon(Icon("warning").overwriteColor("#EA3323"))
dialog.setText(
"Die Eingabe enthält kein Leerzeichen\nBitte prüfen und erneut eingeben"
)
dialog.exec()
return
self.mediaAdd(value)
self.input_file_ident.setFocus()
def mediaAdd(self, identifier):
print("Adding Media", identifier)
user_id = self.activeUser.id
cat = Catalogue()
media = cat.get_book(identifier)
dbg("Adding Media", identifier = identifier)
self.input_file_ident.clear()
self.input_file_ident.setEnabled(False)
user_id = self.activeUser.id
media = Book(signature=identifier)
book_id = self.db.checkMediaExists(media)
print(book_id)
if book_id:
if len(book_id) > 1:
print("Multiple Books found")
dbg(f"Book ID: {book_id}, User ID: {user_id}", media=media)
if not book_id:
dbg("Book not found, searching catalogue")
if config.catalogue == True:
media = cat.get_book(identifier)
if not media:
self.setStatusTipMessage("Buch nicht gefunden")
self.input_file_ident.setEnabled(True)
return
book_id = self.db.insertMedia(media)
self.loanMedia(user_id, [book_id])
else:
newbook = NewBook()
newbook.exec()
if newbook.result() == 1:
media = newbook.book
book_id = self.db.insertMedia(media)
elif book_id:
if isinstance(book_id, list) and len(book_id) > 1:
#print("Multiple Books found")
# TODO: implement book selection dialog
return
else:
if isinstance(book_id, int):
book_id = [book_id]
# check if book is already loaned
loaned = self.db.checkLoanState(book_id[0])
if loaned:
print("Book already loaned")
self.setStatusTip("Book already loaned")
#print("Book already loaned")
self.setStatusTipMessage("Buch bereits entliehen")
# dialog with yes no to create new entry
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Buch bereits entliehen")
dialog.setWindowIcon(Icon("duplicate").icon)
dialog.setText(
"Buch bereits entliehen, soll ein neues hinzugefügt werden?"
)
dialog.setStandardButtons(
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No
)
dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.No)
dialog.exec()
result = dialog.result()
if result == QtWidgets.QMessageBox.StandardButton.No:
self.input_file_ident.setEnabled(True)
return
newentry = NewEntry([book_id[0]])
newentry.exec()
if newentry.result() == 1: # only create dialog if new entry was created
self.setStatusTipMessage("Neues Exemplar hinzugefügt")
#print(created_ids)
self.input_file_ident.setEnabled(True)
newentries = newentry.newIds
if newentries:
for entry in newentries:
book = self.db.getMedia(entry)
self.loanMedia(user_id, [entry], book)
dbg("inserted duplicated book into database")
return
else:
print("Book not loaned, loaning now")
self.db.insertLoan(
user_id,
book_id[0],
self.currentDate.toString(),
self.duedate.date().toString(),
)
self.mediaOverview.insertRow(0)
self.mediaOverview.setItem(
0, 0, QtWidgets.QTableWidgetItem(media.isbn)
)
self.mediaOverview.setItem(
0, 1, QtWidgets.QTableWidgetItem(media.title)
)
self.mediaOverview.setItem(
0, 2, QtWidgets.QTableWidgetItem("Entliehen")
)
#print("Book not loaned, loaning now")
self.loanMedia(user_id, book_id)
# print(media)
# if media:
# print(book_id, type(book_id))
# loaned = self.db.checkLoanState(book_id)
# print(loaned)
# if self.db.checkMediaExists(media):
# print("Book already exists", book_id)
# else:
# book_id = self.db.insertMedia(media)
# print(book_id)
# self.db.insertLoan(
# self.activeUser.id,
# book_id,
# self.currentDate.toString(),
# self.duedate.date().toString(),
# )
# self.mediaOverview.insertRow(0)
# self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(media.isbn))
# self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(media.title))
# self.mediaOverview.setItem(0, 2, QtWidgets.QTableWidgetItem("Entliehen"))
# # add media to database
# # check if book present in database
# print(self.db.getActiveLoans(self.activeUser.id))
# self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id))
def callShortcut(self, shortcut):
print("Calling Shortcut", shortcut)
# check if actions have shortcut key and call them
sysem_shortcuts = None
print(sysem_shortcuts)
def addMedia(self):
mode = self.mode.text()
value = self.input_file_ident.text()
# if vaule is string, call shortcut, else mediaAdd
if mode == "Rückgabe":
self.returnMedia(value)
else:
self.lendMedia(value)
def loanMedia(self, user_id, book_id):
self.db.insertLoan(
user_id,
book_id[0],
self.currentDate.toString("yyyy-MM-dd"),
self.duedate.date().toString("yyyy-MM-dd"),
)
media = self.db.getMedia(book_id[0])
#print(media)
self.mediaOverview.insertRow(0)
self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(media.signature))
self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(media.title))
self.mediaOverview.setItem(0, 2, QtWidgets.QTableWidgetItem("Entliehen"))
self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id))
self.nextReturnDate.setText(
stringToDate(self.db.selectClosestReturnDate(self.activeUser.id)).toString(
"dd.MM.yyyy"
)
)
self.nextReturnDate.show()
self.label_7.show()
self.input_file_ident.setEnabled(True)
def returnMedia(self, identifier):
print("Returning Media", identifier)
#print("Returning Media", identifier)
# get book id from database
# self.
identifier = Book(
isbn=identifier, title=identifier, signature=identifier, ppn=identifier
)
book_id = self.db.checkMediaExists(identifier)
print(book_id)
#print(book_id)
if book_id:
# check if book is already loaned
loaned = self.db.checkLoanState(book_id[0])
if loaned:
print("Book already loaned, returning now")
#print("Book already loaned, returning now")
user = self.db.getUserByLoan(book_id[0])
# set userdata in lineedits
self.activeUser = user
self.setUserData()
book = self.db.returnMedia(book_id[0])
book = self.db.returnMedia(
book_id[0], self.currentDate.toString("yyyy-MM-dd")
)
self.mediaOverview.insertRow(0)
self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(book.isbn))
self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(book.signature))
self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(book.title))
self.mediaOverview.setItem(
0, 2, QtWidgets.QTableWidgetItem("Zurückgegeben")
@@ -227,23 +501,81 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow):
self.btn_show_lentmedia.setText(
self.db.getActiveLoans(self.activeUser.id)
)
#
else:
print("Book not loaned")
#print("Book not loaned")
self.setStatusTipMessage("Buch nicht entliehen")
self.input_file_ident.clear()
else:
print("Book not found")
dbg("Book not found")
#print("Book not found")
#self.input_file_ident.setPlaceholderText(f"Buch {identifier} nicht gefunden")
def lendMedia(self, value):
if value.isnumeric():
self.mediaAdd(value)
else:
if len(value) <= 2:
self.callShortcut(value)
else:
self.mediaAdd(value)
def launch():
def setStatusTipMessage(self, message):
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Information")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Information)
dialog.setWindowIcon(Icon("error").overwriteColor("#EA3323"))
dialog.setText(message)
dialog.exec()
def exit_handler():
dbg("Exiting, creating backup")
app = QtWidgets.QApplication(sys.argv)
#print(backup.backup)
# generate report if monday
if datetime.datetime.now().weekday() == config.report.report_day:
generate_report()
dbg("Generated Report")
Database().renameInactiveUsers()
if config.database.do_backup:
state = backup.createBackup()
# create dialog to show state
if state == True:
return
else:
dialog = QtWidgets.QMessageBox()
# set icon
dialog.setWindowIcon(Icon("backup").icon)
dialog.setWindowTitle("Backup")
dialog.setText("Backup konnte nicht erstellt werden")
dialog.exec()
dbg("Exiting", backupstate=state)
else:
dialog = QtWidgets.QMessageBox()
# set icon
reason = "Unbekannter Grund"
if config.database.do_backup is False:
reason = "Backup deaktiviert"
if backup.backup is False:
reason = "Backuppfad nicht gefunden"
dialog.setWindowIcon(Icon("backup").icon)
dialog.setWindowTitle("Backup nicht möglich")
dialog.setText("Backup konnte nicht erstellt werden\nGrund: {}".format(reason))
dialog.exec()
def launch(*argv):
options = sys.argv
if argv:
options += [arg for arg in argv]
options = [arg for arg in options if arg.startswith("--")]
#print("Launching Main UI")
#print(options)
QtCore.QLocale().setDefault(QtCore.QLocale(QtCore.QLocale.Language.German, QtCore.QLocale.Country.Germany))
SYSTEM_LANGUAGE = QtCore.QLocale().system().name()
print(SYSTEM_LANGUAGE)
# Load base QT translations from the normal place
app = QtWidgets.QApplication([])
main_ui = MainUI()
#translate ui to system language
if SYSTEM_LANGUAGE:
translator = QtCore.QTranslator()
#do not use ascii encoding
translator.load(f"qt_{SYSTEM_LANGUAGE}", "translations")
translator.load("app.qm", "translations")
app.installTranslator(translator)
atexit.register(exit_handler)
sys.exit(app.exec())
# sys.exit(app.exec())

View File

@@ -1,35 +1,44 @@
from .sources.Ui_dialog_multipleUserfound import Ui_Dialog
from PyQt6 import QtCore, QtGui, QtWidgets
from src.schemas import User
from src.utils import Icon
class MultiUserFound(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, users: list[User]):
super(MultiUserFound, self).__init__()
self.setupUi(self)
self.setWindowTitle("Mehrere Benutzer gefunden")
self.setWindowIcon(Icon("multiuser").icon)
self.users = users
self.userdata = None
self.row = None
self.displayUsers()
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Ok
).clicked.connect(self.selectUser)
).clicked.connect(self.accept)
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Cancel
).clicked.connect(self.reject)
self.tableWidget.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeMode.Stretch
)
self.tableWidget.cellClicked.connect(self.selectUser)
def selectUser(self, row):
def selectUser(self, row, column):
#print(row, column)
user = User(
id=self.tableWidget.item(row, 0).text(),
userid=self.tableWidget.item(row, 0).text(),
username=self.tableWidget.item(row, 1).text(),
email=self.tableWidget.item(row, 2).text(),
)
self.userdata = user
self.accept()
def displayUsers(self):
for user in self.users:
self.tableWidget.insertRow(0)
self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem(str(user.id)))
self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem(str(user.userid)))
self.tableWidget.setItem(0, 1, QtWidgets.QTableWidgetItem(user.username))
self.tableWidget.setItem(0, 2, QtWidgets.QTableWidgetItem(user.email))

38
src/ui/newBook.py Normal file
View File

@@ -0,0 +1,38 @@
from .sources.Ui_dialog_addBook import Ui_Dialog
from src.schemas import Book
from src.utils import Icon
from PyQt6 import QtCore, QtGui, QtWidgets
class NewBook(QtWidgets.QDialog, Ui_Dialog):
def __init__(self):
super(NewBook, self).__init__()
self.setupUi(self)
self.setWindowTitle("Buch hinzufügen")
self.setWindowIcon(Icon("addBook").icon)
self.btn_save.setEnabled(False)
self.btn_save.clicked.connect(self.saveBook)
self.btn_cancel.clicked.connect(self.reject)
self.book_title.setFocus()
self.book_title.textChanged.connect(self.checkFields)
self.book_signature.textChanged.connect(self.checkFields)
self.book = None
self.show()
def checkFields(self):
if (
self.book_title.hasAcceptableInput()
and self.book_signature.hasAcceptableInput
):
self.btn_save.setEnabled(True)
else:
self.btn_save.setEnabled(False)
def saveBook(self):
title = self.book_title.text()
signature = self.book_signature.text()
book = Book(title=title, signature=signature)
self.book = book
self.accept()

View File

@@ -2,23 +2,32 @@ from .sources.Ui_dialog_addNewTitleEntry import Ui_Dialog
from PyQt6 import QtWidgets, QtCore, QtGui
from src.logic import Database
from src.schemas import Book
from src.utils import Icon
class NewEntry(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, title):
def __init__(self, title_id: list[int]):
super(NewEntry, self).__init__()
self.setupUi(self)
self.setWindowTitle("Neues Exemplar hinzufügen")
self.setWindowIcon(Icon("newentry").icon)
self.tableWidget.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeMode.Stretch
)
self.db = Database()
self.titles = title
self.titles = title_id
self.newIds = []
self.populateTable()
self.btn_addNewBook.clicked.connect(self.addEntry)
self.buttonBox.accepted.connect(self.insertEntry)
#disable buttonbox accepted
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Ok
).setEnabled(False)
def addEntry(self):
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Ok
).setEnabled(True)
# clone last row and its data
row = self.tableWidget.rowCount()
self.tableWidget.insertRow(row)
@@ -26,16 +35,30 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog):
self.tableWidget.setItem(
row, i, QtWidgets.QTableWidgetItem(self.tableWidget.item(row - 1, i))
)
if i == 2 and "+" in self.tableWidget.item(row, i).text():
entry = self.tableWidget.item(row, i).text().split("+")[1]
entry = str(int(entry) + 1)
self.tableWidget.setItem(
row,
i,
QtWidgets.QTableWidgetItem(
self.tableWidget.item(row, i).text().split("+")[0] + "+" + entry
),
)
def populateTable(self):
for title in self.titles:
print(title)
entry = self.db.getMedia(title)
self.tableWidget.insertRow(0)
self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem(entry.isbn))
self.tableWidget.setItem(0, 1, QtWidgets.QTableWidgetItem(entry.title))
self.tableWidget.setItem(0, 2, QtWidgets.QTableWidgetItem(entry.signature))
self.tableWidget.setItem(0, 3, QtWidgets.QTableWidgetItem(entry.ppn))
#print(title)
entries = self.db.getMediaSimilarSignatureByID(title)
# sort by signature
entries.sort(key=lambda x: x.signature, reverse=True)
for entry in entries:
self.tableWidget.insertRow(0)
self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem(entry.isbn))
self.tableWidget.setItem(0, 1, QtWidgets.QTableWidgetItem(entry.title))
self.tableWidget.setItem(
0, 2, QtWidgets.QTableWidgetItem(entry.signature)
)
self.tableWidget.setItem(0, 3, QtWidgets.QTableWidgetItem(entry.ppn))
def insertEntry(self):
# get all rows, convert them to Book and insert into database
@@ -50,9 +73,10 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog):
signature=signature,
ppn=eval(ppn),
)
print(book)
#print(book)
if not self.db.checkMediaExists(book):
self.db.insertMedia(book)
newBookId = self.db.insertMedia(book)
self.newIds.append(newBookId)
def launch():

105
src/ui/reportUi.py Normal file
View File

@@ -0,0 +1,105 @@
from PyQt6 import QtCore, QtWidgets, QtGui
from .sources.Ui_dialog_generateReport import Ui_Dialog
from src.utils import Icon
from src.utils.reportThread import ReportThread
from src.logic import Database
import os
class ReportUi(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, parent=None):
super(ReportUi, self).__init__(parent)
self.setupUi(self)
self.setWindowIcon(Icon("report").icon)
self.setWindowTitle("Report erstellen")
self.radioButton.hide()
self.db = Database()
self.reportprogress.hide()
# variables
self.maxrecords = 0
self.days = 0
self.rthread = ReportThread()
# buttons
self.generateReport.setEnabled(False)
self.generateReport.clicked.connect(self.generate_report)
self.radio_year.clicked.connect(self.set_days_by_radio)
self.radio_month.clicked.connect(self.set_days_by_radio)
self.radio_week.clicked.connect(self.set_days_by_radio)
self.format_txt.clicked.connect(lambda: self.rthread.setFormat("txt"))
self.format_csv.clicked.connect(lambda: self.rthread.setFormat("tsv"))
self.format_csv.clicked.connect(lambda: self.generateReport.setEnabled(True))
self.format_txt.clicked.connect(lambda: self.generateReport.setEnabled(True))
# sliders
self.dayslider.valueChanged.connect(self.set_days)
self.show()
# labels
self.label_4.hide()
def set_days_by_radio(self):
if self.radio_year.isChecked():
self.set_days(365)
self.dayslider.setValue(365)
elif self.radio_month.isChecked():
self.set_days(30)
self.dayslider.setValue(30)
elif self.radio_week.isChecked():
self.set_days(7)
self.dayslider.setValue(7)
def set_days(self, value):
# if value is not 7,30,365, deactivate radio buttons
if value != 7 and value != 30 and value != 365:
self.radioButton.setChecked(True)
self.days = value
self.dayValue.setText(str(value))
def generate_report(self):
self.rthread.setDays(self.days)
self.rthread.report_signal.connect(self.report_generated)
self.rthread.report_nums_signal.connect(self.show_progress)
self.rthread.report_progress_signal.connect(self.update_progress)
self.rthread.finished.connect(self.reset)
# self.rthread.finished.connect(self.rthread.deleteLater)
self.rthread.start()
def reset(self):
self.days = 0
self.reportprogress.hide()
self.reportprogress.setValue(0)
self.label_4.setText("Fortschritt:")
self.label_4.hide()
def update_progress(self, num):
self.reportlink.clear()
self.reportprogress.setValue(num)
self.label_4.setText("Fortschritt: " + str(num) + "/" + str(self.maxrecords))
if num == self.reportprogress.maximum():
self.label_4.setText("Datei wird generiert")
def show_progress(self, num):
self.reportprogress.show()
self.label_4.show()
self.maxrecords = num
self.reportprogress.setMaximum(self.maxrecords)
def report_generated(self):
self.reportlink.setOpenExternalLinks(True)
fileformat = self.rthread.format
#print(fileformat)
self.reportlink.setText(
f'<a href="file:///{os.getcwd()}/report.{fileformat}">Report</a>'
)
self.reportprogress.hide()
def launch():
import sys
app = QtWidgets.QApplication(sys.argv)
window = ReportUi()
sys.exit(app.exec())

269
src/ui/settings.py Normal file
View File

@@ -0,0 +1,269 @@
from .sources.Ui_dialog_settings import Ui_Dialog
from PyQt6 import QtWidgets, QtCore
from src.utils import Icon
from src import config
from src.utils import debugMessage as dbg
from omegaconf import OmegaConf
import os
class Settings(QtWidgets.QDialog, Ui_Dialog):
def __init__(self):
super(Settings, self).__init__()
self.setupUi(self)
self.setWindowTitle("Einstellungen")
self.setWindowIcon(Icon("settings").icon)
#variables
self.originalSettings = config.to_Omegaconf()
self.changedSettings = config.to_Omegaconf()
self.shortcuts = config.shortcuts
self.shortcuts = self.sortShortcuts(self.shortcuts)
self.settingschanged = False
self.restart_required = False
# buttonbox
self.buttonBox.accepted.connect(self.saveSettings)
self.buttonBox.rejected.connect(self.close)
self.loadSettings()
self.populateShortcuts()
# buttons
self.btn_select_database_backupLocation.clicked.connect(
self.selectBackupLocation
)
self.btn_select_database_path.clicked.connect(self.selectDatabasePath)
self.btn_select_database_name.clicked.connect(self.selectDatabaseName)
self.btn_select_report_path.clicked.connect(self.selectReportPath)
self.returnMode.clicked.connect(self.returnModeSetting)
#other
#stretch columns
self.shortcutchanger.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
def returnModeSetting(self):
currentstate = self.returnMode.isChecked()
if self.originalSettings.advanced_refresh != currentstate:
self.enableButtonBox()
def populateShortcuts(self):
for shortcut in self.shortcuts:
name = shortcut["name"]
default = shortcut["default"]
current = shortcut["current"]
self.addShortcut(name, default, current)
#assume the shortcuts will be changed
self.settingschanged = True
def addShortcut(self, name, default, current):
#remove all pages from shortcutchanger
#add new page with name, default and current
self.shortcutchanger.insertRow(0)
self.shortcutchanger.setItem(0, 0, QtWidgets.QTableWidgetItem(name))
self.shortcutchanger.setItem(0, 1, QtWidgets.QTableWidgetItem(default))
#add keysequenceedit
keysequenceedit = QtWidgets.QKeySequenceEdit()
keysequenceedit.setKeySequence(current)
self.shortcutchanger.setCellWidget(0, 2, keysequenceedit)
def selectBackupLocation(self):
backupLocation = QtWidgets.QFileDialog.getExistingDirectory(
self,
"Select Backup Location",
self.originalSettings.database.backupLocation,
)
self.database_backupLocation.setText(backupLocation)
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Discard
).setEnabled(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(
True
)
self.settingschanged = True
def selectReportPath(self):
reportPath = QtWidgets.QFileDialog.getExistingDirectory(
self, "Select Report Path", self.originalSettings.report.path
)
self.report_path.setText(reportPath)
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Discard
).setEnabled(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(
True
)
self.settingschanged = True
def selectDatabasePath(self):
databasePath = QtWidgets.QFileDialog.getExistingDirectory(
self, "Select Database Path", self.originalSettings.database.path
)
self.database_path.setText(databasePath)
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Discard
).setEnabled(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(
True
)
self.settingschanged = True
self.restart_required = True
def selectDatabaseName(self):
# filepicker with filter to select only .db files if a file is selected, set name to the lineedit and set database_path
databaseName = QtWidgets.QFileDialog.getOpenFileName(
self,
"Select Database",
self.originalSettings.database.path,
"Database Files (*.db)",
)
self.database_path.setText(databaseName[0].rsplit("/", 1)[0])
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Discard
).setEnabled(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(
True
)
self.settingschanged = True
self.restart_required = True
def getShortcuts(self):
shortcuts = []
for row in range(self.shortcutchanger.rowCount()):
name = self.shortcutchanger.item(row, 0).text()
default = self.shortcutchanger.item(row, 1).text()
current = self.shortcutchanger.cellWidget(row, 2).keySequence().toString()
shortcuts.append(
{
"name": name,
"default": default,
"current": current,
}
)
return shortcuts
def sortShortcuts(self, shortcuts):
short = []
for shortcut in shortcuts:
short.append(shortcut)
short.sort(key=lambda x: x["name"])
return short
def saveSettings(self):
# save settings to config file
institution_name = self.institution_name.text()
default_loan_duration = int(self.default_loan_duration.text())
delete_inactive_users = int(self.delete_inactive_user_duration.text())
database_backupLocation = self.database_backupLocation.text()
database_path = self.database_path.text()
database_name = self.database_name.text()
report_day = self.report_day.currentIndex()
report_generate = self.check_generate_report.isChecked()
report_path = self.report_path.text()
refresh_state = self.returnMode.isChecked()
shortcuts = self.getShortcuts()
#shortcuts to omegaconf.DictConfig
shortcuts = OmegaConf.create(shortcuts)
if database_path != self.originalSettings.database.path :
os.makedirs(database_path, exist_ok=True)
self.restart_required = True
# create new Settings
self.changedSettings.institution_name = institution_name
self.changedSettings.loan_duration = default_loan_duration
self.changedSettings.database.backupLocation = database_backupLocation
self.changedSettings.database.path = database_path
self.changedSettings.database.name = database_name
self.changedSettings.delete_inactive_user_duration = delete_inactive_users
self.changedSettings.report.report_day = report_day
self.changedSettings.report.path = report_path
self.changedSettings.report.generate_report = report_generate
self.changedSettings.advanced_refresh = refresh_state
self.changedSettings.shortcuts = shortcuts
changed = self.changedSettings
original = self.originalSettings
if changed == original:
self.settingschanged = False
self.restart_required = False
dbg("Settings not changed")
else:
self.settingschanged = True
#compare if database or shortcuts were changed
database = original.database == changed.database
shortcuts = self.shortcuts == self.sortShortcuts(changed.shortcuts)
if not database or not shortcuts:
self.restart_required = True
dbg(f"Settings changed, restart required: {self.restart_required}",database=database,shortcuts=shortcuts)
# save the new settings
if self.settingschanged:
# save the settings
config.updateValue("institution_name", self.changedSettings.institution_name)
config.updateValue("default_loan_duration", self.changedSettings.loan_duration)
config.updateValue("database.backupLocation", self.changedSettings.database.backupLocation)
config.updateValue("database.path", self.changedSettings.database.path)
config.updateValue("database.name", self.changedSettings.database.name)
config.updateValue("inactive_user_deletion", self.changedSettings.inactive_user_deletion)
config.updateValue("report.report_day", self.changedSettings.report.report_day)
config.updateValue("report.generate_report", self.changedSettings.report.generate_report)
config.updateValue("report.path", self.changedSettings.report.path)
config.updateValue("advanced_refresh", self.changedSettings.advanced_refresh)
config.updateValue("shortcuts", self.changedSettings.shortcuts)
self.originalSettings = self.changedSettings
config.save()
if self.restart_required:
self.restart()
self.close()
def restart(self):
dialog = QtWidgets.QMessageBox()
dialog.setIcon(QtWidgets.QMessageBox.Icon.Information)
dialog.setText("Neustart erforderlich")
dialog.setInformativeText(
"Das Programm muss neu gestartet werden, um die Änderungen zu übernehmen."
)
dialog.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Ok)
dialog.setWindowTitle("Neustart erforderlich")
dialog.setWindowIcon(Icon("restart").icon)
dialog.exec()
def DiscardSettings(self):
self.loadSettings()
self.restart_required = False
self.settingschanged = False
def loadSettings(self):
self.institution_name.setText(config.institution_name)
self.default_loan_duration.setValue(
int(config.loan_duration)
)
self.delete_inactive_user_duration.setValue(
int(config.delete_inactive_user_duration)
)
self.database_backupLocation.setText(
config.database.backupLocation
)
self.database_path.setText(config.database.path)
self.database_name.setText(config.database.name)
self.report_day.setCurrentIndex(config.report.report_day)
self.check_generate_report.setChecked(config.report.generate_report)
self.report_path.setText(config.report.path)
self.returnMode.setChecked(config.advanced_refresh)
pass
def launch():
import sys
app = QtWidgets.QApplication([])
settings = Settings()
settings.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,54 @@
# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_addBook.ui'
#
# Created by: PyQt6 UI code generator 6.6.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(262, 100)
self.gridLayout = QtWidgets.QGridLayout(Dialog)
self.gridLayout.setObjectName("gridLayout")
self.book_signature = QtWidgets.QLineEdit(parent=Dialog)
self.book_signature.setObjectName("book_signature")
self.gridLayout.addWidget(self.book_signature, 1, 1, 1, 1)
self.label_2 = QtWidgets.QLabel(parent=Dialog)
self.label_2.setObjectName("label_2")
self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
self.label = QtWidgets.QLabel(parent=Dialog)
self.label.setObjectName("label")
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.book_title = QtWidgets.QLineEdit(parent=Dialog)
self.book_title.setObjectName("book_title")
self.gridLayout.addWidget(self.book_title, 0, 1, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.btn_save = QtWidgets.QPushButton(parent=Dialog)
self.btn_save.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus)
self.btn_save.setObjectName("btn_save")
self.horizontalLayout.addWidget(self.btn_save)
self.btn_cancel = QtWidgets.QPushButton(parent=Dialog)
self.btn_cancel.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus)
self.btn_cancel.setObjectName("btn_cancel")
self.horizontalLayout.addWidget(self.btn_cancel)
self.gridLayout.addLayout(self.horizontalLayout, 2, 1, 1, 1)
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
Dialog.setTabOrder(self.book_title, self.book_signature)
Dialog.setTabOrder(self.book_signature, self.btn_save)
Dialog.setTabOrder(self.btn_save, self.btn_cancel)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label_2.setText(_translate("Dialog", "Signatur"))
self.label.setText(_translate("Dialog", "Titel"))
self.btn_save.setText(_translate("Dialog", "Speichern"))
self.btn_cancel.setText(_translate("Dialog", "Abbrechen"))

View File

@@ -0,0 +1,8 @@
# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_addBook.ui.VLqeTH'
#
# Created by: PyQt6 UI code generator 6.6.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -53,6 +53,9 @@ class Ui_Dialog(object):
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Nutzer anlegen"))
self.userno.setPlaceholderText(_translate("Dialog", "102888557"))
self.label_2.setText(_translate("Dialog", "Matrikelnummer"))
self.label_3.setText(_translate("Dialog", "Mail"))
self.user_mail.setPlaceholderText(_translate("Dialog", "email@ph-freiburg.de"))
self.label.setText(_translate("Dialog", "Name, Vorname"))
self.username.setPlaceholderText(_translate("Dialog", "Nachname, Vorname"))

View File

@@ -12,6 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
Dialog.resize(400, 300)
self.gridLayout = QtWidgets.QGridLayout(Dialog)
self.gridLayout.setObjectName("gridLayout")

View File

@@ -0,0 +1,119 @@
# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_generateReport.ui'
#
# Created by: PyQt6 UI code generator 6.6.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(375, 245)
Dialog.setMinimumSize(QtCore.QSize(40, 0))
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName("verticalLayout")
self.label = QtWidgets.QLabel(parent=Dialog)
self.label.setObjectName("label")
self.verticalLayout.addWidget(self.label)
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.label_2 = QtWidgets.QLabel(parent=Dialog)
self.label_2.setObjectName("label_2")
self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.radio_week = QtWidgets.QRadioButton(parent=Dialog)
self.radio_week.setObjectName("radio_week")
self.horizontalLayout.addWidget(self.radio_week)
self.radio_month = QtWidgets.QRadioButton(parent=Dialog)
self.radio_month.setObjectName("radio_month")
self.horizontalLayout.addWidget(self.radio_month)
self.radio_year = QtWidgets.QRadioButton(parent=Dialog)
self.radio_year.setObjectName("radio_year")
self.horizontalLayout.addWidget(self.radio_year)
self.gridLayout.addLayout(self.horizontalLayout, 1, 1, 1, 1)
self.dayValue = QtWidgets.QLineEdit(parent=Dialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.dayValue.sizePolicy().hasHeightForWidth())
self.dayValue.setSizePolicy(sizePolicy)
self.dayValue.setMinimumSize(QtCore.QSize(0, 0))
self.dayValue.setMaximumSize(QtCore.QSize(40, 16777215))
self.dayValue.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
self.dayValue.setReadOnly(True)
self.dayValue.setObjectName("dayValue")
self.gridLayout.addWidget(self.dayValue, 0, 2, 1, 1)
self.radioButton = QtWidgets.QRadioButton(parent=Dialog)
self.radioButton.setText("")
self.radioButton.setCheckable(True)
self.radioButton.setObjectName("radioButton")
self.gridLayout.addWidget(self.radioButton, 3, 2, 1, 1)
self.reportlink = QtWidgets.QLabel(parent=Dialog)
self.reportlink.setText("")
self.reportlink.setObjectName("reportlink")
self.gridLayout.addWidget(self.reportlink, 3, 1, 1, 1)
self.dayslider = QtWidgets.QSlider(parent=Dialog)
self.dayslider.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus)
self.dayslider.setMinimum(1)
self.dayslider.setMaximum(365)
self.dayslider.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.dayslider.setInvertedControls(True)
self.dayslider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksAbove)
self.dayslider.setTickInterval(10)
self.dayslider.setObjectName("dayslider")
self.gridLayout.addWidget(self.dayslider, 0, 1, 1, 1)
self.frame = QtWidgets.QFrame(parent=Dialog)
self.frame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Plain)
self.frame.setLineWidth(0)
self.frame.setObjectName("frame")
self.gridLayout_2 = QtWidgets.QGridLayout(self.frame)
self.gridLayout_2.setObjectName("gridLayout_2")
self.format_txt = QtWidgets.QRadioButton(parent=self.frame)
self.format_txt.setObjectName("format_txt")
self.gridLayout_2.addWidget(self.format_txt, 0, 0, 1, 1)
self.format_csv = QtWidgets.QRadioButton(parent=self.frame)
self.format_csv.setObjectName("format_csv")
self.gridLayout_2.addWidget(self.format_csv, 0, 1, 1, 1)
self.gridLayout.addWidget(self.frame, 2, 1, 1, 1)
self.label_3 = QtWidgets.QLabel(parent=Dialog)
self.label_3.setObjectName("label_3")
self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
self.verticalLayout.addLayout(self.gridLayout)
self.label_4 = QtWidgets.QLabel(parent=Dialog)
self.label_4.setObjectName("label_4")
self.verticalLayout.addWidget(self.label_4)
self.reportprogress = QtWidgets.QProgressBar(parent=Dialog)
self.reportprogress.setProperty("value", 24)
self.reportprogress.setTextVisible(True)
self.reportprogress.setInvertedAppearance(False)
self.reportprogress.setObjectName("reportprogress")
self.verticalLayout.addWidget(self.reportprogress)
self.generateReport = QtWidgets.QPushButton(parent=Dialog)
self.generateReport.setObjectName("generateReport")
self.verticalLayout.addWidget(self.generateReport)
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
Dialog.setTabOrder(self.radio_week, self.radio_month)
Dialog.setTabOrder(self.radio_month, self.radio_year)
Dialog.setTabOrder(self.radio_year, self.generateReport)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Wieviele Tage sollen im Bericht erfasst werden?"))
self.label_2.setText(_translate("Dialog", "Tage"))
self.radio_week.setText(_translate("Dialog", "Woche"))
self.radio_month.setText(_translate("Dialog", "Monat"))
self.radio_year.setText(_translate("Dialog", "Jahr"))
self.format_txt.setText(_translate("Dialog", "Text"))
self.format_csv.setText(_translate("Dialog", "Excel"))
self.label_3.setText(_translate("Dialog", "Dateiformat"))
self.label_4.setText(_translate("Dialog", "Fortschritt:"))
self.generateReport.setText(_translate("Dialog", " Bericht erstellen"))

View File

@@ -0,0 +1,8 @@
# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_generateReport.ui.nKmkjJ'
#
# Created by: PyQt6 UI code generator 6.6.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -0,0 +1,196 @@
# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_settings.ui'
#
# Created by: PyQt6 UI code generator 6.6.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(492, 445)
self.formLayout = QtWidgets.QFormLayout(Dialog)
self.formLayout.setObjectName("formLayout")
self.label = QtWidgets.QLabel(parent=Dialog)
self.label.setObjectName("label")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label)
self.institution_name = QtWidgets.QLineEdit(parent=Dialog)
self.institution_name.setObjectName("institution_name")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.institution_name)
self.label_2 = QtWidgets.QLabel(parent=Dialog)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.default_loan_duration = QtWidgets.QSpinBox(parent=Dialog)
self.default_loan_duration.setProperty("value", 7)
self.default_loan_duration.setObjectName("default_loan_duration")
self.horizontalLayout_2.addWidget(self.default_loan_duration)
self.label_13 = QtWidgets.QLabel(parent=Dialog)
self.label_13.setMaximumSize(QtCore.QSize(43, 16777215))
self.label_13.setObjectName("label_13")
self.horizontalLayout_2.addWidget(self.label_13)
self.formLayout.setLayout(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout_2)
self.label_7 = QtWidgets.QLabel(parent=Dialog)
self.label_7.setObjectName("label_7")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_7)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.delete_inactive_user_duration = QtWidgets.QSpinBox(parent=Dialog)
self.delete_inactive_user_duration.setMaximum(9999)
self.delete_inactive_user_duration.setProperty("value", 365)
self.delete_inactive_user_duration.setObjectName("delete_inactive_user_duration")
self.horizontalLayout.addWidget(self.delete_inactive_user_duration)
self.label_12 = QtWidgets.QLabel(parent=Dialog)
self.label_12.setMaximumSize(QtCore.QSize(43, 16777215))
self.label_12.setObjectName("label_12")
self.horizontalLayout.addWidget(self.label_12)
self.formLayout.setLayout(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout)
self.returnMode = QtWidgets.QCheckBox(parent=Dialog)
self.returnMode.setTristate(False)
self.returnMode.setObjectName("returnMode")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.ItemRole.FieldRole, self.returnMode)
self.label_3 = QtWidgets.QLabel(parent=Dialog)
self.label_3.setObjectName("label_3")
self.formLayout.setWidget(5, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3)
self.databasesettings = QtWidgets.QGridLayout()
self.databasesettings.setObjectName("databasesettings")
self.database_name = QtWidgets.QLineEdit(parent=Dialog)
self.database_name.setObjectName("database_name")
self.databasesettings.addWidget(self.database_name, 1, 1, 1, 1)
self.label_4 = QtWidgets.QLabel(parent=Dialog)
self.label_4.setObjectName("label_4")
self.databasesettings.addWidget(self.label_4, 0, 0, 1, 1)
self.label_6 = QtWidgets.QLabel(parent=Dialog)
self.label_6.setObjectName("label_6")
self.databasesettings.addWidget(self.label_6, 2, 0, 1, 1)
self.database_path = QtWidgets.QLineEdit(parent=Dialog)
self.database_path.setObjectName("database_path")
self.databasesettings.addWidget(self.database_path, 0, 1, 1, 1)
self.database_backupLocation = QtWidgets.QLineEdit(parent=Dialog)
self.database_backupLocation.setObjectName("database_backupLocation")
self.databasesettings.addWidget(self.database_backupLocation, 2, 1, 1, 1)
self.label_5 = QtWidgets.QLabel(parent=Dialog)
self.label_5.setObjectName("label_5")
self.databasesettings.addWidget(self.label_5, 1, 0, 1, 1)
self.btn_select_database_path = QtWidgets.QToolButton(parent=Dialog)
self.btn_select_database_path.setObjectName("btn_select_database_path")
self.databasesettings.addWidget(self.btn_select_database_path, 0, 2, 1, 1)
self.btn_select_database_name = QtWidgets.QToolButton(parent=Dialog)
self.btn_select_database_name.setObjectName("btn_select_database_name")
self.databasesettings.addWidget(self.btn_select_database_name, 1, 2, 1, 1)
self.btn_select_database_backupLocation = QtWidgets.QToolButton(parent=Dialog)
self.btn_select_database_backupLocation.setObjectName("btn_select_database_backupLocation")
self.databasesettings.addWidget(self.btn_select_database_backupLocation, 2, 2, 1, 1)
self.formLayout.setLayout(5, QtWidgets.QFormLayout.ItemRole.FieldRole, self.databasesettings)
self.label_9 = QtWidgets.QLabel(parent=Dialog)
self.label_9.setObjectName("label_9")
self.formLayout.setWidget(6, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_9)
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.btn_select_report_path = QtWidgets.QToolButton(parent=Dialog)
self.btn_select_report_path.setObjectName("btn_select_report_path")
self.gridLayout.addWidget(self.btn_select_report_path, 2, 2, 1, 1)
self.label_10 = QtWidgets.QLabel(parent=Dialog)
self.label_10.setText("")
self.label_10.setObjectName("label_10")
self.gridLayout.addWidget(self.label_10, 1, 0, 1, 1)
self.check_generate_report = QtWidgets.QCheckBox(parent=Dialog)
self.check_generate_report.setObjectName("check_generate_report")
self.gridLayout.addWidget(self.check_generate_report, 1, 1, 1, 1)
self.report_path = QtWidgets.QLineEdit(parent=Dialog)
self.report_path.setObjectName("report_path")
self.gridLayout.addWidget(self.report_path, 2, 1, 1, 1)
self.label_8 = QtWidgets.QLabel(parent=Dialog)
self.label_8.setObjectName("label_8")
self.gridLayout.addWidget(self.label_8, 2, 0, 1, 1)
self.label_11 = QtWidgets.QLabel(parent=Dialog)
self.label_11.setObjectName("label_11")
self.gridLayout.addWidget(self.label_11, 0, 0, 1, 1)
self.report_day = QtWidgets.QComboBox(parent=Dialog)
self.report_day.setObjectName("report_day")
self.report_day.addItem("")
self.report_day.addItem("")
self.report_day.addItem("")
self.report_day.addItem("")
self.report_day.addItem("")
self.gridLayout.addWidget(self.report_day, 0, 1, 1, 1)
self.formLayout.setLayout(6, QtWidgets.QFormLayout.ItemRole.FieldRole, self.gridLayout)
self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog)
self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Discard|QtWidgets.QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setObjectName("buttonBox")
self.formLayout.setWidget(9, QtWidgets.QFormLayout.ItemRole.FieldRole, self.buttonBox)
self.shortcutchanger = QtWidgets.QTableWidget(parent=Dialog)
self.shortcutchanger.setObjectName("shortcutchanger")
self.shortcutchanger.setColumnCount(3)
self.shortcutchanger.setRowCount(0)
item = QtWidgets.QTableWidgetItem()
self.shortcutchanger.setHorizontalHeaderItem(0, item)
item = QtWidgets.QTableWidgetItem()
self.shortcutchanger.setHorizontalHeaderItem(1, item)
item = QtWidgets.QTableWidgetItem()
self.shortcutchanger.setHorizontalHeaderItem(2, item)
self.formLayout.setWidget(8, QtWidgets.QFormLayout.ItemRole.FieldRole, self.shortcutchanger)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.label_14 = QtWidgets.QLabel(parent=Dialog)
self.label_14.setObjectName("label_14")
self.verticalLayout.addWidget(self.label_14)
self.label_15 = QtWidgets.QLabel(parent=Dialog)
self.label_15.setObjectName("label_15")
self.verticalLayout.addWidget(self.label_15)
self.formLayout.setLayout(8, QtWidgets.QFormLayout.ItemRole.LabelRole, self.verticalLayout)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept) # type: ignore
self.buttonBox.rejected.connect(Dialog.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Dialog)
Dialog.setTabOrder(self.institution_name, self.database_path)
Dialog.setTabOrder(self.database_path, self.database_name)
Dialog.setTabOrder(self.database_name, self.database_backupLocation)
Dialog.setTabOrder(self.database_backupLocation, self.btn_select_database_path)
Dialog.setTabOrder(self.btn_select_database_path, self.btn_select_database_name)
Dialog.setTabOrder(self.btn_select_database_name, self.btn_select_database_backupLocation)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Name der Einrichtung"))
self.label_2.setText(_translate("Dialog", "Leihdauer"))
self.label_13.setText(_translate("Dialog", "Tage(n)"))
self.label_7.setText(_translate("Dialog", "Inaktive Nutzer\n"
"Löschen nach"))
self.label_12.setText(_translate("Dialog", "Tage(n)"))
self.returnMode.setToolTip(_translate("Dialog", "Wenn aktiv: Wenn ein Medium zurückgegeben wird, wird die nächste Aktion des Moduswechsels zum normalen Rückgabemodus führen"))
self.returnMode.setText(_translate("Dialog", "Erweiterter Rückgabemodus"))
self.label_3.setText(_translate("Dialog", "Datenbank"))
self.label_4.setText(_translate("Dialog", "Speicherort"))
self.label_6.setText(_translate("Dialog", "Sicherungspfad"))
self.label_5.setText(_translate("Dialog", "Datenbankname"))
self.btn_select_database_path.setText(_translate("Dialog", "..."))
self.btn_select_database_name.setText(_translate("Dialog", "..."))
self.btn_select_database_backupLocation.setText(_translate("Dialog", "..."))
self.label_9.setText(_translate("Dialog", "Bericht"))
self.btn_select_report_path.setText(_translate("Dialog", "..."))
self.check_generate_report.setText(_translate("Dialog", "Bericht erstellen"))
self.label_8.setText(_translate("Dialog", "Speicherpfad"))
self.label_11.setText(_translate("Dialog", "Tag"))
self.report_day.setItemText(0, _translate("Dialog", "Montag"))
self.report_day.setItemText(1, _translate("Dialog", "Dienstag"))
self.report_day.setItemText(2, _translate("Dialog", "Mittwoch"))
self.report_day.setItemText(3, _translate("Dialog", "Donnerstag"))
self.report_day.setItemText(4, _translate("Dialog", "Freitag"))
item = self.shortcutchanger.horizontalHeaderItem(0)
item.setText(_translate("Dialog", "Name"))
item = self.shortcutchanger.horizontalHeaderItem(1)
item.setText(_translate("Dialog", "Standard"))
item = self.shortcutchanger.horizontalHeaderItem(2)
item.setText(_translate("Dialog", "Aktuell"))
self.label_14.setText(_translate("Dialog", "Shortcuts"))
self.label_15.setText(_translate("Dialog", "(Erst nach Neustart\n"
"wirksam)"))

View File

@@ -0,0 +1,8 @@
# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_settings.ui.vqAAbY'
#
# Created by: PyQt6 UI code generator 6.6.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -0,0 +1,110 @@
# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\main_Loans.ui'
#
# Created by: PyQt6 UI code generator 6.6.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(899, 658)
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.radio_all = QtWidgets.QRadioButton(parent=self.centralwidget)
self.radio_all.setChecked(True)
self.radio_all.setObjectName("radio_all")
self.horizontalLayout.addWidget(self.radio_all)
self.radio_current = QtWidgets.QRadioButton(parent=self.centralwidget)
self.radio_current.setObjectName("radio_current")
self.horizontalLayout.addWidget(self.radio_current)
self.radio_overdue = QtWidgets.QRadioButton(parent=self.centralwidget)
self.radio_overdue.setObjectName("radio_overdue")
self.horizontalLayout.addWidget(self.radio_overdue)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.verticalLayout.addLayout(self.horizontalLayout)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.searchbar = QtWidgets.QLineEdit(parent=self.centralwidget)
self.searchbar.setObjectName("searchbar")
self.horizontalLayout_2.addWidget(self.searchbar)
self.searchFields = QtWidgets.QComboBox(parent=self.centralwidget)
self.searchFields.setObjectName("searchFields")
self.searchFields.addItem("")
self.searchFields.addItem("")
self.searchFields.addItem("")
self.horizontalLayout_2.addWidget(self.searchFields)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.loanTable = QtWidgets.QTableWidget(parent=self.centralwidget)
self.loanTable.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
self.loanTable.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection)
self.loanTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
self.loanTable.setObjectName("loanTable")
self.loanTable.setColumnCount(7)
self.loanTable.setRowCount(0)
item = QtWidgets.QTableWidgetItem()
self.loanTable.setHorizontalHeaderItem(0, item)
item = QtWidgets.QTableWidgetItem()
self.loanTable.setHorizontalHeaderItem(1, item)
item = QtWidgets.QTableWidgetItem()
self.loanTable.setHorizontalHeaderItem(2, item)
item = QtWidgets.QTableWidgetItem()
self.loanTable.setHorizontalHeaderItem(3, item)
item = QtWidgets.QTableWidgetItem()
self.loanTable.setHorizontalHeaderItem(4, item)
item = QtWidgets.QTableWidgetItem()
self.loanTable.setHorizontalHeaderItem(5, item)
item = QtWidgets.QTableWidgetItem()
self.loanTable.setHorizontalHeaderItem(6, item)
self.verticalLayout.addWidget(self.loanTable)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 899, 22))
self.menubar.setObjectName("menubar")
self.menuDatei = QtWidgets.QMenu(parent=self.menubar)
self.menuDatei.setObjectName("menuDatei")
MainWindow.setMenuBar(self.menubar)
self.actionBeenden = QtGui.QAction(parent=MainWindow)
self.actionBeenden.setObjectName("actionBeenden")
self.menuDatei.addAction(self.actionBeenden)
self.menubar.addAction(self.menuDatei.menuAction())
self.retranslateUi(MainWindow)
self.actionBeenden.triggered.connect(MainWindow.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.radio_all.setText(_translate("MainWindow", "Alle Ausleihen"))
self.radio_current.setText(_translate("MainWindow", "Aktuell Entliehene Medien"))
self.radio_overdue.setText(_translate("MainWindow", "Überzogene Medien"))
self.searchFields.setItemText(0, _translate("MainWindow", "Titel"))
self.searchFields.setItemText(1, _translate("MainWindow", "Signatur"))
self.searchFields.setItemText(2, _translate("MainWindow", "Nutzer"))
item = self.loanTable.horizontalHeaderItem(0)
item.setText(_translate("MainWindow", "ISBN"))
item = self.loanTable.horizontalHeaderItem(1)
item.setText(_translate("MainWindow", "Signatur"))
item = self.loanTable.horizontalHeaderItem(2)
item.setText(_translate("MainWindow", "Titel"))
item = self.loanTable.horizontalHeaderItem(3)
item.setText(_translate("MainWindow", "Nutzerkonto"))
item = self.loanTable.horizontalHeaderItem(4)
item.setText(_translate("MainWindow", "entliehen am"))
item = self.loanTable.horizontalHeaderItem(5)
item.setText(_translate("MainWindow", "entliehen bis"))
item = self.loanTable.horizontalHeaderItem(6)
item.setText(_translate("MainWindow", "Zurückgegeben am"))
self.menuDatei.setTitle(_translate("MainWindow", "Datei"))
self.actionBeenden.setText(_translate("MainWindow", "Beenden"))
self.actionBeenden.setShortcut(_translate("MainWindow", "Q"))

Some files were not shown because too many files have changed in this diff Show More