evaluate sqlmodel

This commit is contained in:
2025-04-13 16:16:10 +02:00
parent a43e2c806c
commit b14a267b5b
107 changed files with 2517 additions and 6 deletions
+9
View File
@@ -0,0 +1,9 @@
deployment/
venv/
kontor.bin
bin/
include/
lib/
lib64/
lib64
env/
+31
View File
@@ -0,0 +1,31 @@
.PHONY: clean virtualenv test docker dist dist-upload
clean:
find . -name '*.py[co]' -delete
virtualenv:
virtualenv --prompt '|> kontor <| ' env
env/bin/pip install -r requirements.txt
env/bin/python setup.py develop
@echo
@echo "VirtualENV Setup Complete. Now run: source env/bin/activate"
@echo
test:
python -m pytest \
-v \
--cov=kontor \
--cov-report=term \
--cov-report=html:coverage-report \
tests/
docker: clean
docker build -t kontor-api:latest .
dist: clean
rm -rf dist/*
python setup.py sdist
python setup.py bdist_wheel
dist-upload:
twine upload dist/*
+1
View File
@@ -0,0 +1 @@
# kontor-gui
View File
+66
View File
@@ -0,0 +1,66 @@
from PySide6.QtCore import Signal, QSortFilterProxyModel
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow, \
QHeaderView
from gui.model_config import KontorModelConfig
from gui.table_model import KontorTableModel
class ComicWindow(QMdiSubWindow):
closed = Signal()
def __init__(self, main_window):
super().__init__()
self.data_views = list()
self._main_window = main_window
self.log = main_window.log
self._init_gui()
self.tick = main_window.tick
self.cross = main_window.cross
def _init_gui(self):
self.setWindowTitle("Comics")
self.setWidget(QWidget())
layout = QVBoxLayout()
self.tabs = QTabWidget()
self.tabs.addTab(self.generate_data_tab("comic"), "Comics")
self.tabs.addTab(self.generate_data_tab("publisher"), "Publisher")
self.tabs.currentChanged.connect(self._tab_changed)
layout.addWidget(self.tabs)
self.setLayout(layout)
self.setWidget(self.tabs)
def closeEvent(self, event):
self.closed.emit()
super().closeEvent(event)
self._main_window.remove_sub_window('comic')
def refresh(self):
# self.log.info("refresh")
self.data_views[self.tabs.currentIndex()].refresh()
def _tab_changed(self, tab_index):
self.data_views[tab_index].refresh()
def update_status(self, message):
self._main_window.update_status(message)
def generate_data_tab(self, table_name):
data_tab = QWidget()
table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name)
model = KontorTableModel(table_config)
layout = QVBoxLayout()
self.data_views.append(model)
data_tab.setLayout(layout)
table_view = QTableView()
header = table_view.horizontalHeader()
header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(model)
table_view.setSortingEnabled(True)
table_view.setModel(proxy_model)
layout.addLayout(table_config.get_filter_layout())
layout.addWidget(table_view)
model.refresh()
return data_tab
+12
View File
@@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
class DataViewMeta(ABC):
@abstractmethod
def get_header(self):
pass
class ComicView(DataViewMeta):
def get_header(self):
pass
+32
View File
@@ -0,0 +1,32 @@
from typing import List
from PySide6.QtCore import QModelIndex, QAbstractTableModel
from PySide6.QtGui import Qt
from gui.data_view import DataViewMeta
class DataViewModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self.main_window = None
self._config = None
self._data = List[DataViewMeta]
def rowCount(self, parent=QModelIndex()):
return len(self._data)
def columnCount(self, parent=QModelIndex()):
return 0
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
return None
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
return None
def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
return False
def flags(self, index):
return None
+106
View File
@@ -0,0 +1,106 @@
from pathlib import Path
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \
QCheckBox, QComboBox
class ExportKontorDialog(QDialog):
def __init__(self, parent=None, kontor_db=None):
super().__init__(parent)
self.parent = parent
self.kontor_db = kontor_db
self.file_name = "data.json"
self.tables = []
self._table_options = {}
self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}}
self.current_export_type = "JSON"
buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox = QDialogButtonBox(buttons)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout = QVBoxLayout()
self.label = QLabel()
self.label.setText("Export DB to data.json")
self.combo_box = QComboBox()
self.combo_box.addItems(["JSON", "YAML", "SQLite"])
self.combo_box.currentTextChanged.connect(self.change_export_type)
file_layout = QHBoxLayout()
file_layout.addWidget(self.label)
file_layout.addWidget(self.combo_box)
file_button = QPushButton("Select file")
file_button.clicked.connect(self.select_file)
file_layout.addWidget(file_button)
layout.addLayout(file_layout)
for table_name in self.kontor_db.get_table_names():
check_box = QCheckBox(table_name)
check_box.setChecked(True)
self.tables.append(table_name)
self._table_options[table_name] = check_box
check_box.stateChanged.connect(self.change_selection)
layout.addWidget(check_box)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
def change_selection(self):
self.tables.clear()
for (name, box) in self._table_options.items():
if box.isChecked():
self.tables.append(name)
def change_export_type(self, text):
self.current_export_type = text
self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}')
def select_file(self):
file_dialog = QFileDialog()
file_dialog.setFileMode(QFileDialog.FileMode.AnyFile)
file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"])
file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}')
if file_dialog.exec():
self.file_name = file_dialog.selectedFiles()[0]
export_file = Path(self.file_name)
self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"])
self.label.setText(f"Export DB to {self.file_name}")
def get_tables_to_export(self) -> list:
return self.tables
class ImportKontorDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.file_name = None
QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.label = QLabel()
self.label.setText("Import DB from data.json")
layout = QVBoxLayout()
file_layout = QHBoxLayout()
file_layout.addWidget(self.label)
file_button = QPushButton("Select file")
file_button.clicked.connect(self.select_file)
file_layout.addWidget(file_button)
layout.addLayout(file_layout)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
def select_file(self):
file_dialog = QFileDialog()
file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile)
if file_dialog.exec():
self.file_name = file_dialog.selectedFiles()[0]
self.label.setText(f"Import DB from {self.file_name}")
+239
View File
@@ -0,0 +1,239 @@
from PySide6.QtCore import Qt, QThreadPool
from PySide6.QtGui import QAction, QIcon, QGuiApplication
from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QProgressBar, QMdiArea
from PySide6.QtWidgets import QLabel, QMainWindow
from sqlalchemy import Engine
from kontor_schema import KontorDB
from .comic_window import ComicWindow
from .media_window import MediaWindow
from .meta_data_window import MetaDataWindow
from .progress import ProgressUpdate
from .dialogs import ExportKontorDialog, ImportKontorDialog
from .model_config import KontorModelConfig
from .table_model import KontorTableModel
from .worker import VideoDownloader
class MainWindow(QMainWindow):
def __init__(self, engine: Engine, log):
super().__init__()
self.downloader = None
self.tick = QIcon('res/tick.png')
self.cross = QIcon('res/cross.png')
self.import_icon = QIcon("res/application-import.png")
self.export_icon = QIcon("res/application-export.png")
self.circle_icon = QIcon("res/arrow-circle-double.png")
self.data = []
self.filter = {}
self.kontor_db = KontorDB(engine, log)
self.log = log
self._subwindows = {}
self.media_dir = "/data/media"
self.dl_tool = "yt-dlp"
self._setup_ui()
def _setup_ui(self):
self.setWindowTitle("Kontor")
self.setMinimumSize(1200, 800)
self._create_actions()
self.mdi_area = QMdiArea()
self.setCentralWidget(self.mdi_area)
self.mdi_area.setObjectName('mdi_area')
self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self._create_menubar()
self._create_toolbars()
self.status_progress = QProgressBar()
self.progress_update = ProgressUpdate(self.status_progress)
self._create_statusbar()
center_point = QGuiApplication.screens()[0].geometry().center()
self.move(center_point - self.frameGeometry().center())
def _create_actions(self):
self.newAction = QAction("&New", self)
self.aboutAction = QAction("&Über...", self)
self.aboutAction.triggered.connect(self.about)
self.showComicWindow = QAction("&Comic Window", self)
self.showComicWindow.triggered.connect(self.show_comic_window)
self.showTyscWindow = QAction("TYSC Window", self)
self.showMediaWindow = QAction("&Media Window", self)
self.showMediaWindow.triggered.connect(self.show_media_window)
self.showMetaDataWindow = QAction("Meta Data Window", self)
self.showMetaDataWindow.triggered.connect(self.show_meta_data_window)
self.importAction = QAction(self.import_icon, "&Import", self)
self.importAction.triggered.connect(self.import_from_file)
self.exportAction = QAction(self.export_icon, "&Export", self)
self.exportAction.triggered.connect(self.export_to_file)
self.refreshAction = QAction(self.circle_icon, "&Refresh", self)
self.refreshAction.triggered.connect(self.refresh)
self.updateTitleAction = QAction("&Update Titles", self)
self.updateTitleAction.triggered.connect(self.update_title)
self.downloadAction = QAction("&Download Videos", self)
self.downloadAction.triggered.connect(self.start_download)
self.checkFileAction = QAction("&Check files", self)
self.checkFileAction.triggered.connect(self.check_files)
self.exitAction = QAction("&Beenden", self)
self.exitAction.setShortcut("Alt+F4")
self.exitAction.triggered.connect(self.close)
def _create_menubar(self):
menu_bar = self.menuBar()
# File menu
file_menu = QMenu("&Datei")
menu_bar.addMenu(file_menu)
file_menu.addAction(self.exitAction)
# Kontor menu
kontor_menu = QMenu("&Kontor")
menu_bar.addMenu(kontor_menu)
kontor_menu.addAction(self.importAction)
kontor_menu.addAction(self.exportAction)
comic_menu = QMenu("&Comic")
comic_menu.addAction(self.showComicWindow)
tysc_menu = QMenu("&TradeYourSportCards")
media_file_menu = QMenu("&MediaFile")
media_file_menu.addAction(self.updateTitleAction)
media_file_menu.addAction(self.downloadAction)
media_file_menu.addAction(self.checkFileAction)
kontor_menu.addMenu(comic_menu)
kontor_menu.addMenu(tysc_menu)
kontor_menu.addMenu(media_file_menu)
window_menu = QMenu("&Window")
layouts_menu = QMenu("&Layouts")
window_menu.addMenu(layouts_menu)
window_menu.addAction(self.showComicWindow)
window_menu.addAction(self.showMediaWindow)
window_menu.addAction(self.showMetaDataWindow)
menu_bar.addMenu(window_menu)
# Help menu
help_menu = QMenu("&Hilfe")
menu_bar.addMenu(help_menu)
help_menu.addAction(self.aboutAction)
def _create_toolbars(self):
# Kontor toolbar
kontor_tool_bar = self.addToolBar("Kontor")
kontor_tool_bar.addAction(self.importAction)
kontor_tool_bar.addAction(self.exportAction)
kontor_tool_bar.addAction(self.refreshAction)
def _create_statusbar(self):
self.statusBar = self.statusBar()
self.statusBar.showMessage("Kontor ready", 6000)
self.status_label = QLabel("")
self.status_progress.setEnabled(False)
self.statusBar.addPermanentWidget(self.status_progress)
def about(self):
QMessageBox.about(self, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0")
def show_comic_window(self):
if 'comic' not in self._subwindows:
comic = ComicWindow(self)
comic.closed.connect(self.sub_window_closed)
self._subwindows['comic'] = comic
self.mdi_area.addSubWindow(comic)
comic.show()
else:
comic = self._subwindows.pop('comic')
comic.close()
self.mdi_area.removeSubWindow(comic)
def show_media_window(self):
if 'media' not in self._subwindows:
media = MediaWindow(self)
media.closed.connect(self.sub_window_closed)
self._subwindows['media'] = media
self.mdi_area.addSubWindow(media)
media.show()
else:
media = self._subwindows.pop('media')
media.close()
self.mdi_area.removeSubWindow(media)
def show_meta_data_window(self):
if 'meta_data' not in self._subwindows:
meta_data = MetaDataWindow(self)
meta_data.closed.connect(self.sub_window_closed)
self._subwindows['meta_data'] = meta_data
self.mdi_area.addSubWindow(meta_data)
meta_data.show()
else:
meta_data = self._subwindows.pop('meta_data')
meta_data.close()
self.mdi_area.removeSubWindow(meta_data)
def remove_sub_window(self, name: str):
self.log.info("remove subwindow %s", name)
if name in self._subwindows:
window = self._subwindows.pop(name)
window.close()
self.mdi_area.removeSubWindow(window)
def sub_window_closed(self):
self.log.info("close subwindow")
def import_from_file(self):
import_dlg = ImportKontorDialog(self)
if import_dlg.exec():
print(f"import DB from file {import_dlg.file_name}")
self.kontor_db.import_db(import_dlg.file_name)
else:
print("do nothing for import")
def export_to_file(self):
export_dlg = ExportKontorDialog(self, self.kontor_db)
if export_dlg.exec():
self.log.info(export_dlg.get_tables_to_export())
self.log.info(f"export DB to {export_dlg.file_name}")
self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000)
self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name)
else:
self.statusBar.showMessage("Export cancelled", 3000)
def update_title(self):
self.log.info("update title for table MediaFile")
self.statusBar.showMessage("update title for table MediaFile", 3000)
self.status_progress.setEnabled(True)
self.kontor_db.update_titles()
self.status_progress.setEnabled(False)
self.refresh()
def start_download(self):
self.status_progress.setEnabled(True)
self.statusBar.showMessage("download videos for table MediaFile", 3000)
self.downloader = VideoDownloader(self.kontor_db, self.log)
self.downloader.setTotalProgress.connect(self.status_progress.setMaximum)
self.downloader.setCurrentProgress.connect(self.downloadProgress)
self.downloader.succeeded.connect(self.downloadSucceeded)
self.downloader.finished.connect(self.downloadFinished)
self.downloader.start()
def downloadProgress(self, value: int):
self.status_progress.setValue(value)
self.refresh()
def downloadSucceeded(self):
self.status_progress.setValue(self.status_progress.maximum())
self.statusBar.showMessage("Download succeeded", 3000)
def downloadFinished(self):
self.status_progress.setEnabled(False)
del self.downloader
def check_files(self):
self.log.info("check files")
self.statusBar.showMessage("check files for table MediaFile", 3000)
self.kontor_db.check_files()
def refresh(self):
self.log.info("refresh")
for (_, window) in self._subwindows.items():
window.refresh()
def update_status(self, message, timeout=3000):
self.statusBar.showMessage(message, timeout=timeout)
+78
View File
@@ -0,0 +1,78 @@
from PySide6.QtCore import Signal, QSortFilterProxyModel
from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView, QHeaderView, QLabel, \
QHBoxLayout, QFormLayout, QLineEdit
from .model_config import KontorModelConfig
from .table_details import KontorTableDetailsView
from .table_model import KontorTableModel
class MediaWindow(QMdiSubWindow):
closed = Signal()
def __init__(self, main_window):
super().__init__()
self.data_views = list()
self._main_window = main_window
self.log = main_window.log
self._init_gui()
self.tick = main_window.tick
self.cross = main_window.cross
def _init_gui(self):
self.setWindowTitle("Media")
self.setWidget(QWidget())
layout = QVBoxLayout()
self.tabs = QTabWidget()
self.tabs.addTab(self.generate_data_tab("media_file"), "Media File")
self.tabs.addTab(self.generate_data_tab("media_video"), "Media Video")
self.tabs.addTab(self.generate_data_tab("media_article"), "Media Article")
self.tabs.addTab(self.generate_data_tab_with_details("media_actor"), "Media Actor")
self.tabs.currentChanged.connect(self._tab_changed)
layout.addWidget(self.tabs)
self.setLayout(layout)
self.setWidget(self.tabs)
def closeEvent(self, event):
self.closed.emit()
super().closeEvent(event)
self._main_window.remove_sub_window('media')
def refresh(self):
self.log.info("MediaWindow.refresh")
self.data_views[self.tabs.currentIndex()].refresh()
def _tab_changed(self, tab_index):
self.data_views[tab_index].refresh()
def update_status(self, message):
self._main_window.update_status(message)
def generate_data_tab(self, table_name):
data_tab = QWidget()
table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name)
model = KontorTableModel(table_config)
layout = QVBoxLayout()
self.data_views.append(model)
data_tab.setLayout(layout)
table_view = QTableView()
proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(model)
table_view.setSortingEnabled(True)
table_view.setModel(proxy_model)
layout.addLayout(table_config.get_filter_layout())
layout.addWidget(table_view)
model.refresh()
table_view.resizeColumnToContents(0)
return data_tab
def generate_data_tab_with_details(self, table_name):
table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name)
model = KontorTableModel(table_config)
self.data_views.append(model)
details_view = KontorTableDetailsView(model)
return details_view.data_view
def cell_selected(self, item):
self.log.info(f"Cell {item.row()}:{item.column()} clicked")
+67
View File
@@ -0,0 +1,67 @@
from PySide6.QtCore import Signal, QSortFilterProxyModel
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow, \
QHeaderView
from gui.model_config import KontorModelConfig
from gui.table_model import KontorTableModel
class MetaDataWindow(QMdiSubWindow):
closed = Signal()
def __init__(self, main_window):
super().__init__()
self.data_views = list()
self._main_window = main_window
self.log = main_window.log
self._init_gui()
self.tick = main_window.tick
self.cross = main_window.cross
def _init_gui(self):
self.setWindowTitle("Meta Data")
self.setWidget(QWidget())
layout = QVBoxLayout()
self.tabs = QTabWidget()
self.tabs.addTab(self.generate_data_tab("module_data"), "Module")
self.tabs.addTab(self.generate_data_tab("meta_data_table"), "Tables")
self.tabs.addTab(self.generate_data_tab("meta_data_column"), "Columns")
self.tabs.currentChanged.connect(self._tab_changed)
layout.addWidget(self.tabs)
self.setLayout(layout)
self.setWidget(self.tabs)
def closeEvent(self, event):
self.closed.emit()
super().closeEvent(event)
self._main_window.remove_sub_window('meta_data')
def refresh(self):
# self.log.info("refresh")
self.data_views[self.tabs.currentIndex()].refresh()
def _tab_changed(self, tab_index):
self.data_views[tab_index].refresh()
def update_status(self, message):
self._main_window.update_status(message)
def generate_data_tab(self, table_name):
data_tab = QWidget()
table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name)
model = KontorTableModel(table_config)
layout = QVBoxLayout()
self.data_views.append(model)
data_tab.setLayout(layout)
table_view = QTableView()
# header = table_view.horizontalHeader()
# header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(model)
table_view.setSortingEnabled(True)
table_view.setModel(proxy_model)
layout.addLayout(table_config.get_filter_layout())
layout.addWidget(table_view)
model.refresh()
table_view.resizeColumnToContents(0)
return data_tab
+55
View File
@@ -0,0 +1,55 @@
from PySide6.QtWidgets import QHBoxLayout, QCheckBox, QMdiSubWindow
from kontor_schema import KontorDB, ColumnEntry
class KontorModelConfig:
def __init__(self, kontor_db: KontorDB, main_window, table_name: str):
self.header = {}
self.filter = {}
self.main_window = main_window
self.log = main_window.log
self._table_name = table_name
self.kontor_db = kontor_db
self.get_table_config()
def __str__(self):
return f"KontorModelConfig({self._table_name})"
def get_table_config(self):
# self.log.info("get_table_config %s", self)
self.header = self.kontor_db.get_column_meta_data(self._table_name)
self.filter = self.kontor_db.get_filters(self._table_name)
# self.log.info("headers: %s", self.header)
# self.log.info("%s filters: %s", self, self.filter)
def filters(self) -> dict:
# self.log.info("%s filters: %s", self, self.filter)
_filters = {}
# print(self.filter["download"].isChecked())
for column, filter_info in self.filter.items():
# print(column, filter_info)
if filter_info[ColumnEntry.COLUMN_WIDGET].isChecked():
_filters[column] = True
# print(f"{filter_rule=}")
# self.log.info("filters -> %s", _filters)
return _filters
def get_data(self) -> list:
# self.log.info("get_data")
data = self.kontor_db.data(self._table_name, self.header, self.filters())
# self.log.info("get_data: %d %s", len(data), data)
return data
def get_filter_layout(self) -> QHBoxLayout:
# self.log.info("get_filter_layout: %s", self.filter)
filter_layout = QHBoxLayout()
for column, filter_info in self.filter.items():
filter_checkbox = QCheckBox()
filter_checkbox.setText(filter_info[ColumnEntry.COLUMN_LABEL])
filter_checkbox.checkStateChanged.connect(self.main_window.refresh)
self.filter[column][ColumnEntry.COLUMN_WIDGET] = filter_checkbox
filter_layout.addWidget(filter_checkbox)
filter_layout.addStretch()
# self.log.info("get_filter_layout: %s", self.filter)
return filter_layout
+18
View File
@@ -0,0 +1,18 @@
from PySide6.QtWidgets import QProgressBar
class ProgressUpdate:
def __init__(self, progress: QProgressBar):
self.start = 0
self.end = 0
self.current = 0
self.progress = progress
def start(self, start_value, end_value):
self.start = start_value
self.end = end_value
self.current = start_value
self.progress.update()
def update(self, current):
self.progress.update()
+60
View File
@@ -0,0 +1,60 @@
from PySide6.QtCore import QSortFilterProxyModel
from PySide6.QtWidgets import QHBoxLayout, QWidget, QTableView, QVBoxLayout, QFormLayout, QLineEdit, QLabel
from .table_model import KontorTableModel
class KontorTableDetailsView:
def __init__(self, table_model: KontorTableModel):
self._data_view: QWidget = QWidget()
self._model = table_model
self.log = table_model.log
self._table_view = QTableView()
self._label = QLabel()
self.init_gui()
def init_gui(self):
self.log.info("KontorTableDetailsView.init_gui()")
layout = QVBoxLayout()
self._data_view.setLayout(layout)
details_layout = QHBoxLayout()
table_with_details = QWidget()
table_with_details.setLayout(details_layout)
self._table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(self._model)
self._table_view.setSortingEnabled(True)
self._table_view.setModel(proxy_model)
self._table_view.clicked.connect(self.update_details)
self._table_view.activated.connect(self.refresh_details)
layout.addLayout(self._model.config.get_filter_layout())
details_layout.addWidget(self._table_view)
form = QWidget()
form_layout = QFormLayout(form)
form.setLayout(form_layout)
title = QLineEdit(form)
form_layout.addRow("ID", self._label)
form_layout.addRow("Title", title)
# layout.addWidget(table_view)
details_layout.addWidget(form)
layout.addWidget(table_with_details)
self._model.refresh()
self._table_view.resizeColumnToContents(0)
@property
def data_view(self):
return self._data_view
def update_details(self, item):
print(f"Cell {item.row()}-{item.column()} selected")
self.log.info(f"Cell {item.row()}-{item.column()} selected")
self._label.setText(self._model.raw_data()[item.row()][0])
def refresh_details(self):
indexes = self._table_view.selectedIndexes()
for index in indexes:
self.log.info(f"refresh_details: Cell {index.row()}-{index.column()} selected")
+128
View File
@@ -0,0 +1,128 @@
from datetime import datetime
from typing import Any
from PySide6.QtCore import QAbstractTableModel, QModelIndex
from PySide6.QtGui import Qt, QColor
from kontor_schema.database import ColumnEntry
from .model_config import KontorModelConfig
def get_display_value(value: Any, column_config: dict, window) -> str:
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %M:%M:%S")
if column_config[ColumnEntry.COLUMN_TYPE] == 'BOOLEAN':
if value == 1:
return window.tick
else:
return window.cross
if value is None:
return ""
# window.log.info(f"unknown type: {column_config[ColumnEntry.COLUMN_TYPE]} - {type(value)}")
return str(value)
def get_edit_value(value, column_config, window):
# window.log.info(f"edit value {value}")
return str(value)
def get_decoration_value(value: Any, column_config: dict, window):
if column_config[ColumnEntry.COLUMN_TYPE] == 'BOOLEAN':
if value == 1:
return window.tick
else:
return window.cross
def get_background_value(value: Any, column_config: dict, window):
if value is None:
return QColor('lightgrey')
class KontorTableModel(QAbstractTableModel):
def __init__(self, model_config: KontorModelConfig):
super().__init__()
self._main_window = model_config.main_window
self._config = model_config
self._data = []
self.log = model_config.log
def __str__(self):
return f"KontorTableModel({self._config})"
@property
def config(self):
return self._config
@property
def raw_data(self):
return self._data
def refresh(self):
# self.log.info("refresh")
data = self._config.get_data()
count = 0
# print(data)
if data is not None:
self.beginResetModel()
self._data.clear()
self._data = data
self.endResetModel()
count = len(data)
# print(data)
# print(self._data)
self.layoutChanged.emit()
self._main_window.update_status(f"{count} Einträge geladen")
def rowCount(self, parent=QModelIndex()):
# self.log.info("rowCount %s: %d", self, len(self._data))
# The length of the outer list.
if self._data is None:
return 0
return len(self._data)
def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole):
# self.log.info(f"{self._config.header[col]}")
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._config.header[col][ColumnEntry.COLUMN_LABEL]
if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole:
return str(col + 1)
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if self._data is None:
return None
value = self._data[index.row()][index.column()]
# print('{}:: {}:: {}:: {}: {}'.format(index, role, self._config.header[index.column()][ColumnEntry.COLUMN_TYPE], value, type(value)))
match role:
case Qt.ItemDataRole.DisplayRole:
return get_display_value(value, self._config.header[index.column()], self._config.main_window)
case Qt.ItemDataRole.EditRole:
return get_edit_value(value, self._config.header[index.column()], self._config.main_window)
case Qt.ItemDataRole.DecorationRole:
return get_decoration_value(value, self._config.header[index.column()], self._config.main_window)
case Qt.ItemDataRole.BackgroundRole:
return get_background_value(value, self._config.header[index.column()], self._config.main_window)
def columnCount(self, index=QModelIndex()):
# self.log.info("rowCount %s: %d", self, len(self._config.header))
return len(self._config.header)
def setData(self, index, value, role=Qt.ItemDataRole.EditRole) -> bool:
# self._config.log.info(f"{index}: {role}")
if role == Qt.ItemDataRole.EditRole:
self._data[index.row()][index.column()] = value
# self._config.log.info(f"{index.row()}-{index.column()}: {self._data[index.row()][index.column()]}")
self.dataChanged.emit(index, index)
return True
if role == Qt.ItemDataRole.CheckStateRole:
print("role == Qt.ItemDataRole.CheckStateRole")
checked = value == Qt.CheckState.Checked
self._data[index.row()][index.column()] = checked
return False
def flags(self, index):
if self._config.header[index.column()][ColumnEntry.COLUMN_NAME] == 'id':
return Qt.ItemFlag.ItemIsEnabled
return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate
+28
View File
@@ -0,0 +1,28 @@
import sys
from PySide6.QtCore import QObject, Signal, QRunnable, Slot, QThread
class VideoDownloader(QThread):
# Signal for the window to establish the maximum value
# of the progress bar.
setTotalProgress = Signal(int)
# Signal to increase the progress.
setCurrentProgress = Signal(int)
# Signal to be emitted when the file has been downloaded successfully.
succeeded = Signal()
def __init__(self, kontor_db, log):
super().__init__()
self.kontor_db = kontor_db
self.log = log
def run(self):
self.log.info("download videos for table MediaFile")
download_entries = self.kontor_db.get_download_list()
self.setTotalProgress.emit(len(download_entries))
for index, entry in enumerate(download_entries):
self.kontor_db.download_file(entry)
self.setCurrentProgress.emit(index)
self.succeeded.emit()
+48
View File
@@ -0,0 +1,48 @@
"""
PySide6 GUI for Kontor
"""
import sys
import logging.config
from pathlib import Path
from platformdirs import PlatformDirs
from PySide6.QtWidgets import QApplication
import yaml
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from kontor_schema.base import Base
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from gui.main_window import MainWindow
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--verbose', '-v', action='count', default=0)
parser.add_argument('--config', '-c', default='kontor-docker')
args = parser.parse_args()
if __name__ == '__main__':
app = QApplication(sys.argv)
dirs = PlatformDirs(args.config)
database_config = Path(dirs.user_config_dir, 'database-config.yaml')
with open(database_config, 'rt') as f:
db_config = yaml.safe_load(f.read())
connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format(
db_config['mariadb']['user'],
db_config['mariadb']['password'],
db_config['mariadb']['host'],
db_config['mariadb']['port'],
db_config['mariadb']['database']
))
logging_config = Path(dirs.user_config_dir, 'logging-config.yaml')
with open(logging_config, 'rt') as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)
logger = logging.getLogger('development')
# engine = create_engine(connect_string, echo=True)
engine = create_engine(connect_string)
Base.metadata.create_all(bind=engine, checkfirst=True)
__session__ = sessionmaker(bind=engine)
window = MainWindow(engine, logger)
window.show()
app.exec()
+26
View File
@@ -0,0 +1,26 @@
[project]
name = "kontor-gui"
version = "0.1.0"
description = "Kontor GUI"
authors = [
{name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"},
]
dependencies = [
"platformdirs",
"pyyaml",
"PySide6",
]
requires-python = ">=3.11"
readme = "README.md"
license = {text = "MIT"}
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[tool.pdm]
distribution = true
[dependency-groups]
dev = ["-e file:///${PROJECT_ROOT}/../kontor-schema#egg=kontor-schema"]
+98
View File
@@ -0,0 +1,98 @@
[app]
# title of your application
title = kontor
# project directory. the general assumption is that project_dir is the parent directory
# of input_file
project_dir = /home/tpeetz/projects/kontor/python/kontor-gui
# source file path
input_file = /home/tpeetz/projects/kontor/python/kontor-gui/main.py
# directory where exec is stored
exec_directory = .
# path to .pyproject project file
project_file =
# application icon
icon = /usr/local/lib/python3.11/dist-packages/PySide6/scripts/deploy_lib/pyside_icon.jpg
[python]
# python path
python_path = /home/tpeetz/projects/kontor/python/kontor-gui/env/bin/python
# python packages to install
packages = Nuitka==2.4.8
# buildozer = for deploying Android application
android_packages = buildozer==1.5.0,cython==0.29.33
[qt]
# comma separated path to qml files required
# normally all the qml files required by the project are added automatically
qml_files =
# excluded qml plugin binaries
excluded_qml_plugins =
# qt modules used. comma separated
modules = Widgets,DBus,Core,Gui
# qt plugins used by the application
plugins = platformthemes,imageformats,platforms,generic,iconengines,egldeviceintegrations,styles,xcbglintegrations,platforms/darwin,platforminputcontexts,accessiblebridge
[android]
# path to pyside wheel
wheel_pyside =
# path to shiboken wheel
wheel_shiboken =
# plugins to be copied to libs folder of the packaged application. comma separated
plugins =
[nuitka]
# usage description for permissions requested by the app as found in the info.plist file
# of the app bundle
# eg = extra_args = --show-modules --follow-stdlib
macos.permissions =
# mode of using nuitka. accepts standalone or onefile. default is onefile.
mode = onefile
# (str) specify any extra nuitka arguments
extra_args = --quiet --noinclude-qt-translations
[buildozer]
# build mode
# possible options = [release, debug]
# release creates an aab, while debug creates an apk
mode = debug
# contrains path to pyside6 and shiboken6 recipe dir
recipe_dir =
# path to extra qt android jars to be loaded by the application
jars_dir =
# if empty uses default ndk path downloaded by buildozer
ndk_path =
# if empty uses default sdk path downloaded by buildozer
sdk_path =
# other libraries to be loaded. comma separated.
# loaded at app startup
local_libs =
# architecture of deployed platform
# possible values = ["aarch64", "armv7a", "i686", "x86_64"]
arch =
+5
View File
@@ -0,0 +1,5 @@
home = /usr/bin
include-system-site-packages = false
version = 3.11.2
executable = /usr/bin/python3.11
command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-gui
+6
View File
@@ -0,0 +1,6 @@
-e ../kontor-schema
platformdirs
pyyaml
PySide6
Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

View File