From 3f65ec55fc72af46daab9a8eadab32ba813dc6f0 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 5 Dec 2024 20:40:13 +0100 Subject: [PATCH 01/16] Add Taskjuggler project file --- kontor.tjp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 kontor.tjp diff --git a/kontor.tjp b/kontor.tjp new file mode 100644 index 0000000..a5d46c6 --- /dev/null +++ b/kontor.tjp @@ -0,0 +1,30 @@ +project kontor "Kontor" "0.1.0" 2024-12-05 +5m { + timezone "Europe/Berlin" + timeformat "%d.%m.%Y" + numberformat "-" "" "" "," 1 + currencyformat "-" "" "" "," 0 + currency "EUR" + + scenario plan "Plan" { + scenario real "Realität" + } +} + +resource gcpce "Google Cloud Compute Engine" { + efficiency 0.0 + rate 0.25 +} + +task flask "Kontor-Flask" { + task import "Import repository kontor-flask into directory flask" +} +task springboot "Springboot Vaadin" { + task import "Import repository kontor-spring into directory springboot" +} + +taskreport "Arbeitsliste" { + formats html + hidetask ~isleaf() + sorttasks plan.end.up +} + -- 2.18.0 From 57e7b9e999389275133d3323bfbb5e6df32c1832 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 14:10:15 +0100 Subject: [PATCH 02/16] first implementation to show Comics and MediaFiles --- .gitignore | 5 ++ gui/application-export.png | Bin 0 -> 513 bytes gui/application-import.png | Bin 0 -> 524 bytes gui/arrow-circle-double.png | Bin 0 -> 836 bytes gui/comic_model.py | 89 ++++++++++++++++++++++++ gui/cross.png | Bin 0 -> 544 bytes gui/kontor.py | 131 ++++++++++++++++++++++++++++++++++++ gui/media_file_model.py | 91 +++++++++++++++++++++++++ gui/resources.py | 7 ++ gui/tick.png | Bin 0 -> 634 bytes 10 files changed, 323 insertions(+) create mode 100644 .gitignore create mode 100644 gui/application-export.png create mode 100644 gui/application-import.png create mode 100644 gui/arrow-circle-double.png create mode 100644 gui/comic_model.py create mode 100644 gui/cross.png create mode 100644 gui/kontor.py create mode 100644 gui/media_file_model.py create mode 100644 gui/resources.py create mode 100644 gui/tick.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45f246d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +.idea/ +bonus +icons +icons-shadowless diff --git a/gui/application-export.png b/gui/application-export.png new file mode 100644 index 0000000000000000000000000000000000000000..555887a28d64bc812c4dfa98a6ff1da1927b7792 GIT binary patch literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G literal 0 HcmV?d00001 diff --git a/gui/comic_model.py b/gui/comic_model.py new file mode 100644 index 0000000..2472670 --- /dev/null +++ b/gui/comic_model.py @@ -0,0 +1,89 @@ +from datetime import datetime + +import mariadb +from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt +from PySide6.QtGui import QColor + + +class ComicTableModel(QAbstractTableModel): + + def __init__(self, db_config, main_window): + super().__init__() + self.main_window = main_window + self._data = [] + self.status_bar = main_window.statusBar + self.mariadb_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + self.refresh() + + def refresh(self): + data = [] + cursor = self.mariadb_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date, title, publisher_id FROM comic") + rows = cursor.fetchall() + for row in rows: + data.append(list(row)) + self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) + self._data = data + + def rowCount(self, parent=QModelIndex()): + # The length of the outer list. + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + match col: + case 0: + return "ID" + case 1: + return "Created" + case 2: + return "Updated" + case 3: + return "Title" + case 4: + return "Verlag" + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col + 1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + value = self._data[index.row()][index.column()] + if role == Qt.ItemDataRole.DisplayRole: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if isinstance(value, str): + return value + if isinstance(value, bytes): + if value == b'\x01': + return "True" + return "False" + return value + if role == Qt.ItemDataRole.DecorationRole: + if isinstance(value, bytes): + # print('{}: {}'.format(value, type(value))) + if value == b'\x01': + return self.main_window.tick + else: + return self.main_window.cross + + + def columnCount(self, index=QModelIndex()): + # The following takes the first sub-list, and returns + # the length (only works if all rows are an equal length) + return len(self._data[0]) + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + if role == Qt.ItemDataRole.CheckStateRole: + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return True + + def flags(self, index): + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/gui/cross.png b/gui/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001 -- 2.18.0 From 9dcc09c586982a072bfe57eca102ed01f19abde4 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 14:13:15 +0100 Subject: [PATCH 03/16] display tick and cross for boolean values --- gui/media_file_model.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/media_file_model.py b/gui/media_file_model.py index 619ce2e..f06d94f 100644 --- a/gui/media_file_model.py +++ b/gui/media_file_model.py @@ -60,11 +60,11 @@ class MediaFileTableModel(QAbstractTableModel): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): return value -# if isinstance(value, bytes): -# if value == b'\x01': -# return self.main_window.tick -# else: -# return self.main_window.cross + if isinstance(value, bytes): + if value == b'\x01': + return self.main_window.tick + else: + return self.main_window.cross return value if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): -- 2.18.0 From d98dc79cf8326f43daccae5f692e0cd163c4a844 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 21:47:27 +0100 Subject: [PATCH 04/16] refresh table when updated --- gui/kontor.py | 27 ++++++++++++++----- gui/media_file_model.py | 36 +++++++++++++++++++++----- gui/{ => res}/application-export.png | Bin gui/{ => res}/application-import.png | Bin gui/{ => res}/arrow-circle-double.png | Bin gui/{ => res}/cross.png | Bin gui/{ => res}/tick.png | Bin 7 files changed, 50 insertions(+), 13 deletions(-) rename gui/{ => res}/application-export.png (100%) rename gui/{ => res}/application-import.png (100%) rename gui/{ => res}/arrow-circle-double.png (100%) rename gui/{ => res}/cross.png (100%) rename gui/{ => res}/tick.png (100%) diff --git a/gui/kontor.py b/gui/kontor.py index a435fe6..be15c66 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -6,8 +6,7 @@ from pathlib import Path import yaml from PySide6.QtGui import QAction, QIcon -from PySide6.QtSql import QSqlDatabase -from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QHBoxLayout, QCheckBox from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs @@ -21,11 +20,11 @@ class MainWindow(QMainWindow): def __init__(self, config): super().__init__() - self.tick = QIcon('tick.png') - self.cross = QIcon('cross.png') - self.import_icon = QIcon("application-import.png") - self.export_icon = QIcon("application-export.png") - self.circle_icon = QIcon("arrow-circle-double.png") + 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.setWindowTitle("Kontor") self.setMinimumSize(800, 500) @@ -35,6 +34,7 @@ class MainWindow(QMainWindow): self._createStatusBar() self.data = [] + self.filter = {} self.central_widget = QWidget() parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) @@ -111,11 +111,24 @@ class MainWindow(QMainWindow): def generate_tab_media_file(self, db_configuration): media_file_tab = QWidget() layout = QVBoxLayout() + filter_layout = QHBoxLayout() + download_checkbox = QCheckBox() + download_checkbox.setText("Download") + download_checkbox.checkStateChanged.connect(self.refresh) + self.filter["download"] = download_checkbox + review_checkbox = QCheckBox() + review_checkbox.setText("Review") + review_checkbox.checkStateChanged.connect(self.refresh) + self.filter["review"] = review_checkbox + filter_layout.addWidget(review_checkbox) + filter_layout.addWidget(download_checkbox) + filter_layout.addStretch() model = MediaFileTableModel(db_configuration, self) self.data.append(model) media_file_tab.setLayout(layout) table_view = QTableView() table_view.setModel(model) + layout.addLayout(filter_layout) layout.addWidget(table_view) return media_file_tab diff --git a/gui/media_file_model.py b/gui/media_file_model.py index f06d94f..e34ad26 100644 --- a/gui/media_file_model.py +++ b/gui/media_file_model.py @@ -11,7 +11,6 @@ class MediaFileTableModel(QAbstractTableModel): super().__init__() self.main_window = main_window self._data = [] - self.status_bar = main_window.statusBar self.mariadb_conn = mariadb.connect( host=db_config['mariadb']['host'], port=db_config['mariadb']['port'], @@ -24,12 +23,37 @@ class MediaFileTableModel(QAbstractTableModel): def refresh(self): data = [] cursor = self.mariadb_conn.cursor() - cursor.execute("SELECT id, url, review, should_download, file_name, cloud_link FROM media_file") + filter_rule = "" + print(self.main_window.filter["download"].isChecked()) + if self.main_window.filter["download"].isChecked(): + print(self.main_window.filter["download"].isChecked()) + filter_rule = "WHERE should_download is true" + if self.main_window.filter["review"].isChecked(): + if len(filter_rule) > 0: + filter_rule += " AND " + else: + filter_rule += "WHERE " + filter_rule += "review is true" + print(f"{filter_rule=}") + try: + cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") + except mariadb.Error as error: + print(error) rows = cursor.fetchall() - for row in rows: - data.append(list(row)) - self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) - self._data = data + print(len(rows)) + if len(rows) > 0: + self.beginResetModel() + for row in rows: + data.append(list(row)) + self._data = data + self.endResetModel() + else: + self._data = None + try: + self.layoutChanged.emit() + except: + + self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. diff --git a/gui/application-export.png b/gui/res/application-export.png similarity index 100% rename from gui/application-export.png rename to gui/res/application-export.png diff --git a/gui/application-import.png b/gui/res/application-import.png similarity index 100% rename from gui/application-import.png rename to gui/res/application-import.png diff --git a/gui/arrow-circle-double.png b/gui/res/arrow-circle-double.png similarity index 100% rename from gui/arrow-circle-double.png rename to gui/res/arrow-circle-double.png diff --git a/gui/cross.png b/gui/res/cross.png similarity index 100% rename from gui/cross.png rename to gui/res/cross.png diff --git a/gui/tick.png b/gui/res/tick.png similarity index 100% rename from gui/tick.png rename to gui/res/tick.png -- 2.18.0 From c6d1e4d7e7e64c3eff2f0473b93a87230690110d Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 23:44:29 +0100 Subject: [PATCH 05/16] fix errors when displaying empty table --- gui/media_file_model.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/media_file_model.py b/gui/media_file_model.py index e34ad26..e5af2a4 100644 --- a/gui/media_file_model.py +++ b/gui/media_file_model.py @@ -35,10 +35,7 @@ class MediaFileTableModel(QAbstractTableModel): filter_rule += "WHERE " filter_rule += "review is true" print(f"{filter_rule=}") - try: - cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") - except mariadb.Error as error: - print(error) + cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") rows = cursor.fetchall() print(len(rows)) if len(rows) > 0: @@ -49,14 +46,13 @@ class MediaFileTableModel(QAbstractTableModel): self.endResetModel() else: self._data = None - try: - self.layoutChanged.emit() - except: - + self.layoutChanged.emit() self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # 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): @@ -78,6 +74,8 @@ class MediaFileTableModel(QAbstractTableModel): 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()] if role == Qt.ItemDataRole.DisplayRole: if isinstance(value, datetime): @@ -101,6 +99,8 @@ class MediaFileTableModel(QAbstractTableModel): def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns # the length (only works if all rows are an equal length) + if self._data is None: + return 5 return len(self._data[0]) def setData(self, index, value, role=Qt.ItemDataRole.EditRole): -- 2.18.0 From 3aed8af868e07657d5dade309a0b9a147486c73b Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 6 Jan 2025 17:07:20 +0100 Subject: [PATCH 06/16] implement generic table model implement generic table model which reads table info from db and constructs table view --- gui/kontor.py | 40 ++++------ gui/model_config.py | 82 +++++++++++++++++++++ gui/{media_file_model.py => table_model.py} | 71 +++++------------- 3 files changed, 117 insertions(+), 76 deletions(-) create mode 100644 gui/model_config.py rename gui/{media_file_model.py => table_model.py} (52%) diff --git a/gui/kontor.py b/gui/kontor.py index be15c66..3b601ad 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -6,17 +6,17 @@ from pathlib import Path import yaml from PySide6.QtGui import QAction, QIcon -from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QHBoxLayout, QCheckBox +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs from comic_model import ComicTableModel -from media_file_model import MediaFileTableModel +from model_config import KontorModelConfig +from table_model import KontorTableModel class MainWindow(QMainWindow): - def __init__(self, config): super().__init__() @@ -28,10 +28,10 @@ class MainWindow(QMainWindow): self.setWindowTitle("Kontor") self.setMinimumSize(800, 500) - self._createActions() - self._createMenuBar() - self._createToolBars() - self._createStatusBar() + self._create_actions() + self._create_menubar() + self._create_toolbars() + self._create_statusbar() self.data = [] self.filter = {} @@ -47,7 +47,7 @@ class MainWindow(QMainWindow): self.setCentralWidget(self.central_widget) - def _createActions(self): + def _create_actions(self): self.newAction = QAction("&New", self) self.aboutAction = QAction("&Über...", self) self.aboutAction.triggered.connect(self.about) @@ -59,7 +59,7 @@ class MainWindow(QMainWindow): self.exitAction.setShortcut("Alt+F4") self.exitAction.triggered.connect(self.close) - def _createMenuBar(self): + def _create_menubar(self): menu_bar = self.menuBar() # File menu file_menu = QMenu("&Datei") @@ -75,14 +75,14 @@ class MainWindow(QMainWindow): menu_bar.addMenu(help_menu) help_menu.addAction(self.aboutAction) - def _createToolBars(self): + 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 _createStatusBar(self): + def _create_statusbar(self): self.statusBar = self.statusBar() self.statusBar.showMessage("Kontor ready", 6000) self.status_label = QLabel("") @@ -110,25 +110,15 @@ class MainWindow(QMainWindow): def generate_tab_media_file(self, db_configuration): media_file_tab = QWidget() + table_config = KontorModelConfig(db_configuration, self, "media_file") + model = KontorTableModel(table_config) layout = QVBoxLayout() - filter_layout = QHBoxLayout() - download_checkbox = QCheckBox() - download_checkbox.setText("Download") - download_checkbox.checkStateChanged.connect(self.refresh) - self.filter["download"] = download_checkbox - review_checkbox = QCheckBox() - review_checkbox.setText("Review") - review_checkbox.checkStateChanged.connect(self.refresh) - self.filter["review"] = review_checkbox - filter_layout.addWidget(review_checkbox) - filter_layout.addWidget(download_checkbox) - filter_layout.addStretch() - model = MediaFileTableModel(db_configuration, self) + # model = MediaFileTableModel(db_configuration, self) self.data.append(model) media_file_tab.setLayout(layout) table_view = QTableView() table_view.setModel(model) - layout.addLayout(filter_layout) + layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) return media_file_tab diff --git a/gui/model_config.py b/gui/model_config.py new file mode 100644 index 0000000..1bea8a5 --- /dev/null +++ b/gui/model_config.py @@ -0,0 +1,82 @@ +import mariadb +from PySide6.QtWidgets import QHBoxLayout, QCheckBox + + +class KontorModelConfig: + + def __init__(self, db_config, main_window, table_name: str): + self.header = [] + self.filter = {} + self.main_window = main_window + self._table = table_name + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + self.get_table_config() + + def get_table_id(self): + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (self._table, )) + rows = cursor.fetchall() + if len(rows) == 1: + return rows[0][0] + return None + + def get_table_config(self): + table_id = self.get_table_id() + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, column_name, column_order FROM meta_data_column WHERE table_id=? AND is_shown is true", (table_id, )) + rows = cursor.fetchall() + self.header.clear() + for (column_id, column_name, column_order) in rows: + self.header.insert(column_order-1, column_name) + print(f"retrieved {len(rows)} columns, set {len(self.header)} headers") + + def get_header(self) -> list: + self.get_table_config() + return self.header + + def get_filter(self) -> str: + filter_rule = "" + # print(self.filter["download"].isChecked()) + if self.filter["download"].isChecked(): + # print(self.filter["download"].isChecked()) + filter_rule = "WHERE should_download is true" + if self.filter["review"].isChecked(): + if len(filter_rule) > 0: + filter_rule += " AND " + else: + filter_rule += "WHERE " + filter_rule += "review is true" + print(f"{filter_rule=}") + return filter_rule + + def get_statement(self) -> str: + filter_rule = self.get_filter() + self.get_table_config() + columns = "" + for index in range(len(self.header)): + if index > 0: + columns += ", " + columns += self.header[index] + statement = f"SELECT {columns} FROM media_file {filter_rule}" + return statement + + def get_filter_layout(self) -> QHBoxLayout: + filter_layout = QHBoxLayout() + download_checkbox = QCheckBox() + download_checkbox.setText("Download") + download_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter["download"] = download_checkbox + review_checkbox = QCheckBox() + review_checkbox.setText("Review") + review_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter["review"] = review_checkbox + filter_layout.addWidget(review_checkbox) + filter_layout.addWidget(download_checkbox) + filter_layout.addStretch() + return filter_layout diff --git a/gui/media_file_model.py b/gui/table_model.py similarity index 52% rename from gui/media_file_model.py rename to gui/table_model.py index e5af2a4..5fc5dc9 100644 --- a/gui/media_file_model.py +++ b/gui/table_model.py @@ -1,41 +1,23 @@ from datetime import datetime -import mariadb -from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt -from PySide6.QtGui import QColor +from PySide6.QtCore import QAbstractTableModel, QModelIndex +from PySide6.QtGui import Qt + +from model_config import KontorModelConfig -class MediaFileTableModel(QAbstractTableModel): +class KontorTableModel(QAbstractTableModel): - def __init__(self, db_config, main_window): + def __init__(self, model_config: KontorModelConfig): super().__init__() - self.main_window = main_window - self._data = [] - self.mariadb_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - self.refresh() + self._main_window = model_config.main_window + self._config = model_config + self._data = None def refresh(self): data = [] - cursor = self.mariadb_conn.cursor() - filter_rule = "" - print(self.main_window.filter["download"].isChecked()) - if self.main_window.filter["download"].isChecked(): - print(self.main_window.filter["download"].isChecked()) - filter_rule = "WHERE should_download is true" - if self.main_window.filter["review"].isChecked(): - if len(filter_rule) > 0: - filter_rule += " AND " - else: - filter_rule += "WHERE " - filter_rule += "review is true" - print(f"{filter_rule=}") - cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") + cursor = self._config.db_conn.cursor() + cursor.execute(self._config.get_statement()) rows = cursor.fetchall() print(len(rows)) if len(rows) > 0: @@ -46,8 +28,8 @@ class MediaFileTableModel(QAbstractTableModel): self.endResetModel() else: self._data = None - self.layoutChanged.emit() - self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) + self.layoutChanged.emit() + self._main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. @@ -57,19 +39,7 @@ class MediaFileTableModel(QAbstractTableModel): def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - match col: - case 0: - return "ID" - case 1: - return "URL" - case 2: - return "Review" - case 3: - return "Download" - case 4: - return "Filename" - case 5: - return "Cloud Link" + return self._config.header[col] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: return str(col+1) @@ -84,24 +54,23 @@ class MediaFileTableModel(QAbstractTableModel): return value if isinstance(value, bytes): if value == b'\x01': - return self.main_window.tick + return self._main_window.tick else: - return self.main_window.cross + return self._main_window.cross return value if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): # print('{}: {}'.format(value, type(value))) if value == b'\x01': - return self.main_window.tick + return self._main_window.tick else: - return self.main_window.cross + return self._main_window.cross def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns # the length (only works if all rows are an equal length) - if self._data is None: - return 5 - return len(self._data[0]) + print(f"Header count: {len(self._config.get_header())}") + return len(self._config.get_header()) def setData(self, index, value, role=Qt.ItemDataRole.EditRole): if role == Qt.ItemDataRole.EditRole: -- 2.18.0 From 78632e0e12a6264058c164be6851b8db886160ed Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 6 Jan 2025 17:09:38 +0000 Subject: [PATCH 07/16] Import kontor-spring into directory springboot --- .gitignore | 8 +- springboot/.gitattributes | 9 + springboot/.gitignore | 32 + springboot/README.md | 3 + springboot/build.gradle | 240 ++++ springboot/frontend/themes/kontor/styles.css | 10 + springboot/frontend/themes/kontor/theme.json | 3 + springboot/gradle.properties | 3 + springboot/gradle/libs.versions.toml | 58 + springboot/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + springboot/gradlew | 249 ++++ springboot/gradlew.bat | 92 ++ springboot/settings.gradle | 24 + .../src/docs/asciidoc/kontor-spring.adoc | 509 +++++++ .../kontor/comics/views/ArtistViewTest.java | 41 + .../kontor/comics/views/ArtistformTest.java | 63 + .../kontor/comics/views/ComicViewTest.java | 43 + .../comics/views/ComicWorkViewTest.java | 43 + .../kontor/comics/views/IssueViewTest.java | 43 + .../comics/views/PublisherViewTest.java | 41 + .../kontor/comics/views/StoryArcViewTest.java | 43 + .../comics/views/TradePaperbackViewTest.java | 47 + .../kontor/comics/views/VolumeViewTest.java | 50 + .../kontor/comics/views/WorktypeViewTest.java | 49 + .../kontor/tysc/views/CardSetViewTest.java | 43 + .../kontor/tysc/views/CardViewTest.java | 43 + .../tysc/views/FieldPositionViewTest.java | 43 + .../kontor/tysc/views/PlayerViewTest.java | 44 + .../kontor/tysc/views/RoosterViewTest.java | 43 + .../kontor/tysc/views/SportViewTest.java | 43 + .../kontor/tysc/views/TeamViewTest.java | 43 + .../kontor/tysc/views/VendorViewTest.java | 43 + .../resources/application.properties | 30 + .../java/de/thpeetz/kontor/Application.java | 24 + .../thpeetz/kontor/admin/AdminConstants.java | 53 + .../thpeetz/kontor/admin/MailProperties.java | 78 ++ .../kontor/admin/SetupModuleAdmin.java | 336 +++++ .../admin/data/AuthorizationMatrix.java | 35 + .../data/AuthorizationMatrixRepository.java | 13 + .../admin/data/MailAccountRepository.java | 8 + .../kontor/admin/data/MetaDataColumn.java | 37 + .../admin/data/MetaDataColumnRepository.java | 10 + .../kontor/admin/data/MetaDataTable.java | 26 + .../admin/data/MetaDataTableRepository.java | 8 + .../thpeetz/kontor/admin/data/ModuleData.java | 25 + .../admin/data/ModuleDataRepository.java | 15 + .../de/thpeetz/kontor/admin/data/Role.java | 30 + .../kontor/admin/data/RoleRepository.java | 18 + .../de/thpeetz/kontor/admin/data/User.java | 62 + .../kontor/admin/data/UserRepository.java | 16 + .../kontor/admin/services/AdminService.java | 110 ++ .../services/KontorUserDetailsService.java | 125 ++ .../kontor/admin/services/MailService.java | 24 + .../admin/services/MetaDataService.java | 101 ++ .../kontor/admin/services/ModuleService.java | 76 ++ .../kontor/admin/views/AdminLayout.java | 36 + .../kontor/admin/views/AuthorizationForm.java | 112 ++ .../kontor/admin/views/AuthorizationView.java | 114 ++ .../thpeetz/kontor/admin/views/LoginView.java | 51 + .../kontor/admin/views/MetaDataForm.java | 113 ++ .../kontor/admin/views/MetaDataView.java | 112 ++ .../kontor/admin/views/ModuleDataForm.java | 100 ++ .../kontor/admin/views/ModuleDataView.java | 118 ++ .../thpeetz/kontor/admin/views/RoleForm.java | 102 ++ .../thpeetz/kontor/admin/views/RoleView.java | 118 ++ .../thpeetz/kontor/admin/views/UserForm.java | 143 ++ .../kontor/admin/views/UserProfileView.java | 73 + .../thpeetz/kontor/admin/views/UserView.java | 135 ++ .../kontor/bookshelf/BookshelfConstants.java | 53 + .../bookshelf/SetupModuleBookshelf.java | 82 ++ .../kontor/bookshelf/data/Article.java | 27 + .../kontor/bookshelf/data/ArticleAuthor.java | 29 + .../data/ArticleAuthorRepository.java | 12 + .../bookshelf/data/ArticleRepository.java | 16 + .../thpeetz/kontor/bookshelf/data/Author.java | 44 + .../bookshelf/data/AuthorRepository.java | 18 + .../thpeetz/kontor/bookshelf/data/Book.java | 42 + .../kontor/bookshelf/data/BookAuthor.java | 29 + .../bookshelf/data/BookAuthorRepository.java | 12 + .../kontor/bookshelf/data/BookRepository.java | 21 + .../bookshelf/data/BookshelfPublisher.java | 32 + .../data/BookshelfPublisherRepository.java | 18 + .../bookshelf/services/BookshelfService.java | 118 ++ .../kontor/bookshelf/views/ArticleForm.java | 106 ++ .../kontor/bookshelf/views/ArticleView.java | 126 ++ .../kontor/bookshelf/views/AuthorForm.java | 117 ++ .../kontor/bookshelf/views/AuthorView.java | 132 ++ .../kontor/bookshelf/views/BookForm.java | 111 ++ .../kontor/bookshelf/views/BookView.java | 124 ++ .../bookshelf/views/BookshelfLayout.java | 34 + .../views/BookshelfPublisherView.java | 131 ++ .../kontor/bookshelf/views/PublisherForm.java | 108 ++ .../thpeetz/kontor/comics/ComicConstants.java | 93 ++ .../kontor/comics/SetupModuleComics.java | 1198 +++++++++++++++++ .../de/thpeetz/kontor/comics/data/Artist.java | 44 + .../kontor/comics/data/ArtistRepository.java | 17 + .../de/thpeetz/kontor/comics/data/Comic.java | 77 ++ .../kontor/comics/data/ComicRepository.java | 19 + .../thpeetz/kontor/comics/data/ComicWork.java | 48 + .../comics/data/ComicWorkRepository.java | 15 + .../de/thpeetz/kontor/comics/data/Issue.java | 42 + .../kontor/comics/data/IssueRepository.java | 17 + .../thpeetz/kontor/comics/data/Publisher.java | 40 + .../comics/data/PublisherRepository.java | 17 + .../thpeetz/kontor/comics/data/StoryArc.java | 31 + .../comics/data/StoryArcRepository.java | 17 + .../kontor/comics/data/TradePaperback.java | 32 + .../comics/data/TradePaperbackRepository.java | 20 + .../de/thpeetz/kontor/comics/data/Volume.java | 42 + .../kontor/comics/data/VolumeRepository.java | 13 + .../thpeetz/kontor/comics/data/Worktype.java | 44 + .../comics/data/WorktypeRepository.java | 18 + .../kontor/comics/services/ComicService.java | 299 ++++ .../kontor/comics/views/ArtistForm.java | 117 ++ .../kontor/comics/views/ArtistView.java | 129 ++ .../kontor/comics/views/ComicForm.java | 130 ++ .../kontor/comics/views/ComicLayout.java | 43 + .../kontor/comics/views/ComicView.java | 139 ++ .../kontor/comics/views/ComicWorkForm.java | 113 ++ .../kontor/comics/views/ComicWorkView.java | 122 ++ .../kontor/comics/views/IssueForm.java | 113 ++ .../kontor/comics/views/IssueView.java | 130 ++ .../kontor/comics/views/PublisherForm.java | 100 ++ .../kontor/comics/views/PublisherView.java | 130 ++ .../kontor/comics/views/StoryArcForm.java | 108 ++ .../kontor/comics/views/StoryArcView.java | 130 ++ .../comics/views/TradePaperBackForm.java | 109 ++ .../comics/views/TradePaperbackView.java | 129 ++ .../kontor/comics/views/VolumeForm.java | 109 ++ .../kontor/comics/views/VolumeView.java | 130 ++ .../kontor/comics/views/WorktypeForm.java | 121 ++ .../kontor/comics/views/WorktypeView.java | 130 ++ .../kontor/common/data/AbstractEntity.java | 34 + .../common/data/AbstractLinkEntity.java | 55 + .../kontor/common/views/AvatarMenuBar.java | 52 + .../kontor/common/views/KontorLayoutUtil.java | 100 ++ .../kontor/common/views/MainLayout.java | 101 ++ .../thpeetz/kontor/common/views/MainView.java | 18 + .../common/views/SeparateMainLayout.java | 31 + .../thpeetz/kontor/mailclient/data/Mail.java | 20 + .../kontor/mailclient/data/MailAccount.java | 29 + .../kontor/mailclient/views/EmailView.java | 222 +++ .../thpeetz/kontor/media/MediaConstants.java | 35 + .../kontor/media/SetupModuleMedia.java | 39 + .../kontor/media/data/MediaArticle.java | 29 + .../media/data/MediaArticleRepository.java | 13 + .../thpeetz/kontor/media/data/MediaFile.java | 40 + .../media/data/MediaFileRepository.java | 14 + .../thpeetz/kontor/media/data/MediaLink.java | 40 + .../thpeetz/kontor/media/data/MediaVideo.java | 40 + .../media/data/MediaVideoRepository.java | 15 + .../media/services/MediaArticleService.java | 42 + .../media/services/MediaFileService.java | 42 + .../media/services/MediaVideoService.java | 39 + .../kontor/media/views/MediaArticleForm.java | 108 ++ .../kontor/media/views/MediaArticleView.java | 171 +++ .../kontor/media/views/MediaFileForm.java | 113 ++ .../kontor/media/views/MediaFileView.java | 186 +++ .../kontor/media/views/MediaVideoForm.java | 113 ++ .../kontor/media/views/MediaVideoView.java | 178 +++ .../kontor/security/SecurityConfig.java | 59 + .../kontor/security/SecurityService.java | 105 ++ .../thpeetz/kontor/tysc/SetupModuleTysc.java | 519 +++++++ .../de/thpeetz/kontor/tysc/TyscConstants.java | 88 ++ .../de/thpeetz/kontor/tysc/data/Card.java | 49 + .../kontor/tysc/data/CardRepository.java | 17 + .../de/thpeetz/kontor/tysc/data/CardSet.java | 41 + .../kontor/tysc/data/CardSetRepository.java | 18 + .../kontor/tysc/data/FieldPosition.java | 52 + .../tysc/data/FieldPositionRepository.java | 22 + .../de/thpeetz/kontor/tysc/data/Player.java | 49 + .../kontor/tysc/data/PlayerRepository.java | 17 + .../de/thpeetz/kontor/tysc/data/Rooster.java | 51 + .../kontor/tysc/data/RoosterRepository.java | 11 + .../de/thpeetz/kontor/tysc/data/Sport.java | 42 + .../kontor/tysc/data/SportRepository.java | 17 + .../de/thpeetz/kontor/tysc/data/Team.java | 50 + .../kontor/tysc/data/TeamRepository.java | 27 + .../de/thpeetz/kontor/tysc/data/Vendor.java | 38 + .../kontor/tysc/data/VendorRepository.java | 17 + .../kontor/tysc/services/CardService.java | 93 ++ .../kontor/tysc/services/SportService.java | 147 ++ .../thpeetz/kontor/tysc/views/CardForm.java | 110 ++ .../kontor/tysc/views/CardSetForm.java | 103 ++ .../kontor/tysc/views/CardSetView.java | 129 ++ .../thpeetz/kontor/tysc/views/CardView.java | 129 ++ .../thpeetz/kontor/tysc/views/PlayerForm.java | 116 ++ .../thpeetz/kontor/tysc/views/PlayerView.java | 130 ++ .../kontor/tysc/views/PositionForm.java | 115 ++ .../kontor/tysc/views/PositionView.java | 130 ++ .../kontor/tysc/views/RoosterForm.java | 117 ++ .../kontor/tysc/views/RoosterView.java | 121 ++ .../thpeetz/kontor/tysc/views/SportForm.java | 103 ++ .../thpeetz/kontor/tysc/views/SportView.java | 129 ++ .../thpeetz/kontor/tysc/views/TeamForm.java | 115 ++ .../thpeetz/kontor/tysc/views/TeamView.java | 130 ++ .../thpeetz/kontor/tysc/views/TyscLayout.java | 41 + .../thpeetz/kontor/tysc/views/VendorForm.java | 115 ++ .../thpeetz/kontor/tysc/views/VendorView.java | 130 ++ .../META-INF/resources/icons/icon.png | Bin 0 -> 45967 bytes .../META-INF/resources/images/offline.png | Bin 0 -> 9507 bytes .../resources/META-INF/resources/offline.html | 38 + springboot/src/main/resources/application.yml | 67 + springboot/src/main/resources/banner.txt | 8 + .../src/main/resources/logback-spring.xml | 42 + .../de/thpeetz/kontor/ApplicationTests.java | 13 + .../kontor/bookshelf/TestConstants.java | 19 + .../data/ArticleAuthorRepositoryTest.java | 74 + .../bookshelf/data/ArticleAuthorTest.java | 77 ++ .../bookshelf/data/ArticleRepositoryTest.java | 61 + .../kontor/bookshelf/data/ArticleTest.java | 45 + .../bookshelf/data/AuthorRepositoryTest.java | 69 + .../kontor/bookshelf/data/AuthorTest.java | 44 + .../data/BookAuthorRepositoryTest.java | 94 ++ .../kontor/bookshelf/data/BookAuthorTest.java | 82 ++ .../bookshelf/data/BookRepositoryTest.java | 87 ++ .../kontor/bookshelf/data/BookTest.java | 56 + .../BookshelfPublisherRepositoryTest.java | 72 + .../data/BookshelfPublisherTest.java | 45 + .../services/BookshelfServiceTest.java | 102 ++ .../kontor/comics/ComicConstantsTest.java | 14 + .../thpeetz/kontor/comics/TestConstants.java | 67 + .../comics/data/ArtistRepositoryTest.java | 73 + .../kontor/comics/data/ArtistTest.java | 48 + .../comics/data/ComicRepositoryTest.java | 53 + .../thpeetz/kontor/comics/data/ComicTest.java | 77 ++ .../comics/data/ComicWorkRepositoryTest.java | 83 ++ .../kontor/comics/data/ComicWorkTest.java | 24 + .../comics/data/IssueRepositoryTest.java | 45 + .../thpeetz/kontor/comics/data/IssueTest.java | 62 + .../comics/data/PublisherRepositoryTest.java | 68 + .../kontor/comics/data/PublisherTest.java | 45 + .../comics/data/StoryArcRepositoryTest.java | 45 + .../kontor/comics/data/StoryArcTest.java | 61 + .../data/TradePaperbackRepositoryTest.java | 59 + .../comics/data/TradePaperbackTest.java | 59 + .../comics/data/VolumeRepositoryTest.java | 61 + .../kontor/comics/data/VolumeTest.java | 64 + .../comics/data/WorktypeRepositoryTest.java | 46 + .../kontor/comics/data/WorktypeTest.java | 83 ++ .../comics/services/ComicServiceTest.java | 350 +++++ .../thpeetz/kontor/media/TestConstants.java | 5 + .../kontor/media/data/MediaArticleTest.java | 32 + .../kontor/media/data/MediaFileTest.java | 37 + .../kontor/media/data/MediaVideoTest.java | 38 + .../services/MediaArticleServiceTest.java | 50 + .../media/services/MediaFileServiceTest.java | 48 + .../media/services/MediaVideoServiceTest.java | 48 + .../de/thpeetz/kontor/tysc/TestConstants.java | 39 + .../kontor/tysc/data/CardRepositoryTest.java | 35 + .../tysc/data/CardSetRepositoryTest.java | 42 + .../thpeetz/kontor/tysc/data/CardSetTest.java | 22 + .../de/thpeetz/kontor/tysc/data/CardTest.java | 22 + .../data/FieldPositionRepositoryTest.java | 59 + .../kontor/tysc/data/FieldPositionTest.java | 44 + .../tysc/data/PlayerRepositoryTest.java | 31 + .../thpeetz/kontor/tysc/data/PlayerTest.java | 53 + .../tysc/data/RoosterRepositoryTest.java | 29 + .../thpeetz/kontor/tysc/data/RoosterTest.java | 47 + .../kontor/tysc/data/SportRepositoryTest.java | 40 + .../thpeetz/kontor/tysc/data/SportTest.java | 44 + .../kontor/tysc/data/TeamRepositoryTest.java | 37 + .../de/thpeetz/kontor/tysc/data/TeamTest.java | 53 + .../tysc/data/VendorRepositoryTest.java | 41 + .../thpeetz/kontor/tysc/data/VendorTest.java | 44 + .../kontor/tysc/services/CardServiceTest.java | 126 ++ .../tysc/services/SportServiceTest.java | 200 +++ .../src/test/resources/application.properties | 30 + 269 files changed, 19844 insertions(+), 4 deletions(-) create mode 100644 springboot/.gitattributes create mode 100644 springboot/.gitignore create mode 100644 springboot/README.md create mode 100644 springboot/build.gradle create mode 100644 springboot/frontend/themes/kontor/styles.css create mode 100644 springboot/frontend/themes/kontor/theme.json create mode 100644 springboot/gradle.properties create mode 100644 springboot/gradle/libs.versions.toml create mode 100644 springboot/gradle/wrapper/gradle-wrapper.jar create mode 100644 springboot/gradle/wrapper/gradle-wrapper.properties create mode 100755 springboot/gradlew create mode 100644 springboot/gradlew.bat create mode 100644 springboot/settings.gradle create mode 100644 springboot/src/docs/asciidoc/kontor-spring.adoc create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java create mode 100644 springboot/src/integrationTest/resources/application.properties create mode 100644 springboot/src/main/java/de/thpeetz/kontor/Application.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/MailProperties.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrix.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrixRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MailAccountRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumnRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTableRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleDataRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/Role.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/RoleRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/UserRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/MailService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/UserForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/UserView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Comic.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Issue.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Volume.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/MainView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/security/SecurityService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Card.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSetRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPositionRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Player.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/PlayerRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/RoosterRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/SportRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/TeamRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/VendorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java create mode 100644 springboot/src/main/resources/META-INF/resources/icons/icon.png create mode 100644 springboot/src/main/resources/META-INF/resources/images/offline.png create mode 100644 springboot/src/main/resources/META-INF/resources/offline.html create mode 100644 springboot/src/main/resources/application.yml create mode 100644 springboot/src/main/resources/banner.txt create mode 100644 springboot/src/main/resources/logback-spring.xml create mode 100644 springboot/src/test/java/de/thpeetz/kontor/ApplicationTests.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/TestConstants.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/TestConstants.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java create mode 100644 springboot/src/test/resources/application.properties diff --git a/.gitignore b/.gitignore index 45f246d..575904a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -__pycache__ .idea/ -bonus -icons -icons-shadowless +__pycache__/ +bonus/ +icons/ +icons-shadowless/ diff --git a/springboot/.gitattributes b/springboot/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/springboot/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/springboot/.gitignore b/springboot/.gitignore new file mode 100644 index 0000000..0294d4c --- /dev/null +++ b/springboot/.gitignore @@ -0,0 +1,32 @@ +.gradle/ +.settings/ +build/ +bin/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +.project +.classpath +.vscode/ +.idea/ +*.lock +logs/ +frontend/generated +frontend/index.html +package*.json +tsconfig.json +types.d.ts +node_modules/ +vite.* +kontor*Db +tags* +kontorHSQLDB* +.vs/ +.winget +src/main/resources/application-local.properties +src/main/resources/application-prod.properties +src/main/resources/application-*.yml diff --git a/springboot/README.md b/springboot/README.md new file mode 100644 index 0000000..f24bbcd --- /dev/null +++ b/springboot/README.md @@ -0,0 +1,3 @@ +# kontor-spring + +Kontor Anwendung mit Spring Boot und Vaadin \ No newline at end of file diff --git a/springboot/build.gradle b/springboot/build.gradle new file mode 100644 index 0000000..8892faf --- /dev/null +++ b/springboot/build.gradle @@ -0,0 +1,240 @@ +buildscript { + configurations.classpath { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'com.burgstaller' && details.requested.name == 'okhttp-digest' && details.requested.version == '1.10') { + details.useTarget "io.github.rburgst:${details.requested.name}:1.21" + details.because 'Dependency has moved' + } + } + } + repositories { + mavenCentral() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + } +} + +plugins { + id 'java' + id 'application' + id 'maven-publish' + id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.0" + id 'jvm-test-suite' + id 'jacoco' + id 'test-report-aggregation' + id 'jacoco-report-aggregation' + alias(libs.plugins.spring.boot) + alias(libs.plugins.spring.dependencies) + alias(libs.plugins.vaadin) + alias(libs.plugins.lombok) + alias(libs.plugins.asciidoctorPdf) + alias(libs.plugins.asciidoctorConvert) + alias(libs.plugins.asciidoctorGems) +} + +repositories { + mavenCentral() + ruby.gems() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + maven { setUrl("https://maven.vaadin.com/vaadin-addons") } +} + +sourceCompatibility = '17' + +configurations { + developmentOnly + runtimeClasspath { + extendsFrom developmentOnly + } +} + +dependencies { + implementation 'com.vaadin:vaadin-core' + implementation 'com.vaadin:vaadin-spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation 'org.springframework.security:spring-security-oauth2-jose' + implementation 'org.springframework.security:spring-security-oauth2-resource-server' + implementation 'com.h2database:h2' + implementation libs.hsqldb + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + implementation 'com.sun.mail:javax.mail:1.6.2' + implementation 'org.hibernate.orm:hibernate-community-dialects' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'com.vaadin:vaadin-testbench-junit5' + testImplementation 'io.projectreactor:reactor-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + asciidoctorGems libs.rouge + //asciidoctorGems libs.diagram +} + +def pdfFile = layout.buildDirectory.file("docs/asciidocPdf/kontor-spring.pdf") +def pdfArtifact = artifacts.add('archives', pdfFile.get().asFile) { + type 'pdf' + builtBy asciidoctorPdf +} + +publishing { + publications { + maven(MavenPublication) { + groupId = group + '.docs' + artifactId = project.name + artifact pdfArtifact + } + bootJava(MavenPublication) { + artifact tasks.named("bootDistTar") + } + } + repositories { + maven { + name = "gitlabPackageRegistry" + url = uri("https://gitlab.com/api/v4/projects/62010300/packages/maven") + credentials(PasswordCredentials) + } + } +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() + +asciidoctorPdf { + dependsOn asciidoctorGemsPrepare + + baseDirFollowsSourceFile() + + asciidoctorj { + modules { + diagram.use() + } + requires 'rouge' + attributes 'build-gradle': file('build.gradle'), + 'endpoint-url': 'https://www.thpeetz.de', + 'source-highlighter': 'rouge', + 'imagesdir': './images', + 'toc': 'left', + 'toc-title': 'Inhaltsverzeichnis', + 'revdate': BUILD_DATE, + 'revnumber': { project.version.toString() }, + 'revremark': 'Entwurf', + 'chapter-label': '', + 'icons': 'font', + 'idprefix': 'id_', + 'idseparator': '-', + 'docinfo1': '' + } +} + +build.dependsOn asciidoctorPdf + +dependencyManagement { + imports { + mavenBom libs.vaadin.bom.get().toString() + } +} + +application { + mainClass = 'de.thpeetz.kontor.Application' +} + +bootRun { + args = ["--spring.profiles.active=${project.properties['profile'] ?: 'prod'}"] +} + +vaadin { + productionMode = true +} + +testing { + suites { + configureEach { + useJUnitJupiter() + dependencies { + implementation project() + implementation 'com.vaadin:vaadin-core' + implementation 'com.vaadin:vaadin-spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.h2database:h2' + implementation libs.hsqldb + implementation libs.sqlite.jdbc + //runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + implementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + implementation 'org.springframework.security:spring-security-test' + implementation 'com.vaadin:vaadin-testbench-junit5' + implementation 'io.projectreactor:reactor-test' + runtimeOnly 'org.junit.platform:junit-platform-launcher' + } + } + test(JvmTestSuite) { + testType = TestSuiteType.UNIT_TEST + targets { + all { + testTask.configure { + reports { + junitXml { + outputPerTestCase = true // defaults to false + mergeReruns = true // defaults to false + } + } + finalizedBy(jacocoTestReport) + } + } + } + } + integrationTest(JvmTestSuite) { + testType = "view-test" + targets { + all { + testTask.configure { + shouldRunAfter(test) + finalizedBy(jacocoTestReport) + } + } + } + } + } +} + +tasks.named('check') { + dependsOn(testing.suites.integrationTest) + dependsOn(testing.suites.test) + dependsOn tasks.named('testAggregateTestReport', TestReport) + dependsOn tasks.named('integrationTestAggregateTestReport', TestReport) +} + + +jacocoTestReport { + dependsOn test, integrationTest + reports { + xml.required = true + csv.required = false + } +} + +reporting { + reports { + testAggregateTestReport(AggregateTestReport) { + testType = TestSuiteType.UNIT_TEST + } + integrationTestAggregateTestReport(AggregateTestReport) { + testType = "view-test" + } + integrationTestCodeCoverageReport(JacocoCoverageReport) { + testType = "view-test" + } + } +} + +wrapper { + gradleVersion = "8.6" +} diff --git a/springboot/frontend/themes/kontor/styles.css b/springboot/frontend/themes/kontor/styles.css new file mode 100644 index 0000000..843559f --- /dev/null +++ b/springboot/frontend/themes/kontor/styles.css @@ -0,0 +1,10 @@ +@media all and (max-width: 1100px) { + .list-view.editing .toolbar, + .list-view.editing .contact-grid { + display: none; + } +} +a[highlight] { + font-weight: bold; + text-decoration: underline; +} diff --git a/springboot/frontend/themes/kontor/theme.json b/springboot/frontend/themes/kontor/theme.json new file mode 100644 index 0000000..0f7a81f --- /dev/null +++ b/springboot/frontend/themes/kontor/theme.json @@ -0,0 +1,3 @@ +{ + "lumoImports" : [ "typography", "color", "spacing", "badge", "utility" ] +} \ No newline at end of file diff --git a/springboot/gradle.properties b/springboot/gradle.properties new file mode 100644 index 0000000..f305204 --- /dev/null +++ b/springboot/gradle.properties @@ -0,0 +1,3 @@ +description='Kontor with Spring Boot' +version=0.1.0-SNAPSHOT +group=de.thpeetz diff --git a/springboot/gradle/libs.versions.toml b/springboot/gradle/libs.versions.toml new file mode 100644 index 0000000..6ec0bbe --- /dev/null +++ b/springboot/gradle/libs.versions.toml @@ -0,0 +1,58 @@ +[versions] +gradle = "8.6" +args4j = "2.33" +commonscli = "1.5.0" +junit = "5.8.2" +logback = "1.1.2" +mockito = "1.9.5" +picoli = "4.7.0" +slf4j = "1.7.22" +hsqldb = "2.7.1" +sqlite = "3.25.2" +spotbugs = "6.0.7" +asciidoctor = "4.0.2" +rouge = "3.15.0" +#diagram = "2.2.2" +diagram = "2.3.1" +sonarqube = "3.3" +cimtConventions = "1.0.0-SNAPSHOT" +springboot = "3.2.5" +springdependencies = "1.1.4" +vaadin = "24.3.8" +lombok = "8.6" + +[libraries] +args4j = { module = "args4j:args4j", version.ref = "args4j" } +commonscli = { module = "commons-cli:commons-cli", version.ref = "commonscli" } +junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } +logbackCore = { module = "ch.qos.logback:logback-core", version.ref = "logback" } +logbackClassic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +mockito = { module = "org.mockito:mockito-all", version.ref = "mockito" } +picocli = { module = "info.picocli:picocli", version.ref = "picoli" } +slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "hsqldb" } +sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } +vaadin-bom = { module = "com.vaadin:vaadin-bom", version.ref = "vaadin" } +asciidoctorGradleJvmGems = { module = "org.asciidoctor:asciidoctor-gradle-jvm-gems", version.ref= "asciidoctor" } +asciidoctorGradleJvm = { module = "org.asciidoctor:asciidoctor-gradle-jvm", version.ref= "asciidoctor" } +asciidoctorGradleJvmPdf = { module = "org.asciidoctor:asciidoctor-gradle-jvm-pdf", version.ref= "asciidoctor" } +rouge = { module = "rubygems:rouge", version.ref = "rouge" } +diagram = { module = "rubygems:asciidoctor-diagram", version.ref = "diagram" } + +[bundles] +logback = ["logbackCore", "logbackClassic"] + +[plugins] +spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugs" } +sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } +asciidoctorPdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor" } +asciidoctorConvert = { id = "org.asciidoctor.jvm.convert", version.ref = "asciidoctor" } +asciidoctorGems = { id = "org.asciidoctor.jvm.gems", version.ref = "asciidoctor" } +javaConvention = { id = "de.cimt.java-conventions", version.ref = "cimtConventions" } +applicationConvention = { id = "de.cimt.application-conventions", version.ref = "cimtConventions" } +libraryConvention = { id = "de.cimt.library-conventions", version.ref = "cimtConventions" } +asciidoctorConvention = { id = "de.cimt.asciidoctor-conventions", version.ref = "cimtConventions" } +spring-boot = { id = "org.springframework.boot", version.ref = "springboot"} +spring-dependencies = { id = "io.spring.dependency-management", version.ref = "springdependencies" } +vaadin = { id = "com.vaadin", version.ref = "vaadin" } +lombok = { id = "io.freefair.lombok", version.ref = "lombok" } diff --git a/springboot/gradle/wrapper/gradle-wrapper.jar b/springboot/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/springboot/gradle/wrapper/gradle-wrapper.properties b/springboot/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a80b22c --- /dev/null +++ b/springboot/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/springboot/gradlew b/springboot/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/springboot/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/springboot/gradlew.bat b/springboot/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/springboot/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/springboot/settings.gradle b/springboot/settings.gradle new file mode 100644 index 0000000..f559dac --- /dev/null +++ b/springboot/settings.gradle @@ -0,0 +1,24 @@ +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.id == 'org.springframework.boot') { + useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") + } + if (requested.id.id == 'org.gradle.toolchains.foojay-resolver') { + useModule("org.gradle.toolchains.foojay-resolver-convention:0.4.0") + } + } + } + repositories { + gradlePluginPortal() + mavenCentral() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + maven { url 'https://plugins.gradle.org/m2/' } + } +// plugins { +// id 'com.vaadin' version "${vaadinVersion}" +// } +} + +rootProject.name = 'kontor-spring' diff --git a/springboot/src/docs/asciidoc/kontor-spring.adoc b/springboot/src/docs/asciidoc/kontor-spring.adoc new file mode 100644 index 0000000..bd4ee00 --- /dev/null +++ b/springboot/src/docs/asciidoc/kontor-spring.adoc @@ -0,0 +1,509 @@ += Projektbeschreibung kontor-spring: Entwicklungs- und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 1.0.0 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Allgemeines + +=== Zweck des Dokumentes + +Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung +im Projekt kontor-spring und der Erstellung der Dokumentation. + +=== Verwendete Tools + +==== Gitea + +Für die Verwaltung des Sourcecode kommt ((Gitea))<> zum Einsatz. +Mit Gitea werden auch die Projektaufgaben verwaltet. + +Das Projekt und das dazugehörige Git Repository sind unter der Adresse + +https://gitea.thpeetz.de/kontor/kontor-spring + +zu finden. + +== Erstellung der Dokumentation + +Die Dokumentation des Projektes wird mit ((Asciidoctor))<> geschrieben. +Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument. + +=== Quellcode Verwaltung + +Die Asciidoctor-Dateien haben die Endung `.adoc`. + +=== Buildsystem + +Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem ((Gradle))<> verwendet. +Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`. + +Der Gradle Build wird über die Datei `build.gradle` definiert. + + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +===== Benutzer ER-Diagramm + +[mermaid, kontor-user-er, png] +.Benutzer ER-Diagramm +.... +erDiagram + user { + string id PK + datetime created_date + datetime last_modified_date + int version + string email + boolean enabled + string firstName + string lastName + string password + string token + boolean tokenExpired + string userName UNIQUE + } + role { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + authorization_matrix { + string id PK + datetime created_date + datetime last_modified_date + int version + string user_id FK + string role_id FK + } + module_data { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean import_data + string module_name UNIQUE + } + user ||--o{ authorization_matrix : "matrix" + role ||--o{ authorization_matrix : "matrix" +.... + + +===== Comics ER-Diagramm + +[mermaid, kontor-comics-er, png] +.Comics ER-Diagramm +.... +erDiagram + comic { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean completed + boolean currentOrder + string title + string publisher_id FK + } + volume { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string comic_id FK + } + issue { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean in_stock + boolean is_read + string issue_number + string comic_id FK + string volume_id FK + } + publisher { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + artist { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + story_arc { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string comic_id FK + } + trade_paperback { + string id PK + datetime created_date + datetime last_modified_date + int version + int issueStart + int issueEnd + string name + string comic_id FK + } + worktype { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + comic_work { + string id PK + datetime created_date + datetime last_modified_date + int version + string artist_id FK + string comic_id FK + string worktype_id FK + } + comic ||--o{ comic_work : "1" + artist ||--o{ comic_work : "1" + worktype ||--o{ comic-work : "1" + publisher ||--o{ comic : "1" + comic ||--o{ issue : "1" + comic ||--o{ volume : "1" + comic ||--o{ story_arc : "1" + comic ||--o{ trade_paperback : "1" + volume ||--o{ issue : "1" +.... + +===== TYSC ER-Diagramm + +[mermaid, kontor-tysc-er, png] +.TYSC ER-Diagramm +.... +erDiagram + sport { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + team { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string short_name + string sport_id FK + } + field_position { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string short_name + string sport_id FK + } + rooster { + string id PK + datetime created_date + datetime last_modified_date + int version + int year + string player_id FK + string position_id FK + string team_id FK + } + player { + string id PK + datetime created_date + datetime last_modified_date + int version + string first_name + string last_name + } + vendor { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + card_set { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean insert_set + string name + boolean parallel_set + string vendor_id FK + } + card { + string id PK + datetime created_date + datetime last_modified_date + int version + int cardNumber + int year + string card_set FK + string rooster_id FK + string vendor_id FK + } + sport ||--o{ team : "1" + sport ||--o{ field_position : "1" + field_position ||--o{ rooster : "1" + player ||--o{ rooster : "1" + team ||--o{ rooster : "1" + vendor ||--o{ card : "1" + card_set ||--o{ card : "1" + rooster ||--o{ card : "1" +.... + +===== Bookshelf ER-Diagramm + +[mermaid, kontor-bookshelf-er, png] +.Bookshelf ER-Diagramm +.... +erDiagram + article { + string id PK + datetime created_date + datetime last_modified_date + int version + string title + } + book { + string id PK + datetime created_date + datetime last_modified_date + int version + string isbn UNIQUE + string title + int year + string publisher_id FK + } + bookshelf_publisher { + string id PK + datetime created_date + datetime last_modified_date + int version + string name UNIQUE + } + author { + string id PK + datetime created_date + datetime last_modified_date + int version + string first_name + string last_name + } + article_author { + string id PK + datetime created_date + datetime last_modified_date + int version + string article_id FK + string author_id FK + } + book_author { + string id PK + datetime created_date + datetime last_modified_date + int version + string book_id FK + string author_id FK + } + publisher ||--o{ book : "1" + article ||--o{ article_author : "1" + author ||--o{ article_author : "1" + book ||--o{ book_author : "1" + author ||--o{ book_author : "1" +.... + +===== Mail ER-Diagramm + +[mermaid, kontor-mail-er, png] +.Mail ER-Diagramm +.... +erDiagram + mail { + string id PK + datetime created_date + datetime last_modified_date + int version + string subject + string content + datetime received_date + datetime sent_date + } + mail_account { + string id PK + datetime created_date + datetime last_modified_date + int version + string host + string password + int port + string protocol + boolean start_tls + string user_name + } + mail_address { + string id PK + datetime created_date + datetime last_modified_date + int version + string internet_address UNIQUE + string personal + string user_id FK + } + user ||--o{ mail_address : "1" +.... + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +== Projektbeschreibung + +=== Ausgangslage + +//==== Rechtliche Vorgaben und Rahmenbedingungen +//=== Rahmenbedingungen + +//==== Vorhandene Regelungen + +=== Projektziele + +=== Projektabgrenzung + +//=== Voraussichtliche Kosten + +//=== Projektrisiken + +//==== Produktivität + +//==== Finanzielle Risiken + +//==== Akzeptanz + +== Projektorganisation + +=== Projekt-Aufbauorganisation + +=== Rollendefinition + +//==== Projektauftraggeber + +//==== Projektausschuss + +//==== Beratung / Qualitätssicherung + +==== Projekteiter + +==== Projektteam + +==== Liste der Stakeholder + +=== Projektablauforganisation + +==== Projekt-Phasen + +===== Erstellung der Projektdokumentation + + +== Verschiedenes + +=== Erreichbarkeiten + +[bibliography] +== Referenzen + +- [[[asciidoctor]]] http://asciidoctor.org +- [[[gitea]]] http://www.gitea.org +- [[[gradle]]] http://www.gradle.org +- [[[jenkins]]] http://jenkins-ci.org + +[glossary] +== Glossar + +[index] +== Index + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java new file mode 100644 index 0000000..e2eec07 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Artist; + +@SpringBootTest +class ArtistViewTest { + + @Autowired + private ArtistView artistView; + + @Test + void formShownWhenArtistSelected() { + Grid grid = artistView.getGrid(); + Artist firstArtist = getFirstItem(grid); + + ArtistForm form = artistView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstArtist); + assertTrue(form.isVisible()); + assertEquals(firstArtist.getName(), form.name.getValue()); + } + + private Artist getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List artists = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(5, count); + return artists.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java new file mode 100644 index 0000000..a6aea11 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java @@ -0,0 +1,63 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.data.Artist; + +@SpringBootTest +class ArtistformTest { + + private Artist artist1; + private static final String ARTISTNAME= "Lee, Stan"; + + @BeforeEach + void setupData() { + artist1 = new Artist(); + artist1.setName(ARTISTNAME); + } + + @Test + void formFieldsPopulated() { + ArtistForm form = new ArtistForm(); + form.setArtist(artist1); + assertEquals(ARTISTNAME, form.name.getValue()); + } + + @Test + void saveEventHasCorrectValues() { + ArtistForm form = new ArtistForm(); + Artist artist = new Artist(); + form.setArtist(artist); + form.name.setValue(ARTISTNAME); + + AtomicReference savedArtistReference = new AtomicReference<>(null); + form.addSaveListener(e -> { + savedArtistReference.set(e.getArtist()); + }); + form.save.click(); + Artist savedArtist = savedArtistReference.get(); + assertEquals(ARTISTNAME, savedArtist.getName()); + } + + @Test + void deleteEventHasCorrectValues() { + ArtistForm form = new ArtistForm(); + Artist artist = new Artist(); + form.setArtist(artist); + form.name.setValue(ARTISTNAME); + + AtomicReference deletedArtistReference = new AtomicReference<>(null); + form.addDeleteListener(e -> { + deletedArtistReference.set(e.getArtist()); + }); + form.delete.click(); + Artist deletedArtist = deletedArtistReference.get(); + assertEquals(ARTISTNAME, deletedArtist.getName()); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java new file mode 100644 index 0000000..084d64e --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Comic; + +@SpringBootTest +public class ComicViewTest { + + @Autowired + private ComicView comicView; + + @Test + void formShownWhenComicSelected() { + Grid grid = comicView.getGrid(); + Comic firstComic = getFirstItem(grid); + + ComicForm form = comicView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstComic); + assertTrue(form.isVisible()); + assertEquals(firstComic.getTitle(), form.title.getValue()); + } + + private Comic getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List comics = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(169, count); + return comics.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java new file mode 100644 index 0000000..0d3113e --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.ComicWork; + +@SpringBootTest +class ComicWorkViewTest { + + @Autowired + private ComicWorkView comicWorkView; + + @Test + void formShownWhenComicSelected() { + Grid grid = comicWorkView.getGrid(); + ComicWork firstComicWork = getFirstItem(grid); + + ComicWorkForm form = comicWorkView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstComicWork); + assertTrue(form.isVisible()); + assertEquals(firstComicWork.getComic(), form.comic.getValue()); + } + + private ComicWork getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List comicWorks = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(18, count); + return comicWorks.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java new file mode 100644 index 0000000..089f8f5 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Issue; + +@SpringBootTest +public class IssueViewTest { + + @Autowired + private IssueView issueView; + + @Test + void formShownWhenIssueSelected() { + Grid grid = issueView.getGrid(); + Issue firstIssue = getFirstItem(grid); + + IssueForm form = issueView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstIssue); + assertTrue(form.isVisible()); + assertEquals(firstIssue.getIssueNumber(), form.issueNumber.getValue()); + } + + private Issue getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List issues = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(750, count); + return issues.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java new file mode 100644 index 0000000..b3bab80 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Publisher; + +@SpringBootTest +class PublisherViewTest { + + @Autowired + private PublisherView publisherView; + + @Test + void formShownWhenPublisherSelected() { + Grid grid = publisherView.getGrid(); + Publisher firstPublisher = getFirstItem(grid); + + PublisherForm form = publisherView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstPublisher); + assertTrue(form.isVisible()); + assertEquals(firstPublisher.getName(), form.name.getValue()); + } + + private Publisher getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List publishers = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(18, count); + return publishers.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java new file mode 100644 index 0000000..56f9c34 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.StoryArc; + +@SpringBootTest +class StoryArcViewTest { + + @Autowired + private StoryArcView storyArcView; + + @Test + void formShownWhenStoryArcSelected() { + Grid grid = storyArcView.getGrid(); + StoryArc firstStoryArc = getFirstItem(grid); + + StoryArcForm form = storyArcView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstStoryArc); + assertTrue(form.isVisible()); + assertEquals(firstStoryArc.getName(), form.name.getValue()); + } + + private StoryArc getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List storyArcs = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(3, count); + return storyArcs.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java new file mode 100644 index 0000000..a3b4119 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java @@ -0,0 +1,47 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.Volume; + +@SpringBootTest +class TradePaperbackViewTest { + + @Autowired + private TradePaperbackView tradePaperbackView; + + @Test + void formShownWhenVolumeSelected() { + Grid grid = tradePaperbackView.getGrid(); + + TradePaperback firstTradePaperback = getFirstItem(grid); + + TradePaperBackForm form = tradePaperbackView.getForm(); + assertFalse(form.isVisible()); + + if (firstTradePaperback != null) { + grid.asSingleSelect().setValue(firstTradePaperback); + assertTrue(form.isVisible()); + assertEquals(firstTradePaperback.getName(), form.name.getValue()); + } + } + + private TradePaperback getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List tradePaperbacks = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(40, count); + return tradePaperbacks.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java new file mode 100644 index 0000000..15e8d15 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java @@ -0,0 +1,50 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Volume; + +@SpringBootTest +class VolumeViewTest { + + @Autowired + private VolumeView volumeView; + + @Test + void formShownWhenVolumeSelected() { + Grid grid = volumeView.getGrid(); + + Volume firstVolume = getFirstItem(grid); + + VolumeForm form = volumeView.getForm(); + assertFalse(form.isVisible()); + + if (firstVolume != null) { + grid.asSingleSelect().setValue(firstVolume); + assertTrue(form.isVisible()); + assertEquals(firstVolume.getName(), form.name.getValue()); + } + } + + private Volume getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List volumes = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(0, count); + if (count > 0) { + return volumes.get(0); + } else { + return null; + } + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java new file mode 100644 index 0000000..2f15672 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Worktype; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootTest +class WorktypeViewTest { + + @Autowired + private WorktypeView worktypeView; + + @Test + void formShownWhenWorktypeSelected() { + Grid grid = worktypeView.getGrid(); + + Worktype firstWorktype = getFirstItem(grid); + + WorktypeForm form = worktypeView.getForm(); + assertFalse(form.isVisible()); + + if (firstWorktype != null) { + grid.asSingleSelect().setValue(firstWorktype); + assertTrue(form.isVisible()); + assertEquals(firstWorktype.getName(), form.name.getValue()); + } + } + + private Worktype getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List worktypes = grid.getListDataView().getItems().collect(Collectors.toList()); + log.info("found worktypes: {}", worktypes); + assertEquals(3, count); + return worktypes.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java new file mode 100644 index 0000000..40d3fbb --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.CardSet; + +@SpringBootTest +class CardSetViewTest { + + @Autowired + private CardSetView cardSetView; + + @Test + void formShownWhenCardSetSelected() { + Grid grid = cardSetView.getGrid(); + CardSet firstCardSet = getFirstItem(grid); + + CardSetForm form = cardSetView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstCardSet); + assertTrue(form.isVisible()); + assertEquals(firstCardSet.getName(), form.name.getValue()); + } + + private CardSet getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List cardSets = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(15, count); + return cardSets.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java new file mode 100644 index 0000000..0a5a3ed --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Card; + +@SpringBootTest +class CardViewTest { + + @Autowired + private CardView cardView; + + @Test + void formShownWhenCardSelected() { + Grid grid = cardView.getGrid(); + Card firstCard = getFirstItem(grid); + + CardForm form = cardView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstCard); + assertTrue(form.isVisible()); + assertEquals(String.valueOf(firstCard.getCardNumber()), form.cardNumber.getValue()); + } + + private Card getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List cards = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(10, count); + return cards.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java new file mode 100644 index 0000000..737b74d --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.FieldPosition; + +@SpringBootTest +class FieldPositionViewTest { + + @Autowired + private PositionView positionView; + + @Test + void formShownWhenPositionSelected() { + Grid grid = positionView.getGrid(); + FieldPosition firstFieldPosition = getFirstItem(grid); + + PositionForm form = positionView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstFieldPosition); + assertTrue(form.isVisible()); + assertEquals(firstFieldPosition.getName(), form.name.getValue()); + } + + private FieldPosition getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List positions = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(44, count); + return positions.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java new file mode 100644 index 0000000..5e7d640 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Player; + +@SpringBootTest +class PlayerViewTest { + + @Autowired + private PlayerView playerView; + + @Test + void formShownWhenPlayerSelected() { + Grid grid = playerView.getGrid(); + Player firstPlayer = getFirstItem(grid); + + PlayerForm form = playerView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstPlayer); + assertTrue(form.isVisible()); + assertEquals(firstPlayer.getLastName(), form.lastName.getValue()); + assertEquals(firstPlayer.getFirstName(), form.firstName.getValue()); + } + + private Player getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List players = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(38, count); + return players.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java new file mode 100644 index 0000000..d04f306 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Rooster; + +@SpringBootTest +class RoosterViewTest { + + @Autowired + private RoosterView roosterView; + + @Test + void formShownWhenRoosterSelected() { + Grid grid = roosterView.getGrid(); + Rooster firstRooster = getFirstItem(grid); + + RoosterForm form = roosterView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstRooster); + assertTrue(form.isVisible()); + assertEquals(firstRooster.getYear(), form.year.getValue()); + } + + private Rooster getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List roosters = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(11, count); + return roosters.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java new file mode 100644 index 0000000..ca86d0a --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Sport; + +@SpringBootTest +class SportViewTest { + + @Autowired + private SportView sportView; + + @Test + void formShownWhenSportSelected() { + Grid grid = sportView.getGrid(); + Sport firstSport = getFirstItem(grid); + + SportForm form = sportView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstSport); + assertTrue(form.isVisible()); + assertEquals(firstSport.getName(), form.name.getValue()); + } + + private Sport getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List sports = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(4, count); + return sports.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java new file mode 100644 index 0000000..da0ea19 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Team; + +@SpringBootTest +class TeamViewTest { + + @Autowired + private TeamView teamView; + + @Test + void formShownWhenTeamSelected() { + Grid grid = teamView.getGrid(); + Team firstTeam = getFirstItem(grid); + + TeamForm form = teamView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstTeam); + assertTrue(form.isVisible()); + assertEquals(firstTeam.getName(), form.name.getValue()); + } + + private Team getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List teams = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(122, count); + return teams.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java new file mode 100644 index 0000000..ef80d22 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Vendor; + +@SpringBootTest +class VendorViewTest { + + @Autowired + private VendorView vendorView; + + @Test + void formShownWhenVendorSelected() { + Grid grid = vendorView.getGrid(); + Vendor firstVendor = getFirstItem(grid); + + VendorForm form = vendorView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstVendor); + assertTrue(form.isVisible()); + assertEquals(firstVendor.getName(), form.name.getValue()); + } + + private Vendor getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List vendors = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(9, count); + return vendors.get(0); + } +} diff --git a/springboot/src/integrationTest/resources/application.properties b/springboot/src/integrationTest/resources/application.properties new file mode 100644 index 0000000..27a1935 --- /dev/null +++ b/springboot/src/integrationTest/resources/application.properties @@ -0,0 +1,30 @@ +server.port=8085 + +spring.hibernate.dialect=org.hibernate.dialect.HSQLDialect +spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect +spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver +spring.datasource.url=jdbc:hsqldb:mem:itDb +spring.datasource.username=sa +spring.datasource.password=sa + +#spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +#spring.datasource.driverClassName=org.sqlite.JDBC +#spring.datasource.url=jdbc:sqlite:file:./kontorITDb?cache=shared +#spring.datasource.username=sa +#spring.datasource.password=sa + +spring.jpa.defer-datasource-initialization = true +#spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.sql.init.mode=always + +spring.mustache.check-template-location = false + +logging.level.org.atmosphere=INFO +logging.level.org.springframework.web=INFO +logging.level.guru.springframework.controllers=DEBUG +logging.level.org.hibernate=INFO +logging.level.de.thpeetz=DEBUG + +jwt.auth.secret=J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc= diff --git a/springboot/src/main/java/de/thpeetz/kontor/Application.java b/springboot/src/main/java/de/thpeetz/kontor/Application.java new file mode 100644 index 0000000..c304f35 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/Application.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.server.PWA; +import com.vaadin.flow.theme.Theme; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Slf4j +@EnableJpaAuditing +@SpringBootApplication +@Theme(value = "kontor") +@PWA(name = "Vaadin Kontor", shortName = "Kontor", offlinePath = "offline.html", offlineResources = { "images/offline.png" }) +public class Application implements AppShellConfigurator { + + public static void main(String[] args) { + log.info("Starting Kontor application"); + SpringApplication.run(Application.class); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java b/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java new file mode 100644 index 0000000..63f11b1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.admin; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; +import de.thpeetz.kontor.admin.views.*; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.views.ComicWorkView; + +public class AdminConstants { + + + private AdminConstants() { + // private constructor to hide the implicit public one + } + + public static final String ADMIN_TITLE = "Verwaltung"; + public static final String AUTHORIZATION = "Berechtigungen"; + public static final String AUTHORIZATION_ROUTE = "/admin/authorization"; + public static final String DATA = "Daten"; + public static final String METADATA_ROUTE = "admin/metadata"; + public static final String ROLE = "Rollen"; + public static final String ROLE_ROUTE = "/admin/role"; + public static final String USER = "Benutzer"; + public static final String USER_ROUTE = "/admin/user"; + public static final String ADMIN = "admin"; + public static final String ADMIN_ROUTE = "/admin"; + + public static RouterLink getUserNavigation() { + return new RouterLink(USER, UserView.class); + } + + public static RouterLink getRoleNavigation() { + return new RouterLink(ROLE, RoleView.class); + } + + public static RouterLink getAuthorizationNavigation() { + return new RouterLink(AUTHORIZATION, AuthorizationView.class); + } + + public static SideNavItem getAdminNavigation() { + SideNavItem administration = new SideNavItem(ADMIN_TITLE, USER_ROUTE, VaadinIcon.GROUP.create()); + administration.addItem(new SideNavItem(USER, USER_ROUTE, VaadinIcon.USERS.create())); + administration.addItem(new SideNavItem(ROLE, RoleView.class)); + SideNavItem data = new SideNavItem(DATA, AUTHORIZATION_ROUTE, VaadinIcon.DATABASE.create()); + data.addItem(new SideNavItem(ComicConstants.COMICWORK, ComicWorkView.class)); + data.addItem(new SideNavItem(AUTHORIZATION, AuthorizationView.class)); + data.addItem(new SideNavItem("Data Import", ModuleDataView.class)); + data.addItem(new SideNavItem("Meta Data", MetaDataView.class)); + administration.addItem(data); + return administration; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/MailProperties.java b/springboot/src/main/java/de/thpeetz/kontor/admin/MailProperties.java new file mode 100644 index 0000000..9e8a6b4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/MailProperties.java @@ -0,0 +1,78 @@ +package de.thpeetz.kontor.admin; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "mail") +public class MailProperties { + + private String protocol; + private String host; + private Integer port; + private String userName; + private String password; + private Boolean startTls; + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public Boolean getStartTls() { + return startTls; + } + + public void setStartTls(Boolean startTls) { + this.startTls = startTls; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("MailProperties{"); + sb.append("protocol='").append(protocol).append('\''); + sb.append(", host='").append(host).append('\''); + sb.append(", port=").append(port); + sb.append(", starttls=").append(startTls); + sb.append(", userName='").append(userName).append('\''); + sb.append(", password='").append(password).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java new file mode 100644 index 0000000..419c519 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -0,0 +1,336 @@ +package de.thpeetz.kontor.admin; + +import de.thpeetz.kontor.admin.data.*; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.services.MetaDataService; +import de.thpeetz.kontor.mailclient.data.MailAccount; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; + +@Slf4j +@Component +public class SetupModuleAdmin implements ApplicationListener { + boolean alreadySetup = false; + + @Autowired + private UserRepository userRepository; + + @Autowired + private AuthorizationMatrixRepository authorizationMatrixRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private MailAccountRepository mailAccountRepository; + + @Autowired + private MailProperties mailProperties; + + @Autowired + private AdminService adminService; + + @Autowired + private MetaDataService metaDataService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + return; + } + + // Create initial roles and users + Role adminRole = adminService.addRole("ROLE_ADMIN"); + Role userRole = adminService.addRole("ROLE_USER"); + List users = userRepository.findAll(); + if (users.isEmpty()) { + User adminUser = initAdminUser(); + initMatrix(adminRole, adminUser); + initMatrix(userRole, adminUser); + } + log.info("MailProperties: {}", mailProperties); + initMail(mailProperties); + + initMetaData(); + } + + private void initMail(MailProperties mailProperties) { + log.info("initMail: Host {} with User {}", mailProperties.getHost(), mailProperties.getUserName()); + if (mailProperties.getHost() == null || mailProperties.getHost().isEmpty()) { + return; + } + boolean addMailAccount = false; + List mailAccounts = mailAccountRepository.findAll(); + if (mailAccounts.isEmpty()) { + addMailAccount = true; + } + for (MailAccount mailAccount : mailAccounts) { + String accountUser = mailAccount.getUserName(); + String propertyUser = mailProperties.getUserName(); + String accountHost = mailAccount.getHost(); + String propertyHost = mailProperties.getHost(); + if (propertyHost.equals(accountHost) && propertyUser.equals(accountUser)) { + log.debug("already configured: {}", mailAccount); + } else { + addMailAccount = true; + } + } + if (addMailAccount) { + log.info("add Mail Account: {}", mailProperties); + MailAccount mailAccount = new MailAccount(); + mailAccount.setProtocol(mailProperties.getProtocol()); + mailAccount.setHost(mailProperties.getHost()); + mailAccount.setPort(mailProperties.getPort()); + mailAccount.setUserName(mailProperties.getUserName()); + mailAccount.setPassword(mailProperties.getPassword()); + mailAccount.setStartTls(mailProperties.getStartTls()); + mailAccountRepository.save(mailAccount); + } + } + + private void initMatrix(Role role, User user) { + log.info("initMatrix: Role {} for User {}", role.getName(), user.getUserName()); + Collection configuredRoles = authorizationMatrixRepository.findByUser(user); + if (configuredRoles.stream().anyMatch(matrix -> matrix.getRole().getId().equals(role.getId()))) { + log.info("Role {} already defined", role.getName()); + } else { + log.info("add Role {} to User {}", role.getName(), user.getUserName()); + final AuthorizationMatrix adminMatrix = new AuthorizationMatrix(); + adminMatrix.setUser(user); + adminMatrix.setRole(role); + authorizationMatrixRepository.save(adminMatrix); + } + } + + private User initAdminUser() { + log.info("initAdminUser"); + User admin = userRepository.findByUserName("admin"); + if (admin == null) { + log.info("User admin not found, will be created."); + admin = new User(); + admin.setFirstName("Admin"); + admin.setLastName("Administrator"); + admin.setUserName("admin"); + admin.setEmail("admin@example.org"); + admin.setPassword(passwordEncoder.encode("admin")); + userRepository.save(admin); + } + return admin; + } + + private void initMetaData() { + log.info("initMetaData"); + MetaDataTable mediaArticleTable = metaDataService.getTable("media_article"); + metaDataService.getColumn(mediaArticleTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(mediaArticleTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(mediaArticleTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(mediaArticleTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(mediaArticleTable, "url", "link_url", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(mediaArticleTable, "review", "review", "BOOLEAN", null, 6); + metaDataService.getColumn(mediaArticleTable, "title", "title", "TEXT", null, 7); + MetaDataTable mediaVideoTable = metaDataService.getTable("media_video"); + metaDataService.getColumn(mediaVideoTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(mediaVideoTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(mediaVideoTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(mediaVideoTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(mediaVideoTable, "url", "link_url", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(mediaVideoTable, "review", "review", "BOOLEAN", null, 6); + metaDataService.getColumn(mediaVideoTable, "should_download", "should_download", "BOOLEAN", null, 7); + metaDataService.getColumn(mediaVideoTable, "title", "title", "TEXT", null, 8); + metaDataService.getColumn(mediaVideoTable, "file_name", "file_name", "TEXT", null, 9); + metaDataService.getColumn(mediaVideoTable, "path", "path", "TEXT", null, 10); + metaDataService.getColumn(mediaVideoTable, "cloud_link", "cloud_link", "TEXT", null, 11); + MetaDataTable mediaFileTable = metaDataService.getTable("media_file"); + metaDataService.getColumn(mediaFileTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(mediaFileTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(mediaFileTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(mediaFileTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(mediaFileTable, "url", "link_url", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(mediaFileTable, "review", "review", "BOOLEAN", null, 6); + metaDataService.getColumn(mediaFileTable, "should_download", "should_download", "BOOLEAN", null, 7); + metaDataService.getColumn(mediaFileTable, "title", "title", "TEXT", null, 8); + metaDataService.getColumn(mediaFileTable, "file_name", "file_name", "TEXT", null, 9); + metaDataService.getColumn(mediaFileTable, "path", "path", "TEXT", null, 10); + metaDataService.getColumn(mediaFileTable, "cloud_link", "cloud_link", "TEXT", null, 11); + MetaDataTable artistTable = metaDataService.getTable("artist"); + metaDataService.getColumn(artistTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(artistTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(artistTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(artistTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(artistTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable publisherTable = metaDataService.getTable("publisher"); + metaDataService.getColumn(publisherTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(publisherTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(publisherTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(publisherTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(publisherTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable comicTable = metaDataService.getTable("comic"); + metaDataService.getColumn(comicTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(comicTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(comicTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(comicTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(comicTable, "completed", "completed", "BOOLEAN", null, 5); + metaDataService.getColumn(comicTable, "current_order", "current_order", "BOOLEAN", null, 6); + metaDataService.getColumn(comicTable, "title", "title", "TEXT", "UNIQUE", 7); + metaDataService.getColumn(comicTable, "publisher_id", "publisher_id", "TEXT", null, 8); + MetaDataTable issueTable = metaDataService.getTable("issue"); + metaDataService.getColumn(issueTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(issueTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(issueTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(issueTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(issueTable, "in_stock", "in_stock", "BOOLEAN", null, 5); + metaDataService.getColumn(issueTable, "is_read", "is_read", "BOOLEAN", null, 6); + metaDataService.getColumn(issueTable, "issue_number", "issue_number", "TEXT", null, 7); + metaDataService.getColumn(issueTable, "comic_id", "comic_id", "TEXT", null, 8); + metaDataService.getColumn(issueTable, "volume_id", "volume_id", "TEXT", null, 9); + MetaDataTable volumeTable = metaDataService.getTable("volume"); + metaDataService.getColumn(volumeTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(volumeTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(volumeTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(volumeTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(volumeTable, "name", "name", "TEXT", null, 5); + metaDataService.getColumn(volumeTable, "comic_id", "comic_id", "TEXT", null, 6); + MetaDataTable tpbTable = metaDataService.getTable("trade_paperback"); + metaDataService.getColumn(tpbTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(tpbTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(tpbTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(tpbTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(tpbTable, "issue_start", "issue_start", "LONG", null, 5); + metaDataService.getColumn(tpbTable, "issue_end", "issue_end", "LONG", null, 6); + metaDataService.getColumn(tpbTable, "name", "name", "TEXT", null, 7); + metaDataService.getColumn(tpbTable, "comic_id", "comic_id", "TEXT", null, 8); + MetaDataTable storyArcTable = metaDataService.getTable("story_arc"); + metaDataService.getColumn(storyArcTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(storyArcTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(storyArcTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(storyArcTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(storyArcTable, "name", "name", "TEXT", null, 5); + metaDataService.getColumn(storyArcTable, "comic_id", "comic_id", "TEXT", null, 6); + MetaDataTable worktypeTable = metaDataService.getTable("worktype"); + metaDataService.getColumn(worktypeTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(worktypeTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(worktypeTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(worktypeTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(worktypeTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable comicworkTable = metaDataService.getTable("comic_work"); + metaDataService.getColumn(comicworkTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(comicworkTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(comicworkTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(comicworkTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(comicworkTable, "artist_id", "artist_id", "TEXT", null, 5); + metaDataService.getColumn(comicworkTable, "comic_id", "comic_id", "TEXT", null, 6); + metaDataService.getColumn(comicworkTable, "work_type_id", "work_type_id", "TEXT", null, 7); + MetaDataTable authorTable = metaDataService.getTable("author"); + metaDataService.getColumn(authorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(authorTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(authorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(authorTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(authorTable, "first_name", "first_name", "TEXT", null, 5); + metaDataService.getColumn(authorTable, "last_name", "last_name", "TEXT", null, 6); + MetaDataTable articleTable = metaDataService.getTable("article"); + metaDataService.getColumn(articleTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(articleTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(articleTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(articleTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(articleTable, "title", "title", "TEXT", "UNIQUE", 5); + MetaDataTable articleAuthorTable = metaDataService.getTable("article_author"); + metaDataService.getColumn(articleAuthorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(articleAuthorTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(articleAuthorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(articleAuthorTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(articleAuthorTable, "article_id", "article_id", "TEXT", null, 5); + metaDataService.getColumn(articleAuthorTable, "author_id", "author_id", "TEXT", null, 6); + MetaDataTable bookTable = metaDataService.getTable("book"); + metaDataService.getColumn(bookTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(bookTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(bookTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(bookTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(bookTable, "isbn", "isbn", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(bookTable, "title", "title", "TEXT", null, 6); + metaDataService.getColumn(bookTable, "year", "year", "LONG", null, 7); + metaDataService.getColumn(bookTable, "publisher_id", "publisher_id", "TEXT", null, 8); + MetaDataTable bookAuthorTable = metaDataService.getTable("book_author"); + metaDataService.getColumn(bookAuthorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(bookAuthorTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(bookAuthorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(bookAuthorTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(bookAuthorTable, "author_id", "author_id", "TEXT", null, 5); + metaDataService.getColumn(bookAuthorTable, "book_id", "book_id", "TEXT", null, 6); + MetaDataTable bookshelfPublisherTable = metaDataService.getTable("bookshelf_publisher"); + metaDataService.getColumn(bookshelfPublisherTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(bookshelfPublisherTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(bookshelfPublisherTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(bookshelfPublisherTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(bookshelfPublisherTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable sportTable = metaDataService.getTable("sport"); + metaDataService.getColumn(sportTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(sportTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(sportTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(sportTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(sportTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable playerTable = metaDataService.getTable("player"); + metaDataService.getColumn(playerTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(playerTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(playerTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(playerTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(playerTable, "first_name", "first_name", "TEXT", null, 5); + metaDataService.getColumn(playerTable, "last_name", "last_name", "TEXT", null, 6); + MetaDataTable teamTable = metaDataService.getTable("team"); + metaDataService.getColumn(teamTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(teamTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(teamTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(teamTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(teamTable, "name", "name", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(teamTable, "short_name", "short_name", "TEXT", null, 6); + metaDataService.getColumn(teamTable, "sport_id", "sport_id", "TEXT", null, 7); + MetaDataTable vendorTable = metaDataService.getTable("vendor"); + metaDataService.getColumn(vendorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(vendorTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(vendorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(vendorTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(vendorTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable fieldPositionTable = metaDataService.getTable("field_position"); + metaDataService.getColumn(fieldPositionTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(fieldPositionTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(fieldPositionTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(fieldPositionTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(fieldPositionTable, "name", "name", "TEXT", null, 5); + metaDataService.getColumn(fieldPositionTable, "short_name", "short_name", "TEXT", null, 6); + metaDataService.getColumn(fieldPositionTable, "sport_id", "sport_id", "TEXT", null, 7); + MetaDataTable roosterTable = metaDataService.getTable("rooster"); + metaDataService.getColumn(roosterTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(roosterTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(roosterTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(roosterTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(roosterTable, "year", "year", "LONG", null, 5); + metaDataService.getColumn(roosterTable, "player_id", "player_id", "TEXT", null, 6); + metaDataService.getColumn(roosterTable, "position_id", "position_id", "TEXT", null, 7); + metaDataService.getColumn(roosterTable, "team_id", "team_id", "TEXT", null, 8); + MetaDataTable cardSetTable = metaDataService.getTable("card_set"); + metaDataService.getColumn(cardSetTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(cardSetTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(cardSetTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(cardSetTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(cardSetTable, "insert_set", "insert_set", "BOOLEAN", null, 5); + metaDataService.getColumn(cardSetTable, "parallel_set", "parallel_set", "BOOLEAN", null, 6); + metaDataService.getColumn(cardSetTable, "name", "name", "TEXT", null, 7); + metaDataService.getColumn(cardSetTable, "vendor_id", "vendor_id", "TEXT", null, 8); + MetaDataTable cardTable = metaDataService.getTable("card"); + metaDataService.getColumn(cardTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(cardTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(cardTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(cardTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(cardTable, "card_number", "card_number", "LONG", null, 5); + metaDataService.getColumn(cardTable, "year", "year", "LONG", null, 6); + metaDataService.getColumn(cardTable, "card_set_id", "card_set_id", "TEXT", null, 7); + metaDataService.getColumn(cardTable, "rooster_id", "rooster_id", "TEXT", null, 8); + metaDataService.getColumn(cardTable, "vendor_id", "vendor_id", "TEXT", null, 9); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrix.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrix.java new file mode 100644 index 0000000..26b5ebb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrix.java @@ -0,0 +1,35 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@Entity +public class AuthorizationMatrix extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "user_id") + @NotNull + private User user; + + @ManyToOne + @JoinColumn(name = "role_id") + @NotNull + private Role role; + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("AuthorizationMatrix{"); + sb.append("user=").append(user.getUserName()); + sb.append(", role=").append(role.getName()); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrixRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrixRepository.java new file mode 100644 index 0000000..25d2fb9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrixRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.admin.data; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AuthorizationMatrixRepository extends JpaRepository { + + List findByUser(User user); + + List findByRole(Role role); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MailAccountRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MailAccountRepository.java new file mode 100644 index 0000000..ffe66ae --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MailAccountRepository.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface MailAccountRepository extends JpaRepository { +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java new file mode 100644 index 0000000..f09f087 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java @@ -0,0 +1,37 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "columnName, table_id"), + uniqueConstraints = @UniqueConstraint(columnNames = {"table_id", "columnOrder"}) +) +public class MetaDataColumn extends AbstractEntity { + + @NotNull + private String columnName; + + private String columnSyncName; + + private String columnType; + + @Nullable + private String columnModifier; + + private Integer columnOrder; + + private Boolean isShown; + + @ManyToOne + @JoinColumn(name = "table_id") + @NotNull + private MetaDataTable table; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumnRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumnRepository.java new file mode 100644 index 0000000..8959dd2 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumnRepository.java @@ -0,0 +1,10 @@ +package de.thpeetz.kontor.admin.data; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MetaDataColumnRepository extends JpaRepository { + + List findByTable(MetaDataTable table); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java new file mode 100644 index 0000000..639666d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java @@ -0,0 +1,26 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.util.LinkedList; +import java.util.List; + +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "tableName"), + uniqueConstraints = @UniqueConstraint(columnNames = {"tableName"}) +) +public class MetaDataTable extends AbstractEntity { + + @NotNull + private String tableName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "table") + private List tableColumns = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTableRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTableRepository.java new file mode 100644 index 0000000..694ee52 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTableRepository.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor.admin.data; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MetaDataTableRepository extends JpaRepository { + + MetaDataTable findByTableName(String tableName); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java new file mode 100644 index 0000000..b884425 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java @@ -0,0 +1,25 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "moduleName"), + uniqueConstraints = @UniqueConstraint(columnNames = {"moduleName"}) +) +public class ModuleData extends AbstractEntity { + + @NotEmpty + private String moduleName; + + private Boolean importData; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleDataRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleDataRepository.java new file mode 100644 index 0000000..484dde3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleDataRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.admin.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ModuleDataRepository extends JpaRepository { + + @Query("select m from ModuleData m where lower(m.moduleName) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + ModuleData findByModuleName(String moduleName); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/Role.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/Role.java new file mode 100644 index 0000000..5ad81c2 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/Role.java @@ -0,0 +1,30 @@ +package de.thpeetz.kontor.admin.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Getter +@Setter +@ToString +@Entity +public class Role extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "role") + @Nullable + private List matrix; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/RoleRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/RoleRepository.java new file mode 100644 index 0000000..932ae29 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/RoleRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.admin.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface RoleRepository extends JpaRepository { + + @Query("select r from Role r " + + "where lower(r.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + @Query("select r from Role r " + + "where lower(r.name) like lower(:name) ") + Role findByName(@Param("name") String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java new file mode 100644 index 0000000..a80b6bc --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java @@ -0,0 +1,62 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import java.util.LinkedList; +import java.util.List; + +@Slf4j +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "userName"), uniqueConstraints = @UniqueConstraint(columnNames = {"userName"})) +public class User extends AbstractEntity { + + private String firstName; + + private String lastName; + + @NotEmpty + private String userName; + + private String email; + + private String password; + + private boolean enabled; + + private boolean tokenExpired; + + private String token; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "user") + @Nullable + private List matrix = new LinkedList<>(); + + public String getFullName() { + StringBuilder fullNamBuilder = new StringBuilder(); + if (firstName != null) { + fullNamBuilder.append(firstName); + } + if (lastName != null) { + if (fullNamBuilder.length() > 0) { + fullNamBuilder.append(" "); + } + fullNamBuilder.append(lastName); + } + return fullNamBuilder.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/UserRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/UserRepository.java new file mode 100644 index 0000000..21d93e0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/UserRepository.java @@ -0,0 +1,16 @@ +package de.thpeetz.kontor.admin.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserRepository extends JpaRepository { + + @Query("select u from User u " + + "where lower(u.lastName) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + User findByUserName(String userName); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java new file mode 100644 index 0000000..77a6448 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java @@ -0,0 +1,110 @@ +package de.thpeetz.kontor.admin.services; + +import java.util.Collection; +import java.util.List; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.admin.data.AuthorizationMatrix; +import de.thpeetz.kontor.admin.data.AuthorizationMatrixRepository; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.data.RoleRepository; +import de.thpeetz.kontor.admin.data.User; +import de.thpeetz.kontor.admin.data.UserRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class AdminService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final AuthorizationMatrixRepository authorizationMatrixRepository; + + public AdminService(UserRepository userRepository, RoleRepository roleRepository, + AuthorizationMatrixRepository authorizationMatrixRepository) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.authorizationMatrixRepository = authorizationMatrixRepository; + } + + public List findAllUsers() { + return userRepository.findAll(); + } + + public List findAllRoles() { + return roleRepository.findAll(); + } + + public Collection findAllRoles(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return roleRepository.findAll(); + } else { + return roleRepository.search(stringFilter); + } + } + + public Role addRole(String roleName) { + Role role = roleRepository.findByName(roleName); + if (role == null) { + log.info("Role {} was not found, will create it.", roleName); + role = new Role(); + role.setName(roleName); + roleRepository.save(role); + } + return role; + } + + public void saveRole(Role role) { + if (role == null) { + log.warn("Role is null. Can't save it."); + } + log.info("saveRole: role={}", role); + roleRepository.save(role); + } + + public void deleteRole(Role role) { + if (role == null) { + log.warn("Role is null. Can't delete it."); + } + log.info("deleteRole: role={}", role); + roleRepository.delete(role); + } + + public List findAllAuthorizationMatrices() { + return authorizationMatrixRepository.findAll(); + } + + public void saveAuthorizationMatrix(AuthorizationMatrix authorizationMatrix) { + if (authorizationMatrix == null) { + log.warn("AuthorizationMatrix is null. Can't save it."); + } + log.info("saveAuthorizationMatrix: authorizationMatrix={}", authorizationMatrix); + authorizationMatrixRepository.save(authorizationMatrix); + } + + public void deleteAuthorizationMatrix(AuthorizationMatrix authorizationMatrix) { + if (authorizationMatrix == null) { + log.warn("AuthorizationMatrix is null. Can't delete it."); + } + log.info("deleteAuthorizationMatrix: authorizationMatrix={}", authorizationMatrix); + authorizationMatrixRepository.delete(authorizationMatrix); + } + + public String getUserFullName(String userName) { + log.debug("get Fullname für user {}", userName); + User user = userRepository.findByUserName(userName); + if (user == null) { + log.info("keinen Eintrag für {} gefunden", userName); + return userName; + } else { + log.info("Voller Name des User {}: {}", userName, user.getFullName()); + return user.getFullName(); + } + } + + public User getUser(String userName) { + log.debug("get User {}", userName); + return userRepository.findByUserName(userName); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java new file mode 100644 index 0000000..f76d6df --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java @@ -0,0 +1,125 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.admin.data.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service("userDetailsService") +public class KontorUserDetailsService implements UserDetailsService { + + private static SecureRandom random = new SecureRandom(); + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private AuthorizationMatrixRepository authorizationMatrixRepository; + + public Collection findAllUsers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return userRepository.findAll(); + } else { + return userRepository.search(stringFilter); + } + } + + public void saveUser(User user) { + if (user == null) { + log.warn("User is null. Can't save it."); + } + log.info("saveUser: user={}", user); + userRepository.save(user); + } + + public void saveUser(User user, List roles) { + if (user == null) { + log.warn("User is null. Can't save it."); + } + log.info("First save user: {}", user); + user = userRepository.save(user); + List copy = roles.stream().collect(Collectors.toList()); + List permissions = user.getMatrix(); + permissions.forEach(matrix -> { + if (roles.contains(matrix.getRole())) { + log.info("Role {} already assigned", matrix.getRole()); + copy.remove(matrix.getRole()); + } else { + log.info("Role {} has to be removed", matrix.getRole()); + authorizationMatrixRepository.delete(matrix); + } + }); + log.info("remaining roles: {}", copy); + for (Role role : copy) { + AuthorizationMatrix matrix = new AuthorizationMatrix(); + matrix.setUser(user); + matrix.setRole(role); + authorizationMatrixRepository.save(matrix); + } + } + + public void deleteUser(User user) { + if (user == null) { + log.warn("User is null. Can't delete it."); + } + log.info("deleteUser: user={}", user); + userRepository.delete(user); + } + + @Override + public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { + + log.info("loadUserByUsername: userName={}", userName); + User user = userRepository.findByUserName(userName); + if (user == null) { + log.info("User not found"); + return null; + } + + Collection authorities = getAuthorities(user); + log.info("User {} hat Rolen: {}", userName, authorities); + return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), + authorities); + } + + private Collection getAuthorities(User user) { + return authorizationMatrixRepository.findByUser(user).stream() + .map(matrix -> matrix.getRole().getName()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + public String getRememberedUser(String id) { + log.info("getRememberedUser: id={}", id); + return "admin"; + } + + public String rememberUser(String username) { + String randomId = new BigInteger(130, random).toString(32); + log.info("rememberUser: username={}", username); + return randomId; + } + + public void removeRememberedUser(String id) { + log.info("removeRememberedUser: id={}", id); + } + + public List findAllRoles() { + return roleRepository.findAll(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MailService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MailService.java new file mode 100644 index 0000000..dbe9270 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MailService.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import de.thpeetz.kontor.admin.data.MailAccountRepository; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class MailService { + + private final MailAccountRepository mailAccountRepository; + + public MailService(MailAccountRepository mailAccountRepository) { + this.mailAccountRepository = mailAccountRepository; + } + + public List findAllMailAccounts() { + return mailAccountRepository.findAll(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java new file mode 100644 index 0000000..293c4d4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java @@ -0,0 +1,101 @@ +package de.thpeetz.kontor.admin.services; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.data.MetaDataColumnRepository; +import de.thpeetz.kontor.admin.data.MetaDataTable; +import de.thpeetz.kontor.admin.data.MetaDataTableRepository; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +@Service +public class MetaDataService { + + private final MetaDataTableRepository metaDataTableRepository; + + private final MetaDataColumnRepository metaDataColumnRepository; + + public MetaDataService(MetaDataTableRepository metaDataTableRepository, MetaDataColumnRepository metaDataColumnRepository) { + this.metaDataTableRepository = metaDataTableRepository; + this.metaDataColumnRepository = metaDataColumnRepository; + } + + public MetaDataTable getTable(String tableName) { + MetaDataTable table = metaDataTableRepository.findByTableName(tableName); + if (table == null) { + log.info("Metadata for table {} not found, will create it", tableName); + table = new MetaDataTable(); + table.setTableName(tableName); + metaDataTableRepository.save(table); + } + return table; + } + + public void getColumn(MetaDataTable table, String columnName, String columnSyncName, String columnType, String columnModifier, Integer columnOrder) { + if (table.getTableColumns().stream().anyMatch(column -> column.getColumnName().equals(columnName))) { + log.info("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); + MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); + if (!column.getColumnName().equals(columnName)) { + log.debug("columnName has to be changed to {}", columnName); + column.setColumnName(columnName); + } + if (!column.getColumnSyncName().equals(columnSyncName)) { + log.debug("columnSyncName has to be changed to {}", columnSyncName); + column.setColumnSyncName(columnSyncName); + } + if (!column.getColumnType().equals(columnType)) { + log.debug("columnType has to be changed to {}", columnType); + column.setColumnType(columnType); + } + if (columnModifier != null && !column.getColumnModifier().equals(columnModifier)) { + log.debug("columnModifier has to be changed to {}", columnModifier); + column.setColumnModifier(columnModifier); + } + if (column.getIsShown() == null) { + log.debug("isShown set to false"); + column.setIsShown(Boolean.FALSE); + } + metaDataColumnRepository.save(column); + } else { + log.info("Column {} of table {} not found, will create it", columnName, table.getTableName()); + MetaDataColumn column = new MetaDataColumn(); + column.setTable(table); + column.setColumnName(columnName); + column.setColumnSyncName(columnSyncName); + column.setColumnType(columnType); + column.setColumnModifier(columnModifier); + column.setColumnOrder(columnOrder); + column.setIsShown(Boolean.FALSE); + metaDataColumnRepository.save(column); + } + } + + public List findAllMetaDataColumns() { + return metaDataColumnRepository.findAll(); + } + + public void deleteMetaDataColumn(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + log.warn("MetaDataColumn is null, can't delete it"); + return; + } + log.debug("deleteMetaDataColumn: MetaDataColumn={}", metaDataColumn); + metaDataColumnRepository.delete(metaDataColumn); + } + + public void saveMetaDataColumn(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + log.warn("MetaDataColumn is null, can't save it"); + return; + } + log.debug("saveMetaDataColumn: MetaDataColumn={}", metaDataColumn); + metaDataColumnRepository.save(metaDataColumn); + } + + public List findAllTables() { + return metaDataTableRepository.findAll(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java new file mode 100644 index 0000000..7596701 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java @@ -0,0 +1,76 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.admin.data.ModuleData; +import de.thpeetz.kontor.admin.data.ModuleDataRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class ModuleService { + + private final ModuleDataRepository moduleDataRepository; + + public ModuleService(ModuleDataRepository moduleDataRepository) { + this.moduleDataRepository = moduleDataRepository; + } + + public List findAll(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return moduleDataRepository.findAll(); + } else { + return moduleDataRepository.search(stringFilter); + } + } + + public ModuleData findByName(String moduleName) { + if (moduleName == null || moduleName.isEmpty()) { + return null; + } else { + return moduleDataRepository.findByModuleName(moduleName); + } + } + + public boolean importData(String moduleName) { + ModuleData module = moduleDataRepository.findByModuleName(moduleName); + if (module != null) { + return module.getImportData(); + } else { + log.info("Module {} not found, should import data", moduleName); + return true; + } + } + + public void setDataImported(String moduleName) { + ModuleData module = moduleDataRepository.findByModuleName(moduleName); + if (module == null) { + log.info("Module {} not found, will create it", moduleName); + module = new ModuleData(); + module.setModuleName(moduleName); + module.setImportData(false); + moduleDataRepository.save(module); + } else { + log.info("Module {} found, change import data", module); + module.setImportData(false); + moduleDataRepository.save(module); + } + } + + public void saveModuleData(ModuleData moduleData) { + if (moduleData == null) { + log.warn("ModuleData is null, can't save it."); + } else { + moduleDataRepository.save(moduleData); + } + } + + public void deleteModuleData(ModuleData moduleData) { + if (moduleData == null) { + log.warn("ModuleData is null, can't delete it."); + } else { + moduleDataRepository.delete(moduleData); + } + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java new file mode 100644 index 0000000..5ed32c5 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java @@ -0,0 +1,36 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AdminLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public AdminLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(AdminConstants.ADMIN_TITLE); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(AdminConstants.getUserNavigation(), AdminConstants.getRoleNavigation(), + AdminConstants.getAuthorizationNavigation()); + return navigation; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationForm.java new file mode 100644 index 0000000..da593f8 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationForm.java @@ -0,0 +1,112 @@ +package de.thpeetz.kontor.admin.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.admin.data.AuthorizationMatrix; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.data.User; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AuthorizationForm extends FormLayout { + + ComboBox user = new ComboBox<>("User"); + ComboBox role = new ComboBox<>("Role"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(AuthorizationMatrix.class); + + public AuthorizationForm(List users, List roles) { + addClassName("authorizationmatrix-form"); + binder.bindInstanceFields(this); + + user.setItems(users); + user.setItemLabelGenerator(User::getUserName); + role.setItems(roles); + role.setItemLabelGenerator(Role::getName); + add(user, role, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setAuthorizationMatrix(AuthorizationMatrix authorizationMatrix) { + binder.setBean(authorizationMatrix); + } + + public abstract static class AuthorizationFormEvent extends ComponentEvent { + private AuthorizationMatrix authorizationMatrix; + + protected AuthorizationFormEvent(AuthorizationForm source, AuthorizationMatrix authorizationMatrix) { + super(source, false); + this.authorizationMatrix = authorizationMatrix; + } + + public AuthorizationMatrix getAuthorizationMatrix() { + return authorizationMatrix; + } + } + + public static class SaveEvent extends AuthorizationFormEvent { + SaveEvent(AuthorizationForm source, AuthorizationMatrix authorizationMatrix) { + super(source, authorizationMatrix); + } + } + + public static class DeleteEvent extends AuthorizationFormEvent { + DeleteEvent(AuthorizationForm source, AuthorizationMatrix authorizationMatrix) { + super(source, authorizationMatrix); + } + } + + public static class CloseEvent extends AuthorizationFormEvent { + CloseEvent(AuthorizationForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationView.java new file mode 100644 index 0000000..435ae8f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationView.java @@ -0,0 +1,114 @@ +package de.thpeetz.kontor.admin.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; + +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.data.AuthorizationMatrix; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.AUTHORIZATION_ROUTE, layout = MainLayout.class) +@PageTitle("Authorization | Admin | Kontor") +public class AuthorizationView extends VerticalLayout { + + Grid grid = new Grid<>(AuthorizationMatrix.class); + AuthorizationForm form; + AdminService service; + + public AuthorizationView(AdminService service) { + this.service = service; + addClassName("authoriaztionmatrix-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("authorizationmatrix-grid"); + grid.setSizeFull(); + grid.setColumns("user.userName", "role.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editAuthorizationMatrix(event.getValue())); + } + + private void configureForm() { + form = new AuthorizationForm(service.findAllUsers(), service.findAllRoles()); + form.setWidth("25em"); + form.addSaveListener(this::saveAuthorizationMatrix); + form.addDeleteListener(this::deleteAuthorizationMatrix); + form.addCloseListener(e -> closeEditor()); + } + + private void saveAuthorizationMatrix(AuthorizationForm.SaveEvent event) { + AuthorizationMatrix authorizationMatrix = event.getAuthorizationMatrix(); + service.saveAuthorizationMatrix(authorizationMatrix); + updateList(); + closeEditor(); + } + + private void deleteAuthorizationMatrix(AuthorizationForm.DeleteEvent event) { + service.deleteAuthorizationMatrix(event.getAuthorizationMatrix()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addAuthorizationMaxtrixButton = new Button("Add permssion", click -> addAuthorizationMatrix()); + HorizontalLayout toolbar = new HorizontalLayout(addAuthorizationMaxtrixButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editAuthorizationMatrix(AuthorizationMatrix authorizationMatrix) { + if (authorizationMatrix == null) { + closeEditor(); + } else { + form.setAuthorizationMatrix(authorizationMatrix); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setAuthorizationMatrix(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addAuthorizationMatrix() { + grid.asSingleSelect().clear(); + editAuthorizationMatrix(new AuthorizationMatrix()); + } + + private void updateList() { + grid.setItems(service.findAllAuthorizationMatrices()); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java new file mode 100644 index 0000000..286bc03 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java @@ -0,0 +1,51 @@ +package de.thpeetz.kontor.admin.views; + +import org.springframework.security.core.context.SecurityContextHolder; + +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.login.LoginForm; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Route("login") +@PageTitle("Login | Vaadin Kontor") +@AnonymousAllowed +public class LoginView extends VerticalLayout implements BeforeEnterObserver { + + private final LoginForm login = new LoginForm(); + + public LoginView() { + addClassName("login-view"); + setSizeFull(); + setAlignItems(Alignment.CENTER); + setJustifyContentMode(JustifyContentMode.CENTER); + + login.setAction("login"); + + add(new H1("Vaadin Kontor")); + add(new Span("Username: user, Password: password")); + add(new Span("Username: admin, Password: password")); + add(login); + } + + @Override + public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { + log.info("beforeEnter: {}", beforeEnterEvent.getLocation()); + log.info("beforeEnter: {}", SecurityContextHolder.getContext().getAuthentication()); + // inform the user about an authentication error + if (beforeEnterEvent.getLocation() + .getQueryParameters() + .getParameters() + .containsKey("error")) { + login.setError(true); + } + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java new file mode 100644 index 0000000..f70c161 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.data.MetaDataTable; + +import java.util.List; + +public class MetaDataForm extends FormLayout { + + ComboBox table = new ComboBox<>("Table"); + TextField columnName = new TextField("Column Name"); + TextField columnSyncName = new TextField("Column Sync Name"); + TextField columnModifier = new TextField("Column Modifier"); + IntegerField columnOrder = new IntegerField("Column Order"); + Checkbox isShown = new Checkbox("Is Shown"); + + Button save = new com.vaadin.flow.component.button.Button("Save"); + Button delete = new com.vaadin.flow.component.button.Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MetaDataColumn.class); + + public MetaDataForm(List tables) { + addClassName("metaData-form"); + binder.bindInstanceFields(this); + + table.setItems(tables); + table.setItemLabelGenerator(MetaDataTable::getTableName); + add(table, columnName, columnSyncName, columnModifier, columnOrder, isShown, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MetaDataForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MetaDataForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MetaDataForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMetaDataColumn(MetaDataColumn metaDataColumn) { + binder.setBean(metaDataColumn); + } + + public abstract static class MetaDataFormEvent extends ComponentEvent { + private MetaDataColumn metaDataColumn; + + protected MetaDataFormEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, false); + this.metaDataColumn = metaDataColumn; + } + + public MetaDataColumn getMetaDataColumn() { + return metaDataColumn; + } + } + + public static class SaveEvent extends MetaDataForm.MetaDataFormEvent { + SaveEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, metaDataColumn); + } + } + + public static class DeleteEvent extends MetaDataForm.MetaDataFormEvent { + DeleteEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, metaDataColumn); + } + } + + public static class CloseEvent extends MetaDataForm.MetaDataFormEvent { + CloseEvent(MetaDataForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MetaDataForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MetaDataForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MetaDataForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java new file mode 100644 index 0000000..661d4a3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -0,0 +1,112 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.services.MetaDataService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.METADATA_ROUTE, layout = MainLayout.class) +@PageTitle("Meta Data | Admin | Kontor") +public class MetaDataView extends VerticalLayout { + + Grid grid = new Grid<>(MetaDataColumn.class); + MetaDataForm form; + MetaDataService service; + + public MetaDataView(MetaDataService service) { + this.service = service; + addClassName("metadata-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("metadata-grid"); + grid.setSizeFull(); + grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editMetaData(event.getValue())); + } + + private void configureForm() { + form = new MetaDataForm(service.findAllTables()); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveMetaData); + form.addDeleteListener(this::deleteMetaData); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMetaData(MetaDataForm.SaveEvent event) { + MetaDataColumn metaDataColumn = event.getMetaDataColumn(); + service.saveMetaDataColumn(metaDataColumn); + updateList(); + closeEditor(); + } + + private void deleteMetaData(MetaDataForm.DeleteEvent event) { + service.deleteMetaDataColumn(event.getMetaDataColumn()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addAuthorizationMaxtrixButton = new Button("Add Meta Data", click -> addMetaDataColumn()); + HorizontalLayout toolbar = new HorizontalLayout(addAuthorizationMaxtrixButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMetaData(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + closeEditor(); + } else { + form.setMetaDataColumn(metaDataColumn); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setMetaDataColumn(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMetaDataColumn() { + grid.asSingleSelect().clear(); + editMetaData(new MetaDataColumn()); + } + + private void updateList() { + grid.setItems(service.findAllMetaDataColumns()); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java new file mode 100644 index 0000000..b2c5f8c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java @@ -0,0 +1,100 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.ModuleData; + +public class ModuleDataForm extends FormLayout { + TextField moduleName = new TextField("Module Name"); + Checkbox importData = new Checkbox("Import Data"); + + Button save = new com.vaadin.flow.component.button.Button("Save"); + Button delete = new com.vaadin.flow.component.button.Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(ModuleData.class); + + public ModuleDataForm() { + addClassName("moduleData-form"); + binder.bindInstanceFields(this); + add(moduleName, importData, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new ModuleDataForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new ModuleDataForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new ModuleDataForm.SaveEvent(this, binder.getBean())); + } + } + + public void setModuleData(ModuleData moduleData) { + binder.setBean(moduleData); + } + + public abstract static class ModuleDataFormEvent extends ComponentEvent { + private ModuleData moduleData; + + protected ModuleDataFormEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, false); + this.moduleData = moduleData; + } + + public ModuleData getModuleData() { + return moduleData; + } + } + + public static class SaveEvent extends ModuleDataForm.ModuleDataFormEvent { + SaveEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, moduleData); + } + } + + public static class DeleteEvent extends ModuleDataForm.ModuleDataFormEvent { + DeleteEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, moduleData); + } + } + + public static class CloseEvent extends ModuleDataForm.ModuleDataFormEvent { + CloseEvent(ModuleDataForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(ModuleDataForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(ModuleDataForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(ModuleDataForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java new file mode 100644 index 0000000..bc335f3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java @@ -0,0 +1,118 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.data.ModuleData; +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = "admin/module", layout = MainLayout.class) +@PageTitle("Module Data | Admin | Kontor") +public class ModuleDataView extends VerticalLayout { + + Grid grid = new Grid<>(ModuleData.class); + TextField filterText = new TextField(); + ModuleDataForm form; + ModuleService service; + + public ModuleDataView(ModuleService service) { + this.service = service; + addClassName("moduleData-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("moduleData-grid"); + grid.setSizeFull(); + grid.setColumns("moduleName", "importData"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editModuleData(event.getValue())); + } + + private void configureForm() { + form = new ModuleDataForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveModuleData); + form.addDeleteListener(this::deleteModuleData); + form.addCloseListener(e -> closeEditor()); + } + + private void saveModuleData(ModuleDataForm.SaveEvent event) { + ModuleData moduleData = event.getModuleData(); + service.saveModuleData(moduleData); + updateList(); + closeEditor(); + } + + private void deleteModuleData(ModuleDataForm.DeleteEvent event) { + service.deleteModuleData(event.getModuleData()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by module name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addModuleDataButton = new Button("Add module", click -> addModuleData()); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addModuleDataButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editModuleData(ModuleData moduleData) { + if (moduleData == null) { + closeEditor(); + } else { + form.setModuleData(moduleData); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setModuleData(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addModuleData() { + grid.asSingleSelect().clear(); + editModuleData(new ModuleData()); + } + + private void updateList() { + grid.setItems(service.findAll(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleForm.java new file mode 100644 index 0000000..671d0f5 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleForm.java @@ -0,0 +1,102 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.admin.data.Role; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RoleForm extends FormLayout { + + TextField name = new TextField("Role name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Role.class); + + public RoleForm() { + addClassName("role-form"); + binder.bindInstanceFields(this); + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setRole(Role role) { + binder.setBean(role); + } + + public abstract static class RoleFormEvent extends ComponentEvent { + private Role role; + + protected RoleFormEvent(RoleForm source, Role role) { + super(source, false); + this.role = role; + } + + public Role getRole() { + return role; + } + } + + public static class SaveEvent extends RoleFormEvent { + SaveEvent(RoleForm source, Role role) { + super(source, role); + } + } + + public static class DeleteEvent extends RoleFormEvent { + DeleteEvent(RoleForm source, Role role) { + super(source, role); + } + } + + public static class CloseEvent extends RoleFormEvent { + CloseEvent(RoleForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleView.java new file mode 100644 index 0000000..1f13c9a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleView.java @@ -0,0 +1,118 @@ +package de.thpeetz.kontor.admin.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.ROLE_ROUTE, layout = MainLayout.class) +@PageTitle("Rollen | Admin | Kontor") +public class RoleView extends VerticalLayout { + Grid grid = new Grid<>(Role.class); + TextField filterText = new TextField(); + RoleForm form; + AdminService service; + + public RoleView(AdminService service) { + this.service = service; + addClassName("user-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("user-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editRole(event.getValue())); + } + + private void configureForm() { + form = new RoleForm(); + form.setWidth("25em"); + form.addSaveListener(this::saveRole); + form.addDeleteListener(this::deleteRole); + form.addCloseListener(e -> closeEditor()); + } + + private void saveRole(RoleForm.SaveEvent event) { + service.saveRole(event.getRole()); + updateList(); + closeEditor(); + } + + private void deleteRole(RoleForm.DeleteEvent event) { + service.deleteRole(event.getRole()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by user name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addUserButton = new Button("Add user", click -> addUser()); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addUserButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editRole(Role role) { + if (role == null) { + closeEditor(); + } else { + form.setRole(role); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setRole(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addUser() { + grid.asSingleSelect().clear(); + editRole(new Role()); + } + + private void updateList() { + grid.setItems(service.findAllRoles(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserForm.java new file mode 100644 index 0000000..21298f3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserForm.java @@ -0,0 +1,143 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.checkbox.CheckboxGroup; +import com.vaadin.flow.component.checkbox.CheckboxGroupVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.EmailField; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.data.User; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class UserForm extends FormLayout { + + TextField userName = new TextField("User name"); + PasswordField password = new PasswordField("Password"); + EmailField email = new EmailField("Email"); + TextField firstName = new TextField("First name"); + TextField lastName = new TextField("Last name"); + Checkbox enabled = new Checkbox("Enabled"); + String originalPassword; + + CheckboxGroup permissions = new CheckboxGroup<>("Permissions"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(User.class); + + public UserForm() { + addClassName("user-form"); + binder.bindInstanceFields(this); + add(userName, password, email, firstName, lastName, enabled, configurePermissionsGroup(), createButtonsLayout()); + } + + private CheckboxGroup configurePermissionsGroup() { + permissions.addThemeVariants(CheckboxGroupVariant.LUMO_VERTICAL); + permissions.setItemLabelGenerator(Role::getName); + permissions.addValueChangeListener(event -> { + log.debug("permissions changed: {}", event); + }); + return permissions; + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setUser(User user) { + binder.setBean(user); + //log.debug("UserForm.setUser: {}", user); + if (user != null) { + this.originalPassword = user.getPassword(); + } else { + this.originalPassword = null; + } + } + + public void setRoles(List roles, User user) { + permissions.setItems(roles); + user.getMatrix().stream().forEach(authorizationMatrix -> { + permissions.select(authorizationMatrix.getRole()); + }); + } + + public boolean hasPasswordChanged(User user) { + return !originalPassword.equals(user.getPassword()); + } + + public abstract static class UserFormEvent extends ComponentEvent { + private User user; + + protected UserFormEvent(UserForm source, User user) { + super(source, false); + this.user = user; + } + + public User getUser() { + return user; + } + } + + public static class SaveEvent extends UserFormEvent { + SaveEvent(UserForm source, User user) { + super(source, user); + } + } + + public static class DeleteEvent extends UserFormEvent { + DeleteEvent(UserForm source, User user) { + super(source, user); + } + } + + public static class CloseEvent extends UserFormEvent { + CloseEvent(UserForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java new file mode 100644 index 0000000..b2a9ef8 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java @@ -0,0 +1,73 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import de.thpeetz.kontor.admin.data.User; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.security.SecurityService; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Route(value="user/profile", layout = MainLayout.class) +@PermitAll +@PageTitle("Profile | User | Kontor") +public class UserProfileView extends VerticalLayout { + + private SecurityService securityService; + + private AdminService adminService; + + TextField firstName = new TextField("First name"); + TextField lastName = new TextField("Last name"); + + Button save = new Button("Save"); + Button close = new Button("Cancel"); + Binder binder = new BeanValidationBinder<>(User.class); + + public UserProfileView(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + binder.bindInstanceFields(this); + + add(firstName, lastName, createButtonsLayout()); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("UserProfileView: {}", user.getUsername()); + binder.setBean(adminService.getUser(user.getUsername())); + }); + + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + close.addClickListener(event -> closeView()); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + log.info("update user profile"); + } + } + + private void closeView() { + log.info("close user profile view"); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserView.java new file mode 100644 index 0000000..9f981ec --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserView.java @@ -0,0 +1,135 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.data.User; +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = "admin/user", layout = MainLayout.class) +@PageTitle("User | Admin | Kontor") +public class UserView extends VerticalLayout { + + Grid grid = new Grid<>(User.class); + TextField filterText = new TextField(); + UserForm form; + KontorUserDetailsService service; + + @Autowired + PasswordEncoder passwordEncoder; + + public UserView(KontorUserDetailsService service) { + this.service = service; + addClassName("user-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("user-grid"); + grid.setSizeFull(); + grid.setColumns("userName", "email", "firstName", "lastName", "enabled"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editUser(event.getValue())); + } + + private void configureForm() { + form = new UserForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveUser); + form.addDeleteListener(this::deleteUser); + form.addCloseListener(e -> closeEditor()); + } + + private void saveUser(UserForm.SaveEvent event) { + User user = event.getUser(); + log.debug("UserView.saveUser: {}", user); + List permissions = form.permissions.getSelectedItems().stream().collect(Collectors.toList()); + log.info("selected permissions: {}", permissions); + if (form.hasPasswordChanged(user)) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + log.debug("password changed for user {}", user); + } + service.saveUser(user, permissions); + updateList(); + closeEditor(); + } + + private void deleteUser(UserForm.DeleteEvent event) { + service.deleteUser(event.getUser()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by user name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addUserButton = new Button("Add user", click -> addUser()); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addUserButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editUser(User user) { + if (user == null) { + closeEditor(); + } else { + form.setUser(user); + form.setRoles(service.findAllRoles(), user); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setUser(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addUser() { + grid.asSingleSelect().clear(); + editUser(new User()); + } + + private void updateList() { + grid.setItems(service.findAllUsers(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java new file mode 100644 index 0000000..58661a0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.bookshelf; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.bookshelf.views.ArticleView; +import de.thpeetz.kontor.bookshelf.views.AuthorView; +import de.thpeetz.kontor.bookshelf.views.BookView; +import de.thpeetz.kontor.bookshelf.views.BookshelfPublisherView; + +public class BookshelfConstants { + + public static final String AUTHOR = "Author"; + public static final String ARTICLE = "Article"; + public static final String BOOK = "Book"; + public static final String BOOKSHELF = "Bookshelf"; + public static final String PUBLISHER = "Publisher"; + public static final String PUBLISHER_ROUTE = "bookshelf/publisher"; + public static final String AUTHOR_ROUTE = "bookshelf/author"; + public static final String BOOK_ROUTE = "bookshelf/book"; + public static final String ARTICLE_ROUTE = "bookshelf/article"; + + public static RouterLink getPublisherLink() { + return new RouterLink(PUBLISHER, BookshelfPublisherView.class); + } + + public static RouterLink getAuthorLink() { + return new RouterLink(AUTHOR, AuthorView.class); + } + + public static RouterLink getBookLink() { + return new RouterLink(BOOK, BookView.class); + } + + public static RouterLink getArticleLink() { + return new RouterLink(ARTICLE, ArticleView.class); + } + + public static SideNavItem getBookshelfNavigation() { + SideNavItem bookshelf = new SideNavItem(BOOKSHELF, BookView.class, VaadinIcon.OPEN_BOOK.create()); + bookshelf.addItem(new SideNavItem(BOOK, BookView.class)); + bookshelf.addItem(new SideNavItem(PUBLISHER, BookshelfPublisherView.class)); + bookshelf.addItem(new SideNavItem(AUTHOR, AuthorView.class)); + bookshelf.addItem(new SideNavItem(ARTICLE, ArticleView.class)); + return bookshelf; + } + + private BookshelfConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java new file mode 100644 index 0000000..abb0e9a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java @@ -0,0 +1,82 @@ +package de.thpeetz.kontor.bookshelf; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.bookshelf.data.ArticleAuthorRepository; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.AuthorRepository; +import de.thpeetz.kontor.bookshelf.data.BookAuthorRepository; +import de.thpeetz.kontor.bookshelf.data.BookRepository; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisherRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleBookshelf implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Autowired + private AuthorRepository authorRepository; + + @Autowired + private ArticleAuthorRepository articleAuthorRepository; + + @Autowired + private BookRepository bookRepository; + + @Autowired + private BookAuthorRepository bookAuthorRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupModuleBookshelf already executed, skipping"); + return; + } + if (!moduleService.importData(BookshelfConstants.BOOKSHELF)) { + log.info("Module Bookshelf should not setup data"); + return; + } + + log.info("Set up Bookshelf data"); + Author douglasadams = createAuthorIfNotFound("Douglas", "Adams"); + moduleService.setDataImported(BookshelfConstants.BOOKSHELF); + } + + private Author createAuthorIfNotFound(String firstName, String lastName) { + log.info("createAuthorIfNotFound {} {}", firstName, lastName); + Author author = authorRepository.findByFirstNameAndLastName(firstName, lastName); + if (author == null) { + log.info("Author {} {} not found, will create it", firstName, lastName); + author = new Author(); + author.setFirstName(firstName); + author.setLastName(lastName); + authorRepository.save(author); + } + return author; + } + + private BookshelfPublisher createPublisherIfNotFound(String publisherName) { + log.info("createPublisherIfNotFound {}", publisherName); + BookshelfPublisher publisher = publisherRepository.findByName(publisherName); + if (publisher == null) { + log.info("Publisher {} not found, will create it", publisherName); + publisher = new BookshelfPublisher(); + publisher.setName(publisherName); + publisherRepository.save(publisher); + } + return publisher; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java new file mode 100644 index 0000000..3ee6a16 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java @@ -0,0 +1,27 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "title"), uniqueConstraints = @UniqueConstraint(columnNames = {"title"})) +public class Article extends AbstractEntity { + + @NotEmpty + private String title; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "article") + @Nullable + private List authors = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java new file mode 100644 index 0000000..c2b3851 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "author_id, article_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"author_id", "article_id"})) +public class ArticleAuthor extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "author_id") + @NotNull + private Author author; + + @ManyToOne + @JoinColumn(name = "article_id") + @NotNull + private Article article; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java new file mode 100644 index 0000000..d7933de --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleAuthorRepository extends JpaRepository { + + List findByAuthor(Author author); + + List findByArticle(Article article); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java new file mode 100644 index 0000000..c6b2602 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java @@ -0,0 +1,16 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ArticleRepository extends JpaRepository { + + @Query("select a from Article a " + + "where lower(a.title) like lower(concat('%', :searchTerm, '%'))") + List

search(@Param("searchTerm") String searchTerm); + + List
findByTitle(String title); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java new file mode 100644 index 0000000..42d83e0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = { + @Index(columnList = "firstName, lastName"), + @Index(columnList = "lastName, firstName") +}, uniqueConstraints = { + @UniqueConstraint(columnNames = { "firstName", "lastName" }) +}) +public class Author extends AbstractEntity { + + @NotEmpty + private String firstName; + + @NotEmpty + private String lastName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "author") + @Nullable + private List bookAuthors = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "author") + @Nullable + private List articleAuthors = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java new file mode 100644 index 0000000..3e87334 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface AuthorRepository extends JpaRepository { + + @Query("select a from Author a " + + "where lower(a.firstName) like lower(concat('%', :searchTerm, '%')) " + + "or lower(a.lastName) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + Author findByFirstNameAndLastName(String firstName, String lastName); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java new file mode 100644 index 0000000..e0f6782 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "title, isbn"), uniqueConstraints = @UniqueConstraint(columnNames = {"isbn"})) +public class Book extends AbstractEntity { + + @NotEmpty + private String title; + + @NotEmpty + private String isbn; + + @Nullable + private int year; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "book") + @Nullable + private List authors = new LinkedList<>(); + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "publisher_id") + @NotNull + @JsonIgnoreProperties({ "books" }) + private BookshelfPublisher publisher; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java new file mode 100644 index 0000000..86a0e10 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "author_id, book_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"author_id", "book_id"})) +public class BookAuthor extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "author_id") + @NotNull + private Author author; + + @ManyToOne + @JoinColumn(name = "book_id") + @NotNull + private Book book; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java new file mode 100644 index 0000000..a0cf650 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BookAuthorRepository extends JpaRepository { + + List findByAuthor(Author author); + + List findByBook(Book book); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java new file mode 100644 index 0000000..e6ee013 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java @@ -0,0 +1,21 @@ +package de.thpeetz.kontor.bookshelf.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface BookRepository extends JpaRepository { + + @Query("select b from Book b " + + "where lower(b.title) like lower(concat('%', :searchTerm, '%')) " + + "or lower(b.isbn) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + List findByTitle(String name); + + List findByTitleIgnoreCase(String name); + + List findByIsbn(String isbn); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java new file mode 100644 index 0000000..da97d7a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "name"), uniqueConstraints = @UniqueConstraint(columnNames = { "name" })) +public class BookshelfPublisher extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", orphanRemoval = true) + @Nullable + List books = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java new file mode 100644 index 0000000..b50ba84 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface BookshelfPublisherRepository extends JpaRepository { + + @Query("select p from BookshelfPublisher p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + BookshelfPublisher findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java new file mode 100644 index 0000000..6d92a2c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java @@ -0,0 +1,118 @@ +package de.thpeetz.kontor.bookshelf.services; + +import java.util.List; + +import de.thpeetz.kontor.bookshelf.data.*; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class BookshelfService { + + private final AuthorRepository authorRepository; + private final ArticleAuthorRepository articleAuthorRepository; + private final ArticleRepository articleRepository; + private final BookRepository bookRepository; + private final BookAuthorRepository bookAuthorRepository; + private final BookshelfPublisherRepository publisherRepository; + + public BookshelfService(AuthorRepository authorRepository, ArticleAuthorRepository articleAuthorRepository, + ArticleRepository articleRepository, BookRepository bookRepository, + BookAuthorRepository bookAuthorRepository, BookshelfPublisherRepository publisherRepository) { + this.authorRepository = authorRepository; + this.articleAuthorRepository = articleAuthorRepository; + this.articleRepository = articleRepository; + this.bookRepository = bookRepository; + this.bookAuthorRepository = bookAuthorRepository; + this.publisherRepository = publisherRepository; + } + + public List findAllPublishers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return publisherRepository.findAll(); + } else { + return publisherRepository.search(stringFilter); + } + } + + public BookshelfPublisher findPublisherByName(String publisherName) { + return publisherRepository.findByName(publisherName); + } + + public void deletePublisher(BookshelfPublisher publisher) { + publisherRepository.delete(publisher); + } + + public void savePublisher(BookshelfPublisher publisher) { + if (publisher == null) { + log.warn("Publisher is null. Are you sure you have connected your form to the application?"); + return; + } + publisherRepository.save(publisher); + } + + public List findAllAuthors(String filter) { + if (filter == null || filter.isEmpty()) { + return authorRepository.findAll(); + } else { + return authorRepository.search(filter); + } + } + + public void saveAuthor(Author author) { + if (author == null) { + log.warn("Author is null. Are you sure you have connected your form to the application?"); + return; + } + authorRepository.save(author); + } + + public void deleteAuthor(Author author) { + authorRepository.delete(author); + } + + public List findAllBooks(String filter) { + if (filter == null || filter.isEmpty()) { + return bookRepository.findAll(); + } else { + return bookRepository.search(filter); + } + } + + public void saveBook(Book book) { + if (book == null) { + log.warn("Book is null. Are you sure you have connected your form to the application?"); + return; + } + bookRepository.save(book); + } + + public void deleteBook(Book book) { + BookshelfPublisher publisher = book.getPublisher(); + publisher.getBooks().remove(book); + publisherRepository.save(publisher); + bookRepository.delete(book); + } + + public List
findAllArticles(String filter) { + if (filter == null || filter.isEmpty()) { + return articleRepository.findAll(); + } else { + return articleRepository.search(filter); + } + } + + public void saveArticle(Article article) { + if (article == null) { + log.warn("Article is null, Are you sure you have connected your form to the application?"); + return; + } + articleRepository.save(article); + } + + public void deleteArticle(Article article) { + articleRepository.delete(article); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java new file mode 100644 index 0000000..7fb7958 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Article; +import de.thpeetz.kontor.bookshelf.data.Author; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class ArticleForm extends FormLayout { + + TextField title = new TextField("Title"); + Grid author = new Grid<>(Author.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder
binder = new BeanValidationBinder<>(Article.class); + + public ArticleForm() { + addClassName("article-form"); + binder.bindInstanceFields(this); + + add(title, author, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new ArticleForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new ArticleForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new ArticleForm.SaveEvent(this, binder.getBean())); + } + } + + public void setArticle(Article article) { + binder.setBean(article); + } + + public abstract static class ArticleFormEvent extends ComponentEvent { + private Article article; + + protected ArticleFormEvent(ArticleForm source, Article article) { + super(source, false); + this.article = article; + } + + public Article getArticle() { + return article; + } + } + + public static class SaveEvent extends ArticleForm.ArticleFormEvent { + SaveEvent(ArticleForm source, Article article) { + super(source, article); + } + } + + public static class DeleteEvent extends ArticleForm.ArticleFormEvent { + DeleteEvent(ArticleForm source, Article article) { + super(source, article); + } + } + + public static class CloseEvent extends ArticleForm.ArticleFormEvent { + CloseEvent(ArticleForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(ArticleForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(ArticleForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(ArticleForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java new file mode 100644 index 0000000..334f577 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java @@ -0,0 +1,126 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Article; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookAuthor; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +import java.util.stream.Collectors; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.ARTICLE_ROUTE, layout = MainLayout.class) +@PageTitle("Article | Bookshelf | Kontor") +public class ArticleView extends VerticalLayout { + + @Getter + Grid
grid = new Grid<>(Article.class); + TextField filterText = new TextField(); + @Getter + ArticleForm form; + BookshelfService service; + + public ArticleView(BookshelfService service) { + this.service = service; + addClassName("article-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("article-grid"); + grid.setSizeFull(); + grid.setColumns("title"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editArticle(event.getValue())); + } + + private void configureForm() { + form = new ArticleForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveArticle); + form.addDeleteListener(this::deleteArticle); + form.addCloseListener(e -> closeEditor()); + } + + private void saveArticle(ArticleForm.SaveEvent event) { + service.saveArticle(event.getArticle()); + updateList(); + closeEditor(); + } + + private void deleteArticle(ArticleForm.DeleteEvent event) { + service.deleteArticle(event.getArticle()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by title or isbn..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addArticleButton = new Button("Add article"); + addArticleButton.addClickListener(click -> addArticle()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addArticleButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editArticle(Article article) { + if (article == null) { + closeEditor(); + } else { + form.setArticle(article); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setArticle(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addArticle() { + grid.asSingleSelect().clear(); + editArticle(new Article()); + } + + public void updateList() { + grid.setItems(service.findAllArticles(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java new file mode 100644 index 0000000..34d0f5d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.listbox.ListBox; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.Book; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class AuthorForm extends FormLayout { + + TextField firstName = new TextField("First Name"); + TextField lastName = new TextField("Last Name"); + ListBox books = new ListBox<>(); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Author.class); + + public AuthorForm() { + addClassName("author-form"); + binder.bindInstanceFields(this); + + books.setHeight("100px"); + //books.setColumns("title", "publisher.name"); + //books.getColumns().forEach(col -> col.setAutoWidth(true)); + add(firstName, lastName, books, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new AuthorForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new AuthorForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new AuthorForm.SaveEvent(this, binder.getBean())); + } + } + + public void setAuthor(Author author) { + binder.setBean(author); + } + + public void setBooks(List books) { + log.info("setting Books: ", books); + this.books.setItems(books); + } + + public abstract static class AuthorFormEvent extends ComponentEvent { + private Author author; + + protected AuthorFormEvent(AuthorForm source, Author author) { + super(source, false); + this.author = author; + } + + public Author getAuthor() { + return author; + } + } + + public static class SaveEvent extends AuthorForm.AuthorFormEvent { + SaveEvent(AuthorForm source, Author author) { + super(source, author); + } + } + + public static class DeleteEvent extends AuthorForm.AuthorFormEvent { + DeleteEvent(AuthorForm source, Author author) { + super(source, author); + } + } + + public static class CloseEvent extends AuthorForm.AuthorFormEvent { + CloseEvent(AuthorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(AuthorForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(AuthorForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(AuthorForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java new file mode 100644 index 0000000..17897b0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java @@ -0,0 +1,132 @@ +package de.thpeetz.kontor.bookshelf.views; + +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.BookAuthor; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.AUTHOR_ROUTE, layout = MainLayout.class) +@PageTitle("Author | Bookshelf | Kontor") +public class AuthorView extends VerticalLayout { + + Grid grid = new Grid<>(Author.class); + TextField filterText = new TextField(); + AuthorForm form; + BookshelfService service; + + public AuthorView(BookshelfService service) { + this.service = service; + addClassName("author-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + public AuthorForm getForm() { + return form; + } + + private void configureGrid() { + grid.addClassName("author-grid"); + grid.setSizeFull(); + grid.setColumns("firstName", "lastName"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editAuthor(event.getValue())); + } + + private void configureForm() { + form = new AuthorForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveAuthor); + form.addDeleteListener(this::deleteAuthor); + form.addCloseListener(e -> closeEditor()); + } + private void saveAuthor(AuthorForm.SaveEvent event) { + service.saveAuthor(event.getAuthor()); + updateList(); + closeEditor(); + } + + private void deleteAuthor(AuthorForm.DeleteEvent event) { + service.deleteAuthor(event.getAuthor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addAuthorButton = new Button("Add author"); + addAuthorButton.addClickListener(click -> addAuthor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addAuthorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editAuthor(Author author) { + if (author == null) { + closeEditor(); + } else { + form.setAuthor(author); + form.setBooks(author.getBookAuthors().stream().map(BookAuthor::getBook).collect(Collectors.toList())); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setAuthor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addAuthor() { + grid.asSingleSelect().clear(); + editAuthor(new Author()); + } + + public void updateList() { + grid.setItems(service.findAllAuthors(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java new file mode 100644 index 0000000..61da274 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java @@ -0,0 +1,111 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class BookForm extends FormLayout { + + TextField title = new TextField("Title"); + TextField isbn = new TextField("ISBN"); + IntegerField year = new IntegerField("Year"); + ComboBox publisher = new ComboBox<>("Publisher"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Book.class); + + public BookForm(List publishers) { + addClassName("book-form"); + binder.bindInstanceFields(this); + + publisher.setItems(publishers); + publisher.setItemLabelGenerator(BookshelfPublisher::getName); + add(title, isbn, year, publisher, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new BookForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new BookForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new BookForm.SaveEvent(this, binder.getBean())); + } + } + + public void setBook(Book book) { + binder.setBean(book); + } + + public abstract static class BookFormEvent extends ComponentEvent { + private Book book; + + protected BookFormEvent(BookForm source, Book book) { + super(source, false); + this.book = book; + } + + public Book getBook() { + return book; + } + } + + public static class SaveEvent extends BookForm.BookFormEvent { + SaveEvent(BookForm source, Book book) { + super(source, book); + } + } + + public static class DeleteEvent extends BookForm.BookFormEvent { + DeleteEvent(BookForm source, Book book) { + super(source, book); + } + } + + public static class CloseEvent extends BookForm.BookFormEvent { + CloseEvent(BookForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(BookForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(BookForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(BookForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java new file mode 100644 index 0000000..9e04dc6 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java @@ -0,0 +1,124 @@ +package de.thpeetz.kontor.bookshelf.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.BOOK_ROUTE, layout = MainLayout.class) +@PageTitle("Book | Bookshelf | Kontor") +public class BookView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(Book.class); + TextField filterText = new TextField(); + @Getter + BookForm form; + BookshelfService service; + + public BookView(BookshelfService service) { + this.service = service; + addClassName("book-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("book-grid"); + grid.setSizeFull(); + grid.setColumns("title", "isbn", "publisher.name", "year"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editBook(event.getValue())); + } + + private void configureForm() { + form = new BookForm(service.findAllPublishers(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveBook); + form.addDeleteListener(this::deleteBook); + form.addCloseListener(e -> closeEditor()); + } + + private void saveBook(BookForm.SaveEvent event) { + service.saveBook(event.getBook()); + updateList(); + closeEditor(); + } + + private void deleteBook(BookForm.DeleteEvent event) { + service.deleteBook(event.getBook()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by title or isbn..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addBookButton = new Button("Add book"); + addBookButton.addClickListener(click -> addBook()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addBookButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editBook(Book book) { + if (book == null) { + closeEditor(); + } else { + form.setBook(book); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setBook(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addBook() { + grid.asSingleSelect().clear(); + editBook(new Book()); + } + + public void updateList() { + grid.setItems(service.findAllBooks(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java new file mode 100644 index 0000000..6105a47 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java @@ -0,0 +1,34 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; + +public class BookshelfLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public BookshelfLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(BookshelfConstants.BOOKSHELF); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(BookshelfConstants.getBookLink(), BookshelfConstants.getArticleLink(), + BookshelfConstants.getPublisherLink(), BookshelfConstants.getAuthorLink()); + return navigation; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java new file mode 100644 index 0000000..de5f9b5 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java @@ -0,0 +1,131 @@ +package de.thpeetz.kontor.bookshelf.views; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.SeparateMainLayout; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.PUBLISHER_ROUTE, layout = MainLayout.class) +@PageTitle("Publisher | Bookshelf | Kontor") +public class BookshelfPublisherView extends VerticalLayout { + + Grid grid = new Grid<>(BookshelfPublisher.class); + TextField filterText = new TextField(); + PublisherForm form; + + BookshelfService service; + + public BookshelfPublisherView(BookshelfService service) { + this.service = service; + addClassName("publisher-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("publisher-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPublisher(event.getValue())); + } + + private void configureForm() { + form = new PublisherForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePublisher); + form.addDeleteListener(this::deletePublisher); + form.addCloseListener(e -> closeEditor()); + } + + private void savePublisher(PublisherForm.SaveEvent event) { + service.savePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + private void deletePublisher(PublisherForm.DeleteEvent event) { + service.deletePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + public Grid getGrid() { + return grid; + } + + public PublisherForm getForm() { + return form; + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPublisherButton = new Button("Add publisher"); + addPublisherButton.addClickListener(click -> addPublisher()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPublisherButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPublisher(BookshelfPublisher publisher) { + if (publisher == null) { + closeEditor(); + } else { + form.setPublisher(publisher); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPublisher(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPublisher() { + grid.asSingleSelect().clear(); + editPublisher(new BookshelfPublisher()); + } + + public void updateList() { + grid.setItems(service.findAllPublishers(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java new file mode 100644 index 0000000..7b48122 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; + +public class PublisherForm extends FormLayout { + private TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder(BookshelfPublisher.class); + + public PublisherForm() { + addClassName("publisher-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public TextField getName() { + return name; + } + + public void setName(TextField name) { + this.name = name; + } + + public void setPublisher(BookshelfPublisher publisher) { + binder.setBean(publisher); + } + + public abstract static class PublisherFormEvent extends ComponentEvent { + private BookshelfPublisher publisher; + + protected PublisherFormEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, false); + this.publisher = publisher; + } + + public BookshelfPublisher getPublisher() { + return publisher; + } + } + + public static class SaveEvent extends PublisherFormEvent { + SaveEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, publisher); + } + } + + public static class DeleteEvent extends PublisherFormEvent { + DeleteEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, publisher); + } + } + + public static class CloseEvent extends PublisherFormEvent { + CloseEvent(PublisherForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java b/springboot/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java new file mode 100644 index 0000000..c89f7b1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java @@ -0,0 +1,93 @@ +package de.thpeetz.kontor.comics; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.comics.views.ArtistView; +import de.thpeetz.kontor.comics.views.ComicView; +import de.thpeetz.kontor.comics.views.ComicWorkView; +import de.thpeetz.kontor.comics.views.IssueView; +import de.thpeetz.kontor.comics.views.PublisherView; +import de.thpeetz.kontor.comics.views.StoryArcView; +import de.thpeetz.kontor.comics.views.TradePaperbackView; +import de.thpeetz.kontor.comics.views.VolumeView; +import de.thpeetz.kontor.comics.views.WorktypeView; + +/** + * The {@code ComicConstants} class contains constant values related to comics. + */ +public class ComicConstants { + + public static final String COMICS = "Comics"; + public static final String COMICS_ROUTE = "comics/comic"; + public static final String PUBLISHER = "Publisher"; + public static final String PUBLISHER_ROUTE = "comics/publisher"; + public static final String COMICWORK = "ComicWork"; + public static final String COMICWORK_ROUTE = "comics/comicwork"; + public static final String WORKTYPE = "Worktype"; + public static final String WORKTYPE_ROUTE = "comics/worktype"; + public static final String ARTIST = "Artist"; + public static final String ARTIST_ROUTE = "comics/artist"; + public static final String TPB = "TradePaperback"; + public static final String TPB_ROUTE = "comics/tradepaperback"; + public static final String STORYARC = "Story Arc"; + public static final String STORYARC_ROTE = "comics/storyarc"; + public static final String ISSUE = "Issue"; + public static final String ISSUE_ROUTE = "comics/issue"; + public static final String VOLUME = "Volume"; + public static final String VOLUME_ROUTE = "comics/volume"; + + public static RouterLink getArtistLink() { + return new RouterLink(ARTIST, ArtistView.class); + } + + public static RouterLink getComicLink() { + return new RouterLink(COMICS, ComicView.class); + } + + public static RouterLink getPublisherLink() { + return new RouterLink(PUBLISHER, PublisherView.class); + } + + public static RouterLink getComicWorkLink() { + return new RouterLink(COMICWORK, ComicWorkView.class); + } + + public static RouterLink getIssueLink() { + return new RouterLink(ISSUE, IssueView.class); + } + + public static RouterLink getTradePaperbackLink() { + return new RouterLink(TPB, TradePaperbackView.class); + } + + public static Component getStoryArcLink() { + return new RouterLink(STORYARC, StoryArcView.class); + } + + public static RouterLink getWorktypeLink() { + return new RouterLink(WORKTYPE, WorktypeView.class); + } + + public static RouterLink getVolumeLink() { + return new RouterLink(VOLUME, VolumeView.class); + } + + public static SideNavItem getComicsNavigation() { + SideNavItem comics = new SideNavItem(COMICS, COMICS_ROUTE, VaadinIcon.RECORDS.create()); + comics.addItem(new SideNavItem(ARTIST, ArtistView.class)); + comics.addItem(new SideNavItem(COMICS, ComicView.class)); + comics.addItem(new SideNavItem(PUBLISHER, PublisherView.class)); + comics.addItem(new SideNavItem(ISSUE, IssueView.class)); + comics.addItem(new SideNavItem(TPB, TradePaperbackView.class)); + comics.addItem(new SideNavItem(STORYARC, StoryArcView.class)); + comics.addItem(new SideNavItem(VOLUME, VolumeView.class)); + return comics; + } + + private ComicConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java b/springboot/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java new file mode 100644 index 0000000..52674fd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java @@ -0,0 +1,1198 @@ +package de.thpeetz.kontor.comics; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ArtistRepository; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicRepository; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.ComicWorkRepository; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.IssueRepository; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.PublisherRepository; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.StoryArcRepository; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.TradePaperbackRepository; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.VolumeRepository; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.data.WorktypeRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleComics implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private PublisherRepository publisherRepository; + + @Autowired + private ArtistRepository artistRepository; + + @Autowired + private WorktypeRepository worktypeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicWorkRepository comicWorkRepository; + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupModuleComics already executed, skipping"); + return; + } + if (!moduleService.importData(ComicConstants.COMICS)) { + log.info("Module comics should not setup data"); + return; + } + log.info("Set up Comics data"); + Publisher marvel = createPublisherIfNotFound("Marvel"); + Publisher alias = createPublisherIfNotFound("Alias"); + Publisher crossgen = createPublisherIfNotFound("Crossgen"); + Publisher image = createPublisherIfNotFound("Image"); + Publisher ddp = createPublisherIfNotFound("Devils Due Publishing"); + Publisher aspen = createPublisherIfNotFound("Aspen"); + Publisher bongo = createPublisherIfNotFound("Bongo Comics"); + Publisher kandora = createPublisherIfNotFound("Kandora"); + Publisher dc = createPublisherIfNotFound("DC"); + Publisher marvelknights = createPublisherIfNotFound("Marvel Knights"); + Publisher wildstorm = createPublisherIfNotFound("WildStorm"); + Publisher cliffhanger = createPublisherIfNotFound("Cliffhanger"); + Publisher darkhorse = createPublisherIfNotFound("Dark Horse Comics"); + Publisher broadsword = createPublisherIfNotFound("Broadsword"); + Publisher dynamite = createPublisherIfNotFound("Dynamite Entertainment"); + Publisher redeagle = createPublisherIfNotFound("Red Eagle Entertainment"); + Publisher topcow = createPublisherIfNotFound("Top Cow Productions"); + Publisher pulpfiction = createPublisherIfNotFound("Pulp Fiction"); + Artist michaelturner = createArtistIfNotFound("Turner, Michael"); + createArtistIfNotFound("Marz, Ron"); + createArtistIfNotFound("Whedon, Joss"); + createArtistIfNotFound("Land, Greg"); + Artist brianbendis = createArtistIfNotFound("Bendis, Brian Michael"); + Worktype writer = createWorktypeIfNotFound("Writer"); + createWorktypeIfNotFound("Penciler"); + createWorktypeIfNotFound("Inker"); + Comic comic1602 = createComicIfNotFound(marvel, "1602", false, false); + createComicIfNotFound(alias, "10th Muse", false, false); + Comic abadazad = createComicIfNotFound(crossgen, "Abadazad", false, false); + Comic amazingfantasy = createComicIfNotFound(marvel, "Amazing Fantasy", false, false); + Comic amazingspiderman = createComicIfNotFound(marvel, "Amazing Spider-Man", false, false); + Comic arana = createComicIfNotFound(marvel, "Arana", false, false); + Comic aria = createComicIfNotFound(image, "Aria", false, false); + createComicIfNotFound(ddp, "Army of Darkness", false, false); + Comic aspenComic = createComicIfNotFound(aspen, "Aspen", false, false); + Comic astonishingxmen = createComicIfNotFound(marvel, "Astonishing X-Men", false, false); + Comic athena = createComicIfNotFound(image, "Athena Inc. The Beginning", false, false); + Comic athenamanhunter = createComicIfNotFound(image, "Athena Inc. The Manhunter Project", false, false); + Comic barbarossa = createComicIfNotFound(kandora, "Barbarossa & The Lost Corsairs", false, false); + Comic bartsimpson = createComicIfNotFound(bongo, "Bart Simpson", false, false); + Comic bartsimpsontree = createComicIfNotFound(bongo, "Bart Simpsons Treehouse of Horror", false, false); + Comic battlepope = createComicIfNotFound(image, "Battle Pope", false, false); + Comic birdsofprey = createComicIfNotFound(dc, "Birds of Prey", false, false); + Comic blackwidow = createComicIfNotFound(marvelknights, "Black Widow", false, false); + Comic blackwidow2 = createComicIfNotFound(marvelknights, "Black Widow 2", false, false); + createComicIfNotFound(image, "Bluntman and Chronic", false, false); + Comic brath = createComicIfNotFound(crossgen, "Brath", false, false); + Comic catwomanrome = createComicIfNotFound(dc, "Catwoman When In Rome", false, false); + Comic crimson = createComicIfNotFound(wildstorm, "Crimson", false, false); + Comic crossgencomic = createComicIfNotFound(crossgen, "Crossgen", false, false); + Comic dangergirl = createComicIfNotFound(cliffhanger, "Danger Girl", false, false); + Comic dangergirlbackinblack = createComicIfNotFound(wildstorm, "Danger Girl Back in Black", false, false); + Comic daringescapes = createComicIfNotFound(image, "Daring Escapes", false, true); + Comic darknesssuperman = createComicIfNotFound(image, "Darkness / Superman", false, false); + Comic darknesstombraider = createComicIfNotFound(image, "Darkness / Tomb Raider", false, false); + Comic darknessvampirella = createComicIfNotFound(image, "Darkness / Vampirella", false, false); + Comic darknessblacksails = createComicIfNotFound(image, "Darkness Black Sails", false, false); + Comic darknessvol2 = createComicIfNotFound(image, "Darkness Vol. 2", false, false); + Comic districtx = createComicIfNotFound(marvel, "District X", false, false); + Comic dragonlance = createComicIfNotFound(ddp, "Dragonlance: Chronicles", false, false); + Comic dreampolice = createComicIfNotFound(marvel, "Dream Police", false, false); + Comic elcazador = createComicIfNotFound(crossgen, "El Cazador", false, false); + Comic elcazadortom = createComicIfNotFound(crossgen, "El Cazador The Bloody Ballad of Blackjack Tom", false, + false); + Comic elsinore = createComicIfNotFound(alias, "Elsinore", false, false); + Comic emmafrost = createComicIfNotFound(marvel, "Emma Frost", false, false); + Comic excalibur = createComicIfNotFound(marvel, "Excalibur", false, false); + Comic fathom = createComicIfNotFound(aspen, "Fathom", false, false); + Comic fathombeginnings = createComicIfNotFound(aspen, "Fathom Beginnings", false, false); + Comic fathomcannon = createComicIfNotFound(aspen, "Fathom Cannon Hawke", false, false); + Comic fathomcannonprelude = createComicIfNotFound(aspen, "Fathom Cannon Hawke Prelude", false, false); + Comic fathomdawnofwar = createComicIfNotFound(aspen, "Fathom Dawn of War", false, false); + Comic fathomprelude = createComicIfNotFound(aspen, "Fathom Prelude", false, false); + Comic fathomswimsuit = createComicIfNotFound(aspen, "Fathom Swimsuit Special", false, false); + Comic fathomswimsuit2000 = createComicIfNotFound(aspen, "Fathom Swimsuit Special 2000", false, false); + Comic fathomvol2 = createComicIfNotFound(aspen, "Fathom Vol. 2", false, false); + Comic fathomkillian = createComicIfNotFound(aspen, "Fathom: Killians Tide", false, false); + Comic flakriot = createComicIfNotFound(image, "Flak Riot", false, false); + Comic freshmen = createComicIfNotFound(image, "Freshmen", false, false); + Comic friendlyneighborspider = createComicIfNotFound(marvel, "Friendly Neighborhood Spider-Man", false, false); + Comic futurama = createComicIfNotFound(bongo, "Futurama", false, false); + Comic futuramaco2 = createComicIfNotFound(bongo, "Futurama Simpsons Crossover Crisis Part 2", false, false); + Comic ghostrider = createComicIfNotFound(marvelknights, "Ghostrider", false, false); + Comic gift = createComicIfNotFound(image, "Gift", false, false); + Comic hackslashtoys = createComicIfNotFound(ddp, "Hack Slash Land of Lost Toys", false, false); + Comic hackslashgirlsgonedead = createComicIfNotFound(ddp, "Hack/Slash: Girls Gone Dead", false, false); + Comic harryjohnson = createComicIfNotFound(pulpfiction, "Harry Johnson", false, false); + Comic hellcop = createComicIfNotFound(image, "Hellcop", false, false); + Comic holidayspecial2004 = createComicIfNotFound(marvel, "Holiday Special 2004", false, false); + Comic housem = createComicIfNotFound(marvel, "House of M", false, false); + Comic hunterkiller = createComicIfNotFound(image, "Hunter-Killer", false, false); + Comic hunterkillerdossier = createComicIfNotFound(image, "Hunter-Killer Dossier", false, false); + Comic ironghost = createComicIfNotFound(image, "Iron Ghost", false, false); + Comic judge = createComicIfNotFound(image, "J.U.D.G.E.: Secret Rage", false, false); + Comic kisskissbangbang = createComicIfNotFound(crossgen, "Kiss Kiss Bang Bang", false, false); + Comic legendofisis = createComicIfNotFound(alias, "Legend of Isis", false, false); + Comic loki = createComicIfNotFound(marvel, "Loki", false, false); + Comic lullaby = createComicIfNotFound(image, "Lullaby", false, false); + Comic magdalenavampirella2 = createComicIfNotFound(topcow, "Magdalena / Vampirella 2", false, false); + Comic marvelknightspider = createComicIfNotFound(marvelknights, "Marvel Knights Spider-Man", false, false); + Comic marville = createComicIfNotFound(marvel, "Marville", false, false); + Comic maryjane = createComicIfNotFound(marvel, "Mary Jane", false, false); + Comic maryjanehome = createComicIfNotFound(marvel, "Mary Jane Homecoming", false, false); + Comic megacity = createComicIfNotFound(ddp, "Megacity 909", false, false); + Comic meridian = createComicIfNotFound(crossgen, "Meridian", false, false); + Comic midnightnation = createComicIfNotFound(image, "Midnight Nation", false, true); + Comic monsterwar = createComicIfNotFound(image, "Monster War", false, false); + Comic monsterwar2005 = createComicIfNotFound(image, "Monster War 2005", false, false); + Comic mystic = createComicIfNotFound(crossgen, "Mystic", false, false); + Comic mystique = createComicIfNotFound(marvel, "Mystique", false, false); + Comic necromancer = createComicIfNotFound(image, "Necromancer", false, false); + Comic negationwar = createComicIfNotFound(crossgen, "Negation War", false, false); + Comic newavengers = createComicIfNotFound(marvel, "New Avengers", false, false); + Comic newmutants = createComicIfNotFound(marvel, "New Mutants", false, false); + Comic newxmen = createComicIfNotFound(marvel, "New X-Men", false, false); + Comic newxmenacademy = createComicIfNotFound(marvel, "New X-Men Academy X", false, false); + Comic newxmenhellions = createComicIfNotFound(marvel, "New X-Men Hellions", false, false); + Comic nightcrawler = createComicIfNotFound(marvel, "Nightcrawler", false, false); + Comic ororo = createComicIfNotFound(marvel, "Ororo: Before the Storm", false, false); + Comic radix = createComicIfNotFound(image, "Radix", false, false); + Comic redsonja = createComicIfNotFound(dynamite, "Red Sonja", false, false); + Comic revelations = createComicIfNotFound(darkhorse, "Revelations", false, false); + Comic rogue = createComicIfNotFound(marvel, "Rogue", false, false); + Comic ruse = createComicIfNotFound(crossgen, "Ruse", false, false); + Comic samurai = createComicIfNotFound(darkhorse, "Samurai: Heaven & Earth", false, false); + Comic scion = createComicIfNotFound(crossgen, "Scion", false, false); + Comic shanna = createComicIfNotFound(marvelknights, "Shanna, The She-Devil", false, false); + Comic shehulk = createComicIfNotFound(marvel, "She-Hulk", false, false); + Comic shehulk2 = createComicIfNotFound(marvel, "She-Hulk 2", false, false); + Comic shijunen = createComicIfNotFound(darkhorse, "Shi Ju-Nen", false, false); + Comic shrek = createComicIfNotFound(darkhorse, "Shrek", false, false); + Comic simpsons = createComicIfNotFound(bongo, "Simpsons", false, false); + Comic sojourn = createComicIfNotFound(crossgen, "Sojourn", false, false); + Comic solus = createComicIfNotFound(crossgen, "Solus", false, false); + Comic soulfire = createComicIfNotFound(aspen, "Soulfire", false, false); + Comic soulfirelight = createComicIfNotFound(aspen, "Soulfire Dying of the Light", false, false); + Comic spectacularspiderman = createComicIfNotFound(marvel, "Spectacular Spider-Man", false, false); + Comic spellbinders = createComicIfNotFound(marvel, "Spellbinders", false, false); + Comic spidermanindia = createComicIfNotFound(marvel, "Spider-Man India", false, false); + Comic spidermanlovesmary = createComicIfNotFound(marvel, "Spider-Man loves Mary Jane", false, false); + Comic spidermanteam = createComicIfNotFound(marvel, "Spider-Man Team Up", false, false); + Comic spidermanbreakout = createComicIfNotFound(marvel, "Spider-Man: Breakout", false, false); + Comic spidermanhousem = createComicIfNotFound(marvel, "Spider-Man: House of M", false, false); + Comic starwars = createComicIfNotFound(darkhorse, "Star Wars", false, false); + Comic stardustkid = createComicIfNotFound(image, "Stardust Kid", false, false); + Comic strange = createComicIfNotFound(marvel, "Strange", false, false); + Comic supergirl = createComicIfNotFound(dc, "Supergirl", false, false); + Comic superman = createComicIfNotFound(dc, "Superman", false, false); + Comic supermanbatman = createComicIfNotFound(dc, "Superman/Batman", false, false); + Comic tarotblackrose = createComicIfNotFound(broadsword, "Tarot: Witch of the Black Rose", false, false); + Comic artgreghorn = createComicIfNotFound(image, "The Art of Greg Horn", false, false); + Comic devilskeeper = createComicIfNotFound(alias, "The Devil´s Keeper", false, false); + Comic thetenth = createComicIfNotFound(image, "The Tenth", false, false); + Comic tombdracula = createComicIfNotFound(marvel, "The Tomb of Dracula", false, false); + Comic robertjordanwheeloftime = createComicIfNotFound(redeagle, "Robert Jordan´s The Wheel of Time: New Spring", + false, false); + Comic thorsonasgard = createComicIfNotFound(marvel, "Thor: Son of Asgard", false, false); + Comic tomstrong = createComicIfNotFound(wildstorm, "Tom Strong", false, false); + Comic tombraider = createComicIfNotFound(image, "Tomb Raider", false, false); + Comic tombraidergreatesttreasure = createComicIfNotFound(image, "Tomb Raider: The Greatest Treasure of All", + false, false); + Comic toxin = createComicIfNotFound(marvel, "Toxin", false, false); + Comic ultimatefantasticfour = createComicIfNotFound(marvel, "Ultimate Fantastic Four", false, false); + Comic ultimatespidermanannual = createComicIfNotFound(marvel, "Ultimate Spider-Man Annual", false, false); + Comic uncannyxmen = createComicIfNotFound(marvel, "Uncanny X-Men", false, false); + Comic vampirella = createComicIfNotFound(dynamite, "Vampirella", false, false); + Comic wildgirl = createComicIfNotFound(wildstorm, "Wild Girl", false, false); + Comic wildcats = createComicIfNotFound(wildstorm, "Wildcats: Nemesis", false, false); + Comic wildsiderz = createComicIfNotFound(wildstorm, "Wildsiderz", false, false); + Comic witchblade = createComicIfNotFound(image, "Witchblade", false, false); + Comic witchbladetombraider = createComicIfNotFound(image, "Witchblade / Tomb Raider", false, false); + Comic wolverine = createComicIfNotFound(marvel, "Wolverine: The End", false, false); + Comic woodboy = createComicIfNotFound(image, "Wood Boy", false, false); + Comic wraithborn = createComicIfNotFound(wildstorm, "Wraithborn", false, false); + Comic x23 = createComicIfNotFound(marvel, "X-23", false, false); + Comic xmenageofapocalypse = createComicIfNotFound(marvel, "X-Men: Age of Apocalypse", false, false); + Comic xmenageofapocalypseoneshot = createComicIfNotFound(marvel, "X-Men: Age of Apocalypse One Shot", false, + false); + Comic xmenkittypryde = createComicIfNotFound(marvel, "X-Men: Kitty Pryde", false, false); + Comic phoenix = createComicIfNotFound(marvel, "X-Men: Phoenix - Endsong", false, false); + Comic xtremexmen = createComicIfNotFound(marvel, "X-treme X-Men", false, false); + Comic armydarknessreanim = createComicIfNotFound(dynamite, "Army of Darkness vs. Re-Animator", false, false); + Comic runaways = createComicIfNotFound(marvel, "Runaways", false, false); + Comic crux = createComicIfNotFound(crossgen, "Crux", false, false); + Comic ariasoulmarket = createComicIfNotFound(image, "Aria: The Soul Market", false, false); + Comic ariasummerspell = createComicIfNotFound(image, "Aria: Summer´s Spell", false, false); + Comic ariaenchantment = createComicIfNotFound(image, "Aria: The Uses of Enchantment", false, false); + Comic armydarknessashes = createComicIfNotFound(darkhorse, "Army of Darkness: Ashes 2 Ashes", false, false); + Comic armydarknessshop = createComicIfNotFound(darkhorse, "Army of Darkness: Shop Till You Drop Dead", false, + false); + createComicIfNotFound(marvel, "X-Men", false, false); + createComicIfNotFound(image, "Bomb Queen II: Queen of Hearts", false, false); + createComicIfNotFound(image, "Bomb Queen III: The Good, The Bad and The Lovely", false, false); + createComicIfNotFound(image, "Bomb Queen IV: Suicide Bomber", false, false); + createComicIfNotFound(wildstorm, "Gen13", false, false); + createComicIfNotFound(aspen, "Iron & The Maiden", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Rebellion", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Knights of the Old Republic", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Legacy", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Dark Times", false, false); + createComicWorkIfNotFound(crossgencomic, michaelturner, writer); + createComicWorkIfNotFound(dangergirl, michaelturner, writer); + createComicWorkIfNotFound(ultimatefantasticfour, michaelturner, writer); + createComicWorkIfNotFound(ultimatespidermanannual, michaelturner, writer); + createComicWorkIfNotFound(uncannyxmen, michaelturner, writer); + createComicWorkIfNotFound(starwars, michaelturner, writer); + createComicWorkIfNotFound(shehulk, michaelturner, writer); + createComicWorkIfNotFound(shehulk2, michaelturner, writer); + createComicWorkIfNotFound(scion, michaelturner, writer); + createComicWorkIfNotFound(newavengers, michaelturner, writer); + createComicWorkIfNotFound(newmutants, michaelturner, writer); + createComicWorkIfNotFound(midnightnation, michaelturner, writer); + createComicWorkIfNotFound(monsterwar, michaelturner, writer); + createComicWorkIfNotFound(monsterwar2005, michaelturner, writer); + createComicWorkIfNotFound(mystic, michaelturner, writer); + createComicWorkIfNotFound(holidayspecial2004, michaelturner, writer); + createComicWorkIfNotFound(hackslashgirlsgonedead, michaelturner, writer); + createComicWorkIfNotFound(uncannyxmen, brianbendis, writer); + createStoryArcIfNotFound("Higher Learning", emmafrost); + createStoryArcIfNotFound("Mind Games", emmafrost); + createStoryArcIfNotFound("Bloom", emmafrost); + createTradePaperbackIfNotFound("Vol. 1", midnightnation, 1, 12); + createTradePaperbackIfNotFound("From The Ashes", sojourn, 1, 6); + createTradePaperbackIfNotFound("The Dragons Tale", sojourn, 7, 12); + createTradePaperbackIfNotFound("The Warriors Tale", sojourn, 13, 18); + createTradePaperbackIfNotFound("The Thiefs Tale", sojourn, 19, 24); + createTradePaperbackIfNotFound("Vol. 1", runaways, 1, 18); + createTradePaperbackIfNotFound("Choices", gift, 1, 5); + createTradePaperbackIfNotFound("Atlantis Rising", crux, 1, 6); + createTradePaperbackIfNotFound("Test Of Time", crux, 7, 12); + createTradePaperbackIfNotFound("Strangers in Atlantis", crux, 13, 18); + createTradePaperbackIfNotFound("Flying Solo", meridian, 1, 7); + createTradePaperbackIfNotFound("Going To Ground", meridian, 8, 14); + createTradePaperbackIfNotFound("Taking The Skies", meridian, 15, 20); + createTradePaperbackIfNotFound("Coming Home", meridian, 21, 26); + createTradePaperbackIfNotFound("Rite of Passage", mystic, 1, 7); + createTradePaperbackIfNotFound("The Demon Queen", mystic, 8, 14); + createTradePaperbackIfNotFound("Siege of Scales", mystic, 15, 20); + createTradePaperbackIfNotFound("Out All Night", mystic, 21, 26); + createTradePaperbackIfNotFound("Single Green Female", shehulk, 1, 6); + createTradePaperbackIfNotFound("Superhuman Law", shehulk, 7, 12); + createTradePaperbackIfNotFound("Conflict of Conscience", scion, 1, 7); + createTradePaperbackIfNotFound("Blood For Blood", scion, 8, 14); + createTradePaperbackIfNotFound("Divided Loyalties", scion, 15, 21); + createTradePaperbackIfNotFound("Sanctuary", scion, 22, 27); + createTradePaperbackIfNotFound("The End Of History", uncannyxmen, 444, 449); + createTradePaperbackIfNotFound("Public Enemies", supermanbatman, 1, 6); + createTradePaperbackIfNotFound("Loyalty And Loss", crimson, 1, 6); + createTradePaperbackIfNotFound("Heaven & Earth", crimson, 7, 12); + createTradePaperbackIfNotFound("Earth Angel", crimson, 13, 18); + createTradePaperbackIfNotFound("Redemption", crimson, 19, 24); + createTradePaperbackIfNotFound("1602", comic1602, 1, 8); + createTradePaperbackIfNotFound("Coming Home", amazingspiderman, 30, 35); + createTradePaperbackIfNotFound("Revelations", amazingspiderman, 36, 39); + createTradePaperbackIfNotFound("Until the Stars Turn Cold", amazingspiderman, 40, 45); + createTradePaperbackIfNotFound("The Life & Death of Spiders", amazingspiderman, 46, 50); + createTradePaperbackIfNotFound("Unintended Consequences", amazingspiderman, 51, 56); + createTradePaperbackIfNotFound("Happy Birthday", amazingspiderman, 500, 502); + createTradePaperbackIfNotFound("Sonderband 1", dangergirl, 1, 2); + createTradePaperbackIfNotFound("Of Like Minds", birdsofprey, 56, 61); + createTradePaperbackIfNotFound("Sensei & Student", birdsofprey, 62, 68); + createIssueIfNotFound("1", phoenix, false, false); + createIssueIfNotFound("2", phoenix, false, false); + createIssueIfNotFound("3", phoenix, false, false); + createIssueIfNotFound("4", phoenix, false, false); + createIssueIfNotFound("5", phoenix, false, false); + createIssueIfNotFound("1", midnightnation, true, false); + createIssueIfNotFound("2", midnightnation, true, false); + createIssueIfNotFound("3", midnightnation, true, false); + createIssueIfNotFound("4", midnightnation, true, false); + createIssueIfNotFound("5", midnightnation, true, false); + createIssueIfNotFound("6", midnightnation, true, false); + createIssueIfNotFound("7", midnightnation, true, false); + createIssueIfNotFound("8", midnightnation, true, false); + createIssueIfNotFound("9", midnightnation, true, false); + createIssueIfNotFound("10", midnightnation, true, false); + createIssueIfNotFound("11", midnightnation, true, false); + createIssueIfNotFound("12", midnightnation, true, false); + createIssueIfNotFound("1", arana, false, false); + createIssueIfNotFound("2", arana, false, false); + createIssueIfNotFound("3", arana, false, false); + createIssueIfNotFound("4", arana, false, false); + createIssueIfNotFound("5", arana, false, false); + createIssueIfNotFound("6", arana, false, false); + createIssueIfNotFound("7", arana, false, false); + createIssueIfNotFound("8", arana, false, false); + createIssueIfNotFound("9", arana, false, false); + createIssueIfNotFound("10", arana, false, false); + createIssueIfNotFound("11", arana, false, false); + createIssueIfNotFound("1", futurama, false, false); + createIssueIfNotFound("2", futurama, false, false); + createIssueIfNotFound("3", futurama, false, false); + createIssueIfNotFound("4", futurama, false, false); + createIssueIfNotFound("5", futurama, false, false); + createIssueIfNotFound("6", futurama, false, false); + createIssueIfNotFound("7", futurama, false, false); + createIssueIfNotFound("8", futurama, false, false); + createIssueIfNotFound("9", futurama, false, false); + createIssueIfNotFound("10", futurama, false, false); + createIssueIfNotFound("11", futurama, false, false); + createIssueIfNotFound("12", futurama, false, false); + createIssueIfNotFound("13", futurama, false, false); + createIssueIfNotFound("14", futurama, false, false); + createIssueIfNotFound("15", futurama, false, false); + createIssueIfNotFound("16", futurama, false, false); + createIssueIfNotFound("17", futurama, false, false); + createIssueIfNotFound("18", futurama, false, false); + createIssueIfNotFound("19", futurama, false, false); + createIssueIfNotFound("20", futurama, false, false); + createIssueIfNotFound("21", futurama, false, false); + createIssueIfNotFound("22", futurama, false, false); + createIssueIfNotFound("1", futuramaco2, false, false); + createIssueIfNotFound("2", futuramaco2, false, false); + createIssueIfNotFound("1", battlepope, false, false); + createIssueIfNotFound("2", battlepope, false, false); + createIssueIfNotFound("3", battlepope, false, false); + createIssueIfNotFound("11", bartsimpsontree, false, false); + createIssueIfNotFound("20", bartsimpson, false, false); + createIssueIfNotFound("21", bartsimpson, false, false); + createIssueIfNotFound("22", bartsimpson, false, false); + createIssueIfNotFound("23", bartsimpson, false, false); + createIssueIfNotFound("24", bartsimpson, false, false); + createIssueIfNotFound("25", bartsimpson, false, false); + createIssueIfNotFound("1", athena, false, false); + createIssueIfNotFound("1", athenamanhunter, false, false); + createIssueIfNotFound("2", athenamanhunter, false, false); + createIssueIfNotFound("3", athenamanhunter, false, false); + createIssueIfNotFound("4", athenamanhunter, false, false); + createIssueIfNotFound("5", athenamanhunter, false, false); + createIssueIfNotFound("6", athenamanhunter, false, false); + createIssueIfNotFound("1", blackwidow, false, false); + createIssueIfNotFound("2", blackwidow, false, false); + createIssueIfNotFound("3", blackwidow, false, false); + createIssueIfNotFound("4", blackwidow, false, false); + createIssueIfNotFound("5", blackwidow, false, false); + createIssueIfNotFound("6", blackwidow, false, false); + createIssueIfNotFound("1", blackwidow2, false, false); + createIssueIfNotFound("2", blackwidow2, false, false); + createIssueIfNotFound("3", blackwidow2, false, false); + createIssueIfNotFound("4", blackwidow2, false, false); + createIssueIfNotFound("5", blackwidow2, false, false); + createIssueIfNotFound("1", ruse, false, false); + createIssueIfNotFound("2", ruse, false, false); + createIssueIfNotFound("3", ruse, false, false); + createIssueIfNotFound("4", ruse, false, false); + createIssueIfNotFound("5", ruse, false, false); + createIssueIfNotFound("6", ruse, false, false); + createIssueIfNotFound("7", ruse, false, false); + createIssueIfNotFound("8", ruse, false, false); + createIssueIfNotFound("9", ruse, false, false); + createIssueIfNotFound("10", ruse, false, false); + createIssueIfNotFound("11", ruse, false, false); + createIssueIfNotFound("12", ruse, false, false); + createIssueIfNotFound("13", ruse, false, false); + createIssueIfNotFound("14", ruse, false, false); + createIssueIfNotFound("15", ruse, false, false); + createIssueIfNotFound("16", ruse, false, false); + createIssueIfNotFound("17", ruse, false, false); + createIssueIfNotFound("18", ruse, false, false); + createIssueIfNotFound("19", ruse, false, false); + createIssueIfNotFound("20", ruse, false, false); + createIssueIfNotFound("21", ruse, false, false); + createIssueIfNotFound("22", ruse, false, false); + createIssueIfNotFound("23", ruse, false, false); + createIssueIfNotFound("24", ruse, false, false); + createIssueIfNotFound("25", ruse, false, false); + createIssueIfNotFound("26", ruse, false, false); + createIssueIfNotFound("1", samurai, false, false); + createIssueIfNotFound("2", samurai, false, false); + createIssueIfNotFound("3", samurai, false, false); + createIssueIfNotFound("4", samurai, false, false); + createIssueIfNotFound("1", amazingfantasy, false, false); + createIssueIfNotFound("2", amazingfantasy, false, false); + createIssueIfNotFound("3", amazingfantasy, false, false); + createIssueIfNotFound("4", amazingfantasy, false, false); + createIssueIfNotFound("5", amazingfantasy, false, false); + createIssueIfNotFound("6", amazingfantasy, false, false); + createIssueIfNotFound("7", amazingfantasy, false, false); + createIssueIfNotFound("8", amazingfantasy, false, false); + createIssueIfNotFound("9", amazingfantasy, false, false); + createIssueIfNotFound("10", amazingfantasy, false, false); + createIssueIfNotFound("11", amazingfantasy, false, false); + createIssueIfNotFound("12", amazingfantasy, false, false); + createIssueIfNotFound("13", amazingfantasy, false, false); + createIssueIfNotFound("14", amazingfantasy, false, false); + createIssueIfNotFound("15", amazingfantasy, false, false); + createIssueIfNotFound("1", excalibur, false, false); + createIssueIfNotFound("2", excalibur, false, false); + createIssueIfNotFound("3", excalibur, false, false); + createIssueIfNotFound("4", excalibur, false, false); + createIssueIfNotFound("5", excalibur, false, false); + createIssueIfNotFound("6", excalibur, false, false); + createIssueIfNotFound("7", excalibur, false, false); + createIssueIfNotFound("8", excalibur, false, false); + createIssueIfNotFound("9", excalibur, false, false); + createIssueIfNotFound("10", excalibur, false, false); + createIssueIfNotFound("11", excalibur, false, false); + createIssueIfNotFound("12", excalibur, false, false); + createIssueIfNotFound("1", emmafrost, false, false); + createIssueIfNotFound("2", emmafrost, false, false); + createIssueIfNotFound("3", emmafrost, false, false); + createIssueIfNotFound("4", emmafrost, false, false); + createIssueIfNotFound("5", emmafrost, false, false); + createIssueIfNotFound("6", emmafrost, false, false); + createIssueIfNotFound("7", emmafrost, false, false); + createIssueIfNotFound("8", emmafrost, false, false); + createIssueIfNotFound("9", emmafrost, false, false); + createIssueIfNotFound("10", emmafrost, false, false); + createIssueIfNotFound("11", emmafrost, false, false); + createIssueIfNotFound("12", emmafrost, false, false); + createIssueIfNotFound("13", emmafrost, false, false); + createIssueIfNotFound("14", emmafrost, false, false); + createIssueIfNotFound("15", emmafrost, false, false); + createIssueIfNotFound("16", emmafrost, false, false); + createIssueIfNotFound("17", emmafrost, false, false); + createIssueIfNotFound("18", emmafrost, false, false); + createIssueIfNotFound("1", catwomanrome, false, false); + createIssueIfNotFound("2", catwomanrome, false, false); + createIssueIfNotFound("3", catwomanrome, false, false); + createIssueIfNotFound("4", catwomanrome, false, false); + createIssueIfNotFound("5", catwomanrome, false, false); + createIssueIfNotFound("6", catwomanrome, false, false); + createIssueIfNotFound("1", districtx, false, false); + createIssueIfNotFound("2", districtx, false, false); + createIssueIfNotFound("3", districtx, false, false); + createIssueIfNotFound("4", districtx, false, false); + createIssueIfNotFound("5", districtx, false, false); + createIssueIfNotFound("6", districtx, false, false); + createIssueIfNotFound("7", districtx, false, false); + createIssueIfNotFound("8", districtx, false, false); + createIssueIfNotFound("9", districtx, false, false); + createIssueIfNotFound("10", districtx, false, false); + createIssueIfNotFound("11", districtx, false, false); + createIssueIfNotFound("12", districtx, false, false); + createIssueIfNotFound("13", districtx, false, false); + createIssueIfNotFound("14", districtx, false, false); + createIssueIfNotFound("1", elcazador, false, false); + createIssueIfNotFound("2", elcazador, false, false); + createIssueIfNotFound("3", elcazador, false, false); + createIssueIfNotFound("4", elcazador, false, false); + createIssueIfNotFound("5", elcazador, false, false); + createIssueIfNotFound("6", elcazador, false, false); + createIssueIfNotFound("1", elcazadortom, false, false); + createIssueIfNotFound("1", elsinore, false, false); + createIssueIfNotFound("1", dreampolice, false, false); + createIssueIfNotFound("1", dragonlance, false, false); + createIssueIfNotFound("2", dragonlance, false, false); + createIssueIfNotFound("1", ghostrider, false, false); + createIssueIfNotFound("2", ghostrider, false, false); + createIssueIfNotFound("3", ghostrider, false, false); + createIssueIfNotFound("4", ghostrider, false, false); + createIssueIfNotFound("5", ghostrider, false, false); + createIssueIfNotFound("6", ghostrider, false, false); + createIssueIfNotFound("1", fathomkillian, false, false); + createIssueIfNotFound("2", fathomkillian, false, false); + createIssueIfNotFound("3", fathomkillian, false, false); + createIssueIfNotFound("4", fathomkillian, false, false); + createIssueIfNotFound("1", maryjane, false, false); + createIssueIfNotFound("2", maryjane, false, false); + createIssueIfNotFound("3", maryjane, false, false); + createIssueIfNotFound("4", maryjane, false, false); + createIssueIfNotFound("1", maryjanehome, false, false); + createIssueIfNotFound("2", maryjanehome, false, false); + createIssueIfNotFound("3", maryjanehome, false, false); + createIssueIfNotFound("4", maryjanehome, false, false); + createIssueIfNotFound("1", marville, false, false); + createIssueIfNotFound("2", marville, false, false); + createIssueIfNotFound("3", marville, false, false); + createIssueIfNotFound("4", marville, false, false); + createIssueIfNotFound("5", marville, false, false); + createIssueIfNotFound("6", marville, false, false); + createIssueIfNotFound("7", marville, false, false); + createIssueIfNotFound("1", megacity, false, false); + createIssueIfNotFound("2", megacity, false, false); + createIssueIfNotFound("3", megacity, false, false); + createIssueIfNotFound("4", megacity, false, false); + createIssueIfNotFound("5", megacity, false, false); + createIssueIfNotFound("6", megacity, false, false); + createIssueIfNotFound("7", megacity, false, false); + createIssueIfNotFound("8", megacity, false, false); + createIssueIfNotFound("1", nightcrawler, false, false); + createIssueIfNotFound("2", nightcrawler, false, false); + createIssueIfNotFound("3", nightcrawler, false, false); + createIssueIfNotFound("4", nightcrawler, false, false); + createIssueIfNotFound("5", nightcrawler, false, false); + createIssueIfNotFound("6", nightcrawler, false, false); + createIssueIfNotFound("7", nightcrawler, false, false); + createIssueIfNotFound("8", nightcrawler, false, false); + createIssueIfNotFound("9", nightcrawler, false, false); + createIssueIfNotFound("10", nightcrawler, false, false); + createIssueIfNotFound("11", nightcrawler, false, false); + createIssueIfNotFound("12", nightcrawler, false, false); + createIssueIfNotFound("1", ororo, false, false); + createIssueIfNotFound("1", radix, false, false); + createIssueIfNotFound("2", radix, false, false); + createIssueIfNotFound("3", radix, false, false); + createIssueIfNotFound("1", rogue, false, false); + createIssueIfNotFound("2", rogue, false, false); + createIssueIfNotFound("3", rogue, false, false); + createIssueIfNotFound("4", rogue, false, false); + createIssueIfNotFound("5", rogue, false, false); + createIssueIfNotFound("6", rogue, false, false); + createIssueIfNotFound("7", rogue, false, false); + createIssueIfNotFound("8", rogue, false, false); + createIssueIfNotFound("9", rogue, false, false); + createIssueIfNotFound("10", rogue, false, false); + createIssueIfNotFound("11", rogue, false, false); + createIssueIfNotFound("12", rogue, false, false); + createIssueIfNotFound("1", shijunen, false, false); + createIssueIfNotFound("2", shijunen, false, false); + createIssueIfNotFound("3", shijunen, false, false); + createIssueIfNotFound("4", shijunen, false, false); + createIssueIfNotFound("1", solus, false, false); + createIssueIfNotFound("2", solus, false, false); + createIssueIfNotFound("3", solus, false, false); + createIssueIfNotFound("4", solus, false, false); + createIssueIfNotFound("5", solus, false, false); + createIssueIfNotFound("6", solus, false, false); + createIssueIfNotFound("7", solus, false, false); + createIssueIfNotFound("8", solus, false, false); + createIssueIfNotFound("1", toxin, false, false); + createIssueIfNotFound("2", toxin, false, false); + createIssueIfNotFound("3", toxin, false, false); + createIssueIfNotFound("4", toxin, false, false); + createIssueIfNotFound("5", toxin, false, false); + createIssueIfNotFound("6", toxin, false, false); + createIssueIfNotFound("1", wildgirl, false, false); + createIssueIfNotFound("2", wildgirl, false, false); + createIssueIfNotFound("3", wildgirl, false, false); + createIssueIfNotFound("4", wildgirl, false, false); + createIssueIfNotFound("5", wildgirl, false, false); + createIssueIfNotFound("6", wildgirl, false, false); + createIssueIfNotFound("1", wildcats, false, false); + createIssueIfNotFound("1", wildsiderz, false, false); + createIssueIfNotFound("19", vampirella, false, false); + createIssueIfNotFound("1", wolverine, false, false); + createIssueIfNotFound("2", wolverine, false, false); + createIssueIfNotFound("3", wolverine, false, false); + createIssueIfNotFound("4", wolverine, false, false); + createIssueIfNotFound("5", wolverine, false, false); + createIssueIfNotFound("6", wolverine, false, false); + createIssueIfNotFound("1", woodboy, false, false); + createIssueIfNotFound("1", wraithborn, false, false); + createIssueIfNotFound("2", wraithborn, false, false); + createIssueIfNotFound("3", wraithborn, false, false); + createIssueIfNotFound("1", x23, false, false); + createIssueIfNotFound("2", x23, false, false); + createIssueIfNotFound("3", x23, false, false); + createIssueIfNotFound("4", x23, false, false); + createIssueIfNotFound("5", x23, false, false); + createIssueIfNotFound("6", x23, false, false); + createIssueIfNotFound("46", xtremexmen, false, false); + createIssueIfNotFound("1", xmenkittypryde, false, false); + createIssueIfNotFound("2", xmenkittypryde, false, false); + createIssueIfNotFound("3", xmenkittypryde, false, false); + createIssueIfNotFound("4", xmenkittypryde, false, false); + createIssueIfNotFound("5", xmenkittypryde, false, false); + createIssueIfNotFound("1", witchbladetombraider, false, false); + createIssueIfNotFound("1", xmenageofapocalypseoneshot, false, false); + createIssueIfNotFound("1", xmenageofapocalypse, false, false); + createIssueIfNotFound("2", xmenageofapocalypse, false, false); + createIssueIfNotFound("3", xmenageofapocalypse, false, false); + createIssueIfNotFound("4", xmenageofapocalypse, false, false); + createIssueIfNotFound("5", xmenageofapocalypse, false, false); + createIssueIfNotFound("6", xmenageofapocalypse, false, false); + createIssueIfNotFound("1", thorsonasgard, false, false); + createIssueIfNotFound("2", thorsonasgard, false, false); + createIssueIfNotFound("3", thorsonasgard, false, false); + createIssueIfNotFound("4", thorsonasgard, false, false); + createIssueIfNotFound("5", thorsonasgard, false, false); + createIssueIfNotFound("6", thorsonasgard, false, false); + createIssueIfNotFound("7", thorsonasgard, false, false); + createIssueIfNotFound("8", thorsonasgard, false, false); + createIssueIfNotFound("9", thorsonasgard, false, false); + createIssueIfNotFound("10", thorsonasgard, false, false); + createIssueIfNotFound("11", thorsonasgard, false, false); + createIssueIfNotFound("12", thorsonasgard, false, false); + createIssueIfNotFound("1", strange, false, false); + createIssueIfNotFound("2", strange, false, false); + createIssueIfNotFound("3", strange, false, false); + createIssueIfNotFound("4", strange, false, false); + createIssueIfNotFound("5", strange, false, false); + createIssueIfNotFound("6", strange, false, false); + createIssueIfNotFound("0", supergirl, false, false); + createIssueIfNotFound("1", supergirl, false, false); + createIssueIfNotFound("2", supergirl, false, false); + createIssueIfNotFound("3", supergirl, false, false); + createIssueIfNotFound("4", supergirl, false, false); + createIssueIfNotFound("1", robertjordanwheeloftime, false, false); + createIssueIfNotFound("2", robertjordanwheeloftime, false, false); + createIssueIfNotFound("1", stardustkid, false, false); + createIssueIfNotFound("2", stardustkid, false, false); + createIssueIfNotFound("3", stardustkid, false, false); + createIssueIfNotFound("207", superman, false, false); + createIssueIfNotFound("208", superman, false, false); + createIssueIfNotFound("209", superman, false, false); + createIssueIfNotFound("210", superman, false, false); + createIssueIfNotFound("211", superman, false, false); + createIssueIfNotFound("212", superman, false, false); + createIssueIfNotFound("213", superman, false, false); + createIssueIfNotFound("214", superman, false, false); + createIssueIfNotFound("215", superman, false, false); + createIssueIfNotFound("17", supermanbatman, false, false); + createIssueIfNotFound("18", supermanbatman, false, false); + createIssueIfNotFound("19", supermanbatman, false, false); + createIssueIfNotFound("20", supermanbatman, false, false); + createIssueIfNotFound("21", supermanbatman, false, false); + createIssueIfNotFound("22", supermanbatman, false, false); + createIssueIfNotFound("1", tombraidergreatesttreasure, false, false); + createIssueIfNotFound("48", tombraider, false, false); + createIssueIfNotFound("49", tombraider, false, false); + createIssueIfNotFound("50", tombraider, false, false); + createIssueIfNotFound("1", tomstrong, false, false); + createIssueIfNotFound("1", tombdracula, false, false); + createIssueIfNotFound("1", thetenth, false, false); + createIssueIfNotFound("1", devilskeeper, false, false); + createIssueIfNotFound("1", artgreghorn, false, false); + createIssueIfNotFound("81", witchblade, false, false); + createIssueIfNotFound("82", witchblade, false, false); + createIssueIfNotFound("83", witchblade, false, false); + createIssueIfNotFound("84", witchblade, false, false); + createIssueIfNotFound("85", witchblade, false, false); + createIssueIfNotFound("86", witchblade, false, false); + createIssueIfNotFound("87", witchblade, false, false); + createIssueIfNotFound("88", witchblade, false, false); + createIssueIfNotFound("89", witchblade, false, false); + createIssueIfNotFound("90", witchblade, false, false); + createIssueIfNotFound("91", witchblade, false, false); + createIssueIfNotFound("92", witchblade, false, false); + createIssueIfNotFound("19", tarotblackrose, false, false); + createIssueIfNotFound("20", tarotblackrose, false, false); + createIssueIfNotFound("21", tarotblackrose, false, false); + createIssueIfNotFound("22", tarotblackrose, false, false); + createIssueIfNotFound("23", tarotblackrose, false, false); + createIssueIfNotFound("24", tarotblackrose, false, false); + createIssueIfNotFound("25", tarotblackrose, false, false); + createIssueIfNotFound("26", tarotblackrose, false, false); + createIssueIfNotFound("27", tarotblackrose, false, false); + createIssueIfNotFound("28", tarotblackrose, false, false); + createIssueIfNotFound("29", tarotblackrose, false, false); + createIssueIfNotFound("30", tarotblackrose, false, false); + createIssueIfNotFound("31", tarotblackrose, false, false); + createIssueIfNotFound("32", tarotblackrose, false, false); + createIssueIfNotFound("33", tarotblackrose, false, false); + createIssueIfNotFound("34", tarotblackrose, false, false); + createIssueIfNotFound("35", tarotblackrose, false, false); + createIssueIfNotFound("1", spectacularspiderman, false, false); + createIssueIfNotFound("2", spectacularspiderman, false, false); + createIssueIfNotFound("3", spectacularspiderman, false, false); + createIssueIfNotFound("4", spectacularspiderman, false, false); + createIssueIfNotFound("5", spectacularspiderman, false, false); + createIssueIfNotFound("6", spectacularspiderman, false, false); + createIssueIfNotFound("7", spectacularspiderman, false, false); + createIssueIfNotFound("8", spectacularspiderman, false, false); + createIssueIfNotFound("9", spectacularspiderman, false, false); + createIssueIfNotFound("10", spectacularspiderman, false, false); + createIssueIfNotFound("11", spectacularspiderman, false, false); + createIssueIfNotFound("12", spectacularspiderman, false, false); + createIssueIfNotFound("13", spectacularspiderman, false, false); + createIssueIfNotFound("14", spectacularspiderman, false, false); + createIssueIfNotFound("15", spectacularspiderman, false, false); + createIssueIfNotFound("16", spectacularspiderman, false, false); + createIssueIfNotFound("17", spectacularspiderman, false, false); + createIssueIfNotFound("18", spectacularspiderman, false, false); + createIssueIfNotFound("19", spectacularspiderman, false, false); + createIssueIfNotFound("20", spectacularspiderman, false, false); + createIssueIfNotFound("21", spectacularspiderman, false, false); + createIssueIfNotFound("22", spectacularspiderman, false, false); + createIssueIfNotFound("23", spectacularspiderman, false, false); + createIssueIfNotFound("24", spectacularspiderman, false, false); + createIssueIfNotFound("25", spectacularspiderman, false, false); + createIssueIfNotFound("26", spectacularspiderman, false, false); + createIssueIfNotFound("1", soulfire, false, false); + createIssueIfNotFound("2", soulfire, false, false); + createIssueIfNotFound("3", soulfire, false, false); + createIssueIfNotFound("4", soulfire, false, false); + createIssueIfNotFound("5", soulfire, false, false); + createIssueIfNotFound("1", soulfirelight, false, false); + createIssueIfNotFound("2", soulfirelight, false, false); + createIssueIfNotFound("3", soulfirelight, false, false); + createIssueIfNotFound("4", soulfirelight, false, false); + createIssueIfNotFound("1", spellbinders, false, false); + createIssueIfNotFound("2", spellbinders, false, false); + createIssueIfNotFound("3", spellbinders, false, false); + createIssueIfNotFound("4", spellbinders, false, false); + createIssueIfNotFound("5", spellbinders, false, false); + createIssueIfNotFound("6", spellbinders, false, false); + createIssueIfNotFound("1", spidermanlovesmary, false, false); + createIssueIfNotFound("1", spidermanindia, false, false); + createIssueIfNotFound("2", spidermanindia, false, false); + createIssueIfNotFound("3", spidermanindia, false, false); + createIssueIfNotFound("4", spidermanindia, false, false); + createIssueIfNotFound("1", spidermanteam, false, false); + createIssueIfNotFound("2", spidermanteam, false, false); + createIssueIfNotFound("3", spidermanteam, false, false); + createIssueIfNotFound("4", spidermanteam, false, false); + createIssueIfNotFound("5", spidermanteam, false, false); + createIssueIfNotFound("1", spidermanbreakout, false, false); + createIssueIfNotFound("2", spidermanbreakout, false, false); + createIssueIfNotFound("3", spidermanbreakout, false, false); + createIssueIfNotFound("4", spidermanbreakout, false, false); + createIssueIfNotFound("5", spidermanbreakout, false, false); + createIssueIfNotFound("1", spidermanhousem, false, false); + createIssueIfNotFound("2", spidermanhousem, false, false); + createIssueIfNotFound("3", spidermanhousem, false, false); + createIssueIfNotFound("4", spidermanhousem, false, false); + createIssueIfNotFound("13", sojourn, false, false); + createIssueIfNotFound("14", sojourn, false, false); + createIssueIfNotFound("15", sojourn, false, false); + createIssueIfNotFound("16", sojourn, false, false); + createIssueIfNotFound("17", sojourn, false, false); + createIssueIfNotFound("18", sojourn, false, false); + createIssueIfNotFound("19", sojourn, false, false); + createIssueIfNotFound("20", sojourn, false, false); + createIssueIfNotFound("21", sojourn, false, false); + createIssueIfNotFound("22", sojourn, false, false); + createIssueIfNotFound("23", sojourn, false, false); + createIssueIfNotFound("24", sojourn, false, false); + createIssueIfNotFound("25", sojourn, false, false); + createIssueIfNotFound("26", sojourn, false, false); + createIssueIfNotFound("27", sojourn, false, false); + createIssueIfNotFound("28", sojourn, false, false); + createIssueIfNotFound("29", sojourn, false, false); + createIssueIfNotFound("30", sojourn, false, false); + createIssueIfNotFound("31", sojourn, false, false); + createIssueIfNotFound("32", sojourn, false, false); + createIssueIfNotFound("33", sojourn, false, false); + createIssueIfNotFound("34", sojourn, false, false); + createIssueIfNotFound("1", shrek, false, false); + createIssueIfNotFound("2", shrek, false, false); + createIssueIfNotFound("1", shanna, false, false); + createIssueIfNotFound("2", shanna, false, false); + createIssueIfNotFound("3", shanna, false, false); + createIssueIfNotFound("4", shanna, false, false); + createIssueIfNotFound("5", shanna, false, false); + createIssueIfNotFound("6", shanna, false, false); + createIssueIfNotFound("7", shanna, false, false); + createIssueIfNotFound("100", simpsons, false, false); + createIssueIfNotFound("101", simpsons, false, false); + createIssueIfNotFound("102", simpsons, false, false); + createIssueIfNotFound("103", simpsons, false, false); + createIssueIfNotFound("104", simpsons, false, false); + createIssueIfNotFound("105", simpsons, false, false); + createIssueIfNotFound("106", simpsons, false, false); + createIssueIfNotFound("107", simpsons, false, false); + createIssueIfNotFound("108", simpsons, false, false); + createIssueIfNotFound("109", simpsons, false, false); + createIssueIfNotFound("110", simpsons, false, false); + createIssueIfNotFound("111", simpsons, false, false); + createIssueIfNotFound("112", simpsons, false, false); + createIssueIfNotFound("1", newxmenhellions, false, false); + createIssueIfNotFound("2", newxmenhellions, false, false); + createIssueIfNotFound("3", newxmenhellions, false, false); + createIssueIfNotFound("4", newxmenhellions, false, false); + createIssueIfNotFound("1", newxmenacademy, false, false); + createIssueIfNotFound("2", newxmenacademy, false, false); + createIssueIfNotFound("3", newxmenacademy, false, false); + createIssueIfNotFound("4", newxmenacademy, false, false); + createIssueIfNotFound("5", newxmenacademy, false, false); + createIssueIfNotFound("6", newxmenacademy, false, false); + createIssueIfNotFound("7", newxmenacademy, false, false); + createIssueIfNotFound("8", newxmenacademy, false, false); + createIssueIfNotFound("9", newxmenacademy, false, false); + createIssueIfNotFound("10", newxmenacademy, false, false); + createIssueIfNotFound("11", newxmenacademy, false, false); + createIssueIfNotFound("12", newxmenacademy, false, false); + createIssueIfNotFound("13", newxmenacademy, false, false); + createIssueIfNotFound("14", newxmenacademy, false, false); + createIssueIfNotFound("15", newxmenacademy, false, false); + createIssueIfNotFound("16", newxmenacademy, false, false); + createIssueIfNotFound("151", newxmen, false, false); + createIssueIfNotFound("152", newxmen, false, false); + createIssueIfNotFound("153", newxmen, false, false); + createIssueIfNotFound("154", newxmen, false, false); + createIssueIfNotFound("1", redsonja, false, false); + createIssueIfNotFound("2", redsonja, false, false); + createIssueIfNotFound("3", redsonja, false, false); + createIssueIfNotFound("1", revelations, false, false); + createIssueIfNotFound("2", revelations, false, false); + createIssueIfNotFound("3", revelations, false, false); + createIssueIfNotFound("4", revelations, false, false); + createIssueIfNotFound("1", kisskissbangbang, false, false); + createIssueIfNotFound("2", kisskissbangbang, false, false); + createIssueIfNotFound("3", kisskissbangbang, false, false); + createIssueIfNotFound("4", kisskissbangbang, false, false); + createIssueIfNotFound("5", kisskissbangbang, false, false); + createIssueIfNotFound("1", loki, false, false); + createIssueIfNotFound("2", loki, false, false); + createIssueIfNotFound("3", loki, false, false); + createIssueIfNotFound("4", loki, false, false); + createIssueIfNotFound("1", lullaby, false, false); + createIssueIfNotFound("2", lullaby, false, false); + createIssueIfNotFound("3", lullaby, false, false); + createIssueIfNotFound("1", legendofisis, false, false); + createIssueIfNotFound("2", legendofisis, false, false); + createIssueIfNotFound("1", judge, false, false); + createIssueIfNotFound("2", judge, false, false); + createIssueIfNotFound("3", judge, false, false); + createIssueIfNotFound("1", friendlyneighborspider, false, false); + createIssueIfNotFound("2", friendlyneighborspider, false, false); + createIssueIfNotFound("3", friendlyneighborspider, false, false); + createIssueIfNotFound("1", gift, false, false); + createIssueIfNotFound("2", gift, false, false); + createIssueIfNotFound("3", gift, false, false); + createIssueIfNotFound("4", gift, false, false); + createIssueIfNotFound("5", gift, false, false); + createIssueIfNotFound("6", gift, false, false); + createIssueIfNotFound("7", gift, false, false); + createIssueIfNotFound("8", gift, false, false); + createIssueIfNotFound("9", gift, false, false); + createIssueIfNotFound("10", gift, false, false); + createIssueIfNotFound("11", gift, false, false); + createIssueIfNotFound("12", gift, false, false); + createIssueIfNotFound("13", gift, false, false); + createIssueIfNotFound("1/2", fathom, false, false); + createIssueIfNotFound("1", fathom, false, false); + createIssueIfNotFound("2", fathom, false, false); + createIssueIfNotFound("3", fathom, false, false); + createIssueIfNotFound("4", fathom, false, false); + createIssueIfNotFound("5", fathom, false, false); + createIssueIfNotFound("6", fathom, false, false); + createIssueIfNotFound("7", fathom, false, false); + createIssueIfNotFound("8", fathom, false, false); + createIssueIfNotFound("9", fathom, false, false); + createIssueIfNotFound("10", fathom, false, false); + createIssueIfNotFound("11", fathom, false, false); + createIssueIfNotFound("12", fathom, false, false); + createIssueIfNotFound("13", fathom, false, false); + createIssueIfNotFound("14", fathom, false, false); + createIssueIfNotFound("1", fathombeginnings, false, false); + createIssueIfNotFound("1", fathomcannon, false, false); + createIssueIfNotFound("2", fathomcannon, false, false); + createIssueIfNotFound("3", fathomcannon, false, false); + createIssueIfNotFound("1", fathomcannonprelude, false, false); + createIssueIfNotFound("1", fathomdawnofwar, false, false); + createIssueIfNotFound("2", fathomdawnofwar, false, false); + createIssueIfNotFound("3", fathomdawnofwar, false, false); + createIssueIfNotFound("1", fathomprelude, false, false); + createIssueIfNotFound("1", fathomswimsuit, false, false); + createIssueIfNotFound("1", fathomswimsuit2000, false, false); + createIssueIfNotFound("0", fathomvol2, false, false); + createIssueIfNotFound("1", fathomvol2, false, false); + createIssueIfNotFound("2", fathomvol2, false, false); + createIssueIfNotFound("3", fathomvol2, false, false); + createIssueIfNotFound("4", fathomvol2, false, false); + createIssueIfNotFound("5", fathomvol2, false, false); + createIssueIfNotFound("1", flakriot, false, false); + createIssueIfNotFound("1", freshmen, false, false); + createIssueIfNotFound("1", daringescapes, false, false); + createIssueIfNotFound("2", daringescapes, false, false); + createIssueIfNotFound("3", daringescapes, false, false); + createIssueIfNotFound("4", daringescapes, false, false); + createIssueIfNotFound("1", brath, false, false); + createIssueIfNotFound("2", brath, false, false); + createIssueIfNotFound("3", brath, false, false); + createIssueIfNotFound("4", brath, false, false); + createIssueIfNotFound("5", brath, false, false); + createIssueIfNotFound("6", brath, false, false); + createIssueIfNotFound("7", brath, false, false); + createIssueIfNotFound("8", brath, false, false); + createIssueIfNotFound("9", brath, false, false); + createIssueIfNotFound("10", brath, false, false); + createIssueIfNotFound("11", brath, false, false); + createIssueIfNotFound("12", brath, false, false); + createIssueIfNotFound("13", brath, false, false); + createIssueIfNotFound("14", brath, false, false); + createIssueIfNotFound("1", astonishingxmen, false, false); + createIssueIfNotFound("2", astonishingxmen, false, false); + createIssueIfNotFound("3", astonishingxmen, false, false); + createIssueIfNotFound("4", astonishingxmen, false, false); + createIssueIfNotFound("5", astonishingxmen, false, false); + createIssueIfNotFound("6", astonishingxmen, false, false); + createIssueIfNotFound("7", astonishingxmen, false, false); + createIssueIfNotFound("8", astonishingxmen, false, false); + createIssueIfNotFound("9", astonishingxmen, false, false); + createIssueIfNotFound("10", astonishingxmen, false, false); + createIssueIfNotFound("11", astonishingxmen, false, false); + createIssueIfNotFound("12", astonishingxmen, false, false); + createIssueIfNotFound("1", barbarossa, false, false); + createIssueIfNotFound("1", aspenComic, false, false); + createIssueIfNotFound("2", aspenComic, false, false); + createIssueIfNotFound("3", aspenComic, false, false); + createIssueIfNotFound("1", armydarknessreanim, false, false); + createIssueIfNotFound("2", armydarknessreanim, false, false); + createIssueIfNotFound("3", armydarknessreanim, false, false); + createIssueIfNotFound("1", hellcop, false, false); + createIssueIfNotFound("2", hellcop, false, false); + createIssueIfNotFound("3", hellcop, false, false); + createIssueIfNotFound("4", hellcop, false, false); + createIssueIfNotFound("1", housem, false, false); + createIssueIfNotFound("2", housem, false, false); + createIssueIfNotFound("3", housem, false, false); + createIssueIfNotFound("0", hunterkiller, false, false); + createIssueIfNotFound("1", hunterkiller, false, false); + createIssueIfNotFound("2", hunterkiller, false, false); + createIssueIfNotFound("3", hunterkiller, false, false); + createIssueIfNotFound("4", hunterkiller, false, false); + createIssueIfNotFound("1", hunterkillerdossier, false, false); + createIssueIfNotFound("1", ironghost, false, false); + createIssueIfNotFound("1", magdalenavampirella2, false, false); + createIssueIfNotFound("20", marvelknightspider, false, false); + createIssueIfNotFound("39", meridian, false, false); + createIssueIfNotFound("40", meridian, false, false); + createIssueIfNotFound("41", meridian, false, false); + createIssueIfNotFound("42", meridian, false, false); + createIssueIfNotFound("43", meridian, false, false); + createIssueIfNotFound("44", meridian, false, false); + createIssueIfNotFound("1", necromancer, false, false); + createIssueIfNotFound("2", necromancer, false, false); + createIssueIfNotFound("3", necromancer, false, false); + createIssueIfNotFound("1", negationwar, false, false); + createIssueIfNotFound("2", negationwar, false, false); + createIssueIfNotFound("1", mystique, false, false); + createIssueIfNotFound("2", mystique, false, false); + createIssueIfNotFound("3", mystique, false, false); + createIssueIfNotFound("4", mystique, false, false); + createIssueIfNotFound("5", mystique, false, false); + createIssueIfNotFound("6", mystique, false, false); + createIssueIfNotFound("7", mystique, false, false); + createIssueIfNotFound("8", mystique, false, false); + createIssueIfNotFound("9", mystique, false, false); + createIssueIfNotFound("10", mystique, false, false); + createIssueIfNotFound("11", mystique, false, false); + createIssueIfNotFound("12", mystique, false, false); + createIssueIfNotFound("13", mystique, false, false); + createIssueIfNotFound("14", mystique, false, false); + createIssueIfNotFound("15", mystique, false, false); + createIssueIfNotFound("16", mystique, false, false); + createIssueIfNotFound("17", mystique, false, false); + createIssueIfNotFound("18", mystique, false, false); + createIssueIfNotFound("19", mystique, false, false); + createIssueIfNotFound("20", mystique, false, false); + createIssueIfNotFound("21", mystique, false, false); + createIssueIfNotFound("22", mystique, false, false); + createIssueIfNotFound("23", mystique, false, false); + createIssueIfNotFound("24", mystique, false, false); + createIssueIfNotFound("1", abadazad, false, false); + createIssueIfNotFound("2", abadazad, false, false); + createIssueIfNotFound("3", abadazad, false, false); + createIssueIfNotFound("503", amazingspiderman, false, false); + createIssueIfNotFound("504", amazingspiderman, false, false); + createIssueIfNotFound("505", amazingspiderman, false, false); + createIssueIfNotFound("506", amazingspiderman, false, false); + createIssueIfNotFound("507", amazingspiderman, false, false); + createIssueIfNotFound("508", amazingspiderman, false, false); + createIssueIfNotFound("509", amazingspiderman, false, false); + createIssueIfNotFound("510", amazingspiderman, false, false); + createIssueIfNotFound("511", amazingspiderman, false, false); + createIssueIfNotFound("512", amazingspiderman, false, false); + createIssueIfNotFound("513", amazingspiderman, false, false); + createIssueIfNotFound("514", amazingspiderman, false, false); + createIssueIfNotFound("515", amazingspiderman, false, false); + createIssueIfNotFound("516", amazingspiderman, false, false); + createIssueIfNotFound("517", amazingspiderman, false, false); + createIssueIfNotFound("518", amazingspiderman, false, false); + createIssueIfNotFound("519", amazingspiderman, false, false); + createIssueIfNotFound("520", amazingspiderman, false, false); + createIssueIfNotFound("521", amazingspiderman, false, false); + createIssueIfNotFound("522", amazingspiderman, false, false); + createIssueIfNotFound("523", amazingspiderman, false, false); + createIssueIfNotFound("524", amazingspiderman, false, false); + createIssueIfNotFound("525", amazingspiderman, false, false); + createIssueIfNotFound("526", amazingspiderman, false, false); + createIssueIfNotFound("1", dangergirlbackinblack, false, false); + createIssueIfNotFound("2", dangergirlbackinblack, false, false); + createIssueIfNotFound("1", darknesssuperman, false, false); + createIssueIfNotFound("2", darknesssuperman, false, false); + createIssueIfNotFound("1", darknesstombraider, false, false); + createIssueIfNotFound("1", darknessvampirella, false, false); + createIssueIfNotFound("1", darknessblacksails, false, false); + createIssueIfNotFound("17", darknessvol2, false, false); + createIssueIfNotFound("18", darknessvol2, false, false); + createIssueIfNotFound("19", darknessvol2, false, false); + createIssueIfNotFound("20", darknessvol2, false, false); + createIssueIfNotFound("21", darknessvol2, false, false); + createIssueIfNotFound("22", darknessvol2, false, false); + createIssueIfNotFound("23", darknessvol2, false, false); + createIssueIfNotFound("1", aria, false, false); + createIssueIfNotFound("2", aria, false, false); + createIssueIfNotFound("3", aria, false, false); + createIssueIfNotFound("4", aria, false, false); + createIssueIfNotFound("1", ariasoulmarket, false, false); + createIssueIfNotFound("2", ariasoulmarket, false, false); + createIssueIfNotFound("3", ariasoulmarket, false, false); + createIssueIfNotFound("4", ariasoulmarket, false, false); + createIssueIfNotFound("5", ariasoulmarket, false, false); + createIssueIfNotFound("6", ariasoulmarket, false, false); + createIssueIfNotFound("1", ariasummerspell, false, false); + createIssueIfNotFound("2", ariasummerspell, false, false); + createIssueIfNotFound("1", ariaenchantment, false, false); + createIssueIfNotFound("2", ariaenchantment, false, false); + createIssueIfNotFound("3", ariaenchantment, false, false); + createIssueIfNotFound("4", ariaenchantment, false, false); + createIssueIfNotFound("1", armydarknessashes, false, false); + createIssueIfNotFound("2", armydarknessashes, false, false); + createIssueIfNotFound("3", armydarknessashes, false, false); + createIssueIfNotFound("4", armydarknessashes, false, false); + createIssueIfNotFound("1", armydarknessshop, false, false); + createIssueIfNotFound("2", armydarknessshop, false, false); + createIssueIfNotFound("1", hackslashtoys, false, false); + createIssueIfNotFound("2", hackslashtoys, false, false); + createIssueIfNotFound("1", harryjohnson, false, false); + createIssueIfNotFound("4", battlepope, false, false); + createIssueIfNotFound("5", battlepope, false, false); + createIssueIfNotFound("6", battlepope, false, false); + createIssueIfNotFound("7", battlepope, false, false); + createIssueIfNotFound("8", battlepope, false, false); + createIssueIfNotFound("9", battlepope, false, false); + createIssueIfNotFound("10", battlepope, false, false); + createIssueIfNotFound("11", battlepope, false, false); + createIssueIfNotFound("12", battlepope, false, false); + List volumes = volumeRepository.findAll(); + volumes.forEach(volume -> volumeRepository.delete(volume)); + moduleService.setDataImported(ComicConstants.COMICS); + } + + private Artist createArtistIfNotFound(String artistName) { + log.info("createArtistIfNotFound {}", artistName); + Artist artist = artistRepository.findByName(artistName); + if (artist == null) { + log.info("Artist {} not found, will create it", artistName); + artist = new Artist(); + artist.setName(artistName); + artistRepository.save(artist); + } + return artist; + } + + private Publisher createPublisherIfNotFound(String publisherName) { + log.info("createPublisherIfNotFound {}", publisherName); + Publisher publisher = publisherRepository.findByName(publisherName); + if (publisher == null) { + log.info("Publisher {} not found, will create it", publisherName); + publisher = new Publisher(); + publisher.setName(publisherName); + publisherRepository.save(publisher); + } + return publisher; + } + + private Worktype createWorktypeIfNotFound(String workTypeName) { + log.info("createWorktypeIfNotFound {}", workTypeName); + Worktype worktype = worktypeRepository.findByName(workTypeName); + if (worktype == null) { + log.info("Worktype {} not found, will create it", workTypeName); + worktype = new Worktype(); + worktype.setName(workTypeName); + worktypeRepository.save(worktype); + } + return worktype; + } + + private Comic createComicIfNotFound(Publisher publisher, String title, boolean currentOrder, boolean completed) { + log.info("createComicIfNotFound {} {} {} {}", publisher, title, currentOrder, completed); + Comic comic = comicRepository.findByTitleAndPublisher(title, publisher); + if (comic == null) { + log.info("Comic {} from {} not found, will create it", title, publisher.getName()); + comic = new Comic(); + comic.setTitle(title); + comic.setPublisher(publisher); + comic.setCurrentOrder(currentOrder); + comic.setCompleted(completed); + comicRepository.save(comic); + } + return comic; + } + + private ComicWork createComicWorkIfNotFound(Comic comic, Artist artist, Worktype worktype) { + log.info("createComicWorkIfNotFound {} {} {}", comic, artist, worktype); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + if (comicWork == null) { + log.info("ComicWork {} from {} for {} not found, will create it", worktype, artist, comic); + comicWork = new ComicWork(); + comicWork.setComic(comic); + comicWork.setArtist(artist); + comicWork.setWorkType(worktype); + comicWorkRepository.save(comicWork); + } + return comicWork; + } + + private void createStoryArcIfNotFound(String name, Comic comic) { + log.info("createStoryArcIfNotFound {} {}", comic, name); + StoryArc storyArc = storyArcRepository.findByNameAndComic(name, comic); + if (storyArc == null) { + log.info("StoryArc {} for {} not found, will create it", name, comic); + storyArc = new StoryArc(); + storyArc.setName(name); + storyArc.setComic(comic); + storyArcRepository.save(storyArc); + } + } + + private void createTradePaperbackIfNotFound(String name, Comic comic, int start, int end) { + log.info("createTradePaperbackIfNotFound {} {} {}-{}", comic, name, start, end); + TradePaperback tradePaperback = tradePaperbackRepository.findByFields(name, comic, start, end); + if (tradePaperback == null) { + log.info("TradePaperback {} for {} with issues {}-{} not found, will create it", name, comic, start, end); + tradePaperback = new TradePaperback(); + tradePaperback.setName(name); + tradePaperback.setComic(comic); + tradePaperback.setIssueStart(start); + tradePaperback.setIssueEnd(end); + tradePaperbackRepository.save(tradePaperback); + } + } + + private void createIssueIfNotFound(String issueNumber, Comic comic, boolean isRead, boolean inStock) { + log.info("createIssueIfNotFound {} {} {} {}", comic, issueNumber, isRead, inStock); + Issue issue = issueRepository.findByComicAndIssueNumber(comic, issueNumber); + if (issue == null) { + log.info("Issue {} for {} not found, will create it", issueNumber, comic); + issue = new Issue(); + issue.setIssueNumber(issueNumber); + issue.setComic(comic); + issue.setIsRead(isRead); + issue.setInStock(inStock); + issueRepository.save(issue); + } + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java new file mode 100644 index 0000000..6a3159d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents an artist in the system. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Slf4j +@Entity +public class Artist extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "artist", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks; + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Artist{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java new file mode 100644 index 0000000..a5b94f2 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ArtistRepository extends JpaRepository { + @Query("select a from Artist a " + + "where lower(a.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByNameIgnoreCase(String name); + + Artist findByName(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Comic.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Comic.java new file mode 100644 index 0000000..bad2914 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Comic.java @@ -0,0 +1,77 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a comic entity. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Comic extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String title; + + @ManyToOne + @JoinColumn(name = "publisher_id") + @NotNull + @JsonIgnoreProperties({ "comics" }) + private Publisher publisher; + + private Boolean currentOrder = false; + + private Boolean completed = false; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List issues = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List storyArcs = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List tradePaperbacks = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List volumes = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Comic{"); + sb.append("title='").append(title).append('\''); + sb.append(", publisher=").append(publisher); + sb.append(", currentOrder=").append(currentOrder); + sb.append(", completed=").append(completed); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java new file mode 100644 index 0000000..3a3e61f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ComicRepository extends JpaRepository { + @Query("select c from Comic c " + + "where lower(c.title) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Comic findByTitleAndPublisher(String title, Publisher publisher); + + List findByTitle(String title); + + List findByTitleIgnoreCase(String title); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java new file mode 100644 index 0000000..840f93c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.comics.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(indexes = {@Index(columnList = "comic_id, artist_id, workType_id") }, + uniqueConstraints = @UniqueConstraint(columnNames = {"comic_id", "artist_id", "workType_id" }) +) +public class ComicWork extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + private Comic comic; + + @ManyToOne + @JoinColumn(name = "artist_id") + @NotNull + private Artist artist; + + @ManyToOne + @JoinColumn(name = "workType_id") + @NotNull + private Worktype workType; + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ComicWork{"); + sb.append("comic=").append(comic); + sb.append(", artist=").append(artist); + sb.append(", workType=").append(workType); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java new file mode 100644 index 0000000..6b3540a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ComicWorkRepository extends JpaRepository { + + @Query("SELECT c from ComicWork c where c.comic = ?1 and c.artist = ?2 and c.workType = ?3") + ComicWork findbyComicAndArtistAndWorktype(Comic comic, Artist artist, Worktype worktype); + + @Query("select c from ComicWork c where c.comic = ?1") + List findByComic(Comic comic); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Issue.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Issue.java new file mode 100644 index 0000000..5a36f7d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Issue.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.comics.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class Issue extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "issues" }) + private Comic comic; + + @ManyToOne + @JoinColumn(name = "volume_id") + @JsonIgnoreProperties({ "issues" }) + @Nullable + private Volume volume; + + @NotEmpty + private String issueNumber; + + private Boolean isRead; + + private Boolean inStock; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java new file mode 100644 index 0000000..af10705 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface IssueRepository extends JpaRepository { + @Query("select i from Issue i " + + "where lower(i.issueNumber) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByComic(Comic comic); + + Issue findByComicAndIssueNumber(Comic comic, String issueNumber); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java new file mode 100644 index 0000000..233d405 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Publisher extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true) + @Nullable + private List comics = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Publisher{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java new file mode 100644 index 0000000..5b5587d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PublisherRepository extends JpaRepository { + @Query("select p from Publisher p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Publisher findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java new file mode 100644 index 0000000..34f2188 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java @@ -0,0 +1,31 @@ +package de.thpeetz.kontor.comics.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class StoryArc extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "storyArcs" }) + private Comic comic; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java new file mode 100644 index 0000000..a74619f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface StoryArcRepository extends JpaRepository { + @Query("select s from StoryArc s " + + "where lower(s.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + StoryArc findByNameAndComic(String name, Comic comic); + + List findByComic(Comic comic); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java new file mode 100644 index 0000000..07ee8d3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.comics.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class TradePaperback extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + private Comic comic; + + private Integer issueStart; + + private Integer issueEnd; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java new file mode 100644 index 0000000..fba26ad --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java @@ -0,0 +1,20 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface TradePaperbackRepository extends JpaRepository { + @Query("select t from TradePaperback t " + + "where lower(t.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByComic(Comic comic); + + List findByNameAndComic(String name, Comic comic); + + @Query("select t from TradePaperback t where t.name = ?1 and t.comic = ?2 and t.issueStart = ?3 and t.issueEnd = ?4") + TradePaperback findByFields(String name, Comic comic, int start, int end); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Volume.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Volume.java new file mode 100644 index 0000000..c8e1eb0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Volume.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class Volume extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "volumes" }) + private Comic comic; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "volume", cascade = CascadeType.REMOVE, orphanRemoval = true) + @Nullable + private List issues = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java new file mode 100644 index 0000000..3b70670 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VolumeRepository extends JpaRepository { + + List findByName(String name); + + List findByComic(Comic comic); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java new file mode 100644 index 0000000..9c83133 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a work type in the application. + * This class extends the AbstractEntity class. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Worktype extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "workType", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Worktype{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java new file mode 100644 index 0000000..2afebaf --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface WorktypeRepository extends JpaRepository { + @Query("select w from Worktype w " + + "where lower(w.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + + Worktype findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java b/springboot/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java new file mode 100644 index 0000000..ae4637f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java @@ -0,0 +1,299 @@ +package de.thpeetz.kontor.comics.services; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ArtistRepository; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicRepository; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.ComicWorkRepository; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.IssueRepository; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.PublisherRepository; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.StoryArcRepository; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.TradePaperbackRepository; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.VolumeRepository; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.data.WorktypeRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class ComicService { + + private final PublisherRepository publisherRepository; + private final ComicRepository comicRepository; + private final ArtistRepository artistRepository; + private final IssueRepository issueRepository; + private final StoryArcRepository storyArcRepository; + private final TradePaperbackRepository tradePaperbackRepository; + private final ComicWorkRepository comicWorkRepository; + private final VolumeRepository volumeRepository; + private final WorktypeRepository worktypeRepository; + + public ComicService(PublisherRepository publisherRepository, ComicRepository comicRepository, + ArtistRepository artistRepository, IssueRepository issueRepository, StoryArcRepository storyArcRepository, + TradePaperbackRepository tradePaperbackRepository, ComicWorkRepository comicWorkRepository, + VolumeRepository volumeRepository, WorktypeRepository worktypeRepository) { + + this.publisherRepository = publisherRepository; + this.comicRepository = comicRepository; + this.artistRepository = artistRepository; + this.issueRepository = issueRepository; + this.storyArcRepository = storyArcRepository; + this.tradePaperbackRepository = tradePaperbackRepository; + this.comicWorkRepository = comicWorkRepository; + this.volumeRepository = volumeRepository; + this.worktypeRepository = worktypeRepository; + } + + public List findAllPublishers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return publisherRepository.findAll(); + } else { + return publisherRepository.search(stringFilter); + } + } + + public Publisher findPublisherByName(String publisherName) { + return publisherRepository.findByName(publisherName); + } + + public void deletePublisher(Publisher publisher) { + publisherRepository.delete(publisher); + } + + public void savePublisher(Publisher publisher) { + if (publisher == null) { + log.warn("Publisher is null. Are you sure you have connected your form to the application?"); + return; + } + publisherRepository.save(publisher); + } + + public List findAllComics(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return comicRepository.findAll(); + } else { + return comicRepository.search(stringFilter); + } + } + + public Comic findComicByTitle(String title) { + List comics = comicRepository.findByTitle(title); + if (comics.size() == 1) { + return comics.get(0); + } + return null; + } + + public void deleteComic(Comic comic) { + Publisher publisher = comic.getPublisher(); + publisher.getComics().remove(comic); + publisherRepository.save(publisher); + comicRepository.delete(comic); + } + + public void saveComic(Comic comic) { + if (comic == null) { + log.warn("Comic is null. Are you sure you have connected your form to the application?"); + return; + } + comicRepository.save(comic); + } + + public List findAllArtists(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return artistRepository.findAll(); + } else { + return artistRepository.search(stringFilter); + } + } + + public Artist findArtistByName(String artistName) { + if (artistName == null || artistName.isEmpty()) { + return null; + } else { + return artistRepository.findByName(artistName); + } + } + + public void deleteArtist(Artist artist) { + artistRepository.delete(artist); + } + + public void saveArtist(Artist artist) { + if (artist == null) { + log.warn("Artist is null. Are you sure you have connected your form to the application?"); + return; + } + artistRepository.save(artist); + } + + public List findAllIssues() { + return issueRepository.findAll(); + } + + public List findAllIssuesForComic(Comic comic) { + if (comic == null) { + return issueRepository.findAll(); + } else { + log.info("Find issues for Comic: {}", comic); + return issueRepository.findByComic(comic); + } + } + + public void saveIssue(Issue issue) { + if (issue == null) { + log.warn("Issue is null. Are you sure you have connected your form to the application?"); + return; + } + issueRepository.save(issue); + } + + public void deleteIssue(Issue issue) { + issueRepository.delete(issue); + } + + public List findAllStoryArcs() { + return storyArcRepository.findAll(); + } + + public List findAllStoryArcsForComic(Comic comic) { + if (comic == null) { + return storyArcRepository.findAll(); + } else { + log.info("Find Story Arc for Comic: {}", comic); + return storyArcRepository.findByComic(comic); + } + } + + public void saveStoryArc(StoryArc storyArc) { + storyArcRepository.save(storyArc); + } + + public void deleteStoryArc(StoryArc storyArc) { + Comic comic = storyArc.getComic(); + comic.getStoryArcs().remove(storyArc); + comicRepository.save(comic); + storyArcRepository.delete(storyArc); + } + + public List findAllTradePaperbacks(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return tradePaperbackRepository.findAll(); + } else { + return tradePaperbackRepository.search(stringFilter); + } + } + + public void deleteTradePaperBack(TradePaperback tradepaperback) { + tradePaperbackRepository.delete(tradepaperback); + } + + public void saveTradePaperBack(TradePaperback tradepaperback) { + if (tradepaperback == null) { + log.warn("TradePaperBack is null. Are you sure you have connected your form to the application?"); + return; + } + tradePaperbackRepository.save(tradepaperback); + } + + public List findAllComicWorks() { + return comicWorkRepository.findAll(); + } + + public void saveComicWork(ComicWork comicWork) { + if (comicWork == null) { + log.warn("ComicWork is null. Are you sure you have connected your form to the application?"); + return; + } + comicWorkRepository.save(comicWork); + } + + public void deleteComicWork(ComicWork comicWork) { + Comic comic = comicWork.getComic(); + comic.getComicWorks().remove(comicWork); + comicRepository.save(comic); + Artist artist = comicWork.getArtist(); + artist.getComicWorks().remove(comicWork); + artistRepository.save(artist); + Worktype worktype = comicWork.getWorkType(); + worktype.getComicWorks().remove(comicWork); + worktypeRepository.save(worktype); + comicWorkRepository.delete(comicWork); + } + + public List findAllVolumes() { + return volumeRepository.findAll(); + } + + public List findAllVolumesForComic(Comic comic) { + if (comic == null) { + return volumeRepository.findAll(); + } else { + log.info("Find Volume for Comic: {}", comic); + return volumeRepository.findByComic(comic); + } + } + + public void saveVolume(Volume volume) { + if (volume == null) { + log.warn("Volume is null. Are you sure you have connected your form to the application?"); + return; + } + volumeRepository.save(volume); + } + + public void deleteVolume(Volume volume) { + Comic comic = volume.getComic(); + comic.getVolumes().remove(volume); + comicRepository.save(comic); + volumeRepository.delete(volume); + } + + public List findAllWorktypes(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return worktypeRepository.findAll(); + } else { + return worktypeRepository.search(stringFilter); + } + } + + public Worktype findWorktypeByName(String worktypeName) { + if (worktypeName == null || worktypeName.isEmpty()) { + return null; + } else { + return worktypeRepository.findByName(worktypeName); + } + } + + public void saveWorktype(Worktype worktype) { + if (worktype == null) { + log.warn("Worktype is null. Are you sure you have connected your form to the application?"); + return; + } + worktypeRepository.save(worktype); + } + + public void deleteWorktype(Worktype worktype) { + List comicWorks = worktype.getComicWorks(); + if (comicWorks == null) { + log.warn("reference to ComicWork is null"); + return; + } + log.info("found {} references to ComicWork", comicWorks.size()); + comicWorks.forEach(comicWork -> { + comicWork.setWorkType(null); + }); + log.info("delete Worktype: {}", worktype); + worktypeRepository.delete(worktype); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java new file mode 100644 index 0000000..43a93d4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ComicWork; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ArtistForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Artist.class); + + public ArtistForm() { + addClassName("artist-form"); + binder.bindInstanceFields(this); + + comicWorks.setColumns("workType.name", "comic.title"); + comicWorks.getColumnByKey("workType.name").setHeader("Work type"); + comicWorks.getColumnByKey("comic.title").setHeader("Comic"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setArtist(Artist artist) { + binder.setBean(artist); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + this.comicWorks.setItems(works); + } + + public abstract static class ArtistFormEvent extends ComponentEvent { + private Artist artist; + + protected ArtistFormEvent(ArtistForm source, Artist artist) { + super(source, false); + this.artist = artist; + } + + public Artist getArtist() { + return artist; + } + } + + public static class SaveEvent extends ArtistFormEvent { + SaveEvent(ArtistForm source, Artist artist) { + super(source, artist); + } + } + + public static class DeleteEvent extends ArtistFormEvent { + DeleteEvent(ArtistForm source, Artist artist) { + super(source, artist); + } + } + + public static class CloseEvent extends ArtistFormEvent { + CloseEvent(ArtistForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java new file mode 100644 index 0000000..f0d294d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = "artist", layout = MainLayout.class) +@PageTitle("Artist | Comics | Kontor") +public class ArtistView extends VerticalLayout { + + Grid grid = new Grid<>(Artist.class); + TextField filterText = new TextField(); + ArtistForm form; + ComicService service; + + public ArtistView(ComicService service) { + this.service = service; + addClassName("artist-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + public ArtistForm getForm() { + return form; + } + + private void configureGrid() { + grid.addClassName("artist-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editArtist(event.getValue())); + } + + private void configureForm() { + form = new ArtistForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveArtist); + form.addDeleteListener(this::deleteArtist); + form.addCloseListener(e -> closeEditor()); + } + + private void saveArtist(ArtistForm.SaveEvent event) { + service.saveArtist(event.getArtist()); + updateList(); + closeEditor(); + } + + private void deleteArtist(ArtistForm.DeleteEvent event) { + service.deleteArtist(event.getArtist()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addArtistButton = new Button("Add artist"); + addArtistButton.addClickListener(click -> addArtist()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addArtistButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editArtist(Artist artist) { + if (artist == null) { + closeEditor(); + } else { + form.setArtist(artist); + form.setComicWorks(artist.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setArtist(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addArtist() { + grid.asSingleSelect().clear(); + editArtist(new Artist()); + } + + public void updateList() { + grid.setItems(service.findAllArtists(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java new file mode 100644 index 0000000..55fc557 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Publisher; + +public class ComicForm extends FormLayout { + + private static final Logger log = LoggerFactory.getLogger(ComicForm.class); + + TextField title = new TextField("Title"); + ComboBox publisher = new ComboBox<>("Publisher"); + Checkbox currentOrder = new Checkbox("Current order"); + Checkbox completed = new Checkbox("Completed"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Comic.class); + + public ComicForm(List publishers) { + addClassName("comic-form"); + binder.bindInstanceFields(this); + + publisher.setItems(publishers); + publisher.setItemLabelGenerator(Publisher::getName); + // comicWorks.addClassName("comic-works-grid"); + // comicWorks.setSizeFull(); + comicWorks.setColumns("workType.name", "artist.name"); + comicWorks.getColumnByKey("workType.name").setHeader("Work type"); + comicWorks.getColumnByKey("artist.name").setHeader("Artist"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + add(title, publisher, currentOrder, completed, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setComic(Comic comic) { + binder.setBean(comic); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + comicWorks.setItems(works); + } + + public abstract static class ComicFormEvent extends ComponentEvent { + private Comic comic; + + protected ComicFormEvent(ComicForm source, Comic comic) { + super(source, false); + this.comic = comic; + } + + public Comic getComic() { + return comic; + } + } + + public static class SaveEvent extends ComicFormEvent { + SaveEvent(ComicForm source, Comic comic) { + super(source, comic); + } + } + + public static class DeleteEvent extends ComicFormEvent { + DeleteEvent(ComicForm source, Comic comic) { + super(source, comic); + } + } + + public static class CloseEvent extends ComicFormEvent { + CloseEvent(ComicForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java new file mode 100644 index 0000000..cffdbdb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a custom layout for the comic view in the application. + * This layout extends the AppLayout class. + */ +@Slf4j +public class ComicLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public ComicLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(ComicConstants.COMICS); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(ComicConstants.getComicLink(), ComicConstants.getPublisherLink(), ComicConstants.getIssueLink(), + ComicConstants.getTradePaperbackLink(), ComicConstants.getStoryArcLink(), + ComicConstants.getVolumeLink(), ComicConstants.getArtistLink(), ComicConstants.getComicWorkLink(), + ComicConstants.getWorktypeLink()); + return navigation; + } +} + diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java new file mode 100644 index 0000000..ec09544 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java @@ -0,0 +1,139 @@ +package de.thpeetz.kontor.comics.views; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.COMICS_ROUTE, layout = MainLayout.class) +@PageTitle("Comic | Comics | Kontor") +public class ComicView extends VerticalLayout { + + Grid grid = new Grid<>(Comic.class); + TextField filterText = new TextField(); + ComicForm form; + ComicService service; + + public ComicView(ComicService service) { + this.service = service; + addClassName("comic-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("comic-grid"); + grid.setSizeFull(); + grid.setColumns("title", "publisher.name", "currentOrder", "completed"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editComic(event.getValue())); + } + + public ComicForm getForm() { + return form; + } + + private void configureForm() { + form = new ComicForm(service.findAllPublishers(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveComic); + form.addDeleteListener(this::deleteComic); + form.addCloseListener(e -> closeEditor()); + } + + private void saveComic(ComicForm.SaveEvent event) { + service.saveComic(event.getComic()); + updateList(); + closeEditor(); + } + + private void deleteComic(ComicForm.DeleteEvent event) { + service.deleteComic(event.getComic()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addComicButton = new Button("Add comic"); + addComicButton.addClickListener(click -> addComic()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addComicButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editComic(Comic comic) { + if (comic == null) { + closeEditor(); + } else { + form.setComic(comic); + if (comic.getComicWorks() == null) { + log.info("No comic works"); + } else { + log.info("Comic works sze: {}", comic.getComicWorks().size()); + } + form.setComicWorks(comic.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setComic(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addComic() { + grid.asSingleSelect().clear(); + editComic(new Comic()); + } + + public void updateList() { + grid.setItems(service.findAllComics(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java new file mode 100644 index 0000000..698a37e --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Worktype; + +public class ComicWorkForm extends FormLayout { + ComboBox comic = new ComboBox<>("Comic"); + ComboBox artist = new ComboBox<>("Artist"); + ComboBox workType = new ComboBox<>("Worktype"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(ComicWork.class); + + public ComicWorkForm(List comics, List artists, List workTypes) { + addClassName("comicwork-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + artist.setItems(artists); + artist.setItemLabelGenerator(Artist::getName); + workType.setItems(workTypes); + workType.setItemLabelGenerator(Worktype::getName); + add(comic, artist, workType, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setComicWork(ComicWork comicWork) { + binder.setBean(comicWork); + } + + public abstract static class ComicWorkFormEvent extends ComponentEvent { + private ComicWork comicWork; + + protected ComicWorkFormEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, false); + this.comicWork = comicWork; + } + + public ComicWork getComicWork() { + return comicWork; + } + } + + public static class SaveEvent extends ComicWorkFormEvent { + SaveEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, comicWork); + } + } + + public static class DeleteEvent extends ComicWorkFormEvent { + DeleteEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, comicWork); + } + } + + public static class CloseEvent extends ComicWorkFormEvent { + CloseEvent(ComicWorkForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java new file mode 100644 index 0000000..449a76c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java @@ -0,0 +1,122 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.COMICWORK_ROUTE, layout = MainLayout.class) +@PageTitle("ComicWork | Comics | Kontor") +public class ComicWorkView extends VerticalLayout { + + Grid grid = new Grid<>(ComicWork.class); + ComicWorkForm form; + ComicService service; + + public ComicWorkView(ComicService service) { + this.service = service; + addClassName("comicWork-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("comic-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "artist.name", "workType.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editComicWork(event.getValue())); + } + + public ComicWorkForm getForm() { + return form; + } + + private void configureForm() { + form = new ComicWorkForm(service.findAllComics(null), service.findAllArtists(null), + service.findAllWorktypes(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveComicWork); + form.addDeleteListener(this::deleteComicWork); + form.addCloseListener(e -> closeEditor()); + } + + private void saveComicWork(ComicWorkForm.SaveEvent event) { + service.saveComicWork(event.getComicWork()); + updateList(); + closeEditor(); + } + + private void deleteComicWork(ComicWorkForm.DeleteEvent event) { + service.deleteComicWork(event.getComicWork()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addComicButton = new Button("Add ComicWork"); + addComicButton.addClickListener(click -> addComicWork()); + + HorizontalLayout toolbar = new HorizontalLayout(addComicButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editComicWork(ComicWork comicWork) { + if (comicWork == null) { + closeEditor(); + } else { + form.setComicWork(comicWork); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setComicWork(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addComicWork() { + grid.asSingleSelect().clear(); + editComicWork(new ComicWork()); + } + + public void updateList() { + grid.setItems(service.findAllComicWorks()); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java new file mode 100644 index 0000000..d537210 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Issue; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class IssueForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + TextField issueNumber = new TextField("Issue number"); + Checkbox isRead = new Checkbox("Read"); + Checkbox inStock = new Checkbox("In stock"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Issue.class); + + public IssueForm(List comics) { + addClassName("issue-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, issueNumber, isRead, inStock, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setIssue(Issue issue) { + binder.setBean(issue); + } + + public abstract static class IssueFormEvent extends ComponentEvent { + private Issue issue; + + protected IssueFormEvent(IssueForm source, Issue issue) { + super(source, false); + this.issue = issue; + } + + public Issue getIssue() { + return issue; + } + } + + public static class SaveEvent extends IssueFormEvent { + SaveEvent(IssueForm source, Issue issue) { + super(source, issue); + } + } + + public static class DeleteEvent extends IssueFormEvent { + DeleteEvent(IssueForm source, Issue issue) { + super(source, issue); + } + } + + public static class CloseEvent extends IssueFormEvent { + CloseEvent(IssueForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java new file mode 100644 index 0000000..4b9d58c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.ISSUE_ROUTE, layout = MainLayout.class) +@PageTitle("Issue | Comics | Kontor") +public class IssueView extends VerticalLayout { + + Grid grid = new Grid<>(Issue.class); + ComboBox comicFilter = new ComboBox<>("Comic"); + IssueForm form; + ComicService service; + + public IssueView(ComicService service) { + this.service = service; + addClassName("issue-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("issue-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "issueNumber", "isRead", "inStock"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editIssue(event.getValue())); + } + + public IssueForm getForm() { + return form; + } + + private void configureForm() { + form = new IssueForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveIssue); + form.addDeleteListener(this::deleteIssue); + form.addCloseListener(e -> closeEditor()); + } + + private void saveIssue(IssueForm.SaveEvent event) { + service.saveIssue(event.getIssue()); + updateList(); + closeEditor(); + } + + private void deleteIssue(IssueForm.DeleteEvent event) { + service.deleteIssue(event.getIssue()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.addValueChangeListener(e -> updateList()); + comicFilter.setClearButtonVisible(true); + + Button addIssueButton = new Button("Add issue", click -> addIssue()); + + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addIssueButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editIssue(Issue issue) { + if (issue == null) { + closeEditor(); + } else { + form.setIssue(issue); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setIssue(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addIssue() { + grid.asSingleSelect().clear(); + editIssue(new Issue()); + } + + private void updateList() { + grid.setItems(service.findAllIssuesForComic(comicFilter.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java new file mode 100644 index 0000000..dbd2f62 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java @@ -0,0 +1,100 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Publisher; + +public class PublisherForm extends FormLayout { + public TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Publisher.class); + + public PublisherForm() { + addClassName("publisher-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPublisher(Publisher publisher) { + binder.setBean(publisher); + } + + public abstract static class PublisherFormEvent extends ComponentEvent { + private Publisher publisher; + + protected PublisherFormEvent(PublisherForm source, Publisher publisher) { + super(source, false); + this.publisher = publisher; + } + + public Publisher getPublisher() { + return publisher; + } + } + + public static class SaveEvent extends PublisherFormEvent { + SaveEvent(PublisherForm source, Publisher publisher) { + super(source, publisher); + } + } + + public static class DeleteEvent extends PublisherFormEvent { + DeleteEvent(PublisherForm source, Publisher publisher) { + super(source, publisher); + } + } + + public static class CloseEvent extends PublisherFormEvent { + CloseEvent(PublisherForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java new file mode 100644 index 0000000..b716889 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.PUBLISHER_ROUTE, layout = MainLayout.class) +@PageTitle("Publisher | Comics | Kontor") +public class PublisherView extends VerticalLayout { + + Grid grid = new Grid<>(Publisher.class); + TextField filterText = new TextField(); + PublisherForm form; + + ComicService service; + + public PublisherView(ComicService service) { + this.service = service; + addClassName("publisher-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("publisher-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPublisher(event.getValue())); + } + + private void configureForm() { + form = new PublisherForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePublisher); + form.addDeleteListener(this::deletePublisher); + form.addCloseListener(e -> closeEditor()); + } + + private void savePublisher(PublisherForm.SaveEvent event) { + service.savePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + private void deletePublisher(PublisherForm.DeleteEvent event) { + service.deletePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + public Grid getGrid() { + return grid; + } + + public PublisherForm getForm() { + return form; + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPublisherButton = new Button("Add publisher"); + addPublisherButton.addClickListener(click -> addPublisher()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPublisherButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPublisher(Publisher publisher) { + if (publisher == null) { + closeEditor(); + } else { + form.setPublisher(publisher); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPublisher(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPublisher() { + grid.asSingleSelect().clear(); + editPublisher(new Publisher()); + } + + public void updateList() { + grid.setItems(service.findAllPublishers(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java new file mode 100644 index 0000000..786a792 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; + +import java.util.List; + +public class StoryArcForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + TextField name = new TextField("Story Arc Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(StoryArc.class); + + public StoryArcForm(List comics) { + addClassName("storyarc-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setStoryArc(StoryArc storyArc) { + binder.setBean(storyArc); + } + + public abstract static class StoryArcFormEvent extends ComponentEvent { + private StoryArc storyArc; + + protected StoryArcFormEvent(StoryArcForm source, StoryArc storyArc) { + super(source, false); + this.storyArc = storyArc; + } + + public StoryArc getStoryArc() { + return storyArc; + } + } + + public static class SaveEvent extends StoryArcFormEvent { + SaveEvent(StoryArcForm source, StoryArc storyArc) { + super(source, storyArc); + } + } + + public static class DeleteEvent extends StoryArcFormEvent { + DeleteEvent(StoryArcForm source, StoryArc storyArc) { + super(source, storyArc); + } + } + + public static class CloseEvent extends StoryArcFormEvent { + CloseEvent(StoryArcForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java new file mode 100644 index 0000000..6044383 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.STORYARC_ROTE, layout = MainLayout.class) +@PageTitle("StoryArc | Comics | Kontor") +public class StoryArcView extends VerticalLayout { + + Grid grid = new Grid<>(StoryArc.class); + ComboBox comicFilter = new ComboBox<>("Comic"); + StoryArcForm form; + ComicService service; + + public StoryArcView(ComicService service) { + this.service = service; + addClassName("storyarc-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + StoryArcForm getForm() { + return form; + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("storyarc-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editStoryArc(event.getValue())); + } + + private void configureForm() { + form = new StoryArcForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveStoryArc); + form.addDeleteListener(this::deleteStoryArc); + form.addCloseListener(e -> closeEditor()); + } + + private void saveStoryArc(StoryArcForm.SaveEvent event) { + service.saveStoryArc(event.getStoryArc()); + updateList(); + closeEditor(); + } + + private void deleteStoryArc(StoryArcForm.DeleteEvent event) { + service.deleteStoryArc(event.getStoryArc()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.addValueChangeListener(e -> updateList()); + comicFilter.setClearButtonVisible(true); + + Button addStoryArcButton = new Button("Add StoryArc", click -> addStoryArc()); + + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addStoryArcButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editStoryArc(StoryArc storyArc) { + if (storyArc == null) { + closeEditor(); + } else { + form.setStoryArc(storyArc); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setStoryArc(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addStoryArc() { + grid.asSingleSelect().clear(); + editStoryArc(new StoryArc()); + } + + private void updateList() { + grid.setItems(service.findAllStoryArcsForComic(comicFilter.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java new file mode 100644 index 0000000..8a1b477 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java @@ -0,0 +1,109 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.TradePaperback; + +public class TradePaperBackForm extends FormLayout { + TextField name = new TextField("Name"); + ComboBox comic = new ComboBox<>("Comic"); + TextField issueStart = new TextField("Issue Start"); + TextField issueEnd = new TextField("Issue End"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(TradePaperback.class); + + public TradePaperBackForm(List comics) { + addClassName("tradepaperback-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(name, comic, issueStart, issueEnd, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setTradePaperBack(TradePaperback tradepaperback) { + binder.setBean(tradepaperback); + } + + public abstract static class TradePaperBackFormEvent extends ComponentEvent { + private TradePaperback tradepaperback; + + protected TradePaperBackFormEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, false); + this.tradepaperback = tradepaperback; + } + + public TradePaperback getTradePaperBack() { + return tradepaperback; + } + } + + public static class SaveEvent extends TradePaperBackFormEvent { + SaveEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, tradepaperback); + } + } + + public static class DeleteEvent extends TradePaperBackFormEvent { + DeleteEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, tradepaperback); + } + } + + public static class CloseEvent extends TradePaperBackFormEvent { + CloseEvent(TradePaperBackForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java new file mode 100644 index 0000000..0215234 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.TPB_ROUTE, layout = MainLayout.class) +@PageTitle("TradePaperBack | Comics | Kontor") +public class TradePaperbackView extends VerticalLayout { + + Grid grid = new Grid<>(TradePaperback.class); + TextField filterText = new TextField(); + TradePaperBackForm form; + ComicService service; + + public TradePaperbackView(ComicService service) { + this.service = service; + addClassName("tradepaperback-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("tradepaperback-grid"); + grid.setSizeFull(); + grid.setColumns("name", "comic.title", "issueStart", "issueEnd"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editTradePaperBack(event.getValue())); + } + + public TradePaperBackForm getForm() { + return form; + } + + private void configureForm() { + form = new TradePaperBackForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveTradePaperBack); + form.addDeleteListener(this::deleteTradePaperBack); + form.addCloseListener(e -> closeEditor()); + } + + private void saveTradePaperBack(TradePaperBackForm.SaveEvent event) { + service.saveTradePaperBack(event.getTradePaperBack()); + updateList(); + closeEditor(); + } + + private void deleteTradePaperBack(TradePaperBackForm.DeleteEvent event) { + service.deleteTradePaperBack(event.getTradePaperBack()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addTradePaperBackButton = new Button("Add TradePaperBack"); + addTradePaperBackButton.addClickListener(click -> addTradePaperBack()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addTradePaperBackButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editTradePaperBack(TradePaperback tradepaperback) { + if (tradepaperback == null) { + closeEditor(); + } else { + form.setTradePaperBack(tradepaperback); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setTradePaperBack(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addTradePaperBack() { + grid.asSingleSelect().clear(); + editTradePaperBack(new TradePaperback()); + } + + public void updateList() { + grid.setItems(service.findAllTradePaperbacks(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java new file mode 100644 index 0000000..9b333a9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java @@ -0,0 +1,109 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.Volume; + +public class VolumeForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + public TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Volume.class); + + public VolumeForm(List comics) { + addClassName("volume-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setVolume(Volume volume) { + binder.setBean(volume); + } + + public abstract static class VolumeFormEvent extends ComponentEvent { + private Volume volume; + + protected VolumeFormEvent(VolumeForm source, Volume volume) { + super(source, false); + this.volume = volume; + } + + public Volume getVolume() { + return volume; + } + } + + public static class SaveEvent extends VolumeFormEvent { + SaveEvent(VolumeForm source, Volume volume) { + super(source, volume); + } + } + + public static class DeleteEvent extends VolumeFormEvent { + DeleteEvent(VolumeForm source, Volume volume) { + super(source, volume); + } + } + + public static class CloseEvent extends VolumeFormEvent { + CloseEvent(VolumeForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java new file mode 100644 index 0000000..47a2201 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.VOLUME_ROUTE, layout = MainLayout.class) +@PageTitle("Volume | Comics | Kontor") +public class VolumeView extends VerticalLayout { + + Grid grid = new Grid<>(Volume.class); + ComboBox comicFilter = new ComboBox<>("Comic"); + VolumeForm form; + ComicService service; + + public VolumeView(ComicService service) { + this.service = service; + addClassName("volume-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + VolumeForm getForm() { + return form; + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("volume-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editVolume(event.getValue())); + } + + private void configureForm() { + form = new VolumeForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveVolume); + form.addDeleteListener(this::deleteVolume); + form.addCloseListener(e -> closeEditor()); + } + + private void saveVolume(VolumeForm.SaveEvent event) { + service.saveVolume(event.getVolume()); + updateList(); + closeEditor(); + } + + private void deleteVolume(VolumeForm.DeleteEvent event) { + service.deleteVolume(event.getVolume()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.addValueChangeListener(e -> updateList()); + comicFilter.setClearButtonVisible(true); + + Button addVolumeButton = new Button("Add Volume", click -> addVolume()); + + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addVolumeButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editVolume(Volume volume) { + if (volume == null) { + closeEditor(); + } else { + form.setVolume(volume); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setVolume(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addVolume() { + grid.asSingleSelect().clear(); + editVolume(new Volume()); + } + + private void updateList() { + grid.setItems(service.findAllVolumesForComic(comicFilter.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java new file mode 100644 index 0000000..0965229 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java @@ -0,0 +1,121 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Worktype; + +public class WorktypeForm extends FormLayout { + + private static final Logger log = LoggerFactory.getLogger(WorktypeForm.class); + + TextField name = new TextField("Name"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Worktype.class); + + public WorktypeForm() { + addClassName("worktype-form"); + binder.bindInstanceFields(this); + + comicWorks.setColumns("comic.title", "artist.name"); + comicWorks.getColumnByKey("comic.title").setHeader("Comic"); + comicWorks.getColumnByKey("artist.name").setHeader("Artist"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + + add(name, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setWorktype(Worktype worktype) { + binder.setBean(worktype); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + this.comicWorks.setItems(works); + } + + public abstract static class WorktypeFormEvent extends ComponentEvent { + private Worktype worktype; + + protected WorktypeFormEvent(WorktypeForm source, Worktype worktype) { + super(source, false); + this.worktype = worktype; + } + + public Worktype getWorktype() { + return worktype; + } + } + + public static class SaveEvent extends WorktypeFormEvent { + SaveEvent(WorktypeForm source, Worktype worktype) { + super(source, worktype); + } + } + + public static class DeleteEvent extends WorktypeFormEvent { + DeleteEvent(WorktypeForm source, Worktype worktype) { + super(source, worktype); + } + } + + public static class CloseEvent extends WorktypeFormEvent { + CloseEvent(WorktypeForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java new file mode 100644 index 0000000..98f3adb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.WORKTYPE_ROUTE, layout = MainLayout.class) +@PageTitle("Worktype | Comics | Kontor") +public class WorktypeView extends VerticalLayout { + + Grid grid = new Grid<>(Worktype.class); + TextField filterText = new TextField(); + WorktypeForm form; + ComicService service; + + public WorktypeView(ComicService service) { + this.service = service; + addClassName("worktype-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("worktype-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editWorktype(event.getValue())); + } + + public WorktypeForm getForm() { + return form; + } + + private void configureForm() { + form = new WorktypeForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveWorktype); + form.addDeleteListener(this::deleteWorktype); + form.addCloseListener(e -> closeEditor()); + } + + private void saveWorktype(WorktypeForm.SaveEvent event) { + service.saveWorktype(event.getWorktype()); + updateList(); + closeEditor(); + } + + private void deleteWorktype(WorktypeForm.DeleteEvent event) { + service.deleteWorktype(event.getWorktype()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addWorktypeButton = new Button("Add worktype"); + addWorktypeButton.addClickListener(click -> addWorktype()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addWorktypeButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editWorktype(Worktype worktype) { + if (worktype == null) { + closeEditor(); + } else { + form.setWorktype(worktype); + form.setComicWorks(worktype.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setWorktype(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addWorktype() { + grid.asSingleSelect().clear(); + editWorktype(new Worktype()); + } + + public void updateList() { + grid.setItems(service.findAllWorktypes(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java b/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java new file mode 100644 index 0000000..f9cfdaf --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java @@ -0,0 +1,34 @@ +package de.thpeetz.kontor.common.data; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.util.Date; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @Version + private int version; + + @CreatedDate + private Date createdDate; + + @LastModifiedDate + private Date lastModifiedDate; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java b/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java new file mode 100644 index 0000000..7dfdf50 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java @@ -0,0 +1,55 @@ +package de.thpeetz.kontor.common.data; + +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.util.Date; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractLinkEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @Version + private int version; + + @CreatedDate + private Date createdDate; + + @LastModifiedDate + private Date lastModifiedDate; + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java new file mode 100644 index 0000000..1678e58 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java @@ -0,0 +1,52 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.avatar.Avatar; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.contextmenu.SubMenu; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.menubar.MenuBar; +import com.vaadin.flow.component.menubar.MenuBarVariant; +import com.vaadin.flow.router.Route; + +import com.vaadin.flow.router.Router; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.views.UserProfileView; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +@Route("avatar-menu-bar") +public class AvatarMenuBar extends Div { + + private final SecurityService securityService; + + final AdminService adminService; + + public AvatarMenuBar(SecurityService securityService, AdminService adminService) { + this.adminService = adminService; + this.securityService = securityService; + + Avatar avatar = new Avatar(); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("AdminService: {}", adminService); + if (user != null) { + String userName = user.getUsername(); + String fullName = adminService.getUserFullName(userName); + avatar.setName(fullName); + } + }); + // avatar.setImage(pictureUrl); + MenuBar menuBar = new MenuBar(); + menuBar.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE); + MenuItem menuItem = menuBar.addItem(avatar); + SubMenu subMenu = menuItem.getSubMenu(); + MenuItem logoutMenuItem = subMenu.addItem("Log out"); + logoutMenuItem.addClickListener(e -> securityService.logout()); + MenuItem profileItem = subMenu.addItem("Profile"); + profileItem.addClickListener(e -> profileItem.getUI().ifPresent(ui -> ui.navigate(UserProfileView.class))); + subMenu.addItem("Settings"); + subMenu.addItem("Help"); + add(menuBar); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java new file mode 100644 index 0000000..4c2112b --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java @@ -0,0 +1,100 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.accordion.Accordion; +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.AppLayout.Section; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.Scroller; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.router.RouterLink; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class KontorLayoutUtil { + + private final AppLayout appLayout; + private HorizontalLayout secondaryNavigation; + + private AdminService adminService; + + private SecurityService securityService; + + public KontorLayoutUtil(AppLayout layout, AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + this.appLayout = layout; + } + + public void setSecondaryNavigation(HorizontalLayout secondaryNavigation) { + this.secondaryNavigation = secondaryNavigation; + } + + public void createHeader(String titleName) { + appLayout.addToDrawer(createTitle(), getScroller()); + appLayout.addToNavbar(getHeader(titleName)); + appLayout.setPrimarySection(Section.DRAWER); + } + + private H1 createTitle() { + H1 appTitle = new H1(new RouterLink("Kontor", MainView.class)); + appTitle.getStyle().set("font-size", "var(--lumo-font-size-l)") + .set("line-height", "var(--lumo-size-l)") + .set("margin", "0 var(--lumo-space-m)"); + return appTitle; + } + + private Scroller getScroller() { + Scroller scroller = new Scroller(getPrimaryNavigation()); + scroller.setClassName(LumoUtility.Padding.SMALL); + return scroller; + } + + private VerticalLayout getHeader(String header) { + DrawerToggle toggle = new DrawerToggle(); + H2 viewTitle = new H2(header); + viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + HorizontalLayout subViews = this.secondaryNavigation; + + AvatarMenuBar avatar = new AvatarMenuBar(securityService, adminService); + HorizontalLayout wrapper = new HorizontalLayout(toggle, viewTitle, avatar); + wrapper.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + wrapper.expand(viewTitle); + wrapper.setWidthFull(); + wrapper.setSpacing(false); + + VerticalLayout viewHeader = new VerticalLayout(wrapper, subViews); + viewHeader.setPadding(false); + viewHeader.setSpacing(false); + return viewHeader; + } + + private SideNav getPrimaryNavigation() { + SideNav sideNav = new SideNav(); + sideNav.addItem(ComicConstants.getComicsNavigation()); + sideNav.addItem(TyscConstants.getTyscNavigation()); + sideNav.addItem(BookshelfConstants.getBookshelfNavigation()); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("User {} found", user.getUsername()); + boolean isAdmin = user.getAuthorities().stream() + .anyMatch(grantedAuthority -> "ROLE_ADMIN".equals(grantedAuthority.getAuthority())); + log.info("User {} hat Admin-Rechte: {}", user, isAdmin); + if (isAdmin) { + sideNav.addItem(AdminConstants.getAdminNavigation()); + } + }); + return sideNav; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java new file mode 100644 index 0000000..96f720f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java @@ -0,0 +1,101 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.Scroller; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.mailclient.views.EmailView; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.GrantedAuthority; + +import java.util.ArrayList; +import java.util.Collection; + +@Slf4j +public class MainLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public MainLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + createHeader("Kontor"); + createDrawer(); + } + + public void createDrawer() { + H1 appTitle = new H1(new RouterLink("Kontor", MainView.class)); + appTitle.getStyle().set("font-size", "var(--lumo-font-size-l)") + .set("line-height", "var(--lumo-size-l)") + .set("margin", "0 var(--lumo-space-m)"); + Scroller scroller = new Scroller(getPrimaryNavigation()); + scroller.setClassName(LumoUtility.Padding.SMALL); + addToDrawer(appTitle, scroller); + } + + public void createHeader(String titleName) { + DrawerToggle toggle = new DrawerToggle(); + H2 viewTitle = new H2(titleName); + viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + + AvatarMenuBar avatar = new AvatarMenuBar(securityService, adminService); + HorizontalLayout wrapper = new HorizontalLayout(toggle, viewTitle, avatar); + wrapper.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + wrapper.expand(viewTitle); + wrapper.setWidthFull(); + wrapper.setSpacing(false); + + VerticalLayout viewHeader = new VerticalLayout(wrapper); + viewHeader.setPadding(false); + viewHeader.setSpacing(false); + + addToNavbar(viewHeader); + setPrimarySection(Section.DRAWER); + } + + private SideNav getPrimaryNavigation() { + SideNav sideNav = new SideNav(); + ArrayList roles = new ArrayList<>(); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("Get roles for user {}", user.getUsername()); + user.getAuthorities().forEach(grantedAuthority -> { + roles.add(grantedAuthority.getAuthority()); + }); + log.info("user {} has roles: {}", user, roles); + }); + sideNav.addItem(ComicConstants.getComicsNavigation()); + sideNav.addItem(TyscConstants.getTyscNavigation()); + sideNav.addItem(BookshelfConstants.getBookshelfNavigation()); + sideNav.addItem(MediaConstants.getMediaNavigation(roles)); + sideNav.addItem(new SideNavItem("Emails", EmailView.class)); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("User {} found", user.getUsername()); + boolean isAdmin = user.getAuthorities().stream() + .anyMatch(grantedAuthority -> "ROLE_ADMIN".equals(grantedAuthority.getAuthority())); + log.info("User {} hat Admin-Rechte: {}", user, isAdmin); + if (isAdmin) { + sideNav.addItem(AdminConstants.getAdminNavigation()); + } + }); + return sideNav; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/MainView.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/MainView.java new file mode 100644 index 0000000..08edac9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/MainView.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.common.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.spring.annotation.SpringComponent; + +@SpringComponent +@Scope("prototype") +@AnonymousAllowed +@Route(value = "/", layout = MainLayout.class) +@PageTitle("Kontor") +public class MainView extends VerticalLayout { + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java new file mode 100644 index 0000000..474d4a7 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java @@ -0,0 +1,31 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.security.SecurityService; + +public class SeparateMainLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public SeparateMainLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader("Kontor"); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + return navigation; + } + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java b/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java new file mode 100644 index 0000000..f3b80a8 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java @@ -0,0 +1,20 @@ +package de.thpeetz.kontor.mailclient.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper=false) +@Entity +public class Mail extends AbstractEntity { + + private String folder; + private String subject; + private String body; + private Date sentDate; + private Date receivedDate; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java b/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java new file mode 100644 index 0000000..840d8da --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.mailclient.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@Entity +public class MailAccount extends AbstractEntity { + + @NotEmpty + private String host; + + private Integer port; + + @NotEmpty + private String protocol; + + private String userName; + + private String password; + + private Boolean startTls; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java b/springboot/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java new file mode 100644 index 0000000..4235dd1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java @@ -0,0 +1,222 @@ +package de.thpeetz.kontor.mailclient.views; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.NoSuchProviderException; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.internet.InternetAddress; + +import com.sun.mail.imap.IMAPFolder; +import com.vaadin.flow.component.Composite; +import com.vaadin.flow.component.accordion.Accordion; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.menubar.MenuBar; +import com.vaadin.flow.component.messages.MessageList; +import com.vaadin.flow.component.messages.MessageListItem; +import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment; +import com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.theme.lumo.LumoUtility.Gap; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import de.thpeetz.kontor.admin.services.MailService; +import de.thpeetz.kontor.common.views.MainLayout; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@PageTitle("Email") +@AnonymousAllowed +@Route(value = "mail", layout = MainLayout.class) +@RouteAlias(value = "kontor/mail", layout = MainLayout.class) +public class EmailView extends Composite { + + MenuBar menuBar = new MenuBar(); + Accordion accordion = new Accordion(); + SideNav folderList = new SideNav(); + MailService mailService; + + public EmailView(MailService mailService) { + this.mailService = mailService; + configureView(); + updateFolderList(); + } + + private void updateFolderList() { + List accounts = mailService.findAllMailAccounts(); + MailAccount account = accounts.get(0); + + Properties properties = new Properties(); + properties.put(String.format("mail.%s.host", account.getProtocol()), account.getHost()); + properties.put(String.format("mail.%s.port", account.getProtocol()), account.getPort().toString()); + properties.put("mail.imap.starttls.enable", "true"); + + Session session = Session.getDefaultInstance(properties); + Store store; + try { + store = session.getStore(account.getProtocol()); + store.connect(account.getUserName(), account.getPassword()); + Folder rootFolder = store.getDefaultFolder(); + Folder[] folders = rootFolder.list(); + for (Folder value : folders) { + SideNavItem folder = addSubFolder(value); + folderList.addItem(folder); + } + log.info("{} folders found", folders.length); + Folder folder = store.getFolder("INBOX"); + IMAPFolder imapFolder = (IMAPFolder) folder; + imapFolder.open(Folder.READ_ONLY); + Message[] messages = imapFolder.getMessages(); + log.info("Folder {} opened", folder.getFullName()); + int messageCount = folder.getMessageCount(); + log.info("{} messages found", messages.length); + for (Message message : messages) { + log.info("Message {}: {}, {}, {}", message.getMessageNumber(), message.getSubject(), message.getReceivedDate(), message.getSentDate()); + //log.info("Message: {}", message.getAllRecipients()); + Address[] addresses; + printAddress(message.getRecipients(Message.RecipientType.TO)); + printAddress(message.getRecipients(Message.RecipientType.CC)); + printAddress(message.getRecipients(Message.RecipientType.BCC)); + } + store.close(); + log.info("Connection closed"); + } catch (NoSuchProviderException e) { + throw new RuntimeException(e); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + } + + private void printAddress(Address[] addresses) { + if (addresses != null) { + for (Address address: addresses) { + log.info("Address {}: {}", address.getType(), address.toString()); + InternetAddress internetAddress = (InternetAddress)address; + log.info("InternetAddress {}: {}", internetAddress.getPersonal(), internetAddress.getAddress()); + } + } + } + + private SideNavItem addSubFolder(Folder mailFolder) throws MessagingException { + log.debug("addSubFolder: {}", mailFolder.getName()); + Span counter = new Span(String.valueOf(mailFolder.getMessageCount())); + counter.getElement().getThemeList().add("badge contrast pill"); + counter.getElement().setAttribute("aria-label", String.format("%d unread messages", mailFolder.getMessageCount())); + SideNavItem folder = new SideNavItem(mailFolder.getName()); + folder.setSuffixComponent(counter); + Folder[] folders = mailFolder.list(); + log.info("{} subfolders found", folders.length); + for (Folder value : folders) { + SideNavItem subFolder = addSubFolder(value); + folder.addItem(subFolder); + } + return folder; + } + + private void configureView() { + HorizontalLayout layoutMenubar = new HorizontalLayout(); + HorizontalLayout layoutRow2 = new HorizontalLayout(); + VerticalLayout layoutColumn2 = new VerticalLayout(); + VerticalLayout layoutColumn3 = new VerticalLayout(); + MessageList messageList = new MessageList(); + VerticalLayout layoutColumn4 = new VerticalLayout(); + Paragraph textMedium = new Paragraph(); + getContent().setWidth("100%"); + getContent().getStyle().set("flex-grow", "1"); + layoutMenubar.addClassName(Gap.MEDIUM); + layoutMenubar.setWidth("100%"); + layoutMenubar.setHeight("min-content"); + menuBar.setWidth("min-content"); + setMenuBarSampleData(menuBar); + layoutRow2.addClassName(Gap.MEDIUM); + layoutRow2.setWidth("100%"); + layoutRow2.getStyle().set("flex-grow", "1"); + layoutColumn2.getStyle().set("flex-grow", "1"); + accordion.setWidth("100%"); + setAccordionSampleData(accordion); + layoutColumn3.setHeightFull(); + layoutRow2.setFlexGrow(1.0, layoutColumn3); + layoutColumn3.setWidth("100%"); + layoutColumn3.getStyle().set("flex-grow", "1"); + layoutColumn3.setJustifyContentMode(JustifyContentMode.START); + layoutColumn3.setAlignItems(Alignment.START); + messageList.setWidth("100%"); + setMessageListSampleData(messageList); + layoutColumn4.setWidthFull(); + layoutColumn3.setFlexGrow(1.0, layoutColumn4); + layoutColumn4.setWidth("100%"); + layoutColumn4.getStyle().set("flex-grow", "1"); + textMedium.setText( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); + textMedium.setWidth("100%"); + textMedium.getStyle().set("font-size", "var(--lumo-font-size-m)"); + getContent().add(layoutMenubar); + layoutMenubar.add(menuBar); + getContent().add(layoutRow2); + layoutRow2.add(layoutColumn2); + layoutColumn2.add(folderList); + layoutRow2.add(layoutColumn3); + layoutColumn3.add(messageList); + layoutColumn3.add(layoutColumn4); + layoutColumn4.add(textMedium); + } + + private void setMenuBarSampleData(MenuBar menuBar) { + menuBar.addItem("View"); + menuBar.addItem("Edit"); + menuBar.addItem("Share"); + menuBar.addItem("Move"); + } + + private void setAccordionSampleData(Accordion accordion) { + Span name = new Span("Sophia Williams"); + Span email = new Span("sophia.williams@company.com"); + Span phone = new Span("(501) 555-9128"); + VerticalLayout personalInformationLayout = new VerticalLayout(name, email, phone); + personalInformationLayout.setSpacing(false); + personalInformationLayout.setPadding(false); + accordion.add("Personal information", personalInformationLayout); + Span street = new Span("4027 Amber Lake Canyon"); + Span zipCode = new Span("72333-5884 Cozy Nook"); + Span city = new Span("Arkansas"); + VerticalLayout billingAddressLayout = new VerticalLayout(); + billingAddressLayout.setSpacing(false); + billingAddressLayout.setPadding(false); + billingAddressLayout.add(street, zipCode, city); + accordion.add("Billing address", billingAddressLayout); + Span cardBrand = new Span("Mastercard"); + Span cardNumber = new Span("1234 5678 9012 3456"); + Span expiryDate = new Span("Expires 06/21"); + VerticalLayout paymentLayout = new VerticalLayout(); + paymentLayout.setSpacing(false); + paymentLayout.setPadding(false); + paymentLayout.add(cardBrand, cardNumber, expiryDate); + accordion.add("Payment", paymentLayout); + } + + private void setMessageListSampleData(MessageList messageList) { + MessageListItem message1 = new MessageListItem("Nature does not hurry, yet everything gets accomplished.", + LocalDateTime.now().minusDays(1).toInstant(ZoneOffset.UTC), "Matt Mambo"); + message1.setUserColorIndex(1); + MessageListItem message2 = new MessageListItem( + "Using your talent, hobby or profession in a way that makes you contribute with something good to this world is truly the way to go.", + LocalDateTime.now().minusMinutes(55).toInstant(ZoneOffset.UTC), "Linsey Listy"); + message2.setUserColorIndex(2); + messageList.setItems(message1, message2); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java b/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java new file mode 100644 index 0000000..866f7b3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java @@ -0,0 +1,35 @@ +package de.thpeetz.kontor.media; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import de.thpeetz.kontor.media.views.MediaArticleView; +import de.thpeetz.kontor.media.views.MediaFileView; +import de.thpeetz.kontor.media.views.MediaVideoView; + +import java.util.ArrayList; + +public class MediaConstants { + + public static final String MEDIA = "Media"; + public static final String MEDIAFILE_ROUTE = "media/mediafile"; + public static final String MEDIAVIDEO_ROUTE = "media/mediavideo"; + public static final String MEDIAARTICLE_ROUTE = "media/mediaarticle"; + public static final String MEDIA_ROLE = "ROLE_MEDIA"; + private static final String MEDIAFILE = "Media Files"; + private static final String MEDIAVIDEO = "Media Videos"; + private static final String MEDIAARTICLE = "Media Article"; + + public static SideNavItem getMediaNavigation(ArrayList roles) { + SideNavItem media = new SideNavItem(MEDIA, MEDIAFILE_ROUTE, VaadinIcon.VIMEO.create()); + media.addItem(new SideNavItem(MEDIAVIDEO, MediaVideoView.class)); + media.addItem(new SideNavItem(MEDIAARTICLE, MediaArticleView.class)); + if (roles.contains(MEDIA_ROLE)) { + media.addItem(new SideNavItem(MEDIAFILE, MediaFileView.class)); + } + return media; + } + + private MediaConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java b/springboot/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java new file mode 100644 index 0000000..c9ce7a1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.media; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.services.ModuleService; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleMedia implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private AdminService adminService; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + adminService.addRole(MediaConstants.MEDIA_ROLE); + if (alreadySetup) { + log.info("SetupModuleMedia already executed, skipping"); + return; + } + if (!moduleService.importData(MediaConstants.MEDIA)) { + log.info("Module media should not setup data"); + return; + } + + log.info("Set up Media data"); + moduleService.setDataImported(MediaConstants.MEDIA); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java new file mode 100644 index 0000000..c664a3a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "url"), uniqueConstraints = @UniqueConstraint(columnNames = {"url"})) +public class MediaArticle extends AbstractEntity { + + @NotEmpty + private String url; + + private boolean review; + + @Nullable + private String title; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java new file mode 100644 index 0000000..a25702e --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaArticleRepository extends JpaRepository { + @Query("select m from MediaArticle m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java new file mode 100644 index 0000000..fa7b934 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "url" }) }) +public class MediaFile extends AbstractEntity { + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java new file mode 100644 index 0000000..7db70e1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java @@ -0,0 +1,14 @@ +package de.thpeetz.kontor.media.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface MediaFileRepository extends JpaRepository { + @Query("select m from MediaFile m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java new file mode 100644 index 0000000..c70db48 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "url"), uniqueConstraints = @UniqueConstraint(columnNames = {"url"})) +public class MediaLink extends AbstractEntity { + + @NotEmpty + private String url; + + private String title; + + private boolean review; + + @OneToOne + @JoinColumn(name = "video_id", referencedColumnName = "id") + @Nullable + private MediaVideo mediaVideo; + + @OneToOne + @JoinColumn(name = "article_id", referencedColumnName = "id") + @Nullable + private MediaArticle mediaArticle; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java new file mode 100644 index 0000000..d093eb1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "url" }) }) +public class MediaVideo extends AbstractEntity { + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java new file mode 100644 index 0000000..d898186 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaVideoRepository extends JpaRepository { + + @Query("select m from MediaVideo m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java new file mode 100644 index 0000000..fc78d5d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.data.MediaArticleRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaArticleService { + + private final MediaArticleRepository mediaArticleRepository; + + public MediaArticleService(MediaArticleRepository mediaArticleRepository) { + this.mediaArticleRepository = mediaArticleRepository; + } + + public List findAllMediaArticles(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + log.info("Found {} entries", mediaArticleRepository.count()); + return mediaArticleRepository.findAll(); + } else { + List results = mediaArticleRepository.search(stringFilter); + log.info("Found {} entries", results.size()); + return results; + } + } + + public void saveMediaArticle(MediaArticle mediaArticle) { + if (mediaArticle == null) { + log.warn("MediaArticle is null. Are you sure you have connected your form to the application?"); + return; + } + mediaArticleRepository.save(mediaArticle); + } + + public void deleteMediaArticle(MediaArticle mediaArticle) { + mediaArticleRepository.delete(mediaArticle); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java new file mode 100644 index 0000000..a7fe0a1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.MediaFile; +import de.thpeetz.kontor.media.data.MediaFileRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaFileService { + + private final MediaFileRepository mediaFileRepository; + + public MediaFileService(MediaFileRepository mediaFileRepository) { + this.mediaFileRepository = mediaFileRepository; + } + + public List findAllMediaFiles(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + log.info("Found " + mediaFileRepository.count()+ " entries"); + return mediaFileRepository.findAll(); + } else { + List results = mediaFileRepository.search(stringFilter); + log.info("Found " + results.size() + " entries"); + return results; + } + } + + public void saveMediaFile(MediaFile mediaFile) { + if (mediaFile == null) { + log.warn("MediaFile is null. Are you sure you have connected your form to the application?"); + return; + } + mediaFileRepository.save(mediaFile); + } + + public void deleteMediaFile(MediaFile mediaFile) { + mediaFileRepository.delete(mediaFile); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java new file mode 100644 index 0000000..61299fd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.MediaVideo; +import de.thpeetz.kontor.media.data.MediaVideoRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaVideoService { + + private final MediaVideoRepository mediaVideoRepository; + + public MediaVideoService(MediaVideoRepository mediaVideoRepository) { + this.mediaVideoRepository = mediaVideoRepository; + } + + public List findAllMediaVideos(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return mediaVideoRepository.findAll(); + } else { + return mediaVideoRepository.search(stringFilter); + } + } + + public void saveMediaVideo(MediaVideo mediaVideo) { + if (mediaVideo == null) { + log.warn("MediaFile is null. Are you sure you have connected your form to the application?"); + return; + } + mediaVideoRepository.save(mediaVideo); + } + + public void deleteMediaVideo(MediaVideo mediaVideo) { + mediaVideoRepository.delete(mediaVideo); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java new file mode 100644 index 0000000..d658e4e --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaArticle; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MediaArticleForm extends FormLayout { + + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + Checkbox review = new Checkbox("Review"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaArticle.class); + + public MediaArticleForm() { + addClassName("mediaarticle-form"); + binder.bindInstanceFields(this); + add(url, 2); + add(title, 2); + add(review, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MediaArticleForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MediaArticleForm.CloseEvent(this))); + + binder.addStatusChangeListener(event -> { + save.setEnabled(event.getBinder().isValid()); + }); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MediaArticleForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMediaArticle(MediaArticle mediaArticle) { + binder.setBean(mediaArticle); + } + + public abstract static class MediaArticleFormEvent extends ComponentEvent { + private MediaArticle mediaArticle; + + protected MediaArticleFormEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, false); + this.mediaArticle = mediaArticle; + } + + public MediaArticle getMediaArticle() { + return mediaArticle; + } + } + + public static class SaveEvent extends MediaArticleForm.MediaArticleFormEvent { + SaveEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, mediaArticle); + } + } + + public static class DeleteEvent extends MediaArticleForm.MediaArticleFormEvent { + DeleteEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, mediaArticle); + } + } + + public static class CloseEvent extends MediaArticleForm.MediaArticleFormEvent { + CloseEvent(MediaArticleForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MediaArticleForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MediaArticleForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MediaArticleForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java new file mode 100644 index 0000000..67079d4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java @@ -0,0 +1,171 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.services.MediaArticleService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAARTICLE_ROUTE, layout = MainLayout.class) +@PageTitle("MediaArticle | Media | Kontor") +public class MediaArticleView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaArticle.class, false); + Grid.Column idColumn = grid.addColumn(MediaArticle::getId).setHeader("ID").setResizable(true); + Grid.Column urlColumn = grid.addColumn(MediaArticle::getUrl).setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaArticle::getTitle).setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediaArticle -> createStatusIcon(mediaArticle.isReview())) + .setHeader("Überprüfung") + .setWidth("6rem"); + TextField searchField = new TextField(); + @Getter + MediaArticleForm form; + MediaArticleService service; + + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + + public MediaArticleView(MediaArticleService service) { + this.service = service; + addClassName("mediaarticle-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediaarticle-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaArticle(event.getValue())); + } + + private void configureForm() { + form = new MediaArticleForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaArticle); + form.addDeleteListener(this::deleteMediaArticle); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaArticle(MediaArticleForm.SaveEvent event) { + service.saveMediaArticle(event.getMediaArticle()); + updateList(); + closeEditor(); + } + + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + + private void deleteMediaArticle(MediaArticleForm.DeleteEvent event) { + service.deleteMediaArticle(event.getMediaArticle()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaArticleButton = new Button("Add MediaArticle"); + addMediaArticleButton.addClickListener(click -> addMediaArticle()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + MediaArticleView.ColumnToggleContextMenu columnToggleContextMenu = new MediaArticleView.ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("URL", urlColumn); + columnToggleContextMenu.addColumnToggleItem("Titel", titleColumn); + columnToggleContextMenu.addColumnToggleItem("Überprüfung", reviewColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaArticleButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaArticle(MediaArticle mediaArticle) { + if (mediaArticle == null) { + closeEditor(); + } else { + form.setMediaArticle(mediaArticle); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaArticle(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaArticle() { + grid.asSingleSelect().clear(); + editMediaArticle(new MediaArticle()); + } + + public void updateList() { + grid.setItems(service.findAllMediaArticles(searchField.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java new file mode 100644 index 0000000..b1042fa --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaFile; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MediaFileForm extends FormLayout { + + TextField id = new TextField("ID"); + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + TextField fileName = new TextField("Dateiname"); + TextField cloudLink = new TextField("Cloud Link"); + Checkbox review = new Checkbox("Review"); + Checkbox shouldDownload = new Checkbox("Download"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaFile.class); + + public MediaFileForm() { + addClassName("mediafile-form"); + binder.bindInstanceFields(this); + id.setReadOnly(true); + add(id, 2); + add(url, 2); + add(title, 2); + add(fileName, 2); + add(cloudLink, 2); + add(review, shouldDownload, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(event -> save.setEnabled(event.getBinder().isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setMediaFile(MediaFile mediaFile) { + binder.setBean(mediaFile); + } + + @Getter + public abstract static class MediaFileFormEvent extends ComponentEvent { + private final MediaFile mediaFile; + + protected MediaFileFormEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, false); + this.mediaFile = mediaFile; + } + + } + + public static class SaveEvent extends MediaFileFormEvent { + SaveEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, mediaFile); + } + } + + public static class DeleteEvent extends MediaFileFormEvent { + DeleteEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, mediaFile); + } + } + + public static class CloseEvent extends MediaFileFormEvent { + CloseEvent(MediaFileForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java new file mode 100644 index 0000000..0784b8d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java @@ -0,0 +1,186 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.data.MediaFile; +import de.thpeetz.kontor.media.services.MediaFileService; +import jakarta.annotation.security.RolesAllowed; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@RolesAllowed("MEDIA") +@Route(value = "media/mediafile", layout = MainLayout.class) +@PageTitle("MediaFile | Media | Kontor") +public class MediaFileView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaFile.class, false); + Grid.Column idColumn = grid.addColumn(MediaFile::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(MediaFile::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(MediaFile::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column urlColumn = grid.addColumn(MediaFile::getUrl) + .setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaFile::getTitle) + .setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column fileNameColumn = grid.addColumn(MediaFile::getFileName) + .setHeader("Dateiname").setResizable(true).setSortable(true); + Grid.Column cloudLinkColumn = grid.addColumn(MediaFile::getCloudLink) + .setHeader("Cloud Link").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediafile -> createStatusIcon(mediafile.isReview())). + setHeader("Überprüfung").setWidth("6rem").setSortable(true); + Grid.Column shouldDownloadColumn = grid.addComponentColumn(mediafile -> createStatusIcon(mediafile.isShouldDownload())). + setHeader("Download?").setWidth("6rem").setSortable(true); + TextField searchField = new TextField(); + @Getter + MediaFileForm form; + MediaFileService service; + + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + + public MediaFileView(MediaFileService service) { + this.service = service; + addClassName("mediafile-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediafile-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + //idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaFile(event.getValue())); + } + + private void configureForm() { + form = new MediaFileForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaFile); + form.addDeleteListener(this::deleteMediaFile); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaFile(MediaFileForm.SaveEvent event) { + service.saveMediaFile(event.getMediaFile()); + updateList(); + closeEditor(); + } + + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + + private void deleteMediaFile(MediaFileForm.DeleteEvent event) { + service.deleteMediaFile(event.getMediaFile()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaFileButton = new Button("Add MediaFile"); + addMediaFileButton.addClickListener(click -> addMediaFile()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("Erstellt", createdColumn); + columnToggleContextMenu.addColumnToggleItem("Geändert", modifiedColumn); + columnToggleContextMenu.addColumnToggleItem("URL", urlColumn); + columnToggleContextMenu.addColumnToggleItem("Titel", titleColumn); + columnToggleContextMenu.addColumnToggleItem("Dateiname", fileNameColumn); + columnToggleContextMenu.addColumnToggleItem("Cloud Link", cloudLinkColumn); + columnToggleContextMenu.addColumnToggleItem("Überprüfung", reviewColumn); + columnToggleContextMenu.addColumnToggleItem("Download?", shouldDownloadColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaFileButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaFile(MediaFile mediaFile) { + if (mediaFile == null) { + closeEditor(); + } else { + form.setMediaFile(mediaFile); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaFile(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaFile() { + grid.asSingleSelect().clear(); + editMediaFile(new MediaFile()); + } + + public void updateList() { + grid.setItems(service.findAllMediaFiles(searchField.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java new file mode 100644 index 0000000..dbc8d47 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaVideo; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MediaVideoForm extends FormLayout { + + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + TextField fileName = new TextField("Dateiname"); + TextField cloudLink = new TextField("Cloud Link"); + Checkbox review = new Checkbox("Review"); + Checkbox shouldDownload = new Checkbox("Download"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaVideo.class); + + public MediaVideoForm() { + addClassName("mediavideo-form"); + binder.bindInstanceFields(this); + add(url, 2); + add(title, 2); + add(fileName, 2); + add(cloudLink, 2); + add(review, shouldDownload, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(event -> { + save.setEnabled(event.getBinder().isValid()); + }); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setMediaVideo(MediaVideo mediaVideo) { + binder.setBean(mediaVideo); + } + + public abstract static class MediaVideoFormEvent extends ComponentEvent { + private MediaVideo mediaVideo; + + protected MediaVideoFormEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, false); + this.mediaVideo = mediaVideo; + } + + public MediaVideo getMediaVideo() { + return mediaVideo; + } + } + + public static class SaveEvent extends MediaVideoFormEvent { + SaveEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, mediaVideo); + } + } + + public static class DeleteEvent extends MediaVideoFormEvent { + DeleteEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, mediaVideo); + } + } + + public static class CloseEvent extends MediaVideoFormEvent { + CloseEvent(MediaVideoForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java new file mode 100644 index 0000000..8b0c131 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java @@ -0,0 +1,178 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaVideo; +import de.thpeetz.kontor.media.services.MediaVideoService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAVIDEO_ROUTE, layout = MainLayout.class) +@PageTitle("MediaVideo | Media | Kontor") +public class MediaVideoView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaVideo.class, false); + Grid.Column idColumn = grid.addColumn(MediaVideo::getId).setHeader("ID").setResizable(true); + Grid.Column urlColumn = grid.addColumn(MediaVideo::getUrl).setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaVideo::getTitle).setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column fileNameColumn = grid.addColumn(MediaVideo::getFileName).setHeader("Dateiname").setResizable(true).setSortable(true); + Grid.Column cloudLinkColumn = grid.addColumn(MediaVideo::getCloudLink).setHeader("Cloud Link").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediaVideo -> createStatusIcon(mediaVideo.isReview())) + .setHeader("Überprüfung") + .setWidth("6rem"); + Grid.Column shouldDownloadColumn = grid.addComponentColumn(mediaVideo -> createStatusIcon(mediaVideo.isShouldDownload())) + .setHeader("Download?") + .setWidth("6rem"); + TextField searchField = new TextField(); + @Getter + MediaVideoForm form; + MediaVideoService service; + + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + + public MediaVideoView(MediaVideoService service) { + this.service = service; + addClassName("mediavideo-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediavideo-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaVideo(event.getValue())); + } + + private void configureForm() { + form = new MediaVideoForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaVideo); + form.addDeleteListener(this::deleteMediaVideo); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaVideo(MediaVideoForm.SaveEvent event) { + service.saveMediaVideo(event.getMediaVideo()); + updateList(); + closeEditor(); + } + + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + + private void deleteMediaVideo(MediaVideoForm.DeleteEvent event) { + service.deleteMediaVideo(event.getMediaVideo()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaVideoButton = new Button("Add MediaVideo"); + addMediaVideoButton.addClickListener(click -> addMediaVideo()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + MediaVideoView.ColumnToggleContextMenu columnToggleContextMenu = new MediaVideoView.ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("URL", urlColumn); + columnToggleContextMenu.addColumnToggleItem("Titel", titleColumn); + columnToggleContextMenu.addColumnToggleItem("Dateiname", fileNameColumn); + columnToggleContextMenu.addColumnToggleItem("Cloud Link", cloudLinkColumn); + columnToggleContextMenu.addColumnToggleItem("Überprüfung", reviewColumn); + columnToggleContextMenu.addColumnToggleItem("Download?", shouldDownloadColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaVideoButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaVideo(MediaVideo mediaVideo) { + if (mediaVideo == null) { + closeEditor(); + } else { + form.setMediaVideo(mediaVideo); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaVideo(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaVideo() { + grid.asSingleSelect().clear(); + editMediaVideo(new MediaVideo()); + } + + public void updateList() { + grid.setItems(service.findAllMediaVideos(searchField.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java b/springboot/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java new file mode 100644 index 0000000..ae77838 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.security; + +import com.vaadin.flow.spring.security.VaadinWebSecurity; + +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import de.thpeetz.kontor.admin.views.LoginView; + +import java.util.Base64; + +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@EnableWebSecurity +@Configuration +public class SecurityConfig extends VaadinWebSecurity { + + public static final String LOGOUT_URL = "/"; + + @Value("${jwt.auth.secret}") + private String authSecret; + + @Autowired + private KontorUserDetailsService userDetailsService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(auth -> auth.requestMatchers( + AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/images/*.png")).permitAll()); + super.configure(http); + setLoginView(http, LoginView.class); + setStatelessAuthentication(http, new SecretKeySpec(Base64.getDecoder().decode(authSecret), JwsAlgorithms.HS256), + "de.thpeetz.kontor"); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(encoder()); + return authProvider; + } + + @Bean + public PasswordEncoder encoder() { + return new BCryptPasswordEncoder(11); + } +} \ No newline at end of file diff --git a/springboot/src/main/java/de/thpeetz/kontor/security/SecurityService.java b/springboot/src/main/java/de/thpeetz/kontor/security/SecurityService.java new file mode 100644 index 0000000..4d17d31 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/security/SecurityService.java @@ -0,0 +1,105 @@ +package de.thpeetz.kontor.security; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Component; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinServletRequest; +import com.vaadin.flow.server.VaadinServletResponse; +import com.vaadin.flow.spring.security.AuthenticationContext; + +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Component +@Service +public class SecurityService { + + private static final String LOGOUT_SUCCESS_URL = "/"; + + private final AuthenticationContext authenticationContext; + + @Autowired + private KontorUserDetailsService userService; + + public SecurityService(AuthenticationContext authenticationContext) { + this.authenticationContext = authenticationContext; + } + + public Optional getAuthenticatedUser() { + String name = SecurityContextHolder.getContext().getAuthentication().getName(); + log.info("getAuthenticatedUser: {}", name); + + return Optional.ofNullable(userService.loadUserByUsername(name)); + } + + /*public static UserDetails getAuthenticatedUser() { + SecurityContext context = SecurityContextHolder.getContext(); + final Authentication authentication = context.getAuthentication(); + log.info("Authentication: {}", authentication); + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof UserDetails) { + return (UserDetails) authentication.getPrincipal(); + } + return null; + }*/ + + public void logout() { + log.info("logout"); + authenticationContext.logout(); + clearCookies(); + + UI.getCurrent().getPage().setLocation(LOGOUT_SUCCESS_URL); + SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); + logoutHandler.logout(VaadinServletRequest.getCurrent().getHttpServletRequest(), null, null); + clearCookies(); + } + + public boolean isLoggedIn() { + log.info("User {}", getAuthenticatedUser()); + return getAuthenticatedUser().isPresent(); + } + + private static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; + private static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; + + private void clearCookies() { + log.info("clearCookies"); + clearCookie(JWT_HEADER_AND_PAYLOAD_COOKIE_NAME); + clearCookie(JWT_SIGNATURE_COOKIE_NAME); + } + + private void clearCookie(String cookieName) { + log.info("clearCookie: {}", cookieName); + HttpServletRequest request = VaadinServletRequest.getCurrent() + .getHttpServletRequest(); + HttpServletResponse response = VaadinServletResponse.getCurrent() + .getHttpServletResponse(); + + Cookie k = new Cookie(cookieName, null); + k.setPath(getRequestContextPath(request)); + k.setMaxAge(0); + k.setSecure(request.isSecure()); + k.setHttpOnly(false); + response.addCookie(k); + } + + private String getRequestContextPath(HttpServletRequest request) { + log.info("getRequestContextPath"); + final String contextPath = request.getContextPath(); + return "".equals(contextPath) ? "/" : contextPath; + } +} \ No newline at end of file diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java new file mode 100644 index 0000000..bf6e1e4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java @@ -0,0 +1,519 @@ +package de.thpeetz.kontor.tysc; + +import de.thpeetz.kontor.tysc.data.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.tysc.data.FieldPosition; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleTysc implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private SportRepository sportRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private VendorRepository vendorRepository; + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private PlayerRepository playerRepository; + + @Autowired + private CardSetRepository cardSetRepository; + + @Autowired + private RoosterRepository roosterRepository; + + @Autowired + private CardRepository cardRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupDataLoader(TYSC) already executed, skipping"); + return; + } + if (!moduleService.importData(TyscConstants.TYSC)) { + log.info("Module TradeYourSportsCards should not setup data"); + return; + } + log.info("Setp up TYSC data"); + Sport football = createSportIfNotFound("Football"); + Sport baseball = createSportIfNotFound("Baseball"); + Sport basketball = createSportIfNotFound("Basketball"); + Sport hockey = createSportIfNotFound("Hockey"); + createTeamIfNotFound(football, "Buffalo Bills", "Bills"); + Team colts = createTeamIfNotFound(football, "Indianapolis Colts", "Colts"); + createTeamIfNotFound(football, "Miami Dolphins", "Dolphins"); + Team patriots = createTeamIfNotFound(football, "New England Patriots", "Patriots"); + createTeamIfNotFound(football, "New York Jets", "Jets"); + Team ravens = createTeamIfNotFound(football, "Baltimore Ravens", "Ravens"); + createTeamIfNotFound(football, "Cincinnati Bengals", "Bengals"); + Team browns = createTeamIfNotFound(football, "Cleveland Browns", "Browns"); + createTeamIfNotFound(football, "Jacksonville Jaguars", "Jaguars"); + Team steelers = createTeamIfNotFound(football, "Pittsburgh Steelers", "Steelers"); + createTeamIfNotFound(football, "Tennessee Titans", "Titans"); + createTeamIfNotFound(football, "Denver Broncos", "Broncos"); + createTeamIfNotFound(football, "Kansas City Chiefs", "Chiefs"); + createTeamIfNotFound(football, "Oakland Raiders", "Raiders"); + createTeamIfNotFound(football, "San Diego Chargers", "Chargers"); + createTeamIfNotFound(football, "Seattle Seahawks", "Seahawks"); + createTeamIfNotFound(football, "Arizona Cardinals", "Cardinals"); + createTeamIfNotFound(football, "Dallas Cowboys", "Cowboys"); + createTeamIfNotFound(football, "New York Giants", "Giants"); + createTeamIfNotFound(football, "Philadelphia Eagles", "Eagles"); + Team redskins = createTeamIfNotFound(football, "Washington Redskins", "Redskins"); + createTeamIfNotFound(football, "Chicago Bears", "Bears"); + createTeamIfNotFound(football, "Detroit Lions", "Lions"); + createTeamIfNotFound(football, "Green Bay Packers", "Packers"); + createTeamIfNotFound(football, "Minnesota Vikings", "Vikings"); + createTeamIfNotFound(football, "Tampa Bay Buccaneers", "Buccaneers"); + createTeamIfNotFound(football, "Atlanta Falcons", "Falcons"); + createTeamIfNotFound(football, "Carolina Panthers", "Panthers"); + createTeamIfNotFound(football, "New Orleans Saints", "Saints"); + createTeamIfNotFound(football, "St.Louis Rams", "Rams"); + createTeamIfNotFound(football, "San Francisco 49ers", "49ers"); + createTeamIfNotFound(football, "Houston Texans", "Texans"); + createTeamIfNotFound(football, "Houston Oilers", "Oilers"); + createTeamIfNotFound(baseball, "Baltimore Orioles", "Orioles"); + createTeamIfNotFound(baseball, "Boston Red Sox", "Red Sox"); + createTeamIfNotFound(baseball, "New York Yankees", "Yankees"); + createTeamIfNotFound(baseball, "Tampa Bay Devil Rays", "Devil Rays"); + createTeamIfNotFound(baseball, "Toronto Blue Jays", "Blue Jays"); + createTeamIfNotFound(baseball, "Chicago White Sox", "White Sox"); + createTeamIfNotFound(baseball, "Cleveland Indians", "Indians"); + createTeamIfNotFound(baseball, "Detroit Tigers", "Tigers"); + createTeamIfNotFound(baseball, "Kansas City Royals", "Royals"); + createTeamIfNotFound(baseball, "Minnesota Twins", "Twins"); + createTeamIfNotFound(baseball, "Anaheim Angels", "Angels"); + createTeamIfNotFound(baseball, "Oakland Athletics", "Athletics"); + createTeamIfNotFound(baseball, "Seattle Mariners", "Mariners"); + createTeamIfNotFound(baseball, "Texas Rangers", "Rangers"); + createTeamIfNotFound(baseball, "Atlanta Braves", "Braves"); + createTeamIfNotFound(baseball, "Florida Marlins", "Marlins"); + createTeamIfNotFound(baseball, "Montreal Expos", "Expos"); + createTeamIfNotFound(baseball, "New York Mets", "Mets"); + createTeamIfNotFound(baseball, "Philadelphia Phillies", "Phillies"); + createTeamIfNotFound(baseball, "Chicago Cubs", "Cubs"); + createTeamIfNotFound(baseball, "Cincinnati Reds", "Reds"); + createTeamIfNotFound(baseball, "Houston Astros", "Astros"); + createTeamIfNotFound(baseball, "Milwaukee Brewers", "Brewers"); + createTeamIfNotFound(baseball, "Pittsburgh Pirates", "Pirates"); + createTeamIfNotFound(baseball, "St.Louis Cardinals", "Cardinals"); + createTeamIfNotFound(baseball, "Arizona Diamondbacks", "Diamondbacks"); + createTeamIfNotFound(baseball, "Colorado Rockies", "Rockies"); + createTeamIfNotFound(baseball, "Los Angeles Dodgers", "Dodgers"); + createTeamIfNotFound(baseball, "San Diego Padres", "Padres"); + createTeamIfNotFound(baseball, "San Francisco Giants", "Giants"); + createTeamIfNotFound(basketball, "Boston Celtics", "Celtics"); + createTeamIfNotFound(basketball, "Miami Heat", "Heat"); + createTeamIfNotFound(basketball, "New Jersey Nets", "Mets"); + createTeamIfNotFound(basketball, "New York Knicks", "Knicks"); + createTeamIfNotFound(basketball, "Orlando Magic", "Magic"); + createTeamIfNotFound(basketball, "Philadelphia 76ers", "76ers"); + createTeamIfNotFound(basketball, "Washington Wizards", "Wizards"); + createTeamIfNotFound(basketball, "Atlanta Hawks", "Hawks"); + createTeamIfNotFound(basketball, "Charlotte Hornets", "Hornets"); + createTeamIfNotFound(basketball, "Chicago Bulls", "Bulls"); + createTeamIfNotFound(basketball, "Cleveland Cavaliers", "Cavaliers"); + createTeamIfNotFound(basketball, "Detroit Pistons", "Pistons"); + createTeamIfNotFound(basketball, "Indiana Pacers", "Pacers"); + createTeamIfNotFound(basketball, "Milwaukee Bucks", "Bucks"); + createTeamIfNotFound(basketball, "Toronto Raptors", "Raptors"); + createTeamIfNotFound(basketball, "Dallas Mavericks", "Mavericks"); + createTeamIfNotFound(basketball, "Denver Nuggets", "Nuggets"); + createTeamIfNotFound(basketball, "Houston Rockets", "Rockets"); + createTeamIfNotFound(basketball, "Minnesota Timberwolves", "Timberwolves"); + createTeamIfNotFound(basketball, "San Antonio Spurs", "Spurs"); + createTeamIfNotFound(basketball, "Utah Jazz", "Jazz"); + createTeamIfNotFound(basketball, "Vancouver Grizzlies", "Grizzlies"); + createTeamIfNotFound(basketball, "Golden State Warriors", "Warriors"); + createTeamIfNotFound(basketball, "Los Angeles Clippers", "Clippers"); + createTeamIfNotFound(basketball, "Los Angeles Lakers", "Lakers"); + createTeamIfNotFound(basketball, "Phoenix Suns", "Suns"); + createTeamIfNotFound(basketball, "Portland Trail Blazers", "Blazers"); + createTeamIfNotFound(basketball, "Sacramento Kings", "Kings"); + createTeamIfNotFound(basketball, "Seattle SuperSonics", "SuperSonics"); + createTeamIfNotFound(hockey, "Boston Bruins", "Bruins"); + createTeamIfNotFound(hockey, "Buffalo Sabres", "Sabres"); + createTeamIfNotFound(hockey, "Montreal Canadiens", "Canadiens"); + createTeamIfNotFound(hockey, "Ottawa Senators", "Senators"); + createTeamIfNotFound(hockey, "Toronto Maple Leafs", "Maple Leafs"); + createTeamIfNotFound(hockey, "New Jersey Devils", "Devils"); + createTeamIfNotFound(hockey, "New York Islanders", "Islanders"); + createTeamIfNotFound(hockey, "New York Rangers", "Rangers"); + createTeamIfNotFound(hockey, "Philadelphia Flyers", "Flyers"); + createTeamIfNotFound(hockey, "Pittsburgh Penguins", "Penguins"); + createTeamIfNotFound(hockey, "Atlanta Trashers", "Trashers"); + createTeamIfNotFound(hockey, "Carolina Hurricanes", "Hurricanes"); + createTeamIfNotFound(hockey, "Florida Panthers", "Panthers"); + createTeamIfNotFound(hockey, "Tampa Bay Lightnings", "Lightnings"); + createTeamIfNotFound(hockey, "Washington Capitals", "Capitals"); + createTeamIfNotFound(hockey, "Chicago Blackhawks", "Blackhawks"); + createTeamIfNotFound(hockey, "Columbus Blue Jackets", "Blue Jackets"); + createTeamIfNotFound(hockey, "Detroit Red Wings", "Red Wings"); + createTeamIfNotFound(hockey, "Nashville Predators", "Predators"); + createTeamIfNotFound(hockey, "St.Louis Blues", "Blues"); + createTeamIfNotFound(hockey, "Calgary Flames", "Flames"); + createTeamIfNotFound(hockey, "Colorado Avalanche", "Avalanche"); + createTeamIfNotFound(hockey, "Edmonton Oilers", "Oilers"); + createTeamIfNotFound(hockey, "Minnesota Wild", "Wild"); + createTeamIfNotFound(hockey, "Vancouver Canucks", "Canucks"); + createTeamIfNotFound(hockey, "Anaheim Mighty Ducks", "Mighty Ducks"); + createTeamIfNotFound(hockey, "Dallas Stars", "Stars"); + createTeamIfNotFound(hockey, "Los Angeles Kings", "Kings"); + createTeamIfNotFound(hockey, "Phoenix Coyotes", "Coyotes"); + createTeamIfNotFound(hockey, "San Jose Sharks", "Sharks"); + FieldPosition qb = createPosition(football, "QB", "Quarterback"); + FieldPosition rb = createPosition(football, "RB", "Running Back"); + FieldPosition wr = createPosition(football, "WR", "Wide Receiver"); + FieldPosition te = createPosition(football, "TE", "Tight End"); + FieldPosition fb = createPosition(football, "FB", "Fullback"); + createPosition(football, "OL", "Offensive Line"); + createPosition(football, "DL", "Defensive Line"); + FieldPosition lb = createPosition(football, "LB", "Linebacker"); + createPosition(football, "DB", "Defensive Back"); + createPosition(football, "DE", "Defensive End"); + createPosition(football, "K", "Kicker"); + createPosition(football, "P", "Punter"); + createPosition(football, "S", "Safety"); + createPosition(football, "KR", "Kick Returner"); + createPosition(football, "PR", "Punt Returner"); + createPosition(football, "LS", "Long Snapper"); + createPosition(football, "LG", "Left Guard"); + createPosition(football, "RG", "Right Guard"); + createPosition(football, "OF", "Offensive Tackle"); + createPosition(football, "DB", "Defensive Back"); + createPosition(football, "CB", "Cornerback"); + createPosition(football, "DT", "Defensive Tackle"); + createPosition(football, "NT", "Nose Tackle"); + createPosition(football, "OLB", "Outside Linebacker"); + createPosition(football, "ILB", "Inside Linebacker"); + createPosition(football, "SS", "Strong Safety"); + createPosition(baseball, "P", "Pitcher"); + createPosition(baseball, "C", "Catcher"); + createPosition(baseball, "1B", "First Base"); + createPosition(baseball, "2B", "Second Base"); + createPosition(baseball, "3B", "Third Base"); + createPosition(baseball, "SS", "Shortstop"); + createPosition(baseball, "LF", "Left Field"); + createPosition(baseball, "CF", "Center Field"); + createPosition(baseball, "RF", "Right Field"); + createPosition(basketball, "PG", "Point Guard"); + createPosition(basketball, "SG", "Shooting Guard"); + createPosition(basketball, "SF", "Small Forward"); + createPosition(basketball, "PF", "Power Forward"); + createPosition(basketball, "C", "Center"); + createPosition(hockey, "G", "Goalie"); + createPosition(hockey, "D", "Defense"); + createPosition(hockey, "LW", "Left Wing"); + createPosition(hockey, "RW", "Right Wing"); + createPosition(hockey, "C", "Center"); + Vendor pacific = createVendorIfNotFound("Pacific"); + Vendor fleer = createVendorIfNotFound("Fleer"); + Vendor bowman = createVendorIfNotFound("Bowman"); + Vendor leaf = createVendorIfNotFound("Leaf"); + Vendor upperdeck = createVendorIfNotFound("Upper Deck"); + createVendorIfNotFound("Topps"); + createVendorIfNotFound("Donruss"); + createVendorIfNotFound("Score"); + createVendorIfNotFound("Flair"); + createCardSetIfNotFound("Mystique Big Buzz", fleer, false, true); + createCardSetIfNotFound("Mystique Gold", fleer, true, false); + createCardSetIfNotFound("Pacific Copper", pacific, true, false); + createCardSetIfNotFound("Pacific Gold", pacific, true, false); + CardSet pacificbase = createCardSetIfNotFound(pacific.getName(), pacific, false, false); + createCardSetIfNotFound(fleer.getName(), fleer, false, false); + createCardSetIfNotFound(bowman.getName(), bowman, false, false); + createCardSetIfNotFound(leaf.getName(), leaf, false, false); + createCardSetIfNotFound("Ultra", fleer, false, false); + createCardSetIfNotFound("Mystique", fleer, false, false); + createCardSetIfNotFound("Finest Hour", pacific, false, false); + createCardSetIfNotFound("SP", upperdeck, false, false); + createCardSetIfNotFound("SPX", upperdeck, false, false); + createCardSetIfNotFound("SP Authentic", upperdeck, false, false); + createCardSetIfNotFound("Black Diamond", upperdeck, false, false); + Player jeromepathon = createPlayerIfNotFound("Jerome", "Pathon"); + Player bruschi = createPlayerIfNotFound("Tedy", "Bruschi"); + Player couch = createPlayerIfNotFound("Tim", "Couch"); + Player shea = createPlayerIfNotFound("Aaron", "Shea"); + Player jamallewis = createPlayerIfNotFound("Jamal", "Lewis"); + Player jermainelewis = createPlayerIfNotFound("Jermaine", "Lewis"); + Player tonybanks = createPlayerIfNotFound("Tony", "Banks"); + Player chrisfuamatu = createPlayerIfNotFound("Chris", "Fuamatu-Ma'afala"); + Player jeromebettis = createPlayerIfNotFound("Jerome", "Bettis"); + Player kordellstewart = createPlayerIfNotFound("Kordell", "Stewart"); + createPlayerIfNotFound("Warren", "Moon"); + createPlayerIfNotFound("Kevin", "Lockett"); + createPlayerIfNotFound("Rich", "Gannon"); + createPlayerIfNotFound("James", "Jett"); + createPlayerIfNotFound("Mack", "Strong"); + createPlayerIfNotFound("Brock", "Huard"); + createPlayerIfNotFound("Ricky", "Watters"); + createPlayerIfNotFound("Troy", "Aikman"); + createPlayerIfNotFound("David", "LaFleur"); + createPlayerIfNotFound("Chris", "Brazzell"); + createPlayerIfNotFound("Ron", "Dayne"); + createPlayerIfNotFound("Na", "Brown"); + createPlayerIfNotFound("Torrance", "Small"); + createPlayerIfNotFound("Chad", "Lewis"); + createPlayerIfNotFound("Adrian", "Murrell"); + createPlayerIfNotFound("Chris", "Chandler"); + createPlayerIfNotFound("Danny", "Kanell"); + createPlayerIfNotFound("Ricky", "Williams"); + createPlayerIfNotFound("Jeff", "Garcia"); + createPlayerIfNotFound("Tai", "Streets"); + createPlayerIfNotFound("Charlie", "Garner"); + createPlayerIfNotFound("Drew", "Bledsoe"); + createPlayerIfNotFound("Antowain", "Smith"); + createPlayerIfNotFound("Terry", "Glenn"); + createPlayerIfNotFound("Jerry", "Rice"); + createPlayerIfNotFound("Terrell", "Owens"); + createPlayerIfNotFound("Isaac", "Bruce"); + createPlayerIfNotFound("Trung", "Canidate"); + Rooster pathoncoltswr2001 = createRoosterIfNotFound(jeromepathon, colts, wr, 2001); + Rooster bruschipatriotslb2001 = createRoosterIfNotFound(bruschi, patriots, lb, 2001); + Rooster couchbrownsqb2001 = createRoosterIfNotFound(couch, browns, qb, 2001); + Rooster sheabrownste2001 = createRoosterIfNotFound(shea, browns, te, 2001); + Rooster jamallewisravensrb2001 = createRoosterIfNotFound(jamallewis, ravens, rb, 2001); + Rooster jermainelewisravenswr2001 = createRoosterIfNotFound(jermainelewis, ravens, wr, 2001); + Rooster tonybanksravensqb2001 = createRoosterIfNotFound(tonybanks, ravens, qb, 2001); + createRoosterIfNotFound(tonybanks, redskins, qb, 2002); + Rooster chrisfuamatusteelersfb2001 = createRoosterIfNotFound(chrisfuamatu, steelers, fb, 2001); + Rooster jeromebettissteelersrb2001 = createRoosterIfNotFound(jeromebettis, steelers, rb, 2001); + Rooster kordellstewartsteelersqb2001 = createRoosterIfNotFound(kordellstewart, steelers, qb, 2001); + createCardIfNotFound(185, 2001, pacific, pacificbase, pathoncoltswr2001); + createCardIfNotFound(250, 2001, pacific, pacificbase, bruschipatriotslb2001); + createCardIfNotFound(103, 2001, pacific, pacificbase, couchbrownsqb2001); + createCardIfNotFound(112, 2001, pacific, pacificbase, sheabrownste2001); + createCardIfNotFound(37, 2001, pacific, pacificbase, jamallewisravensrb2001); + createCardIfNotFound(38, 2001, pacific, pacificbase, jermainelewisravenswr2001); + createCardIfNotFound(31, 2001, pacific, pacificbase, tonybanksravensqb2001); + createCardIfNotFound(338, 2001, pacific, pacificbase, chrisfuamatusteelersfb2001); + createCardIfNotFound(335, 2001, pacific, pacificbase, jeromebettissteelersrb2001); + createCardIfNotFound(345, 2001, pacific, pacificbase, kordellstewartsteelersqb2001); + /* + * INSERT INTO `spieler` (`ID`,`name`) VALUES + * (11,'Moon, Warren'), + * (12,'Lockett, Kevin'), + * (13,'Gannon, Rich'), + * (14,'Jett, James'), + * (15,'Strong, Mack'), + * (16,'Huard, Brock'), + * (17,'Watters, Ricky'), + * (18,'Aikman, Troy'), + * (19,'LaFleur, David'), + * (20,'Brazzell, Chris'), + * (21,'Dayne, Ron'), + * (22,'Brown, Na'), + * (23,'Small, Torrance'), + * (24,'Lewis, Chad'), + * (25,'Murrell, Adrian'), + * (26,'Smith, Maurice'), + * (27,'Chandler, Chris'), + * (28,'Kanell, Danny') + * ,(29,'Williams, Ricky'), + * (30,'Garcia, Jeff'), + * (31,'Streets, Tai'), + * (32,'Garner, Charlie'), + * (33,'Rice, Jerry'), + * (34,'Owens, Terrell'), + * (35,'Bruce, Isaac'), + * (36,'Canidate, Trung'); + * + * INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES + * (1,1,'Buffalo Bills','Bills'), + * (2,1,'Indianapolis Colts','Colts'), + * (3,1,'Miami Dolphins','Dolphins'), + * (4,1,'New England Patriots','Patriots'), + * (5,1,'New York Jets','Jets'), + * (6,1,'Baltimore Ravens','Ravens'), + * (7,1,'Cincinnati Bengals','Bengals'), + * (8,1,'Cleveland Browns','Browns'), + * (9,1,'Jacksonville Jaguars','Jaguars'), + * (10,1,'Pittsburgh Steelers','Steelers'), + * (11,1,'Tennessee Titans','Titans'), + * (12,1,'Denver Broncos','Broncos'), + * (13,1,'Kansas City Chiefs','Chiefs'), + * (14,1,'Oakland Raiders','Raiders'), + * (15,1,'San Diego Chargers','Chargers'), + * (16,1,'Seattle Seahawks','Seahawks'), + * (17,1,'Arizona Cardinals','Cardinals'), + * (18,1,'Dallas Cowboys','Cowboys'), + * (19,1,'New York Giants','Giants'), + * (20,1,'Philadelphia Eagles','Eagles'), + * (21,1,'Washington Redskins','Redskins'), + * (22,1,'Chicago Bears','Bears'), + * (23,1,'Detroit Lions','Lions'), + * (24,1,'Green Bay Packers','Packers'), + * (25,1,'Minnesota Vikings','Vikings'), + * (26,1,'Tampa Bay Buccaneers','Buccaneers'), + * (27,1,'Atlanta Falcons','Falcons'), + * (28,1,'Carolina Panthers','Panthers'); + * (29,1,'New Orleans Saints','Saints'), + * (30,1,'St.Louis Rams','Rams'), + * (31,1,'San Francisco 49ers','49ers'), + * + * INSERT INTO `karte` + * (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,` + * inserts_id`,`rookie`,`jahr`,`nummer`) VALUES + * (11,11,13,1,1,0,0,'N',2001,213), + * (12,12,13,1,1,0,0,'N',2001,212), + * (13,13,14,1,1,0,0,'N',2001,311), + * (14,14,14,1,1,0,0,'N',2001,312), + * (15,15,16,1,1,0,0,'N',2001,403), + * (16,16,16,1,1,0,0,'N',2001,397), + * (17,17,16,1,1,0,0,'N',2001,404), + * (18,18,18,1,1,0,0,'N',2001,116), + * (19,19,18,1,1,0,0,'N',2001,122), + * (20,20,18,1,1,0,0,'N',2001,117), + * (21,21,19,1,1,0,0,'N',2001,281), + * (22,22,19,1,1,0,0,'N',2001,321), + * (23,23,20,1,1,0,0,'N',2001,331), + * (24,24,20,1,1,0,0,'N',2001,324), + * (25,25,21,1,1,0,0,'N',2001,445), + * (26,26,27,1,1,0,0,'N',2001,28), + * (27,27,27,1,1,0,0,'N',2001,17), + * (28,28,27,1,1,0,0,'N',2001,23), + * (29,29,29,1,1,0,0,'N',2001,273); + * (30,30,31,1,1,0,0,'N',2001,380), + * (31,31,31,1,1,0,0,'N',2001,390), + * (32,32,31,1,1,0,0,'N',2001,381), + * (33,33,31,1,1,0,0,'N',2001,387), + * (34,34,31,1,1,0,0,'N',2001,386), + * (35,35,30,1,1,0,0,'N',2001,349), + * (36,36,30,1,1,0,0,'N',2001,350), + * (37,37,44,5,8,0,0,'N',1994,106); + */ + + alreadySetup = true; + moduleService.setDataImported(TyscConstants.TYSC); + } + + private Sport createSportIfNotFound(String sport) { + log.info("createSportIfNotFound: {}", sport); + Sport sportEntity = sportRepository.findByName(sport); + if (sportEntity == null) { + log.info("Sport {} not found, will create it", sport); + sportEntity = new Sport(); + sportEntity.setName(sport); + sportRepository.save(sportEntity); + } + return sportEntity; + } + + private Team createTeamIfNotFound(Sport football, String name, String shortName) { + log.info("createTeamIfNotFound: {}", name); + Team team = teamRepository.findByName(name); + if (team == null) { + log.info("Team {} not found, will create it", name); + team = new Team(); + team.setName(name); + team.setShortName(shortName); + team.setSport(football); + teamRepository.save(team); + } + return team; + } + + private FieldPosition createPosition(Sport sport, String shortName, String name) { + log.info("createPosition: {} for Sport {}", name, sport.getName()); + FieldPosition fieldPosition = fieldPositionRepository.findByShortNameAndSport(shortName, sport); + if (fieldPosition == null) { + log.info("Position {} not found, will create it", name); + fieldPosition = new FieldPosition(); + fieldPosition.setShortName(shortName); + fieldPosition.setName(name); + fieldPosition.setSport(sport); + fieldPositionRepository.save(fieldPosition); + } + return fieldPosition; + } + + private Vendor createVendorIfNotFound(String name) { + log.info("createVendorIfNotFound: {}", name); + Vendor vendor = vendorRepository.findByName(name); + if (vendor == null) { + log.info("Vendor {} not found, will create it", name); + vendor = new Vendor(); + vendor.setName(name); + vendorRepository.save(vendor); + } + return vendor; + } + + private Player createPlayerIfNotFound(String firstName, String lastName) { + log.info("createPlayerIfNotFound: {} {}", firstName, lastName); + Player player = playerRepository.findByFirstNameAndLastName(firstName, lastName); + if (player == null) { + log.info("Player {} {} not found, will create it", firstName, lastName); + player = new Player(); + player.setFirstName(firstName); + player.setLastName(lastName); + playerRepository.save(player); + } + return player; + } + + private CardSet createCardSetIfNotFound(String setname, Vendor vendor, boolean parallelSet, boolean insertSet) { + log.info("createCardSetIfNotFound: {} {}", setname, vendor.getName()); + CardSet cardSet = cardSetRepository.findByNameAndVendor(setname, vendor); + if (cardSet == null) { + log.info("CardType {} not found, will create it", cardSet); + cardSet = new CardSet(); + cardSet.setName(setname); + cardSet.setVendor(vendor); + cardSet.setParallelSet(parallelSet); + cardSet.setInsertSet(insertSet); + cardSetRepository.save(cardSet); + } + return cardSet; + } + + private Rooster createRoosterIfNotFound(Player player, Team team, FieldPosition fieldPosition, int year) { + log.info("createRoosterIfNotFound; {} {} {} {}", player.getFirstName(), player.getLastName(), team.getName(), + year); + Rooster rooster = roosterRepository.findByReferences(player, team, fieldPosition, year); + if (rooster == null) { + log.info("Rooster {} not found, will create it", player); + rooster = new Rooster(); + rooster.setPlayer(player); + rooster.setTeam(team); + rooster.setPosition(fieldPosition); + rooster.setYear(year); + roosterRepository.save(rooster); + } + return rooster; + } + + private void createCardIfNotFound(int cardNumber, int year, Vendor vendor, CardSet cardset, Rooster rooster) { + log.info("createCardIfNotFound: vendor={} cardset={} rooster={} cardNumber={} year={}", vendor, cardset, + rooster, cardNumber, year); + Card card = cardRepository.search(vendor, cardset, rooster, cardNumber, year); + if (card == null) { + card = new Card(); + card.setVendor(vendor); + card.setCardSet(cardset); + card.setRooster(rooster); + card.setCardNumber(cardNumber); + card.setYear(year); + cardRepository.save(card); + } + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java new file mode 100644 index 0000000..cc41c93 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java @@ -0,0 +1,88 @@ +package de.thpeetz.kontor.tysc; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.tysc.views.CardSetView; +import de.thpeetz.kontor.tysc.views.CardView; +import de.thpeetz.kontor.tysc.views.PlayerView; +import de.thpeetz.kontor.tysc.views.PositionView; +import de.thpeetz.kontor.tysc.views.RoosterView; +import de.thpeetz.kontor.tysc.views.SportView; +import de.thpeetz.kontor.tysc.views.TeamView; +import de.thpeetz.kontor.tysc.views.VendorView; + +/** + * The {@code TyscConstants} class contains constant values related to tysc. + */ +public class TyscConstants { + + public static final String TYSC = "TradeYourSportsCards"; + public static final String TYSC_ROLE = "ROLE_TYSC"; + public static final String VENDOR = "Vendor"; + public static final String VENDOR_ROUTE = "tysc/vendor"; + public static final String CARDSET = "Card Set"; + public static final String CARDSET_ROUTE = "tysc/cardset"; + public static final String CARD = "Card"; + public static final String CARD_ROUTE = "tysc/card"; + public static final String SPORT = "Sport"; + public static final String SPORT_ROUTE = "tysc/sport"; + public static final String TEAM = "Team"; + public static final String TEAM_ROUTE = "tysc/team"; + public static final String POSITION = "Position"; + public static final String POSITION_ROUTE = "tysc/position"; + public static final String PLAYER = "Player"; + public static final String PLAYER_ROUTE = "tysc/player"; + public static final String ROOSTER = "Rooster"; + public static final String ROOSTER_ROUTE = "tysc/rooster"; + + public static RouterLink getSportLink() { + return new RouterLink(SPORT, SportView.class); + } + + public static RouterLink getTeamLink() { + return new RouterLink(TEAM, TeamView.class); + } + + public static RouterLink getPlayerLink() { + return new RouterLink(PLAYER, PlayerView.class); + } + + public static RouterLink getPositionLink() { + return new RouterLink(POSITION, PositionView.class); + } + + public static RouterLink getRoosterLink() { + return new RouterLink(ROOSTER, RoosterView.class); + } + + public static RouterLink getVendorLink() { + return new RouterLink(VENDOR, VendorView.class); + } + + public static RouterLink getCardSetLink() { + return new RouterLink(CARDSET, CardSetView.class); + } + + public static RouterLink getCardLink() { + return new RouterLink(CARD, CardView.class); + } + + public static SideNavItem getTyscNavigation() { + SideNavItem tysc = new SideNavItem(TYSC, VENDOR_ROUTE, VaadinIcon.ARCHIVE.create()); + tysc.addItem(new SideNavItem(SPORT, SportView.class)); + tysc.addItem(new SideNavItem(TEAM, TeamView.class)); + tysc.addItem(new SideNavItem(PLAYER, PlayerView.class)); + tysc.addItem(new SideNavItem(POSITION, PositionView.class)); + tysc.addItem(new SideNavItem(ROOSTER, RoosterView.class)); + tysc.addItem(new SideNavItem(CARDSET, CardSetView.class)); + tysc.addItem(new SideNavItem(CARD, CardView.class)); + tysc.addItem(new SideNavItem(VENDOR, VendorView.class)); + return tysc; + } + + private TyscConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Card.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Card.java new file mode 100644 index 0000000..90ec739 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Card.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.tysc.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "cardNumber, year")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "cardNumber", "year", "vendor_id", "cardSet_id" })} +) +public class Card extends AbstractEntity { + + private int cardNumber; + + private int year; + + @ManyToOne + @JoinColumn(name = "vendor_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private Vendor vendor; + + @ManyToOne + @JoinColumn(name = "cardSet_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private CardSet cardSet; + + @ManyToOne + @JoinColumn(name = "rooster_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private Rooster rooster; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardRepository.java new file mode 100644 index 0000000..c894b28 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CardRepository extends JpaRepository { + + @Query("SELECT c FROM Card c WHERE c.vendor = ?1 AND c.cardSet = ?2 and c.rooster = ?3 and c.cardNumber = ?4 and c.year = ?5") + Card search(Vendor vendor, CardSet cardset, Rooster rooster, int cardNumber, int year); + + @Query("select c from Card c " + + "where str(c.cardNumber) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java new file mode 100644 index 0000000..c72188c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name, vendor_id")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name", "vendor_id" })} +) +public class CardSet extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne() + @JoinColumn(name = "vendor_id") + @NotNull + @JsonIgnoreProperties({ "cardSets" }) + private Vendor vendor; + + private boolean parallelSet = false; + + private boolean insertSet = false; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSetRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSetRepository.java new file mode 100644 index 0000000..237953e --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSetRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CardSetRepository extends JpaRepository { + + List findByName(String name); + + CardSet findByNameAndVendor(String name, Vendor vendor); + + @Query("select c from CardSet c " + + "where lower(c.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java new file mode 100644 index 0000000..a806e70 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java @@ -0,0 +1,52 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name, sport_id")}, + uniqueConstraints = { + @UniqueConstraint(columnNames = { "name", "sport_id" }), + @UniqueConstraint(columnNames = { "shortName", "sport_id" }) + } +) +public class FieldPosition extends AbstractEntity { + + @NotEmpty + private String name; + + @NotEmpty + private String shortName; + + @ManyToOne + @JoinColumn(name = "sport_id") + @NotNull + @JsonIgnoreProperties({ "positions" }) + private Sport sport; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "position") + @Nullable + private List roosters; +} \ No newline at end of file diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPositionRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPositionRepository.java new file mode 100644 index 0000000..0f649cc --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPositionRepository.java @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface FieldPositionRepository extends JpaRepository { + + @Query("select p from FieldPosition p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findBySport(Sport sport); + + FieldPosition findByShortName(String searchTerm); + + List findByShortNameIgnoreCase(String shortName); + + FieldPosition findByShortNameAndSport(String shortName, Sport sport); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Player.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Player.java new file mode 100644 index 0000000..6517959 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Player.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = { + @Index(columnList = "firstName, lastName"), + @Index(columnList = "lastName, firstName") +}, uniqueConstraints = { + @UniqueConstraint(columnNames = { "firstName", "lastName" }) +}) +public class Player extends AbstractEntity { + + @NotNull + private String firstName; + + @NotNull + private String lastName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "player") + @Nullable + private List roosters; + + public String getFullName() { + StringBuilder fullName = new StringBuilder(); + fullName.append(this.lastName); + fullName.append(", "); + fullName.append(this.firstName); + return fullName.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/PlayerRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/PlayerRepository.java new file mode 100644 index 0000000..8431abd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/PlayerRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PlayerRepository extends JpaRepository { + + @Query("select p from Player p " + + "where lower(p.firstName) like lower(concat('%', :searchTerm, '%')) " + + "or lower(p.lastName) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + Player findByFirstNameAndLastName(String firstName, String lastName); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java new file mode 100644 index 0000000..6410b07 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java @@ -0,0 +1,51 @@ +package de.thpeetz.kontor.tysc.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "team_id, player_id, position_id")}, + uniqueConstraints = {@UniqueConstraint(name = "uniqueRooster", columnNames = {"year", "team_id", "player_id", "position_id"})} +) +public class Rooster extends AbstractEntity { + + private int year; + + @ManyToOne + @JoinColumn(name = "team_id") + @NotNull + private Team team; + + @ManyToOne + @JoinColumn(name = "player_id") + @NotNull + private Player player; + + @ManyToOne + @JoinColumn(name = "position_id") + @NotNull + private FieldPosition position; + + @Override + public String toString() { + return "Rooster{" + + "year=" + year + + ", team=" + team.getName() + + ", player=" + player.getFullName() + + ", position=" + position.getName() + + '}'; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/RoosterRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/RoosterRepository.java new file mode 100644 index 0000000..a24dda9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/RoosterRepository.java @@ -0,0 +1,11 @@ +package de.thpeetz.kontor.tysc.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface RoosterRepository extends JpaRepository{ + + @Query("SELECT r FROM Rooster r WHERE r.player = ?1 AND r.team = ?2 AND r.position = ?3 AND r.year = ?4") + Rooster findByReferences(Player player, Team team, FieldPosition position, int year); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java new file mode 100644 index 0000000..39333fd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name" })} +) +public class Sport extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "sport") + @Nullable + private List teams = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "sport") + @Nullable + private List positions = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/SportRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/SportRepository.java new file mode 100644 index 0000000..35efe85 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/SportRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface SportRepository extends JpaRepository { + @Query("select s from Sport s " + + "where lower(s.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Sport findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java new file mode 100644 index 0000000..b61fdd2 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java @@ -0,0 +1,50 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name" })} +) +public class Team extends AbstractEntity { + + @NotEmpty + private String name; + + @NotEmpty + private String shortName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "team") + @Nullable + private List roosters; + + @ManyToOne + @JoinColumn(name = "sport_id") + @NotNull + @JsonIgnoreProperties({ "teams" }) + private Sport sport; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/TeamRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/TeamRepository.java new file mode 100644 index 0000000..9f518d9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/TeamRepository.java @@ -0,0 +1,27 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * The repository interface for managing teams. + */ +public interface TeamRepository extends JpaRepository { + + /** + * Searches for teams based on a search term. + * + * @param searchTerm the search term to match against team names + * @return a list of teams matching the search term + */ + @Query("select t from Team t " + + "where lower(t.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Team findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java new file mode 100644 index 0000000..f405565 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java @@ -0,0 +1,38 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a vendor entity. + */ +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = "name")} +) +public class Vendor extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "vendor") + @Nullable + private List cardSets; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/VendorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/VendorRepository.java new file mode 100644 index 0000000..4c7a304 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/VendorRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface VendorRepository extends JpaRepository { + + @Query("select v from Vendor v where lower(v.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Vendor findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java new file mode 100644 index 0000000..1cfedbd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java @@ -0,0 +1,93 @@ +package de.thpeetz.kontor.tysc.services; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardRepository; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.CardSetRepository; +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.data.VendorRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class CardService { + + private VendorRepository vendorRepository; + + private CardSetRepository cardSetRepository; + + private CardRepository cardRepository; + + public CardService(VendorRepository vendorRepository, CardSetRepository cardSetRepository, CardRepository cardRepository) { + this.vendorRepository = vendorRepository; + this.cardSetRepository = cardSetRepository; + this.cardRepository = cardRepository; + } + + public List findAllVendors(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return vendorRepository.findAll(); + } else { + return vendorRepository.search(stringFilter); + } + } + + public void deleteVendor(Vendor vendor) { + vendorRepository.delete(vendor); + } + + public Vendor saveVendor(Vendor vendor) { + if (vendor == null) { + log.warn("Vendor is null. Are you sure you have connected your form to the application?"); + return null; + } + return vendorRepository.save(vendor); + } + + public List findAllCards(String stringFilter) { + log.info("find cards with filter: {}", stringFilter); + if (stringFilter == null || stringFilter.isEmpty()) { + List cards = cardRepository.findAll(); + log.debug("found {} cards", cards.size()); + return cards; + } else { + return cardRepository.search(stringFilter); + } + } + + public void deleteCard(Card card) { + cardRepository.delete(card); + } + + public Card saveCard(Card card) { + if (card == null) { + log.warn("Card is null. Are you sure you have connected your form to the application?"); + return null; + } + return cardRepository.save(card); + } + + public List findAllCardSets(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return cardSetRepository.findAll(); + } else { + return cardSetRepository.search(stringFilter); + } + } + + public CardSet saveCardSet(CardSet cardSet) { + if (cardSet == null) { + log.warn("CardSet is null. Are you sure you have connected your form to the application?"); + return null; + } + return cardSetRepository.save(cardSet); + } + + public void deleteCardSet(CardSet cardSet) { + cardSetRepository.delete(cardSet); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java new file mode 100644 index 0000000..05482bf --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java @@ -0,0 +1,147 @@ +package de.thpeetz.kontor.tysc.services; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.PlayerRepository; +import de.thpeetz.kontor.tysc.data.FieldPositionRepository; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.RoosterRepository; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.data.SportRepository; +import de.thpeetz.kontor.tysc.data.Team; +import de.thpeetz.kontor.tysc.data.TeamRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class SportService { + + @Autowired + private SportRepository sportRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private PlayerRepository playerRepository; + + @Autowired + private RoosterRepository roosterRepository; + + public List findAllSports(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return sportRepository.findAll(); + } else { + return sportRepository.search(stringFilter); + } + } + + public void deleteSport(Sport sport) { + sportRepository.delete(sport); + } + + public Sport saveSport(Sport sport) { + if (sport == null) { + log.warn("Sport is null. Are you sure you have connected your form to the application?"); + return null; + } + return sportRepository.save(sport); + } + + public List findAllPlayers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return playerRepository.findAll(); + } else { + return playerRepository.search(stringFilter); + } + } + + public Player savePlayer(Player player) { + if (player == null) { + log.warn("Player is null. Are you sure you have connected yout form to the application?"); + return null; + } + return playerRepository.save(player); + } + + public void deletePlayer(Player player) { + playerRepository.delete(player); + } + + public List findAllTeams(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return teamRepository.findAll(); + } else { + return teamRepository.search(stringFilter); + } + } + + public void deleteTeam(Team team) { + teamRepository.delete(team); + } + + public void saveTeam(Team team) { + if (team == null) { + log.warn("Team is null. Are you sure you have connected your form to the application?"); + return; + } + teamRepository.save(team); + } + + public List findAllPositions(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return fieldPositionRepository.findAll(); + } else { + return fieldPositionRepository.search(stringFilter); + } + } + + public List findAllPositionsForSport(Sport sport) { + if (sport == null) { + return fieldPositionRepository.findAll(); + } else { + log.info("Find positions for Sport: {}", sport); + return fieldPositionRepository.findBySport(sport); + } + } + + public void savePosition(FieldPosition position) { + if (position == null) { + log.warn("Position is null. Are you sure you have connected your form to the application?"); + return; + } + fieldPositionRepository.save(position); + } + + public void deletePosition(FieldPosition position) { + fieldPositionRepository.delete(position); + } + + public List findAllRoosters() { + return roosterRepository.findAll(); + } + + public Rooster findRoosterByFields(Team team, Player player, FieldPosition position, Integer year) { + return roosterRepository.findByReferences(player, team, position, year); + } + + public void saveRooster(Rooster rooster) { + if (rooster == null) { + log.warn(""); + return; + } + roosterRepository.save(rooster); + } + + public void deleteRooster(Rooster rooster) { + roosterRepository.delete(rooster); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java new file mode 100644 index 0000000..174afdc --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java @@ -0,0 +1,110 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.Rooster; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CardForm extends FormLayout { + + TextField cardNumber = new TextField("CardNumber"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Card.class); + + public CardForm() { + addClassName("card-form"); + binder.bindInstanceFields(this); + + add(cardNumber, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setCard(Card card) { + binder.setBean(card); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + } + + public abstract static class CardFormEvent extends ComponentEvent { + private Card card; + + protected CardFormEvent(CardForm source, Card card) { + super(source, false); + this.card = card; + } + + public Card getCard() { + return card; + } + } + + public static class SaveEvent extends CardFormEvent { + SaveEvent(CardForm source, Card card) { + super(source, card); + } + } + + public static class DeleteEvent extends CardFormEvent { + DeleteEvent(CardForm source, Card card) { + super(source, card); + } + } + + public static class CloseEvent extends CardFormEvent { + CloseEvent(CardForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java new file mode 100644 index 0000000..d0e7e89 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java @@ -0,0 +1,103 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.CardSet; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CardSetForm extends FormLayout { + + TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(CardSet.class); + + public CardSetForm() { + addClassName("cardSet-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setCardSet(CardSet cardSet) { + binder.setBean(cardSet); + } + + public abstract static class CardSetFormEvent extends ComponentEvent { + private CardSet cardSet; + + protected CardSetFormEvent(CardSetForm source, CardSet cardSet) { + super(source, false); + this.cardSet = cardSet; + } + + public CardSet getCardSet() { + return cardSet; + } + } + + public static class SaveEvent extends CardSetFormEvent { + SaveEvent(CardSetForm source, CardSet cardSet) { + super(source, cardSet); + } + } + + public static class DeleteEvent extends CardSetFormEvent { + DeleteEvent(CardSetForm source, CardSet cardSet) { + super(source, cardSet); + } + } + + public static class CloseEvent extends CardSetFormEvent { + CloseEvent(CardSetForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java new file mode 100644 index 0000000..1a49896 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.CARDSET_ROUTE, layout = MainLayout.class) +@PageTitle("CardSet | Tysc | Kontor") +public class CardSetView extends VerticalLayout { + + Grid grid = new Grid<>(CardSet.class); + TextField filterText = new TextField(); + CardSetForm form; + CardService service; + + public CardSetView(CardService service) { + this.service = service; + addClassName("cardSet-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("cardSet-grid"); + grid.setSizeFull(); + grid.setColumns("name", "vendor.name", "parallelSet", "insertSet"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editCardSet(event.getValue())); + } + + public CardSetForm getForm() { + return form; + } + + private void configureForm() { + form = new CardSetForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveCardSet); + form.addDeleteListener(this::deleteCardSet); + form.addCloseListener(e -> closeEditor()); + } + + private void saveCardSet(CardSetForm.SaveEvent event) { + service.saveCardSet(event.getCardSet()); + updateList(); + closeEditor(); + } + + private void deleteCardSet(CardSetForm.DeleteEvent event) { + service.deleteCardSet(event.getCardSet()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addCardSetButton = new Button("Add cardSet"); + addCardSetButton.addClickListener(click -> addCardSet()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addCardSetButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editCardSet(CardSet cardSet) { + if (cardSet == null) { + closeEditor(); + } else { + form.setCardSet(cardSet); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setCardSet(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addCardSet() { + grid.asSingleSelect().clear(); + editCardSet(new CardSet()); + } + + public void updateList() { + grid.setItems(service.findAllCardSets(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java new file mode 100644 index 0000000..89cc1ce --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.CARD_ROUTE, layout = MainLayout.class) +@PageTitle("Card | Tysc | Kontor") +public class CardView extends VerticalLayout { + + Grid grid = new Grid<>(Card.class); + TextField filterText = new TextField(); + CardForm form; + CardService service; + + public CardView(CardService service) { + this.service = service; + addClassName("card-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("card-grid"); + grid.setSizeFull(); + grid.setColumns("cardNumber", "year", "vendor.name", "cardSet.name", "rooster.player.fullName"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editCard(event.getValue())); + } + + public CardForm getForm() { + return form; + } + + private void configureForm() { + form = new CardForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveCard); + form.addDeleteListener(this::deleteCard); + form.addCloseListener(e -> closeEditor()); + } + + private void saveCard(CardForm.SaveEvent event) { + service.saveCard(event.getCard()); + updateList(); + closeEditor(); + } + + private void deleteCard(CardForm.DeleteEvent event) { + service.deleteCard(event.getCard()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addCardButton = new Button("Add card"); + addCardButton.addClickListener(click -> addCard()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addCardButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editCard(Card card) { + if (card == null) { + closeEditor(); + } else { + form.setCard(card); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setCard(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addCard() { + grid.asSingleSelect().clear(); + editCard(new Card()); + } + + public void updateList() { + grid.setItems(service.findAllCards(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java new file mode 100644 index 0000000..9730103 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java @@ -0,0 +1,116 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PlayerForm extends FormLayout { + + TextField firstName = new TextField("First Name"); + TextField lastName = new TextField("Last Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Player.class); + + public PlayerForm() { + addClassName("player-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("team.name", "position.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(firstName, lastName, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPlayer(Player player) { + binder.setBean(player); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class PlayerFormEvent extends ComponentEvent { + private Player player; + + protected PlayerFormEvent(PlayerForm source, Player player) { + super(source, false); + this.player = player; + } + + public Player getPlayer() { + return player; + } + } + + public static class SaveEvent extends PlayerFormEvent { + SaveEvent(PlayerForm source, Player player) { + super(source, player); + } + } + + public static class DeleteEvent extends PlayerFormEvent { + DeleteEvent(PlayerForm source, Player player) { + super(source, player); + } + } + + public static class CloseEvent extends PlayerFormEvent { + CloseEvent(PlayerForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java new file mode 100644 index 0000000..56d9588 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.PLAYER_ROUTE, layout = MainLayout.class) +@PageTitle("Player | Tysc | Kontor") +public class PlayerView extends VerticalLayout { + + Grid grid = new Grid<>(Player.class); + TextField filterText = new TextField(); + PlayerForm form; + SportService service; + + public PlayerView(SportService service) { + this.service = service; + addClassName("player-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("player-grid"); + grid.setSizeFull(); + grid.setColumns("firstName", "lastName"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPlayer(event.getValue())); + } + + public PlayerForm getForm() { + return form; + } + + private void configureForm() { + form = new PlayerForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePlayer); + form.addDeleteListener(this::deletePlayer); + form.addCloseListener(e -> closeEditor()); + } + + private void savePlayer(PlayerForm.SaveEvent event) { + service.savePlayer(event.getPlayer()); + updateList(); + closeEditor(); + } + + private void deletePlayer(PlayerForm.DeleteEvent event) { + service.deletePlayer(event.getPlayer()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPlayerButton = new Button("Add player"); + addPlayerButton.addClickListener(click -> addPlayer()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPlayerButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPlayer(Player player) { + if (player == null) { + closeEditor(); + } else { + form.setPlayer(player); + form.setRoosters(player.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPlayer(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPlayer() { + grid.asSingleSelect().clear(); + editPlayer(new Player()); + } + + public void updateList() { + grid.setItems(service.findAllPlayers(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java new file mode 100644 index 0000000..f5b49bb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Rooster; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PositionForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(FieldPosition.class); + + public PositionForm() { + addClassName("position-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("player.fullName", "team.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPosition(FieldPosition position) { + binder.setBean(position); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class PositionFormEvent extends ComponentEvent { + private FieldPosition position; + + protected PositionFormEvent(PositionForm source, FieldPosition position) { + super(source, false); + this.position = position; + } + + public FieldPosition getPosition() { + return position; + } + } + + public static class SaveEvent extends PositionFormEvent { + SaveEvent(PositionForm source, FieldPosition position) { + super(source, position); + } + } + + public static class DeleteEvent extends PositionFormEvent { + DeleteEvent(PositionForm source, FieldPosition position) { + super(source, position); + } + } + + public static class CloseEvent extends PositionFormEvent { + CloseEvent(PositionForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java new file mode 100644 index 0000000..ad71bf0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.POSITION_ROUTE, layout = MainLayout.class) +@PageTitle("Position | Tysc | Kontor") +public class PositionView extends VerticalLayout { + + Grid grid = new Grid<>(FieldPosition.class); + TextField filterText = new TextField(); + PositionForm form; + SportService service; + + public PositionView(SportService service) { + this.service = service; + addClassName("position-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("position-grid"); + grid.setSizeFull(); + grid.setColumns("shortName", "name", "sport.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPosition(event.getValue())); + } + + public PositionForm getForm() { + return form; + } + + private void configureForm() { + form = new PositionForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePosition); + form.addDeleteListener(this::deletePosition); + form.addCloseListener(e -> closeEditor()); + } + + private void savePosition(PositionForm.SaveEvent event) { + service.savePosition(event.getPosition()); + updateList(); + closeEditor(); + } + + private void deletePosition(PositionForm.DeleteEvent event) { + service.deletePosition(event.getPosition()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPositionButton = new Button("Add position"); + addPositionButton.addClickListener(click -> addPosition()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPositionButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPosition(FieldPosition fieldPosition) { + if (fieldPosition == null) { + closeEditor(); + } else { + form.setPosition(fieldPosition); + form.setRoosters(fieldPosition.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPosition(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPosition() { + grid.asSingleSelect().clear(); + editPosition(new FieldPosition()); + } + + public void updateList() { + grid.setItems(service.findAllPositions(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java new file mode 100644 index 0000000..5f9d259 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Team; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RoosterForm extends FormLayout { + IntegerField year = new IntegerField("Year"); + ComboBox team = new ComboBox<>("Team"); + ComboBox player = new ComboBox<>("Player"); + ComboBox position = new ComboBox<>("Position"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Rooster.class); + + public RoosterForm(List teams, List players, List positions) { + addClassName("rooster-form"); + binder.bindInstanceFields(this); + + team.setItems(teams); + team.setItemLabelGenerator(Team::getName); + player.setItems(players); + player.setItemLabelGenerator(Player::getFullName); + position.setItems(positions); + position.setItemLabelGenerator(FieldPosition::getName); + add(year, team, player, position, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setRooster(Rooster rooster) { + binder.setBean(rooster); + } + + public abstract static class RoosterFormEvent extends ComponentEvent { + private Rooster rooster; + + protected RoosterFormEvent(RoosterForm source, Rooster rooster) { + super(source, false); + this.rooster = rooster; + } + + public Rooster getRooster() { + return rooster; + } + } + + public static class SaveEvent extends RoosterFormEvent { + SaveEvent(RoosterForm source, Rooster rooster) { + super(source, rooster); + } + } + + public static class DeleteEvent extends RoosterFormEvent { + DeleteEvent(RoosterForm source, Rooster rooster) { + super(source, rooster); + } + } + + public static class CloseEvent extends RoosterFormEvent { + CloseEvent(RoosterForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java new file mode 100644 index 0000000..92da6eb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java @@ -0,0 +1,121 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.ROOSTER_ROUTE, layout = MainLayout.class) +@PageTitle("Rooster | Tysc | Kontor") +public class RoosterView extends VerticalLayout { + + Grid grid = new Grid<>(Rooster.class); + RoosterForm form; + SportService service; + + public RoosterView(SportService service) { + this.service = service; + addClassName("rooster-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("rooster-grid"); + grid.setSizeFull(); + grid.setColumns("year", "team.name", "player.fullName", "position.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editRooster(event.getValue())); + } + + public RoosterForm getForm() { + return form; + } + + private void configureForm() { + form = new RoosterForm(service.findAllTeams(null), service.findAllPlayers(null), service.findAllPositions(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveRooster); + form.addDeleteListener(this::deleteRooster); + form.addCloseListener(e -> closeEditor()); + } + + private void saveRooster(RoosterForm.SaveEvent event) { + service.saveRooster(event.getRooster()); + updateList(); + closeEditor(); + } + + private void deleteRooster(RoosterForm.DeleteEvent event) { + service.deleteRooster(event.getRooster()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addRoosterButton = new Button("Add Rooster"); + addRoosterButton.addClickListener(click -> addRooster()); + + HorizontalLayout toolbar = new HorizontalLayout(addRoosterButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editRooster(Rooster rooster) { + if (rooster == null) { + closeEditor(); + } else { + form.setRooster(rooster); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setRooster(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addRooster() { + grid.asSingleSelect().clear(); + editRooster(new Rooster()); + } + + public void updateList() { + grid.setItems(service.findAllRoosters()); + } +} \ No newline at end of file diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java new file mode 100644 index 0000000..30582e1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java @@ -0,0 +1,103 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Sport; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SportForm extends FormLayout { + + TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Sport.class); + + public SportForm() { + addClassName("sport-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setSport(Sport sport) { + binder.setBean(sport); + } + + public abstract static class SportFormEvent extends ComponentEvent { + private Sport sport; + + protected SportFormEvent(SportForm source, Sport sport) { + super(source, false); + this.sport = sport; + } + + public Sport getSport() { + return sport; + } + } + + public static class SaveEvent extends SportFormEvent { + SaveEvent(SportForm source, Sport sport) { + super(source, sport); + } + } + + public static class DeleteEvent extends SportFormEvent { + DeleteEvent(SportForm source, Sport sport) { + super(source, sport); + } + } + + public static class CloseEvent extends SportFormEvent { + CloseEvent(SportForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java new file mode 100644 index 0000000..c6008c3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.SPORT_ROUTE, layout = MainLayout.class) +@PageTitle("Sport | Tysc | Kontor") +public class SportView extends VerticalLayout { + + Grid grid = new Grid<>(Sport.class); + TextField filterText = new TextField(); + SportForm form; + SportService service; + + public SportView(SportService service) { + this.service = service; + addClassName("sport-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("sport-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editSport(event.getValue())); + } + + public SportForm getForm() { + return form; + } + + private void configureForm() { + form = new SportForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveSport); + form.addDeleteListener(this::deleteSport); + form.addCloseListener(e -> closeEditor()); + } + + private void saveSport(SportForm.SaveEvent event) { + service.saveSport(event.getSport()); + updateList(); + closeEditor(); + } + + private void deleteSport(SportForm.DeleteEvent event) { + service.deleteSport(event.getSport()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addSportButton = new Button("Add sport"); + addSportButton.addClickListener(click -> addSport()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addSportButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editSport(Sport sport) { + if (sport == null) { + closeEditor(); + } else { + form.setSport(sport); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setSport(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addSport() { + grid.asSingleSelect().clear(); + editSport(new Sport()); + } + + public void updateList() { + grid.setItems(service.findAllSports(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java new file mode 100644 index 0000000..63cb56b --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Team; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TeamForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Team.class); + + public TeamForm() { + addClassName("team-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("player.fullName", "position.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setTeam(Team team) { + binder.setBean(team); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class TeamFormEvent extends ComponentEvent { + private Team team; + + protected TeamFormEvent(TeamForm source, Team team) { + super(source, false); + this.team = team; + } + + public Team getTeam() { + return team; + } + } + + public static class SaveEvent extends TeamFormEvent { + SaveEvent(TeamForm source, Team team) { + super(source, team); + } + } + + public static class DeleteEvent extends TeamFormEvent { + DeleteEvent(TeamForm source, Team team) { + super(source, team); + } + } + + public static class CloseEvent extends TeamFormEvent { + CloseEvent(TeamForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java new file mode 100644 index 0000000..1e48f98 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Team; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.TEAM_ROUTE, layout = MainLayout.class) +@PageTitle("Team | Tysc | Kontor") +public class TeamView extends VerticalLayout { + + Grid grid = new Grid<>(Team.class); + TextField filterText = new TextField(); + TeamForm form; + SportService service; + + public TeamView(SportService service) { + this.service = service; + addClassName("team-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("team-grid"); + grid.setSizeFull(); + grid.setColumns("name", "shortName", "sport.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editTeam(event.getValue())); + } + + public TeamForm getForm() { + return form; + } + + private void configureForm() { + form = new TeamForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveTeam); + form.addDeleteListener(this::deleteTeam); + form.addCloseListener(e -> closeEditor()); + } + + private void saveTeam(TeamForm.SaveEvent event) { + service.saveTeam(event.getTeam()); + updateList(); + closeEditor(); + } + + private void deleteTeam(TeamForm.DeleteEvent event) { + service.deleteTeam(event.getTeam()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addTeamButton = new Button("Add team"); + addTeamButton.addClickListener(click -> addTeam()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addTeamButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editTeam(Team team) { + if (team == null) { + closeEditor(); + } else { + form.setTeam(team); + form.setRoosters(team.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setTeam(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addTeam() { + grid.asSingleSelect().clear(); + editTeam(new Team()); + } + + public void updateList() { + grid.setItems(service.findAllTeams(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java new file mode 100644 index 0000000..66fb09b --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a custom layout for the comic view in the application. + * This layout extends the AppLayout class. + */ +@Slf4j +public class TyscLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public TyscLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(TyscConstants.TYSC); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(TyscConstants.getSportLink(), TyscConstants.getTeamLink(), TyscConstants.getPositionLink(), + TyscConstants.getPlayerLink(), TyscConstants.getRoosterLink(), TyscConstants.getVendorLink(), + TyscConstants.getCardSetLink(), TyscConstants.getCardLink()); + return navigation; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java new file mode 100644 index 0000000..254b1ff --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.data.CardSet; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VendorForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid cardSets = new Grid<>(CardSet.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Vendor.class); + + public VendorForm() { + addClassName("vendor-form"); + binder.bindInstanceFields(this); + + cardSets.setColumns("name", "parallelSet"); + cardSets.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, cardSets, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setVendor(Vendor vendor) { + binder.setBean(vendor); + } + + public void setCardSets(List sets) { + log.info("Setting card sets: {}", sets); + this.cardSets.setItems(sets); + } + + public abstract static class VendorFormEvent extends ComponentEvent { + private Vendor vendor; + + protected VendorFormEvent(VendorForm source, Vendor vendor) { + super(source, false); + this.vendor = vendor; + } + + public Vendor getVendor() { + return vendor; + } + } + + public static class SaveEvent extends VendorFormEvent { + SaveEvent(VendorForm source, Vendor vendor) { + super(source, vendor); + } + } + + public static class DeleteEvent extends VendorFormEvent { + DeleteEvent(VendorForm source, Vendor vendor) { + super(source, vendor); + } + } + + public static class CloseEvent extends VendorFormEvent { + CloseEvent(VendorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java new file mode 100644 index 0000000..08b5d32 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.VENDOR_ROUTE, layout = MainLayout.class) +@PageTitle("Vendor | Tysc | Kontor") +public class VendorView extends VerticalLayout { + + Grid grid = new Grid<>(Vendor.class); + TextField filterText = new TextField(); + VendorForm form; + CardService service; + + public VendorView(CardService service) { + this.service = service; + addClassName("vendor-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("vendor-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editVendor(event.getValue())); + } + + public VendorForm getForm() { + return form; + } + + private void configureForm() { + form = new VendorForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveVendor); + form.addDeleteListener(this::deleteVendor); + form.addCloseListener(e -> closeEditor()); + } + + private void saveVendor(VendorForm.SaveEvent event) { + service.saveVendor(event.getVendor()); + updateList(); + closeEditor(); + } + + private void deleteVendor(VendorForm.DeleteEvent event) { + service.deleteVendor(event.getVendor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addVendorButton = new Button("Add vendor"); + addVendorButton.addClickListener(click -> addVendor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addVendorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editVendor(Vendor vendor) { + if (vendor == null) { + closeEditor(); + } else { + form.setVendor(vendor); + form.setCardSets(vendor.getCardSets()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setVendor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addVendor() { + grid.asSingleSelect().clear(); + editVendor(new Vendor()); + } + + public void updateList() { + grid.setItems(service.findAllVendors(filterText.getValue())); + } +} diff --git a/springboot/src/main/resources/META-INF/resources/icons/icon.png b/springboot/src/main/resources/META-INF/resources/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..77bb2c9ad73c16d2878c34fef9443c91466178df GIT binary patch literal 45967 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 literal 0 HcmV?d00001 diff --git a/springboot/src/main/resources/META-INF/resources/images/offline.png b/springboot/src/main/resources/META-INF/resources/images/offline.png new file mode 100644 index 0000000000000000000000000000000000000000..723e24ce7525be86a728d07a8e98f6e1753b745b GIT binary patch literal 9507 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 literal 0 HcmV?d00001 diff --git a/springboot/src/main/resources/META-INF/resources/offline.html b/springboot/src/main/resources/META-INF/resources/offline.html new file mode 100644 index 0000000..791d1e8 --- /dev/null +++ b/springboot/src/main/resources/META-INF/resources/offline.html @@ -0,0 +1,38 @@ + + + + + + + Offline | Vaadin CRM + + + + +
+ VaadinCRM is offline +

Oh deer, you're offline

+

Your internet connection is offline. Get back online to continue using Vaadin CRM.

+
+ + + diff --git a/springboot/src/main/resources/application.yml b/springboot/src/main/resources/application.yml new file mode 100644 index 0000000..fd8dfff --- /dev/null +++ b/springboot/src/main/resources/application.yml @@ -0,0 +1,67 @@ +server: + port: 8085 +app: + name: 'Kontor' + shortName: 'Kontor' + description: 'Kontor is a Spring Boot application' +spring: + profiles: + active: local,dev,test,prod + devtools: + add-properties: false + datasource: + driverClassName: org.mariadb.jdbc.Driver + url: jdbc:mariadb://localhost:3306/kontor + username: 'kontor' + password: 'kontor' + #driverClassName: org.hsqldb.jdbc.JDBCDriver + #url: jdbc:hsqldb:file:kontorHSQLDB + #username: 'sa' + #password: 'sa' + #jpa + #database-platform: org.hibernate.community.dialect.SQLiteDialect + #datasource + #driverClassName: org.sqlite.JDBC + #url: "jdbc:sqlite:file:./kontorDb?cache=shared" + #username=sa + #password=sa + jpa: + defer-datasource-initialization: true + #hibernate.ddl-auto=create-drop + hibernate: + ddl-auto: update + #ddl-auto: create-drop + show-sql: false + sql: + init: + mode: never + mustache: + check-template-location: false +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always +logging: + level: + org: + atmosphere: INFO + hibernate: INFO + springframework: + web: INFO + guru: + springframework: + controllers: DEBUG +jwt: + auth: + secret: 'J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=' +mail: + protocol: 'imap' + host: 'corky.svpdata.eu' + port: 143 + userName: 'thomas.peetz@thpeetz.de' + password: 'fS9f4JYDIO7A' + starttls: true diff --git a/springboot/src/main/resources/banner.txt b/springboot/src/main/resources/banner.txt new file mode 100644 index 0000000..b21c682 --- /dev/null +++ b/springboot/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +,--. ,--. ,--. +| .' / ,---. ,--,--, ,-' '-. ,---. ,--.--. +| . ' | .-. || \'-. .-'| .-. || .--' +| |\ \' '-' '| || | | | ' '-' '| | +`--' '--' `---' `--''--' `--' `---' `--' + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} \ No newline at end of file diff --git a/springboot/src/main/resources/logback-spring.xml b/springboot/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b085f8c --- /dev/null +++ b/springboot/src/main/resources/logback-spring.xml @@ -0,0 +1,42 @@ + + + + + + + + + %white(%d{ISO8601}) %highlight(%-5level) [%green(%t)] %yellow(%C{1}) : %msg%n%throwable + + + + + + ${LOGS}/spring-boot-logger.log + + %d %p %C{1} [%t] %m%n + + + + + ${LOGS}/archived/spring-boot-logger-%d{yyyy-MM-dd}.%i.log + + + 10MB + + + + + + + + + + + + diff --git a/springboot/src/test/java/de/thpeetz/kontor/ApplicationTests.java b/springboot/src/test/java/de/thpeetz/kontor/ApplicationTests.java new file mode 100644 index 0000000..abf5c95 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/ApplicationTests.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java new file mode 100644 index 0000000..bb9d04d --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.bookshelf; + +public class TestConstants { + + public static final Integer ARTICLEAUTHOR_COUNT = 0; + public static final Integer ARTICLE_COUNT = 0; + public static final Integer AUTHOR_COUNT = 1; + public static final Integer BOOKAUTHOR_COUNT = 0; + public static final Integer BOOK_COUNT = 0; + public static final Integer PUBLISHER_COUNT = 0; + public static final String ARTICLE_TITLE = "Title"; + public static final String AUTHOR_FIRSTNAME = "Firstname"; + public static final String AUTHOR_LASTNAME = "Lastname"; + public static final String PUBLISHER_NIKOL = "Nikol Verlags GmbH"; + public static final String PUBLISHER_NAME = "Publisher"; + public static final String PUBLISHER_SEARCH = "kol"; + public static final String BOOK_TITLE = "Book Title"; + public static final String BOOK_ISBN = "978-3-123-467-890"; +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java new file mode 100644 index 0000000..b54b976 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java @@ -0,0 +1,74 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class ArticleAuthorRepositoryTest { + + @Autowired + ArticleAuthorRepository articleAuthorRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + ArticleRepository articleRepository; + + @Test + @Order(1) + void saveArticleAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + ArticleAuthor articleAuthor = new ArticleAuthor(); + articleAuthor.setArticle(article); + articleAuthor.setAuthor(author); + ArticleAuthor savedArticleAuthor = articleAuthorRepository.save(articleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT+1, articleAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void testFindByArticle() { + Article article = articleRepository.findByTitle(TestConstants.ARTICLE_TITLE).get(0); + assertEquals(1, articleAuthorRepository.findByArticle(article).size()); + } + + @Test + @Order(3) + void testFindByAuthor() { + Author author = authorRepository.findByFirstNameAndLastName(TestConstants.AUTHOR_FIRSTNAME, TestConstants.AUTHOR_LASTNAME); + assertEquals(1, articleAuthorRepository.findByAuthor(author).size()); + } + + @Test + @Order(4) + void deleteArticleAuthor() { + assertEquals(1, articleAuthorRepository.findAll().size()); + ArticleAuthor articleAuthor = articleAuthorRepository.findAll().get(0); + Author author = articleAuthor.getAuthor(); + author.getArticleAuthors().remove(articleAuthor); + author = authorRepository.save(author); + Article article = articleAuthor.getArticle(); + article.getAuthors().remove(articleAuthor); + article = articleRepository.save(article); + articleAuthorRepository.delete(articleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + authorRepository.delete(author); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + articleRepository.delete(article); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java new file mode 100644 index 0000000..bd14fd3 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java @@ -0,0 +1,77 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +@Disabled +class ArticleAuthorTest { + + @Autowired + ArticleRepository articleRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + ArticleAuthorRepository articleAuthorRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void exceptionThrownWhenSavingEmptyValues() { + ArticleAuthor articleAuthor = new ArticleAuthor(); + assertThrows(TransactionSystemException.class, () -> { + articleAuthorRepository.save(articleAuthor); + }); + } + + @Test + @Order(3) + void exceptionThrownWhenSavingIdenticalEntry() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + ArticleAuthor articleAuthor = new ArticleAuthor(); + articleAuthor.setArticle(article); + articleAuthor.setAuthor(author); + ArticleAuthor savedArticleAuthor = articleAuthorRepository.save(articleAuthor); + ArticleAuthor articleAuthor1 = new ArticleAuthor(); + articleAuthor1.setArticle(article); + articleAuthor1.setAuthor(author); + assertThrows(DataIntegrityViolationException.class, () -> { + articleAuthorRepository.save(articleAuthor1); + }); + articleAuthorRepository.delete(savedArticleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + savedAuthor.getArticleAuthors().remove(savedArticleAuthor); + authorRepository.save(savedAuthor); + authorRepository.delete(savedAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + savedArticle.getAuthors().remove(savedArticleAuthor); + articleRepository.save(savedArticle); + articleRepository.delete(savedArticle); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java new file mode 100644 index 0000000..7d74119 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ArticleRepositoryTest { + + @Autowired + ArticleRepository articleRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } + + @Test + @Order(2) + void saveArticle() { + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + articleRepository.save(article); + assertEquals(TestConstants.ARTICLE_COUNT+1, articleRepository.findAll().size()); + } + + @Test + @Order(3) + void search() { + List
articles = articleRepository.search(TestConstants.ARTICLE_TITLE.substring(2,5)); + assertEquals(1, articles.size()); + assertEquals(0, articleRepository.search(TestConstants.BOOK_TITLE).size()); + } + + @Test + @Order(4) + void findByTitle() { + List
articles = articleRepository.search(TestConstants.ARTICLE_TITLE); + assertEquals(1, articles.size()); + } + + @Test + @Order(5) + void deleteArticle() { + List
articles = articleRepository.findAll(); + assertEquals(1, articles.size()); + assertEquals(0, articles.get(0).getAuthors().size()); + articleRepository.delete(articles.get(0)); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java new file mode 100644 index 0000000..2179dfe --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class ArticleTest { + + @Autowired + ArticleRepository articleRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOK_COUNT, articleRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyFields() { + Article article = new Article(); + assertThrows(TransactionSystemException.class, () -> { + articleRepository.save(article); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalTitle() { + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + Article article1 = new Article(); + article1.setTitle(article.getTitle()); + assertThrows(DataIntegrityViolationException.class, ()-> { + articleRepository.save(article1); + }); + articleRepository.delete(savedArticle); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java new file mode 100644 index 0000000..f984d11 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java @@ -0,0 +1,69 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +@Disabled +class AuthorRepositoryTest { + + @Autowired + AuthorRepository authorRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } + + @Test + @Order(2) + void saveAutor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + } + + @Test + @Order(3) + void findAuthor() { + Author existingAuthor = authorRepository.findAll().get(0); + assertNotNull(existingAuthor); + Author found = authorRepository.findByFirstNameAndLastName(existingAuthor.getFirstName(), existingAuthor.getLastName()); + assertEquals(existingAuthor, found); + } + + @Test + @Order(4) + void searchAuthor() { + List authors = authorRepository.search("glas"); + assertEquals(1, authors.size()); + assertEquals("Douglas", authors.get(0).getFirstName()); + List authors2 = authorRepository.search("dams"); + assertEquals(1, authors2.size()); + assertEquals("Adams", authors2.get(0).getLastName()); + } + + @Test + @Order(5) + void deleteAuthor() { + Author existingAuthor = authorRepository.findByFirstNameAndLastName(TestConstants.AUTHOR_FIRSTNAME, TestConstants.AUTHOR_LASTNAME); + assertNotNull(existingAuthor); + authorRepository.delete(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java new file mode 100644 index 0000000..149c263 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class AuthorTest { + + @Autowired + private AuthorRepository authorRepository; + + @Test + void throwExceptionWhenPlayerSavedWithEmptyName() { + Author author = new Author(); + assertThrows(TransactionSystemException.class, () -> { + authorRepository.save(author); + }); + } + + @Test + void throwExceptionWhenPlayerSavedWithExistingName() { + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + Author existingAuthor = new Author(); + existingAuthor.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + existingAuthor.setLastName(TestConstants.AUTHOR_LASTNAME); + existingAuthor = authorRepository.save(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + Author author = new Author(); + author.setFirstName(existingAuthor.getFirstName()); + author.setLastName(existingAuthor.getLastName()); + assertThrows(DataIntegrityViolationException.class, () -> { + authorRepository.save(author); + }); + assertNotNull(existingAuthor); + authorRepository.delete(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java new file mode 100644 index 0000000..4df5fd9 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java @@ -0,0 +1,94 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class BookAuthorRepositoryTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + BookAuthorRepository bookAuthorRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + @Order(1) + void saveBookAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT+1, bookshelfPublisherRepository.findAll().size()); + Book book = new Book(); + book.setTitle(TestConstants.ARTICLE_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(savedPublisher); + Book savedBook = bookRepository.save(book); + assertEquals(TestConstants.BOOK_COUNT+1, bookRepository.findAll().size()); + BookAuthor bookAuthor = new BookAuthor(); + bookAuthor.setBook(savedBook); + bookAuthor.setAuthor(savedAuthor); + BookAuthor savedBookAuthor = bookAuthorRepository.save(bookAuthor); + assertEquals(TestConstants.BOOKAUTHOR_COUNT+1, bookAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void testFindByBook() { + List bookAuthors = bookAuthorRepository.findAll(); + assertEquals(1, bookAuthors.size()); + Book book = bookAuthors.get(0).getBook(); + assertEquals(1, bookAuthorRepository.findByBook(book).size()); + } + + @Test + @Order(3) + void testFindByAuthor() { + List bookAuthors = bookAuthorRepository.findAll(); + assertEquals(1, bookAuthors.size()); + Author author = bookAuthors.get(0).getAuthor(); + assertEquals(1, bookAuthorRepository.findByAuthor(author).size()); + } + + @Test + @Order(4) + void deleteBookAuthor() { + BookAuthor bookAuthor = bookAuthorRepository.findAll().get(0); + Author author = bookAuthor.getAuthor(); + author.getBookAuthors().remove(bookAuthor); + author = authorRepository.save(author); + Book book = bookAuthor.getBook(); + book.getAuthors().remove(bookAuthor); + BookshelfPublisher publisher = book.getPublisher(); + publisher.getBooks().remove(book); + bookshelfPublisherRepository.save(publisher); + book = bookRepository.save(book); + bookshelfPublisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + bookAuthorRepository.delete(bookAuthor); + assertEquals(TestConstants.BOOKAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + authorRepository.delete(author); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java new file mode 100644 index 0000000..dc79341 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java @@ -0,0 +1,82 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@Disabled +class BookAuthorTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + BookAuthorRepository bookAuthorRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOKAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyValues() { + BookAuthor bookAuthor = new BookAuthor(); + assertThrows(TransactionSystemException.class, () -> { + bookAuthorRepository.save(bookAuthor); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalEntry() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(publisher); + Book savedBook = bookRepository.save(book); + BookAuthor bookAuthor = new BookAuthor(); + bookAuthor.setBook(book); + bookAuthor.setAuthor(author); + BookAuthor savedBookAuthor = bookAuthorRepository.save(bookAuthor); + BookAuthor bookAuthor1 = new BookAuthor(); + bookAuthor1.setBook(book); + bookAuthor1.setAuthor(author); + assertThrows(DataIntegrityViolationException.class, () -> { + bookAuthorRepository.save(bookAuthor1); + }); + savedAuthor.getBookAuthors().remove(savedBookAuthor); + authorRepository.save(savedAuthor); + savedBook.getAuthors().remove(savedBookAuthor); + bookRepository.save(savedBook); + savedPublisher.getBooks().remove(savedBook); + bookshelfPublisherRepository.save(savedPublisher); + authorRepository.delete(savedAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + bookRepository.delete(savedBook); + assertEquals(TestConstants.ARTICLE_COUNT, bookRepository.findAll().size()); + bookshelfPublisherRepository.delete(savedPublisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + bookAuthorRepository.delete(savedBookAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java new file mode 100644 index 0000000..969d465 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java @@ -0,0 +1,87 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class BookRepositoryTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + @Order(1) + void checkInitialLoad(){ + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } + + @Test + @Order(2) + void saveBook() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(publisher); + bookRepository.save(book); + assertEquals(TestConstants.BOOK_COUNT+1, bookRepository.findAll().size()); + } + + @Test + @Order(3) + void search() { + List books = bookRepository.search(TestConstants.BOOK_TITLE.substring(2,5)); + assertEquals(1, books.size()); + assertEquals(1, bookRepository.search(TestConstants.BOOK_ISBN.substring(3,7)).size()); + assertEquals(0, bookRepository.search("Article").size()); + } + + @Test + @Order(4) + void findByTitle() { + List books = bookRepository.findByTitle(TestConstants.BOOK_TITLE); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_ISBN, books.get(0).getIsbn()); + } + + @Test + @Order(5) + void findByTitleIgnoreCase() { + List books = bookRepository.findByTitleIgnoreCase(TestConstants.BOOK_TITLE.toLowerCase()); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_ISBN, books.get(0).getIsbn()); + } + + @Test + @Order(6) + void findByIsbn() { + List books = bookRepository.findByIsbn(TestConstants.BOOK_ISBN); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_TITLE, books.get(0).getTitle()); + } + + @Test + @Order(7) + void deleteBook() { + List books = bookRepository.findByIsbn(TestConstants.BOOK_ISBN); + Book book = books.get(0); + BookshelfPublisher publisher = book.getPublisher(); + bookshelfPublisherRepository.delete(publisher); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java new file mode 100644 index 0000000..0ac3324 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java @@ -0,0 +1,56 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import de.thpeetz.kontor.comics.data.Publisher; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class BookTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyFields() { + Book book = new Book(); + assertThrows(TransactionSystemException.class, () -> { + bookRepository.save(book); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalISBN() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(savedPublisher); + Book savedBook = bookRepository.save(book); + Book book1 = new Book(); + book1.setTitle(book.getTitle()); + book1.setIsbn(book.getIsbn()); + assertThrows(TransactionSystemException.class, ()-> { + bookRepository.save(book1); + }); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + bookshelfPublisherRepository.delete(savedPublisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java new file mode 100644 index 0000000..98b60fa --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java @@ -0,0 +1,72 @@ +package de.thpeetz.kontor.bookshelf.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class BookshelfPublisherRepositoryTest { + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + @Order(2) + void savePublisher() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + } + + @Test + @Order(3) + void findPublisherByName() { + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + log.info("Liste der Publisher: {}", publisherRepository.findAll()); + BookshelfPublisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + assertEquals(TestConstants.PUBLISHER_NAME, publisher.getName()); + BookshelfPublisher notFound = publisherRepository.findByName(TestConstants.PUBLISHER_SEARCH); + assertNull(notFound); + List publishers = publisherRepository + .findByNameIgnoreCase(TestConstants.PUBLISHER_NAME.toLowerCase()); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(4) + void searchPublisher() { + List publishers = publisherRepository.search(TestConstants.PUBLISHER_NAME.substring(2, 6)); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(5) + void deletePublisher() { + BookshelfPublisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + publisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java new file mode 100644 index 0000000..7a73ae1 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.bookshelf.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.bookshelf.TestConstants; + +@SpringBootTest +class BookshelfPublisherTest { + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + void throwExceptionWhenPublisherSavedWithEmptyName() { + BookshelfPublisher publisher = new BookshelfPublisher(); + assertThrows(TransactionSystemException.class, () -> { + publisherRepository.save(publisher); + }); + } + + @Test + void savePublisherWithIdenticalName() { + BookshelfPublisher publisher1 = new BookshelfPublisher(); + publisher1.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher1); + BookshelfPublisher publisher2 = new BookshelfPublisher(); + publisher2.setName(TestConstants.PUBLISHER_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + publisherRepository.save(publisher2); + }); + publisherRepository.delete(publisher1); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java new file mode 100644 index 0000000..b78a899 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java @@ -0,0 +1,102 @@ +package de.thpeetz.kontor.bookshelf.services; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class BookshelfServiceTest { + + @Autowired + private BookshelfService bookshelfService; + + @Test + @Order(1) + void testFindAllPublishers() { + List publishers = bookshelfService.findAllPublishers(null); + assertEquals(TestConstants.PUBLISHER_COUNT, publishers.size()); + } + + @Test + @Order(2) + void testSavePublisher() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + bookshelfService.savePublisher(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, bookshelfService.findAllPublishers(null).size()); + } + + @Test + @Order(3) + void testFindPublisherByName() { + assertEquals(TestConstants.PUBLISHER_COUNT + 1, bookshelfService.findAllPublishers(null).size()); + BookshelfPublisher publisher = bookshelfService.findPublisherByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + } + + @Test + @Order(4) + void testDeletePublisher() { + List publishers = bookshelfService.findAllPublishers(TestConstants.PUBLISHER_NAME); + assertEquals(1, publishers.size()); + bookshelfService.deletePublisher(publishers.get(0)); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfService.findAllPublishers(null).size()); + } + + @Test + @Order(5) + void testFindAllAuthors() { + assertEquals(TestConstants.AUTHOR_COUNT, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(6) + void testSaveAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + bookshelfService.saveAuthor(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(7) + void testDeleteAuthor() { + List authors = bookshelfService.findAllAuthors(TestConstants.AUTHOR_FIRSTNAME); + assertEquals(1, authors.size()); + bookshelfService.deleteAuthor(authors.get(0)); + assertEquals(TestConstants.AUTHOR_COUNT, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(8) + void findAllBooks() { + assertEquals(TestConstants.BOOK_COUNT, bookshelfService.findAllBooks(null).size()); + } + + @Test + @Order(9) + void saveBook() { + assertEquals(TestConstants.BOOK_COUNT, bookshelfService.findAllBooks(null).size()); + } + + @Test + @Order(10) + void deleteBook() { + List books = bookshelfService.findAllBooks(null); + assertEquals(0, books.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java new file mode 100644 index 0000000..b1e50ce --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java @@ -0,0 +1,14 @@ +package de.thpeetz.kontor.comics; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ComicConstantsTest { + + @Test + void getArtistConstants() { + assertEquals("Artist", ComicConstants.ARTIST); + assertEquals("comics/artist", ComicConstants.ARTIST_ROUTE); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/TestConstants.java b/springboot/src/test/java/de/thpeetz/kontor/comics/TestConstants.java new file mode 100644 index 0000000..b77f55d --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/TestConstants.java @@ -0,0 +1,67 @@ +package de.thpeetz.kontor.comics; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.services.ComicService; + +public class TestConstants { + public static final String ARTIST_NAME = "Lastname, Firstname"; + public static final String COMIC_TITLE = "TestComic"; + public static final String WORKTYPE_INKER = "Inker"; + public static final String ISSUE_ISSUENUMBER = "Issuenumber"; + public static final String PUBLISHER_NAME = "Publisher"; + public static final String SOJOURN_TPB_COMIC_TITLE = "Sojourn"; + public static final String SOJOURN_TPB_NAME = "The Dragons Tale"; + public static final String STORYARC_NAME = "StoryArc"; + public static final String TRADEPAPERBACK_NAME = "TradePaperback"; + public static final String VOLUME_NAME = "Volume"; + public static final String WORKTYPE_NAME = "Worktype"; + public static final int ARTIST_COUNT = 5; + public static final int BATTLE_POPE_ISSUE_COUNT = 12; + public static final int COMIC_COUNT = 169; + public static final int COMICWORK_COUNT = 18; + public static final int ISSUE_COUNT = 750; + public static final int MARVEL_COMIC_COUNT = 50; + public static final int PUBLISHER_COUNT = 18; + public static final int SOJOURN_TPB_COUNT = 4; + public static final int SOJOURN_TPB_START = 7; + public static final int SOJOURN_TPB_END = 12; + public static final int STORYARC_COUNT = 3; + public static final int TRADEPAPERBACK_COUNT = 40; + public static final int VOLUME_COUNT = 0; + public static final int WORKTYPE_COUNT = 3; + + public static Comic getComicWithStoryArcs(ComicService service) { + return service.findComicByTitle("Emma Frost"); + } + + public static Comic getComicWithTradePaperbacks(ComicService service) { + return service.findComicByTitle(SOJOURN_TPB_COMIC_TITLE); + } + + public static Comic getComicWithIssues(ComicService service) { + return service.findComicByTitle("Battle Pope"); + } + + public static Comic getComicWithoutReferences(ComicService service) { + return service.findComicByTitle("Gen13"); + } + + public static Artist getArtistWithoutReferences(ComicService service) { + return service.findArtistByName("Marz, Ron"); + } + + public static Worktype getWorktypeWithoutReferences(ComicService service) { + return service.findWorktypeByName(WORKTYPE_INKER); + } + + public static Publisher getPublisherForComicTests(ComicService service) { + return service.findPublisherByName("Marvel"); + } + + public static Publisher getAnotherPublisherForComicTests(ComicService service) { + return service.findPublisherByName("DC"); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java new file mode 100644 index 0000000..96d53f8 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java @@ -0,0 +1,73 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ArtistRepositoryTest { + + private Artist stanLee; + private static final String ARTISTNAME = "Lee, Stan"; + + @Autowired + ArtistRepository artistRepository; + + @BeforeEach + void setupData() { + stanLee = new Artist(); + stanLee.setName(ARTISTNAME); + } + + @Test + @Order(1) + void checkInitialLoad() { + int count = artistRepository.findAll().size(); + assertEquals(5, count); + } + + @Test + @Order(2) + void saveArtist() { + int count = artistRepository.findAll().size(); + artistRepository.save(stanLee); + assertEquals(count+1, artistRepository.findAll().size()); + } + + @Test + @Order(3) + void findArtist() { + List artists = artistRepository.findByNameIgnoreCase("lEE, sTAN"); + assertTrue(artists.size() > 0); + assertEquals(artists.get(0).getName(), stanLee.getName()); + } + + @Test + @Order(4) + void searchArtist() { + List artists = artistRepository.search("Lee"); + assertEquals(1, artists.size()); + assertEquals(ARTISTNAME, artists.get(0).getName()); + List artists2 = artistRepository.search("Stan"); + assertEquals(1, artists2.size()); + assertEquals(ARTISTNAME, artists2.get(0).getName()); + } + + @Test + @Order(5) + void deleteArtist() { + int count = artistRepository.findAll().size(); + Artist artist = artistRepository.findByName(ARTISTNAME); + artistRepository.delete(artist); + assertEquals(count-1, artistRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java new file mode 100644 index 0000000..855795e --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class ArtistTest { + + @Autowired + private ArtistRepository artistRepository; + + @Test + void checkInitialDataLoad() { + List artists = artistRepository.findAll(); + assertEquals(TestConstants.ARTIST_COUNT, artists.size()); + } + + @Test + void throwExceptionWhenArtistSavedWithEmptyName() { + Artist artist1 = new Artist(); + assertThrows(TransactionSystemException.class, () -> { + artistRepository.save(artist1); + }); + } + + @Test + void saveArtistWithIdenticalName() { + String artistName = "Lastname, Firstname"; + Artist artist1 = new Artist(); + artist1.setName(artistName); + artistRepository.save(artist1); + Artist artist2 = new Artist(); + artist2.setName(artistName); + assertThrows(DataIntegrityViolationException.class, () -> { + artistRepository.save(artist2); + }); + artistRepository.delete(artist1); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java new file mode 100644 index 0000000..70c0d54 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ComicRepositoryTest { + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private PublisherRepository publisherRepository; + + @Test + void testFindByTitle() { + List comics = comicRepository.findByTitle("Emma Frost"); + assertEquals(1, comics.size()); + assertEquals("Emma Frost", comics.get(0).getTitle()); + Publisher publisher = comics.get(0).getPublisher(); + assertEquals("Marvel", publisher.getName()); + } + + @Test + void testFindByTitleIgnoreCase() { + List comics = comicRepository.findByTitleIgnoreCase("x-men"); + assertEquals(1, comics.size()); + assertEquals("X-Men", comics.get(0).getTitle()); + } + @Test + void testFindByTitleAndPublisher() { + Publisher publisher = publisherRepository.findByName("Marvel"); + assertNotNull(publisher); + Comic found = comicRepository.findByTitleAndPublisher("Emma Frost", publisher); + assertNotNull(found); + assertEquals("Emma Frost", found.getTitle()); + assertEquals("Marvel", found.getPublisher().getName()); + + } + + @Test + void testSearch() { + List comics = comicRepository.search("X-men"); + assertEquals(11, comics.size()); + assertTrue(comics.stream().map(comic -> comic.getTitle()).collect(Collectors.toList()).contains("Astonishing X-Men")); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java new file mode 100644 index 0000000..5560346 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java @@ -0,0 +1,77 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicTest { + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void checkInitialDataLoad() { + List comics = comicRepository.findAll(); + assertEquals(TestConstants.COMIC_COUNT, comics.size()); + } + + @Test + @Order(2) + void exceptionThrownWhenSavingComicWithoutPublisher() { + Comic comic = new Comic(); + comic.setTitle(TestConstants.COMIC_TITLE); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + } + + @Test + @Order(3) + void exceptionThrownWhenSavingComicWithEmptyTitle() { + Comic comic = new Comic(); + Publisher publisher = TestConstants.getPublisherForComicTests(comicService); + comic.setPublisher(publisher); + assertNull(comic.getTitle()); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + comic.setTitle(""); + assertTrue(comic.getTitle().isEmpty()); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + } + + @Test + @Order(4) + void exceptionThrownWhenSavingComicWithSameNameFromDifferentPublishers() { + Publisher publisher = TestConstants.getPublisherForComicTests(comicService); + Comic comic = new Comic(); + comic.setTitle(TestConstants.SOJOURN_TPB_COMIC_TITLE); + comic.setPublisher(publisher); + assertThrows(DataIntegrityViolationException.class, () -> { + comicRepository.save(comic); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java new file mode 100644 index 0000000..0902b84 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java @@ -0,0 +1,83 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicWorkRepositoryTest { + + @Autowired + private ComicWorkRepository comicWorkRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private WorktypeRepository worktypeRepository; + + @Autowired + private ArtistRepository artistRepository; + + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void checkInitialLoad() { + int count = comicWorkRepository.findAll().size(); + assertEquals(18, count); + } + + @Test + @Order(2) + void saveComicWork() { + int count = comicWorkRepository.findAll().size(); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = new ComicWork(); + comicWork.setArtist(artist); + comicWork.setComic(comic); + comicWork.setWorkType(worktype); + comicWorkRepository.save(comicWork); + assertEquals(count + 1, comicWorkRepository.findAll().size()); + } + + @Test + @Order(3) + void findByComicAndArtistAndComicWork() { + assertEquals(19, comicWorkRepository.count()); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + assertNotNull(comicWork); + assertEquals(comicWork.getArtist().getName(), artist.getName()); + assertEquals(comicWork.getComic().getTitle(), comic.getTitle()); + assertEquals(comicWork.getWorkType().getName(), worktype.getName()); + ComicWork notFound = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, null); + assertNull(notFound); + } + + @Test + @Order(4) + void deleteComicWork() { + long count = comicWorkRepository.count(); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + assertNotNull(comicWork); + comicService.deleteComicWork(comicWork); + assertEquals(count - 1, comicWorkRepository.count()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java new file mode 100644 index 0000000..ac2eac2 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +public class ComicWorkTest { + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + List comicWorks = comicService.findAllComicWorks(); + assertEquals(18, comicWorks.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java new file mode 100644 index 0000000..8d3be93 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class IssueRepositoryTest { + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private ComicService comicService; + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithIssues(comicService); + List issues = issueRepository.findByComic(comic); + assertEquals(TestConstants.BATTLE_POPE_ISSUE_COUNT, issues.size()); + } + + @Test + void testFindByComicAndIssueNumber() { + Comic comic = TestConstants.getComicWithIssues(comicService); + Issue issue = issueRepository.findByComicAndIssueNumber(comic, "12"); + assertNotNull(issue); + assertFalse(issue.getIsRead()); + } + + @Test + void testSearch() { + List issues = issueRepository.search("2"); + assertEquals(187, issues.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java new file mode 100644 index 0000000..1760fd1 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java @@ -0,0 +1,62 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class IssueTest { + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + List issues = comicService.findAllIssues(); + assertEquals(TestConstants.ISSUE_COUNT, issues.size()); + } + + @Test + void exceptionThrownWhenSavingIssueWithEmptyNumber() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + Issue issue = new Issue(); + issue.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + issueRepository.save(issue); + }); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithoutComic() { + Issue issue = new Issue(); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + assertThrows(TransactionSystemException.class, () -> { + issueRepository.save(issue); + }); + } + + @Test + void saveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getStoryArcs().size()); + Issue issue = new Issue(); + issue.setComic(comic); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + Issue savedInstance = issueRepository.save(issue); + assertNotNull(savedInstance); + comicService.deleteIssue(savedInstance); + assertEquals(0, comic.getStoryArcs().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java new file mode 100644 index 0000000..50175b4 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java @@ -0,0 +1,68 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class PublisherRepositoryTest { + + @Autowired + private PublisherRepository publisherRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + @Order(2) + void savePublisher() { + Publisher publisher = new Publisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + } + + @Test + @Order(3) + void findPublisherByName() { + Publisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + assertEquals(TestConstants.PUBLISHER_NAME, publisher.getName()); + Publisher notFound = publisherRepository.findByName("Cow"); + assertNull(notFound); + List publishers = publisherRepository + .findByNameIgnoreCase(TestConstants.PUBLISHER_NAME.toLowerCase()); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(4) + void searchPublisher() { + List publishers = publisherRepository.search("Cow"); + assertEquals(1, publishers.size()); + assertEquals("Top Cow Productions", publishers.get(0).getName()); + } + + @Test + @Order(5) + void deletePublisher() { + Publisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + publisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java new file mode 100644 index 0000000..264a1d0 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class PublisherTest { + + @Autowired + private PublisherRepository publisherRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + void throwExceptionWhenPublisherSavedWithEmptyName() { + Publisher publisher = new Publisher(); + assertThrows(TransactionSystemException.class, () -> { + publisherRepository.save(publisher); + }); + } + + @Test + void savePublisherWithIdenticalName() { + Publisher publisher1 = new Publisher(); + publisher1.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher1); + Publisher publisher2 = new Publisher(); + publisher2.setName(TestConstants.PUBLISHER_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + publisherRepository.save(publisher2); + }); + publisherRepository.delete(publisher1); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java new file mode 100644 index 0000000..52bad7e --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class StoryArcRepositoryTest { + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private ComicService comicService; + + @Test + void testSearch() { + List storyArcs = storyArcRepository.search("Learn"); + assertNotNull(storyArcs); + assertEquals(1, storyArcs.size()); + } + + @Test + void findByComic() { + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + List storyArcs = storyArcRepository.findByComic(comic); + assertNotNull(storyArcs); + assertEquals(3, storyArcs.size()); + } + + @Test + void findByNameAndComic() { + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + StoryArc storyArc = storyArcRepository.findByNameAndComic("Higher Learning", comic); + assertNotNull(storyArc); + assertEquals("Emma Frost", storyArc.getComic().getTitle()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java new file mode 100644 index 0000000..07134ca --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class StoryArcTest { + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.STORYARC_COUNT, storyArcRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + StoryArc storyArc = new StoryArc(); + storyArc.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + storyArcRepository.save(storyArc); + }); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithoutComic() { + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + assertThrows(TransactionSystemException.class, () -> { + storyArcRepository.save(storyArc); + }); + } + + @Test + void saveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getStoryArcs().size()); + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + storyArc.setComic(comic); + StoryArc savedInstance = storyArcRepository.save(storyArc); + assertNotNull(savedInstance); + comicService.deleteStoryArc(savedInstance); + assertEquals(0, comic.getStoryArcs().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java new file mode 100644 index 0000000..2c66c12 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class TradePaperbackRepositoryTest { + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private ComicService comicService; + + @Test + void testSearch() { + List tradePaperbacks = tradePaperbackRepository.search("Dragon"); + assertNotNull(tradePaperbacks); + assertEquals(1, tradePaperbacks.size()); + } + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + List tradePaperbacks = tradePaperbackRepository.findByComic(comic); + assertNotNull(tradePaperbacks); + assertEquals(TestConstants.SOJOURN_TPB_COUNT, tradePaperbacks.size()); + } + + @Test + void testFindByNameAndComic() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + List tradePaperbacks = tradePaperbackRepository + .findByNameAndComic(TestConstants.SOJOURN_TPB_NAME, comic); + assertNotNull(tradePaperbacks); + assertTrue(tradePaperbacks.size() > 0); + assertEquals(TestConstants.SOJOURN_TPB_COMIC_TITLE, tradePaperbacks.get(0).getComic().getTitle()); + } + + @Test + void testFindByFields() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + TradePaperback tradePaperback = tradePaperbackRepository.findByFields(TestConstants.SOJOURN_TPB_NAME, + comic, TestConstants.SOJOURN_TPB_START, TestConstants.SOJOURN_TPB_END); + assertNotNull(tradePaperback); + assertEquals(TestConstants.SOJOURN_TPB_COMIC_TITLE, tradePaperback.getComic().getTitle()); + assertEquals(TestConstants.SOJOURN_TPB_END, tradePaperback.getIssueEnd()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java new file mode 100644 index 0000000..61df1d6 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class TradePaperbackTest { + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, tradePaperbackRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingTradePaperbackWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + tradePaperbackRepository.save(tradePaperback); + }); + } + + @Test + void exceptionThrownWhenSavingTradePaperbackWithoutComic() { + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + assertThrows(TransactionSystemException.class, () -> { + tradePaperbackRepository.save(tradePaperback); + }); + } + + @Test + void saveTradePaperback() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getTradePaperbacks().size()); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + tradePaperback.setComic(comic); + TradePaperback savedInstance = tradePaperbackRepository.save(tradePaperback); + assertNotNull(savedInstance); + comicService.deleteTradePaperBack(savedInstance); + assertEquals(0, comic.getTradePaperbacks().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java new file mode 100644 index 0000000..ce04a11 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class VolumeRepositoryTest { + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + + List found = volumeRepository.findByComic(comic); + assertEquals(1, found.size()); + + comicService.deleteVolume(found.get(0)); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } + + @Test + void testFindByName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + + List found = volumeRepository.findByName(TestConstants.VOLUME_NAME); + assertEquals(1, found.size()); + + comicService.deleteVolume(found.get(0)); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java new file mode 100644 index 0000000..7189965 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java @@ -0,0 +1,64 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class VolumeTest { + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } + + @Test + void exceptionThrownWhenSavingVolumeWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Volume volume = new Volume(); + volume.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + volumeRepository.save(volume); + }); + } + + @Test + void exceptionThrownWhenSavingVolumeWithoutComic() { + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + assertThrows(TransactionSystemException.class, () -> { + volumeRepository.save(volume); + }); + } + + @Test + void saveVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + comicService.deleteVolume(savedInstance); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java new file mode 100644 index 0000000..5dae5e8 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java @@ -0,0 +1,46 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class WorktypeRepositoryTest { + + @Autowired + private WorktypeRepository worktypeRepository; + + @Test + void findWorktypeByName() { + Worktype found = worktypeRepository.findByName("Writer"); + assertNotNull(found); + Worktype notFound = worktypeRepository.findByName("er"); + assertNull(notFound); + } + + @Test + void findWorktypeByNameIgnoreCase() { + List worktypes = worktypeRepository.findByNameIgnoreCase("Writer".toLowerCase()); + assertNotNull(worktypes); + assertEquals(1, worktypes.size()); + assertEquals("Writer", worktypes.get(0).getName()); + } + + @Test + void searchWorktype() { + List worktypes = worktypeRepository.search("er"); + assertEquals(3, worktypes.size()); + assertTrue(worktypes.stream().map(worktype -> worktype.getName()).collect(Collectors.toList()).contains("Writer")); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java new file mode 100644 index 0000000..3145ee9 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java @@ -0,0 +1,83 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class WorktypeTest { + + @Autowired + private WorktypeRepository worktypeRepository; + + @Test + @Order(1) + void checkInitialDataLoad() { + List worktypes = worktypeRepository.findAll(); + assertEquals(3, worktypes.size()); + } + + @Test + @Order(2) + void findWorktypeByName() { + Worktype found = worktypeRepository.findByName(TestConstants.WORKTYPE_INKER); + assertNotNull(found); + assertEquals(TestConstants.WORKTYPE_INKER, found.getName()); + Worktype notFound = worktypeRepository.findByName("er"); + assertNull(notFound); + } + + @Test + @Order(3) + void throwExceptionWhenArtistSavedWithEmptyName() { + Worktype worktype1 = new Worktype(); + assertThrows(TransactionSystemException.class, () -> { + worktypeRepository.save(worktype1); + }); + } + + @Test + @Order(4) + void saveWorktypeWithIdenticalName() { + Worktype worktype1 = new Worktype(); + worktype1.setName(TestConstants.WORKTYPE_NAME); + worktypeRepository.save(worktype1); + Worktype worktype2 = new Worktype(); + worktype2.setName(TestConstants.WORKTYPE_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + worktypeRepository.save(worktype2); + }); + worktypeRepository.delete(worktype1); + } + + @Test + @Order(5) + void saveWorktype() { + Worktype worktype = new Worktype(); + worktype.setName(TestConstants.WORKTYPE_NAME); + worktypeRepository.save(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT + 1, worktypeRepository.count()); + } + + @Test + @Order(6) + void deleteWorktype() { + Worktype worktype = worktypeRepository.findByName(TestConstants.WORKTYPE_NAME); + assertNotNull(worktype); + worktypeRepository.delete(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT, worktypeRepository.count()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java new file mode 100644 index 0000000..d449298 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java @@ -0,0 +1,350 @@ +package de.thpeetz.kontor.comics.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.Worktype; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicServiceTest { + + private static final Logger log = LoggerFactory.getLogger(ComicServiceTest.class); + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void testFindAllArtists() { + assertEquals(TestConstants.ARTIST_COUNT, comicService.findAllArtists(null).size()); + assertEquals(1, comicService.findAllArtists("Turn").size()); + } + + @Test + @Order(1) + void testFindArtistByName() { + assertNull(comicService.findArtistByName("Lee, Stan")); + Artist artist = comicService.findArtistByName("Turner, Michael"); + assertNotNull(artist); + assertNotNull(artist.getComicWorks()); + assertFalse(artist.getComicWorks().isEmpty()); + } + + @Test + @Order(2) + void testSaveArtist() { + Artist artist = new Artist(); + artist.setName(TestConstants.ARTIST_NAME); + comicService.saveArtist(artist); + assertEquals(TestConstants.ARTIST_COUNT + 1, comicService.findAllArtists(null).size()); + } + + @Test + @Order(3) + void testDeleteArtist() { + List artists = comicService.findAllArtists(TestConstants.ARTIST_NAME); + assertEquals(1, artists.size()); + comicService.deleteArtist(artists.get(0)); + assertEquals(TestConstants.ARTIST_COUNT, comicService.findAllArtists(null).size()); + } + + @Test + @Order(4) + void testFindAllPublishers() { + assertEquals(TestConstants.PUBLISHER_COUNT, comicService.findAllPublishers(null).size()); + assertEquals(1, comicService.findAllPublishers("Cow").size()); + } + + @Test + @Order(4) + void testFindPublisherByName() { + Publisher publisher = comicService.findPublisherByName("Marvel"); + assertNotNull(publisher); + assertNotNull(publisher.getComics()); + assertEquals(TestConstants.MARVEL_COMIC_COUNT, publisher.getComics().size()); + assertNull(comicService.findPublisherByName(TestConstants.PUBLISHER_NAME)); + } + + @Test + @Order(5) + void testSavePublisher() { + Publisher publisher = new Publisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + comicService.savePublisher(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, comicService.findAllPublishers(null).size()); + } + + @Test + @Order(6) + void testDeletePublisher() { + List publishers = comicService.findAllPublishers(TestConstants.PUBLISHER_NAME); + assertEquals(1, publishers.size()); + comicService.deletePublisher(publishers.get(0)); + assertEquals(TestConstants.PUBLISHER_COUNT, comicService.findAllPublishers(null).size()); + } + + @Test + @Order(7) + void testFindAllWorktypes() { + assertEquals(TestConstants.WORKTYPE_COUNT, comicService.findAllWorktypes(null).size()); + assertEquals(1, comicService.findAllWorktypes("ite").size()); + } + + @Test + @Order(7) + void testFindWorktypeByName() { + Worktype worktype = comicService.findWorktypeByName("Writer"); + assertNotNull(worktype); + assertNotNull(worktype.getComicWorks()); + assertFalse(worktype.getComicWorks().isEmpty()); + worktype = comicService.findWorktypeByName("Inker"); + assertNotNull(worktype); + assertNotNull(worktype.getComicWorks()); + assertTrue(worktype.getComicWorks().isEmpty()); + assertNull(comicService.findWorktypeByName(TestConstants.WORKTYPE_NAME)); + } + + @Test + @Order(8) + void testSaveWorktype() { + Worktype worktype = new Worktype(); + worktype.setName(TestConstants.WORKTYPE_NAME); + comicService.saveWorktype(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT + 1, comicService.findAllWorktypes(null).size()); + } + + @Test + @Order(9) + void testDeleteWorktype() { + List worktypes = comicService.findAllWorktypes(TestConstants.WORKTYPE_NAME); + assertEquals(1, worktypes.size()); + comicService.deleteWorktype(worktypes.get(0)); + assertEquals(TestConstants.WORKTYPE_COUNT, comicService.findAllWorktypes(null).size()); + } + + @Test + @Order(10) + void testFindComicByTitle() { + assertNull(comicService.findComicByTitle(null)); + assertNotNull(comicService.findComicByTitle("Danger Girl")); + assertNull(comicService.findComicByTitle("x-men")); + } + + @Test + @Order(11) + void testSaveComic() { + Publisher marvel = comicService.findAllPublishers("Marvel").get(0); + Comic comic = new Comic(); + comic.setTitle(TestConstants.COMIC_TITLE); + comic.setPublisher(marvel); + comicService.saveComic(comic); + assertEquals(TestConstants.COMIC_COUNT + 1, comicService.findAllComics(null).size()); + } + + @Test + @Order(12) + void testDeleteComic() { + assertEquals(TestConstants.COMIC_COUNT + 1, comicService.findAllComics(null).size()); + List comics = comicService.findAllComics(TestConstants.COMIC_TITLE); + assertEquals(1, comics.size()); + Comic comic = comicService.findComicByTitle(TestConstants.COMIC_TITLE); + assertNotNull(comic); + comicService.deleteComic(comic); + Publisher marvel = comicService.findPublisherByName("Marvel"); + assertNotNull(marvel.getComics()); + assertEquals(TestConstants.MARVEL_COMIC_COUNT, marvel.getComics().size()); + assertEquals(0, comicService.findAllComics(TestConstants.COMIC_TITLE).size()); + assertEquals(TestConstants.COMIC_COUNT, comicService.findAllComics(null).size()); + } + + @Test + @Order(13) + void testFindAllIssues() { + assertEquals(TestConstants.ISSUE_COUNT, comicService.findAllIssues().size()); + } + + @Test + @Order(14) + void testFindAllIssuesForComic() { + Comic comic = TestConstants.getComicWithIssues(comicService); + List issues = comicService.findAllIssuesForComic(comic); + assertEquals(12, issues.size()); + } + + @Test + @Order(15) + void testSaveIssue() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Issue issue = new Issue(); + issue.setComic(comic); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + comicService.saveIssue(issue); + assertEquals(TestConstants.ISSUE_COUNT + 1, comicService.findAllIssues().size()); + comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic.getIssues()); + assertEquals(1, comic.getIssues().size()); + } + + @Test + @Order(16) + void testDeleteIssue() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List issues = comicService.findAllIssuesForComic(comic); + assertEquals(1, issues.size()); + comicService.deleteIssue(issues.get(0)); + assertEquals(TestConstants.ISSUE_COUNT, comicService.findAllIssues().size()); + } + + @Test + @Order(17) + void testFindAllTradePaperbacks() { + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, comicService.findAllTradePaperbacks(null).size()); + assertEquals(7, comicService.findAllTradePaperbacks("of").size()); + } + + @Test + @Order(18) + void testSaveTradePaperBack() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setComic(comic); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + comicService.saveTradePaperBack(tradePaperback); + assertEquals(TestConstants.TRADEPAPERBACK_COUNT + 1, comicService.findAllTradePaperbacks(null).size()); + } + + @Test + @Order(19) + void testDeleteTradePaperBack() { + List tradePaperbacks = comicService.findAllTradePaperbacks(TestConstants.TRADEPAPERBACK_NAME); + assertEquals(1, tradePaperbacks.size()); + comicService.deleteTradePaperBack(tradePaperbacks.get(0)); + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, comicService.findAllTradePaperbacks(null).size()); + } + + @Test + @Order(20) + void testFindAllStoryArcs() { + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(21) + void testFindAllStoryArcsForComic() { + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcsForComic(null).size()); + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + assertEquals(3, comicService.findAllStoryArcsForComic(comic).size()); + } + + @Test + @Order(22) + void testSaveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + storyArc.setComic(comic); + comicService.saveStoryArc(storyArc); + assertEquals(TestConstants.STORYARC_COUNT + 1, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(23) + void testDeleteStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List storyArcs = comicService.findAllStoryArcsForComic(comic); + assertEquals(1, storyArcs.size()); + comicService.deleteStoryArc(storyArcs.get(0)); + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(24) + void testFindAllVolumes() { + assertEquals(TestConstants.VOLUME_COUNT, comicService.findAllVolumes().size()); + } + + @Test + @Order(25) + void testSaveVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + comicService.saveVolume(volume); + assertEquals(TestConstants.VOLUME_COUNT + 1, comicService.findAllVolumes().size()); + } + + @Test + @Order(26) + void testFindAllVolumesForComic() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List volumes = comicService.findAllVolumesForComic(comic); + assertEquals(1, volumes.size()); + } + + @Test + @Order(27) + void testDeleteVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List volumes = comicService.findAllVolumesForComic(comic); + comicService.deleteVolume(volumes.get(0)); + assertEquals(TestConstants.VOLUME_COUNT, comicService.findAllVolumes().size()); + } + + @Test + @Order(28) + void testFindAllComicWorks() { + assertEquals(TestConstants.COMICWORK_COUNT, comicService.findAllComicWorks().size()); + } + + @Test + @Order(29) + void testSaveComicWork() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Artist artist = TestConstants.getArtistWithoutReferences(comicService); + Worktype worktype = TestConstants.getWorktypeWithoutReferences(comicService); + ComicWork comicWork = new ComicWork(); + comicWork.setComic(comic); + comicWork.setArtist(artist); + comicWork.setWorkType(worktype); + comicService.saveComicWork(comicWork); + assertEquals(TestConstants.COMICWORK_COUNT + 1, comicService.findAllComicWorks().size()); + } + + @Test + @Order(30) + void testDeleteComicWork() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Artist artist = TestConstants.getArtistWithoutReferences(comicService); + Worktype worktype = TestConstants.getWorktypeWithoutReferences(comicService); + List comicWorks = comic.getComicWorks(); + assertNotNull(comicWorks); + assertNotNull(artist.getComicWorks()); + assertNotNull(worktype.getComicWorks()); + assertEquals(1, comicWorks.size()); + assertEquals(1, artist.getComicWorks().size()); + assertEquals(1, worktype.getComicWorks().size()); + ComicWork comicWork = comicWorks.get(0); + comicService.deleteComicWork(comicWork); + assertEquals(TestConstants.COMICWORK_COUNT, comicService.findAllComicWorks().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/TestConstants.java b/springboot/src/test/java/de/thpeetz/kontor/media/TestConstants.java new file mode 100644 index 0000000..32ee883 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/TestConstants.java @@ -0,0 +1,5 @@ +package de.thpeetz.kontor.media; + +public class TestConstants { + public static final String URL = "https://example.com/link.mp4"; +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java new file mode 100644 index 0000000..2f2adc7 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.media.services.MediaArticleService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest +public class MediaArticleTest { + + @Autowired + private MediaArticleRepository mediaArticleRepository; + + @Test + void checkInitialLoad() { + assertTrue(mediaArticleRepository.findAll().isEmpty()); + } + + @Test + void checkDefaultValues() { + MediaArticle mediaArticle = new MediaArticle(); + assertNull(mediaArticle.getUrl()); + assertNull(mediaArticle.getTitle()); + assertNull(mediaArticle.getCreatedDate()); + assertNull(mediaArticle.getLastModifiedDate()); + assertNull(mediaArticle.getId()); + assertFalse(mediaArticle.isReview()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java new file mode 100644 index 0000000..6ca61ce --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java @@ -0,0 +1,37 @@ +package de.thpeetz.kontor.media.data; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +public class MediaFileTest { + + @Autowired + private MediaFileRepository mediaFileRepository; + + @Test + void checkInitialDataLoad() { + List mediaFileList = mediaFileRepository.findAll(); + assertTrue(mediaFileList.isEmpty()); + } + + @Test + void checkDefaultValues() { + MediaFile mediaFile = new MediaFile(); + assertNull(mediaFile.getUrl()); + assertNull(mediaFile.getTitle()); + assertNull(mediaFile.getFileName()); + assertNull(mediaFile.getPath()); + assertNull(mediaFile.getCloudLink()); + assertNull(mediaFile.getCreatedDate()); + assertNull(mediaFile.getLastModifiedDate()); + assertNull(mediaFile.getId()); + assertFalse(mediaFile.isReview()); + assertFalse(mediaFile.isShouldDownload()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java new file mode 100644 index 0000000..1a40335 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java @@ -0,0 +1,38 @@ +package de.thpeetz.kontor.media.data; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest +public class MediaVideoTest { + + @Autowired + private MediaVideoRepository mediaVideoRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertTrue(mediaVideoRepository.findAll().isEmpty()); + } + + @Test + @Order(2) + void checkDefaultValues() { + MediaVideo mediaVideo = new MediaVideo(); + assertNull(mediaVideo.getUrl()); + assertNull(mediaVideo.getTitle()); + assertNull(mediaVideo.getFileName()); + assertNull(mediaVideo.getPath()); + assertNull(mediaVideo.getCloudLink()); + assertNull(mediaVideo.getCreatedDate()); + assertNull(mediaVideo.getLastModifiedDate()); + assertNull(mediaVideo.getId()); + assertFalse(mediaVideo.isReview()); + assertFalse(mediaVideo.isShouldDownload()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java new file mode 100644 index 0000000..6178aa8 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java @@ -0,0 +1,50 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MediaArticleServiceTest { + + @Autowired + MediaArticleService mediaArticleService; + + @Test + @Order(1) + void findAllMediaArticles() { + assertTrue(mediaArticleService.findAllMediaArticles(null).isEmpty()); + } + + @Test + @Order(2) + void saveMediaArticle() { + int mediaArticleCount = mediaArticleService.findAllMediaArticles(null).size(); + MediaArticle mediaArticle = new MediaArticle(); + mediaArticle.setUrl(TestConstants.URL); + mediaArticleService.saveMediaArticle(mediaArticle); + assertEquals(++mediaArticleCount, mediaArticleService.findAllMediaArticles(null).size()); + } + + @Test + @Order(3) + void deleteMediaArticle() { + int mediaArticleCount = mediaArticleService.findAllMediaArticles(null).size(); + List mediaArticleList = mediaArticleService.findAllMediaArticles(TestConstants.URL); + assertEquals(1, mediaArticleService.findAllMediaArticles(null).size()); + mediaArticleService.deleteMediaArticle(mediaArticleList.get(0)); + assertEquals(--mediaArticleCount, mediaArticleService.findAllMediaArticles(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java new file mode 100644 index 0000000..2201be5 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaFile; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MediaFileServiceTest { + + @Autowired + private MediaFileService mediaFileService; + + @Test + @Order(1) + void testFindAllMediaFiles() { + assertTrue(mediaFileService.findAllMediaFiles(null).isEmpty()); + } + + @Test + @Order(2) + void testSaveMediaFile() { + int mediaFileCount = mediaFileService.findAllMediaFiles(null).size(); + MediaFile mediaFile = new MediaFile(); + mediaFile.setUrl(TestConstants.URL); + mediaFileService.saveMediaFile(mediaFile); + assertEquals(mediaFileCount +1, mediaFileService.findAllMediaFiles(null).size()); + } + + @Test + @Order(3) + void testDeleteMediaFile() { + int mediaFileCount = mediaFileService.findAllMediaFiles(null).size(); + List mediaFileList = mediaFileService.findAllMediaFiles(TestConstants.URL); + assertEquals(1, mediaFileList.size()); + mediaFileService.deleteMediaFile(mediaFileList.get(0)); + assertEquals(--mediaFileCount, mediaFileService.findAllMediaFiles(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java new file mode 100644 index 0000000..36a6fb1 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.data.MediaVideo; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Slf4j +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MediaVideoServiceTest { + + @Autowired + private MediaVideoService mediaVideoService; + + @Test + void findAllMediaVideos() { + assertTrue(mediaVideoService.findAllMediaVideos(null).isEmpty()); + } + + @Test + void saveMediaVideo() { + int mediaVideoCount = mediaVideoService.findAllMediaVideos(null).size(); + MediaVideo mediaVideo = new MediaVideo(); + mediaVideo.setUrl(TestConstants.URL); + mediaVideoService.saveMediaVideo(mediaVideo); + assertEquals(++mediaVideoCount, mediaVideoService.findAllMediaVideos(null).size()); + } + + @Test + void deleteMediaVideo() { + int mediaVideoCount = mediaVideoService.findAllMediaVideos(null).size(); + List mediaVideoList = mediaVideoService.findAllMediaVideos(TestConstants.URL); + assertEquals(1, mediaVideoList.size()); + mediaVideoService.deleteMediaVideo(mediaVideoList.get(0)); + assertEquals(--mediaVideoCount, mediaVideoService.findAllMediaVideos(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java new file mode 100644 index 0000000..a05c1dd --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.tysc; + +public class TestConstants { + + public static final Integer CARD_COUNT = 10; + public static final Integer CARD_CARDNUMBER = 999; + public static final Integer CARD_YEAR = 2000; + public static final Integer CARDSET_COUNT = 15; + public static final Integer FOOTBALL_TEAM_COUNT = 33; + public static final Integer FOOTBALL_POSITION_COUNT = 25; + public static final Integer PLAYER_COUNT = 38; + public static final Integer PLAYER_CHRIS_COUNT = 3; + public static final Integer POSITION_COUNT = 44; + public static final Integer POSITION_CENTER_COUNT = 3; + public static final Integer ROOSTER_COUNT = 11; + public static final Integer ROOSTER_YEAR = 1900; + public static final Integer SPORT_COUNT = 4; + public static final Integer TEAM_COUNT = 122; + public static final Integer VENDOR_COUNT = 9; + public static final String CARDSET_NAME = "CardSet"; + public static final String CARDSET_MYSTIQUE_NAME = "Mystique"; + public static final String DOLPHINS_NAME = "Miami Dolphins"; + public static final Object DOLPHINS_SHORT = "Dolphins"; + public static final String FOOTBALL_NAME = "Football"; + public static final String PLAYER_FIRSTNAME = "FirstName"; + public static final String PLAYER_LASTNAME = "LastName"; + public static final String PLAYER_CHRIS_NAME = "Chris"; + public static final String POSITION_CENTER = "Center"; + public static final String POSITION_NAME = "Position"; + public static final String POSITION_SHORTNAME = "POS"; + public static final String QUARTERBACK_NAME = "Quarterback"; + public static final String QUARTERBACK_SHORTNAME = "QB"; + public static final String SPORT_NAME = "Sport"; + public static final String TEAM_NAME = "Musterstadt Team"; + public static final String TEAM_SHORTNAME = "Team"; + public static final String TOPPS_NAME = "Topps"; + public static final String VENDOR_FLEER = "Fleer"; + public static final String VENDOR_NAME = "Vendor"; +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardRepositoryTest.java new file mode 100644 index 0000000..e2ff8b2 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardRepositoryTest.java @@ -0,0 +1,35 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CardRepositoryTest { + + @Autowired + private CardRepository cardRepository; + + @Test + void testSearchByFields() { + List cards = cardRepository.findAll(); + Card existingCard = cards.get(3); + Vendor vendor = existingCard.getVendor(); + CardSet cardSet = existingCard.getCardSet(); + Rooster rooster = existingCard.getRooster(); + int cardNumber = existingCard.getCardNumber(); + int year = existingCard.getYear(); + Card card = cardRepository.search(vendor, cardSet, rooster, cardNumber, year); + assertNotNull(card); + } + + @Test + void testSearch() { + assertEquals(2, cardRepository.search("2").size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetRepositoryTest.java new file mode 100644 index 0000000..71a08ba --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetRepositoryTest.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class CardSetRepositoryTest { + + @Autowired + private CardSetRepository cardSetRepository; + + @Autowired + private VendorRepository vendorRepository; + + @Test + void testFindByName() { + List cardSets = cardSetRepository.findByName(TestConstants.CARDSET_MYSTIQUE_NAME); + assertEquals(1, cardSets.size()); + } + + @Test + void testFindByNameAndVendor() { + Vendor fleer = vendorRepository.findByName(TestConstants.VENDOR_FLEER); + assertNotNull(fleer); + CardSet cardSet = cardSetRepository.findByNameAndVendor(TestConstants.CARDSET_MYSTIQUE_NAME, fleer); + assertNotNull(cardSet); + } + + @Test + void testSearch() { + List cardSets = cardSetRepository.search("SP"); + assertEquals(3, cardSets.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java new file mode 100644 index 0000000..a5699d6 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.services.CardService; + +@SpringBootTest +class CardSetTest { + + @Autowired + private CardService cardService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java new file mode 100644 index 0000000..0d71d8d --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.services.CardService; + +@SpringBootTest +class CardTest { + + @Autowired + private CardService cardService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionRepositoryTest.java new file mode 100644 index 0000000..a9273db --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionRepositoryTest.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class FieldPositionRepositoryTest { + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private SportRepository sportRepository; + + @Test + void testFindByShortName() { + FieldPosition position = fieldPositionRepository.findByShortName(TestConstants.QUARTERBACK_SHORTNAME); + assertNotNull(position); + assertEquals(TestConstants.QUARTERBACK_NAME, position.getName()); + } + + @Test + void testFindByShortNameAndSport() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + FieldPosition position = fieldPositionRepository.findByShortNameAndSport(TestConstants.QUARTERBACK_SHORTNAME, sport); + assertNotNull(position); + assertEquals(TestConstants.QUARTERBACK_NAME, position.getName()); + } + + @Test + void testFindByShortNameIgnoreCase() { + List positions = fieldPositionRepository + .findByShortNameIgnoreCase(TestConstants.QUARTERBACK_SHORTNAME.toLowerCase()); + assertNotNull(positions); + assertEquals(1, positions.size()); + assertEquals(TestConstants.QUARTERBACK_NAME, positions.get(0).getName()); + } + + @Test + void testFindBySport() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + List positions = fieldPositionRepository.findBySport(sport); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, positions.size()); + } + + @Test + void testSearch() { + List positions = fieldPositionRepository.search("back"); + assertEquals(8, positions.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java new file mode 100644 index 0000000..8a8b9b7 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class FieldPositionTest { + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(Integer.toUnsignedLong(TestConstants.POSITION_COUNT), fieldPositionRepository.count()); + } + + @Test + void throwExceptionWhenPositionSavedWithEmptyName() { + FieldPosition fieldPosition = new FieldPosition(); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + fieldPosition.setName("Position"); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + } + + @Test + void throwExceptionWhenPositionSavedWithExistingName() { + FieldPosition existingFieldPosition = fieldPositionRepository.findAll().get(11); + FieldPosition fieldPosition = new FieldPosition(); + fieldPosition.setName(existingFieldPosition.getName()); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerRepositoryTest.java new file mode 100644 index 0000000..7cbd540 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerRepositoryTest.java @@ -0,0 +1,31 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class PlayerRepositoryTest { + + @Autowired + private PlayerRepository playerRepository; + + @Test + void testFindByFirstNameAndLastName() { + Player existingPlayer = playerRepository.findAll().get(11); + String firstName = existingPlayer.getFirstName(); + String lastName = existingPlayer.getLastName(); + Player found = playerRepository.findByFirstNameAndLastName(firstName, lastName); + assertEquals(existingPlayer.getFullName(), found.getFullName()); + } + + @Test + void testSearch() { + List players = playerRepository.search("can"); + assertEquals(1, players.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java new file mode 100644 index 0000000..941537b --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class PlayerTest { + + @Autowired + private PlayerRepository playerRepository; + + @Test + @Order(1) + void checkInitialDataLoad() { + List players = playerRepository.findAll(); + assertEquals(TestConstants.PLAYER_COUNT, players.size()); + } + + @Test + @Order(2) + void throwExceptionWhenPlayerSavedWithEmptyName() { + Player player = new Player(); + assertThrows(TransactionSystemException.class, () -> { + playerRepository.save(player); + }); + } + + @Test + @Order(3) + void throwExceptionWhenPlayerSavedWithExistingName() { + Player existingPlayer = playerRepository.findAll().get(10); + assertNotNull(existingPlayer); + Player player = new Player(); + player.setFirstName(existingPlayer.getFirstName()); + player.setLastName(existingPlayer.getLastName()); + assertThrows(DataIntegrityViolationException.class, () -> { + playerRepository.save(player); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterRepositoryTest.java new file mode 100644 index 0000000..fa56d90 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterRepositoryTest.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RoosterRepositoryTest { + + @Autowired + private RoosterRepository roosterRepository; + + @Test + void testFindByReferences() { + List roosters = roosterRepository.findAll(); + Rooster existingRooster = roosters.get(4); + Team team = existingRooster.getTeam(); + Player player = existingRooster.getPlayer(); + FieldPosition position = existingRooster.getPosition(); + int year = existingRooster.getYear(); + Rooster rooster = roosterRepository.findByReferences(player, team, position, year); + assertNotNull(rooster); + assertEquals(existingRooster.getId(), rooster.getId()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java new file mode 100644 index 0000000..caffea4 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java @@ -0,0 +1,47 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class RoosterTest { + + @Autowired + private RoosterRepository roosterRepository; + + @Test + void checkInitialDataLoad() { + List roosters = roosterRepository.findAll(); + assertEquals(TestConstants.ROOSTER_COUNT, roosters.size()); + } + + @Test + void checkFetchingData() { + Rooster rooster = roosterRepository.findAll().get(4); + assertNotNull(rooster.getTeam()); + assertNotNull(rooster.getPlayer()); + assertNotNull(rooster.getPosition()); + } + + @Test + void exceptionThrownWhenSavingDuplicateRooster() { + List roosters = roosterRepository.findAll(); + Rooster existingRooster = roosters.get(5); + Rooster rooster = new Rooster(); + rooster.setPlayer(existingRooster.getPlayer()); + rooster.setTeam(existingRooster.getTeam()); + rooster.setPosition(existingRooster.getPosition()); + rooster.setYear(existingRooster.getYear()); + assertThrows(DataIntegrityViolationException.class, () -> { + roosterRepository.save(rooster); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportRepositoryTest.java new file mode 100644 index 0000000..c0db4be --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportRepositoryTest.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class SportRepositoryTest { + + @Autowired + private SportRepository sportRepository; + + @Test + void testFindByName() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + assertNotNull(sport); + assertEquals(TestConstants.FOOTBALL_TEAM_COUNT, sport.getTeams().size()); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, sport.getPositions().size()); + } + + @Test + void testFindByNameIgnoreCase() { + List sports = sportRepository.findByNameIgnoreCase(TestConstants.FOOTBALL_NAME.toLowerCase()); + assertNotNull(sports); + assertEquals(TestConstants.FOOTBALL_TEAM_COUNT, sports.get(0).getTeams().size()); + } + + @Test + void testSearch() { + List sports = sportRepository.search("ball"); + assertEquals(3, sports.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java new file mode 100644 index 0000000..e47bb50 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class SportTest { + + @Autowired + private SportRepository sportRepository; + + @Test + void checkInitialDataLoad() { + List sports = sportRepository.findAll(); + assertEquals(TestConstants.SPORT_COUNT, sports.size()); + } + + @Test + void exceptionThrownWhenSavingSportWithEmptyName() { + Sport sport = new Sport(); + assertThrows(TransactionSystemException.class, () -> { + sportRepository.save(sport); + }); + } + + @Test + void exceptionThrownWhenSavingSportWithExistingName() { + Sport existingSport = sportRepository.findAll().get(0); + Sport sport = new Sport(); + sport.setName(existingSport.getName()); + assertThrows(DataIntegrityViolationException.class, () -> { + sportRepository.save(sport); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamRepositoryTest.java new file mode 100644 index 0000000..86f4200 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamRepositoryTest.java @@ -0,0 +1,37 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class TeamRepositoryTest { + + @Autowired + private TeamRepository teamRepository; + + @Test + void testFindByName() { + Team team = teamRepository.findByName(TestConstants.DOLPHINS_NAME); + assertEquals(TestConstants.DOLPHINS_SHORT, team.getShortName()); + } + + @Test + void testFindByNameIgnoreCase() { + List teams = teamRepository.findByNameIgnoreCase(TestConstants.DOLPHINS_NAME.toLowerCase()); + assertEquals(1, teams.size()); + assertEquals(TestConstants.DOLPHINS_SHORT, teams.get(0).getShortName()); + } + + @Test + void testSearch() { + List teams = teamRepository.search("dol"); + assertEquals(1, teams.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java new file mode 100644 index 0000000..7bd4ba2 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class TeamTest { + + @Autowired + private TeamRepository teamRepository; + + @Test + void checkInitialDataLoad() { + List teams = teamRepository.findAll(); + assertEquals(TestConstants.TEAM_COUNT, teams.size()); + } + + @Test + void throwExceptionWhenTeamSavedWithEmptyName() { + Team team = new Team(); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } + + @Test + void throwExceptionWhenTeamSavedWithoutSport() { + Team team = new Team(); + team.setName(TestConstants.TEAM_NAME); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } + + @Test + void throwExceptionWhenTeamSavedWithExistingName() { + Team existingTeam = teamRepository.findAll().get(11); + Team team = new Team(); + team.setName(existingTeam.getName()); + team.setSport(existingTeam.getSport()); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorRepositoryTest.java new file mode 100644 index 0000000..9db7ef7 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorRepositoryTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class VendorRepositoryTest { + + @Autowired + private VendorRepository vendorRepository; + + @Test + void testFindByName() { + Vendor vendor = vendorRepository.findByName(TestConstants.TOPPS_NAME); + assertNotNull(vendor); + assertEquals(TestConstants.TOPPS_NAME, vendor.getName()); + } + + @Test + void testFindByNameIgnoreCase() { + List vendors = vendorRepository.findByNameIgnoreCase(TestConstants.TOPPS_NAME.toLowerCase()); + assertNotNull(vendors); + assertEquals(1, vendors.size()); + assertEquals(TestConstants.TOPPS_NAME, vendors.get(0).getName()); + } + + @Test + void testSearch() { + List vendors = vendorRepository.search("pp"); + assertNotNull(vendors); + assertEquals(2, vendors.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java new file mode 100644 index 0000000..a16bd77 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class VendorTest { + + @Autowired + private VendorRepository vendorRepository; + + @Test + void checkInitialDataLoad() { + List vendors = vendorRepository.findAll(); + assertEquals(TestConstants.VENDOR_COUNT, vendors.size()); + } + + @Test + void exceptionThrownWhenSavingVendortWithEmptyName() { + Vendor vendor = new Vendor(); + assertThrows(TransactionSystemException.class, () -> { + vendorRepository.save(vendor); + }); + } + + @Test + void exceptionThrownWhenSavingSportWithExistingName() { + Vendor vendor = new Vendor(); + vendor.setName(TestConstants.TOPPS_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + vendorRepository.save(vendor); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java new file mode 100644 index 0000000..e215217 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java @@ -0,0 +1,126 @@ +package de.thpeetz.kontor.tysc.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Vendor; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class CardServiceTest { + + @Autowired + private CardService cardService; + + @Autowired + private SportService sportService; + + @Test + @Order(1) + void testFindAllVendors() { + assertEquals(TestConstants.VENDOR_COUNT, cardService.findAllVendors(null).size()); + assertEquals(2, cardService.findAllVendors("pp").size()); + } + + @Test + @Order(2) + void testSaveVendor() { + Vendor vendor = new Vendor(); + vendor.setName(TestConstants.VENDOR_NAME); + vendor = cardService.saveVendor(vendor); + assertNotNull(vendor); + assertEquals(TestConstants.VENDOR_COUNT + 1, cardService.findAllVendors(null).size()); + } + + @Test + @Order(3) + void testDeleteVendor() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_NAME); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + cardService.deleteVendor(vendor); + assertEquals(TestConstants.VENDOR_COUNT, cardService.findAllVendors(null).size()); + } + + @Test + @Order(4) + void testFindAllCardSets() { + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + assertEquals(1, cardService.findAllCardSets("Ultra").size()); + } + + @Test + @Order(5) + void testSaveCardSet() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_FLEER); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + CardSet cardSet = new CardSet(); + cardSet.setName(TestConstants.CARDSET_NAME); + cardSet.setVendor(vendor); + cardSet.setInsertSet(false); + cardSet.setParallelSet(false); + cardService.saveCardSet(cardSet); + assertEquals(TestConstants.CARDSET_COUNT + 1, cardService.findAllCardSets(null).size()); + } + + @Test + @Order(6) + void testDeleteCardSet() { + List cardSets = cardService.findAllCardSets(TestConstants.CARDSET_NAME); + assertEquals(1, cardSets.size()); + CardSet cardSet = cardSets.get(0); + cardService.deleteCardSet(cardSet); + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + } + + @Test + @Order(7) + void testFindAllCards() { + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + assertEquals(1, cardService.findAllCards("112").size()); + } + + @Test + @Order(8) + void testSaveCard() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_FLEER); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + List cardSets = cardService.findAllCardSets(TestConstants.CARDSET_MYSTIQUE_NAME); + assertEquals(3, cardSets.size()); + CardSet cardSet = cardSets.get(0); + List roosters = sportService.findAllRoosters(); + Rooster rooster = roosters.get(0); + Card card = new Card(); + card.setCardNumber(TestConstants.CARD_CARDNUMBER); + card.setCardSet(cardSet); + card.setVendor(vendor); + card.setRooster(rooster); + card.setYear(TestConstants.CARD_YEAR); + cardService.saveCard(card); + assertEquals(TestConstants.CARD_COUNT + 1, cardService.findAllCards(null).size()); + } + + @Test + @Order(9) + void testDeleteCard() { + List cards = cardService.findAllCards(TestConstants.CARD_CARDNUMBER.toString()); + assertEquals(1, cards.size()); + Card card = cards.get(0); + cardService.deleteCard(card); + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java new file mode 100644 index 0000000..30b7ed6 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java @@ -0,0 +1,200 @@ +package de.thpeetz.kontor.tysc.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.data.Team; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class SportServiceTest { + + @Autowired + private SportService sportService; + + + @Test + @Order(1) + void testFindAllSports() { + List sports = sportService.findAllSports(null); + assertEquals(TestConstants.SPORT_COUNT, sports.size()); + assertEquals(3, sportService.findAllSports("ball").size()); + } + + @Test + @Order(2) + void testSaveSport() { + Sport sport = new Sport(); + sport.setName(TestConstants.SPORT_NAME); + sport = sportService.saveSport(sport); + assertNotNull(sport); + assertEquals(TestConstants.SPORT_COUNT+1, sportService.findAllSports(null).size()); + } + + @Test + @Order(3) + void testDeleteSport() { + List sports = sportService.findAllSports(TestConstants.SPORT_NAME); + assertEquals(1, sports.size()); + Sport sport = sports.get(0); + sportService.deleteSport(sport); + assertEquals(TestConstants.SPORT_COUNT, sportService.findAllSports(null).size()); + } + + @Test + @Order(4) + void testFindAllPlayers() { + assertEquals(TestConstants.PLAYER_COUNT, sportService.findAllPlayers(null).size()); + assertEquals(TestConstants.PLAYER_CHRIS_COUNT, sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).size()); + } + + @Test + @Order(5) + void testSavePlayer() { + Player player = new Player(); + player.setFirstName(TestConstants.PLAYER_FIRSTNAME); + player.setLastName(TestConstants.PLAYER_LASTNAME); + player = sportService.savePlayer(player); + assertNotNull(player); + } + + @Test + @Order(6) + void testDeletePlayer() { + List players = sportService.findAllPlayers(TestConstants.PLAYER_LASTNAME); + assertEquals(1, players.size()); + Player player = players.get(0); + sportService.deletePlayer(player); + assertEquals(TestConstants.PLAYER_COUNT, sportService.findAllPlayers(null).size()); + } + + + @Test + @Order(7) + void testFindAllPositions() { + assertEquals(TestConstants.POSITION_COUNT, sportService.findAllPositions(null).size()); + assertEquals(TestConstants.POSITION_CENTER_COUNT, sportService.findAllPositions(TestConstants.POSITION_CENTER).size()); + } + + @Test + @Order(8) + void testFindAllPositionsForSport() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + assertEquals(1, sports.size()); + Sport football = sports.get(0); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, sportService.findAllPositionsForSport(football).size()); + } + + @Test + @Order(9) + void testSavePosition() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + Sport football = sports.get(0); + FieldPosition position = new FieldPosition(); + position.setSport(football); + position.setName(TestConstants.POSITION_NAME); + position.setShortName(TestConstants.POSITION_SHORTNAME); + sportService.savePosition(position); + assertEquals(TestConstants.POSITION_COUNT+1, sportService.findAllPositions(null).size()); + } + + @Test + @Order(10) + void testDeletePosition() { + List positions = sportService.findAllPositions(TestConstants.POSITION_NAME); + assertEquals(1, positions.size()); + FieldPosition position = positions.get(0); + sportService.deletePosition(position); + assertEquals(TestConstants.POSITION_COUNT, sportService.findAllPositions(null).size()); + } + + @Test + @Order(11) + void testFindAllTeams() { + assertEquals(TestConstants.TEAM_COUNT, sportService.findAllTeams(null).size()); + assertEquals(1, sportService.findAllTeams("sharks").size()); + } + + @Test + @Order(12) + void testSaveTeam() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + Sport football = sports.get(0); + Team team = new Team(); + team.setSport(football); + team.setName(TestConstants.TEAM_NAME); + team.setShortName(TestConstants.TEAM_SHORTNAME); + sportService.saveTeam(team); + assertEquals(TestConstants.TEAM_COUNT+1, sportService.findAllTeams(null).size()); + } + + @Test + @Order(13) + void testDeleteTeam() { + List teams = sportService.findAllTeams(TestConstants.TEAM_NAME); + assertEquals(1, teams.size()); + Team team = teams.get(0); + sportService.deleteTeam(team); + assertEquals(TestConstants.TEAM_COUNT, sportService.findAllTeams(null).size()); + } + + @Test + @Order(14) + void testFindAllRoosters() { + assertEquals(TestConstants.ROOSTER_COUNT, sportService.findAllRoosters().size()); + } + + @Test + @Order(15) + void testFindRoosterByFields() { + List roosters = sportService.findAllRoosters(); + Rooster existingRooster = roosters.get(4); + Team team = existingRooster.getTeam(); + Player player = existingRooster.getPlayer(); + FieldPosition position = existingRooster.getPosition(); + int year = existingRooster.getYear(); + Rooster rooster = sportService.findRoosterByFields(team, player, position, year); + assertNotNull(rooster); + assertEquals(existingRooster.getId(), rooster.getId()); + } + + @Test + @Order(16) + void testSaveRooster() { + Rooster rooster = new Rooster(); + Player player = sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).get(0); + rooster.setPlayer(player); + Team team = sportService.findAllTeams(TestConstants.DOLPHINS_NAME).get(0); + rooster.setTeam(team); + FieldPosition position = sportService.findAllPositions(TestConstants.QUARTERBACK_NAME).get(0); + rooster.setPosition(position); + rooster.setYear(TestConstants.ROOSTER_YEAR); + sportService.saveRooster(rooster); + assertEquals(TestConstants.ROOSTER_COUNT+1, sportService.findAllRoosters().size()); + } + + @Test + @Order(17) + void testDeleteRooster() { + Team team = sportService.findAllTeams(TestConstants.DOLPHINS_NAME).get(0); + Player player = sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).get(0); + FieldPosition position = sportService.findAllPositions(TestConstants.QUARTERBACK_NAME).get(0); + Rooster rooster = sportService.findRoosterByFields(team, player, position, TestConstants.ROOSTER_YEAR); + assertNotNull(rooster); + sportService.deleteRooster(rooster); + assertEquals(TestConstants.ROOSTER_COUNT, sportService.findAllRoosters().size()); + } +} diff --git a/springboot/src/test/resources/application.properties b/springboot/src/test/resources/application.properties new file mode 100644 index 0000000..53be2f4 --- /dev/null +++ b/springboot/src/test/resources/application.properties @@ -0,0 +1,30 @@ +server.port=8085 + +spring.hibernate.dialect=org.hibernate.dialect.HSQLDialect +spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect +spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver +spring.datasource.url=jdbc:hsqldb:mem:testDb +spring.datasource.username=sa +spring.datasource.password=sa + +#spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +#spring.datasource.driverClassName=org.sqlite.JDBC +#spring.datasource.url=jdbc:sqlite:file:./kontorTesrDb?cache=shared +#spring.datasource.username=sa +#spring.datasource.password=sa + +spring.jpa.defer-datasource-initialization = true +#spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.sql.init.mode=always + +spring.mustache.check-template-location = false + +logging.level.org.atmosphere=INFO +logging.level.org.springframework.web=INFO +logging.level.guru.springframework.controllers=DEBUG +logging.level.org.hibernate=INFO +logging.level.de.thpeetz=DEBUG + +jwt.auth.secret=J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc= -- 2.18.0 From 1a7da0ab9f2ed890b719ec0028d8a375242f19f5 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 7 Jan 2025 01:48:03 +0100 Subject: [PATCH 08/16] add column header info --- .gitignore | 1 + .gitlab-ci.yml | 36 ++ gui/model_config.py | 72 ++-- gui/table_model.py | 11 +- .../kontor/admin/SetupModuleAdmin.java | 358 +++++++++--------- .../kontor/admin/data/MetaDataColumn.java | 6 + .../admin/services/MetaDataService.java | 20 +- .../kontor/admin/views/MetaDataForm.java | 8 +- .../kontor/admin/views/MetaDataView.java | 2 +- 9 files changed, 289 insertions(+), 225 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitignore b/.gitignore index 575904a..2330d74 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ bonus/ icons/ icons-shadowless/ +.vscode/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..7231f34 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,36 @@ +image: gradle:8.6-jdk21-alpine + +stages: + - build + - test + - publish + +# Disable the Gradle daemon for Continuous Integration servers as correctness +# is usually a priority over speed in CI environments. Using a fresh +# runtime for each build is more reliable since the runtime is completely +# isolated from any previous builds. +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - GRADLE_USER_HOME="$(pwd)/.gradle" + - export GRADLE_USER_HOME + +build: + stage: build + script: + - cd springboot + - gradle assemble --no-daemon + +test: + stage: test + script: + - cd springboot + - gradle check --no-daemon + +publish: + stage: publish + script: + - cd springboot + - gradle --no-daemon publish -PgitlabPackageRegistryUsername=gitlab-ci-token -PgitlabPackageRegistryPassword="$CI_JOB_TOKEN" + diff --git a/gui/model_config.py b/gui/model_config.py index 1bea8a5..a9c6702 100644 --- a/gui/model_config.py +++ b/gui/model_config.py @@ -5,10 +5,11 @@ from PySide6.QtWidgets import QHBoxLayout, QCheckBox class KontorModelConfig: def __init__(self, db_config, main_window, table_name: str): - self.header = [] + self.header = {} self.filter = {} self.main_window = main_window self._table = table_name + self._table_id = None self.db_conn = mariadb.connect( host=db_config['mariadb']['host'], port=db_config['mariadb']['port'], @@ -19,64 +20,65 @@ class KontorModelConfig: self.get_table_config() def get_table_id(self): + if self._table_id is not None: + return cursor = self.db_conn.cursor() cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (self._table, )) rows = cursor.fetchall() if len(rows) == 1: - return rows[0][0] - return None + self._table_id = rows[0][0] def get_table_config(self): - table_id = self.get_table_id() + if self._table_id is None: + self.get_table_id() cursor = self.db_conn.cursor() - cursor.execute("SELECT id, column_name, column_order FROM meta_data_column WHERE table_id=? AND is_shown is true", (table_id, )) + cursor.execute("SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", (self._table_id, )) rows = cursor.fetchall() self.header.clear() - for (column_id, column_name, column_order) in rows: - self.header.insert(column_order-1, column_name) - print(f"retrieved {len(rows)} columns, set {len(self.header)} headers") - - def get_header(self) -> list: - self.get_table_config() - return self.header + order = 0 + for (column_name, column_order, column_label) in rows: + self.header[order] = { 'column': column_name, 'label': column_label, 'order': column_order} + order += 1 + # print(f"retrieved {len(rows)} columns, set {len(self.header)} headers") + cursor.execute("SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", (self._table_id, )) + rows = cursor.fetchall() + for row in rows: + self.filter[row[0]] = {'label': row[1], 'widget': None} + # print(f"retrieved {len(rows)} filters: {self.filter}") def get_filter(self) -> str: filter_rule = "" # print(self.filter["download"].isChecked()) - if self.filter["download"].isChecked(): - # print(self.filter["download"].isChecked()) - filter_rule = "WHERE should_download is true" - if self.filter["review"].isChecked(): - if len(filter_rule) > 0: - filter_rule += " AND " - else: - filter_rule += "WHERE " - filter_rule += "review is true" - print(f"{filter_rule=}") + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + # print(column, filter_info, filter_rule, len(filter_rule)) + if len(filter_rule) < 1: + filter_rule += "WHERE " + if len(filter_rule) > 8: + filter_rule += " AND " + filter_rule += f"{column} is true" + # print(f"{filter_rule=}") return filter_rule def get_statement(self) -> str: filter_rule = self.get_filter() - self.get_table_config() + # self.get_table_config() columns = "" - for index in range(len(self.header)): + for index, column in self.header.items(): if index > 0: columns += ", " - columns += self.header[index] + columns += column['column'] statement = f"SELECT {columns} FROM media_file {filter_rule}" return statement def get_filter_layout(self) -> QHBoxLayout: filter_layout = QHBoxLayout() - download_checkbox = QCheckBox() - download_checkbox.setText("Download") - download_checkbox.checkStateChanged.connect(self.main_window.refresh) - self.filter["download"] = download_checkbox - review_checkbox = QCheckBox() - review_checkbox.setText("Review") - review_checkbox.checkStateChanged.connect(self.main_window.refresh) - self.filter["review"] = review_checkbox - filter_layout.addWidget(review_checkbox) - filter_layout.addWidget(download_checkbox) + for column, filter_info in self.filter.items(): + filter_checkbox = QCheckBox() + filter_checkbox.setText(filter_info['label']) + filter_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter[column]['widget'] = filter_checkbox + filter_layout.addWidget(filter_checkbox) filter_layout.addStretch() return filter_layout diff --git a/gui/table_model.py b/gui/table_model.py index 5fc5dc9..81eec89 100644 --- a/gui/table_model.py +++ b/gui/table_model.py @@ -19,7 +19,7 @@ class KontorTableModel(QAbstractTableModel): cursor = self._config.db_conn.cursor() cursor.execute(self._config.get_statement()) rows = cursor.fetchall() - print(len(rows)) + # print(len(rows)) if len(rows) > 0: self.beginResetModel() for row in rows: @@ -39,7 +39,7 @@ class KontorTableModel(QAbstractTableModel): def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - return self._config.header[col] + return self._config.header[col]['column'] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: return str(col+1) @@ -48,6 +48,7 @@ class KontorTableModel(QAbstractTableModel): return None value = self._data[index.row()][index.column()] if role == Qt.ItemDataRole.DisplayRole: + # print('{}: {}'.format(value, type(value))) if isinstance(value, datetime): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): @@ -57,7 +58,7 @@ class KontorTableModel(QAbstractTableModel): return self._main_window.tick else: return self._main_window.cross - return value + return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): # print('{}: {}'.format(value, type(value))) @@ -69,8 +70,8 @@ class KontorTableModel(QAbstractTableModel): def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns # the length (only works if all rows are an equal length) - print(f"Header count: {len(self._config.get_header())}") - return len(self._config.get_header()) + # print(f"Header count: {len(self._config.get_header())}") + return len(self._config.header) def setData(self, index, value, role=Qt.ItemDataRole.EditRole): if role == Qt.ItemDataRole.EditRole: diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index 419c519..de81e3b 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -128,209 +128,209 @@ public class SetupModuleAdmin implements ApplicationListener column.getColumnName().equals(columnName))) { log.info("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); @@ -54,9 +54,21 @@ public class MetaDataService { log.debug("columnModifier has to be changed to {}", columnModifier); column.setColumnModifier(columnModifier); } - if (column.getIsShown() == null) { - log.debug("isShown set to false"); - column.setIsShown(Boolean.FALSE); + if (!column.getIsShown().equals(isShown)) { + log.debug("isShown has to be change to {}}", isShown); + column.setIsShown(isShown); + } + if (columnLabel != null && !columnLabel.equals(column.getColumnLabel())) { + log.debug("columnLabel has to be change to {}}", columnLabel); + column.setColumnLabel(columnLabel); + } + if (showFilter != null &&!showFilter.equals(column.getShowFilter())) { + log.debug("showFilter has to be change to {}}", showFilter); + column.setShowFilter(showFilter); + } + if (filterLabel != null && !filterLabel.equals(column.getFilterLabel())) { + log.debug("filterLabel has to be change to {}}", filterLabel); + column.setFilterLabel(filterLabel); } metaDataColumnRepository.save(column); } else { diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java index f70c161..19d4459 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -26,6 +26,7 @@ public class MetaDataForm extends FormLayout { TextField columnModifier = new TextField("Column Modifier"); IntegerField columnOrder = new IntegerField("Column Order"); Checkbox isShown = new Checkbox("Is Shown"); + Checkbox showFilter = new Checkbox("Show Filter"); Button save = new com.vaadin.flow.component.button.Button("Save"); Button delete = new com.vaadin.flow.component.button.Button("Delete"); @@ -39,7 +40,12 @@ public class MetaDataForm extends FormLayout { table.setItems(tables); table.setItemLabelGenerator(MetaDataTable::getTableName); - add(table, columnName, columnSyncName, columnModifier, columnOrder, isShown, createButtonsLayout()); + add(table, 2); + add(columnName, 2); + add(columnSyncName, 2); + add(columnModifier, 2); + add(columnOrder, 2); + add(isShown, showFilter, createButtonsLayout()); } private HorizontalLayout createButtonsLayout() { diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java index 661d4a3..f3b2509 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -42,7 +42,7 @@ public class MetaDataView extends VerticalLayout { private void configureGrid() { grid.addClassName("metadata-grid"); grid.setSizeFull(); - grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown"); + grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown", "showFilter"); grid.getColumns().forEach(col -> col.setAutoWidth(true)); grid.asSingleSelect().addValueChangeListener(event -> editMetaData(event.getValue())); } -- 2.18.0 From c7691153310d061a08e66e89c3206d4006a8e393 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 7 Jan 2025 21:18:14 +0100 Subject: [PATCH 09/16] implement general data tab view --- gui/data.py | 79 +++++++++++++++++ gui/dialogs.py | 85 +++++++++++++++++++ gui/kontor.py | 54 +++++++----- gui/model_config.py | 48 +++-------- gui/table_model.py | 24 +++--- .../kontor/admin/SetupModuleAdmin.java | 30 +++---- 6 files changed, 236 insertions(+), 84 deletions(-) create mode 100644 gui/data.py create mode 100644 gui/dialogs.py diff --git a/gui/data.py b/gui/data.py new file mode 100644 index 0000000..bb4436c --- /dev/null +++ b/gui/data.py @@ -0,0 +1,79 @@ +import mariadb + + +class KontorDB: + + def __init__(self, db_config): + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + + def get_table_id(self, table_name): + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (table_name, )) + row = cursor.fetchone() + cursor.close() + return row[0] + + def get_table_names(self) -> list: + tables_names = [] + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, table_name from meta_data_table") + rows = cursor.fetchall() + for (_, table_name) in rows: + tables_names.append(table_name) + cursor.close() + return tables_names + + def get_column_meta_data(self, table_id): + cursor = self.db_conn.cursor() + meta_data = {} + cursor.execute("SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", (table_id, )) + rows = cursor.fetchall() + order = 0 + for (column_name, column_order, column_label) in rows: + meta_data[order] = { 'column': column_name, 'label': column_label, 'order': column_order} + order += 1 + cursor.close() + # print(f"retrieved {len(rows)} columns, set {len(meta_data)} headers") + return meta_data + + def get_filters(self, table_id): + cursor = self.db_conn.cursor() + filters = {} + cursor.execute("SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", (table_id, )) + rows = cursor.fetchall() + for row in rows: + filters[row[0]] = {'label': row[1], 'widget': None} + cursor.close() + # print(f"retrieved {len(rows)} filters: {filters}") + return filters + + def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: + data = [] + cursor = self.db_conn.cursor() + cursor.execute(self.get_statement(table_name, columns, where_clause)) + rows = cursor.fetchall() + print(len(rows)) + for row in rows: + # print(f"KontorDB.get_data: {row}") + data.append(list(row)) + cursor.close() + print(f"KontorDB.getData: return {len(data)}") + return data + + def get_statement(self, table: str, header: dict, where_clause): + columns = "" + for index, column in header.items(): + if index > 0: + columns += ", " + columns += column['column'] + if len(columns) == 0: + columns = "*" + statement = f"SELECT {columns} FROM {table} {where_clause}" + print(f"{statement=}") + return statement diff --git a/gui/dialogs.py b/gui/dialogs.py new file mode 100644 index 0000000..ae69c3d --- /dev/null +++ b/gui/dialogs.py @@ -0,0 +1,85 @@ +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ + QGroupBox, QCheckBox + + +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 = None + self.tables = [] + self._table_options = {} + + buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(buttons) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.label = QLabel() + self.label.setText("Export DB to 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) + + for table_name in self.kontor_db.get_table_names(): + check_box = QCheckBox(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 select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + 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}") diff --git a/gui/kontor.py b/gui/kontor.py index 3b601ad..771c466 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -11,6 +11,8 @@ from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs from comic_model import ComicTableModel +from dialogs import ExportKontorDialog, ImportKontorDialog +from data import KontorDB from model_config import KontorModelConfig from table_model import KontorTableModel @@ -35,12 +37,13 @@ class MainWindow(QMainWindow): self.data = [] self.filter = {} + self.kontor_db = KontorDB(config) self.central_widget = QWidget() parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) self.tabs = QTabWidget() - self.tabs.addTab(self.generate_tab_comics(config), "Comics") - self.tabs.addTab(self.generate_tab_media_file(config), "MediaFile") + self.tabs.addTab(self.generate_data_tab("comic"), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") self.tabs.currentChanged.connect(self._tab_changed) #label.setAlignment(Qt.AlignmentFlag.AlignCenter) parent_layout.addWidget(self.tabs) @@ -52,9 +55,13 @@ class MainWindow(QMainWindow): self.aboutAction = QAction("&Über...", self) self.aboutAction.triggered.connect(self.about) 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.downloadAction = QAction("&Download Videos", self) self.exitAction = QAction("&Beenden", self) self.exitAction.setShortcut("Alt+F4") self.exitAction.triggered.connect(self.close) @@ -70,6 +77,10 @@ class MainWindow(QMainWindow): menu_bar.addMenu(kontor_menu) kontor_menu.addAction(self.importAction) kontor_menu.addAction(self.exportAction) + media_file_menu = QMenu("&MediaFile") + media_file_menu.addAction(self.updateTitleAction) + media_file_menu.addAction(self.downloadAction) + kontor_menu.addMenu(media_file_menu) # Help menu help_menu = QMenu("&Hilfe") menu_bar.addMenu(help_menu) @@ -91,36 +102,41 @@ class MainWindow(QMainWindow): def about(self): QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") + def import_from_file(self): + import_dlg = ImportKontorDialog(self) + if import_dlg.exec(): + print(f"import DB from file {import_dlg.file_name}") + else: + print("no nothing for import") + pass + + def export_to_file(self): + export_dlg = ExportKontorDialog(self, self.kontor_db) + if export_dlg.exec(): + print(export_dlg.get_tables_to_export()) + self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + else: + self.statusBar.showMessage("Export cancelled", 3000) + def refresh(self): self.data[self.tabs.currentIndex()].refresh() def _tab_changed(self, tab_index): self.data[tab_index].refresh() - def generate_tab_comics(self, db_configuration): - comic_tab = QWidget() - layout = QVBoxLayout() - comic_tab.setLayout(layout) - model = ComicTableModel(db_configuration, self) - self.data.append(model) - table_view = QTableView() - table_view.setModel(model) - layout.addWidget(table_view) - return comic_tab - - def generate_tab_media_file(self, db_configuration): - media_file_tab = QWidget() - table_config = KontorModelConfig(db_configuration, self, "media_file") + def generate_data_tab(self, table_name): + data_tab = QWidget() + table_config = KontorModelConfig(self.kontor_db, self, table_name) model = KontorTableModel(table_config) layout = QVBoxLayout() - # model = MediaFileTableModel(db_configuration, self) self.data.append(model) - media_file_tab.setLayout(layout) + data_tab.setLayout(layout) table_view = QTableView() table_view.setModel(model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) - return media_file_tab + model.refresh() + return data_tab if __name__ == '__main__': diff --git a/gui/model_config.py b/gui/model_config.py index a9c6702..ba3e510 100644 --- a/gui/model_config.py +++ b/gui/model_config.py @@ -1,50 +1,30 @@ import mariadb from PySide6.QtWidgets import QHBoxLayout, QCheckBox +from data import KontorDB + class KontorModelConfig: - def __init__(self, db_config, main_window, table_name: str): + def __init__(self, kontor_db: KontorDB, main_window, table_name: str): self.header = {} self.filter = {} self.main_window = main_window self._table = table_name self._table_id = None - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) + self.kontor_db = kontor_db self.get_table_config() def get_table_id(self): if self._table_id is not None: return - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (self._table, )) - rows = cursor.fetchall() - if len(rows) == 1: - self._table_id = rows[0][0] + self._table_id = self.kontor_db.get_table_id(self._table) def get_table_config(self): if self._table_id is None: self.get_table_id() - cursor = self.db_conn.cursor() - cursor.execute("SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", (self._table_id, )) - rows = cursor.fetchall() - self.header.clear() - order = 0 - for (column_name, column_order, column_label) in rows: - self.header[order] = { 'column': column_name, 'label': column_label, 'order': column_order} - order += 1 - # print(f"retrieved {len(rows)} columns, set {len(self.header)} headers") - cursor.execute("SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", (self._table_id, )) - rows = cursor.fetchall() - for row in rows: - self.filter[row[0]] = {'label': row[1], 'widget': None} - # print(f"retrieved {len(rows)} filters: {self.filter}") + self.header = self.kontor_db.get_column_meta_data(self._table_id) + self.filter = self.kontor_db.get_filters(self._table_id) def get_filter(self) -> str: filter_rule = "" @@ -61,16 +41,10 @@ class KontorModelConfig: # print(f"{filter_rule=}") return filter_rule - def get_statement(self) -> str: - filter_rule = self.get_filter() - # self.get_table_config() - columns = "" - for index, column in self.header.items(): - if index > 0: - columns += ", " - columns += column['column'] - statement = f"SELECT {columns} FROM media_file {filter_rule}" - return statement + def get_data(self) -> list: + data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) + print(f"KontorModelConfig.get_data: {len(data)}") + return data def get_filter_layout(self) -> QHBoxLayout: filter_layout = QHBoxLayout() diff --git a/gui/table_model.py b/gui/table_model.py index 81eec89..a28e1cc 100644 --- a/gui/table_model.py +++ b/gui/table_model.py @@ -12,24 +12,22 @@ class KontorTableModel(QAbstractTableModel): super().__init__() self._main_window = model_config.main_window self._config = model_config - self._data = None + self._data = [] def refresh(self): - data = [] - cursor = self._config.db_conn.cursor() - cursor.execute(self._config.get_statement()) - rows = cursor.fetchall() - # print(len(rows)) - if len(rows) > 0: + data = self._config.get_data() + count = 0 + # print(data) + if data is not None: self.beginResetModel() - for row in rows: - data.append(list(row)) + self._data.clear() self._data = data self.endResetModel() - else: - self._data = None + count = len(data) + # print(data) + # print(self._data) self.layoutChanged.emit() - self._main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) + self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. @@ -39,7 +37,7 @@ class KontorTableModel(QAbstractTableModel): def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - return self._config.header[col]['column'] + return self._config.header[col]['label'] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: return str(col+1) diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index de81e3b..81c6c6a 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -128,22 +128,22 @@ public class SetupModuleAdmin implements ApplicationListener Date: Wed, 8 Jan 2025 08:05:09 +0100 Subject: [PATCH 10/16] prevent NPE --- .../java/de/thpeetz/kontor/admin/services/MetaDataService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java index 904a7e1..3f1c773 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java @@ -54,7 +54,7 @@ public class MetaDataService { log.debug("columnModifier has to be changed to {}", columnModifier); column.setColumnModifier(columnModifier); } - if (!column.getIsShown().equals(isShown)) { + if (isShown != null && !isShown.equals(column.getIsShown())) { log.debug("isShown has to be change to {}}", isShown); column.setIsShown(isShown); } -- 2.18.0 From 8d31a926926fd10e8357ec6565625f09388f55ef Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 10:39:52 +0100 Subject: [PATCH 11/16] improve view of meta data by using icons for boolean and add search --- .../kontor/admin/SetupModuleAdmin.java | 2 +- .../kontor/admin/data/MetaDataColumn.java | 4 + .../admin/data/MetaDataColumnRepository.java | 6 + .../admin/services/MetaDataService.java | 13 +- .../kontor/admin/views/MetaDataForm.java | 14 ++- .../kontor/admin/views/MetaDataView.java | 117 +++++++++++++++++- .../media/services/MediaFileService.java | 4 +- 7 files changed, 143 insertions(+), 17 deletions(-) diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index 81c6c6a..4ac89e0 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -177,7 +177,7 @@ public class SetupModuleAdmin implements ApplicationListener { List findByTable(MetaDataTable table); + + @Query("select m from MetaDataColumn m " + + "where lower(m.columnName) like lower(concat('%', :searchTerm, '%')) or lower(m.columnLabel) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); } diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java index 3f1c773..98ae1c3 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java @@ -36,7 +36,7 @@ public class MetaDataService { public void getColumn(MetaDataTable table, String columnName, String columnSyncName, String columnType, String columnModifier, Integer columnOrder, Boolean isShown, String columnLabel, Boolean showFilter, String filterLabel) { if (table.getTableColumns().stream().anyMatch(column -> column.getColumnName().equals(columnName))) { - log.info("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); + log.debug("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); if (!column.getColumnName().equals(columnName)) { log.debug("columnName has to be changed to {}", columnName); @@ -85,8 +85,15 @@ public class MetaDataService { } } - public List findAllMetaDataColumns() { - return metaDataColumnRepository.findAll(); + public List findAllMetaDataColumns(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + log.debug("Found " + metaDataColumnRepository.count()+ " entries"); + return metaDataColumnRepository.findAll(); + } else { + List results = metaDataColumnRepository.search(stringFilter); + log.debug("Found " + results.size() + " entries"); + return results; + } } public void deleteMetaDataColumn(MetaDataColumn metaDataColumn) { diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java index 19d4459..b65b099 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -26,7 +26,9 @@ public class MetaDataForm extends FormLayout { TextField columnModifier = new TextField("Column Modifier"); IntegerField columnOrder = new IntegerField("Column Order"); Checkbox isShown = new Checkbox("Is Shown"); + TextField columnLabel = new TextField("Column Label"); Checkbox showFilter = new Checkbox("Show Filter"); + TextField filterLabel = new TextField("Filter Label"); Button save = new com.vaadin.flow.component.button.Button("Save"); Button delete = new com.vaadin.flow.component.button.Button("Delete"); @@ -40,12 +42,12 @@ public class MetaDataForm extends FormLayout { table.setItems(tables); table.setItemLabelGenerator(MetaDataTable::getTableName); - add(table, 2); - add(columnName, 2); - add(columnSyncName, 2); - add(columnModifier, 2); - add(columnOrder, 2); - add(isShown, showFilter, createButtonsLayout()); + add(table, columnName, columnSyncName, columnModifier, columnOrder); + add(isShown, columnLabel); + isShown.addClickListener(click -> columnLabel.setEnabled(isShown.getValue())); + add(showFilter, filterLabel); + showFilter.addClickListener(click -> filterLabel.setEnabled(showFilter.getValue())); + add(createButtonsLayout()); } private HorizontalLayout createButtonsLayout() { diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java index f3b2509..780126a 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -2,9 +2,18 @@ package de.thpeetz.kontor.admin.views; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridSortOrder; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.provider.SortDirection; +import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.spring.annotation.SpringComponent; @@ -13,9 +22,13 @@ import de.thpeetz.kontor.admin.data.MetaDataColumn; import de.thpeetz.kontor.admin.services.MetaDataService; import de.thpeetz.kontor.common.views.MainLayout; import jakarta.annotation.security.RolesAllowed; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Scope; +import java.util.ArrayList; +import java.util.List; + @Slf4j @SpringComponent @Scope("prototype") @@ -24,10 +37,56 @@ import org.springframework.context.annotation.Scope; @PageTitle("Meta Data | Admin | Kontor") public class MetaDataView extends VerticalLayout { - Grid grid = new Grid<>(MetaDataColumn.class); + Grid grid = new Grid<>(MetaDataColumn.class, false); + Grid.Column idColumn = grid.addColumn(MetaDataColumn::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(MetaDataColumn::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(MetaDataColumn::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column versionColumn = grid.addColumn(MetaDataColumn::getVersion) + .setHeader("Version").setResizable(true).setSortable(true); + Grid.Column tableColumn = grid.addColumn(MetaDataColumn::getTableName) + .setHeader("Table").setResizable(true).setSortable(true); + Grid.Column columnNameColumn = grid.addColumn(MetaDataColumn::getColumnName) + .setHeader("Column Name").setResizable(true).setSortable(true); + Grid.Column columnSyncNameColumn = grid.addColumn(MetaDataColumn::getColumnSyncName) + .setHeader("Column Sync Name").setResizable(true).setSortable(true); + Grid.Column columnTypeColumn = grid.addColumn(MetaDataColumn::getColumnType) + .setHeader("Column Type").setResizable(true).setSortable(true); + Grid.Column columnModifierColumn = grid.addColumn(MetaDataColumn::getColumnModifier) + .setHeader("Column Modifier").setResizable(true).setSortable(true); + Grid.Column columnOrderColumn = grid.addColumn(MetaDataColumn::getColumnOrder) + .setHeader("Column Order").setResizable(true).setSortable(true); + Grid.Column isShownColumn = grid.addComponentColumn(metaDataColumn -> createStatusIcon(metaDataColumn.getIsShown())). + setHeader("Anzeige?").setWidth("6rem").setSortable(true); + Grid.Column columnLabelColumn = grid.addColumn(MetaDataColumn::getColumnLabel) + .setHeader("Spaltenname").setResizable(true).setSortable(true); + Grid.Column showFilterColumn = grid.addComponentColumn(metaDataColumn -> createStatusIcon(metaDataColumn.getShowFilter())). + setHeader("Zeige Filter").setWidth("6rem").setSortable(true); + Grid.Column filterLabelColumn = grid.addColumn(MetaDataColumn::getFilterLabel) + .setHeader("Filter Name").setResizable(true).setSortable(true); + TextField searchField = new TextField(); + @Getter MetaDataForm form; MetaDataService service; + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + public MetaDataView(MetaDataService service) { this.service = service; addClassName("metadata-view"); @@ -42,8 +101,17 @@ public class MetaDataView extends VerticalLayout { private void configureGrid() { grid.addClassName("metadata-grid"); grid.setSizeFull(); - grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown", "showFilter"); + //grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown", "columnLabel", "showFilter", "filterLabel"); grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + createdColumn.setVisible(false); + modifiedColumn.setVisible(false); + versionColumn.setVisible(false); + grid.setMultiSort(true); + List sortOrder = new ArrayList(); + sortOrder.add(new GridSortOrder(tableColumn, SortDirection.ASCENDING)); + sortOrder.add(new GridSortOrder(columnOrderColumn, SortDirection.ASCENDING)); + grid.sort(sortOrder); grid.asSingleSelect().addValueChangeListener(event -> editMetaData(event.getValue())); } @@ -56,6 +124,19 @@ public class MetaDataView extends VerticalLayout { form.addCloseListener(e -> closeEditor()); } + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + private void saveMetaData(MetaDataForm.SaveEvent event) { MetaDataColumn metaDataColumn = event.getMetaDataColumn(); service.saveMetaDataColumn(metaDataColumn); @@ -79,8 +160,34 @@ public class MetaDataView extends VerticalLayout { } private HorizontalLayout getToolbar() { - Button addAuthorizationMaxtrixButton = new Button("Add Meta Data", click -> addMetaDataColumn()); - HorizontalLayout toolbar = new HorizontalLayout(addAuthorizationMaxtrixButton); + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMetaDataButton = new Button("Add Meta Data"); + addMetaDataButton.addClickListener(click -> addMetaDataColumn()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + MetaDataView.ColumnToggleContextMenu columnToggleContextMenu = new MetaDataView.ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("Erstellt", createdColumn); + columnToggleContextMenu.addColumnToggleItem("Geändert", modifiedColumn); + columnToggleContextMenu.addColumnToggleItem(versionColumn.getHeaderText(), versionColumn); + columnToggleContextMenu.addColumnToggleItem(tableColumn.getHeaderText(), tableColumn); + columnToggleContextMenu.addColumnToggleItem(columnNameColumn.getHeaderText(), columnNameColumn); + columnToggleContextMenu.addColumnToggleItem(columnSyncNameColumn.getHeaderText(), columnSyncNameColumn); + columnToggleContextMenu.addColumnToggleItem(columnTypeColumn.getHeaderText(), columnTypeColumn); + columnToggleContextMenu.addColumnToggleItem(columnModifierColumn.getHeaderText(), columnModifierColumn); + columnToggleContextMenu.addColumnToggleItem(columnOrderColumn.getHeaderText(), columnOrderColumn); + columnToggleContextMenu.addColumnToggleItem(isShownColumn.getHeaderText(), isShownColumn); + columnToggleContextMenu.addColumnToggleItem(columnLabelColumn.getHeaderText(), columnLabelColumn); + columnToggleContextMenu.addColumnToggleItem(showFilterColumn.getHeaderText(), showFilterColumn); + columnToggleContextMenu.addColumnToggleItem(filterLabelColumn.getHeaderText(), filterLabelColumn); + + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMetaDataButton, menuButton); toolbar.addClassName("toolbar"); return toolbar; } @@ -107,6 +214,6 @@ public class MetaDataView extends VerticalLayout { } private void updateList() { - grid.setItems(service.findAllMetaDataColumns()); + grid.setItems(service.findAllMetaDataColumns(searchField.getValue())); } } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java index a7fe0a1..ffb0a24 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java @@ -19,11 +19,11 @@ public class MediaFileService { public List findAllMediaFiles(String stringFilter) { if (stringFilter == null || stringFilter.isEmpty()) { - log.info("Found " + mediaFileRepository.count()+ " entries"); + log.debug("Found " + mediaFileRepository.count()+ " entries"); return mediaFileRepository.findAll(); } else { List results = mediaFileRepository.search(stringFilter); - log.info("Found " + results.size() + " entries"); + log.debug("Found " + results.size() + " entries"); return results; } } -- 2.18.0 From 24e4c0b58e61302d96dba9c9b251f301865db7de Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 14:15:29 +0100 Subject: [PATCH 12/16] import export dialog by adding selection of export type --- gui/dialogs.py | 25 +++++++++++++++++++++++-- gui/kontor.py | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/gui/dialogs.py b/gui/dialogs.py index ae69c3d..f7e51d1 100644 --- a/gui/dialogs.py +++ b/gui/dialogs.py @@ -1,5 +1,7 @@ +from pathlib import Path + from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ - QGroupBox, QCheckBox + QGroupBox, QCheckBox, QComboBox class ExportKontorDialog(QDialog): @@ -12,17 +14,26 @@ class ExportKontorDialog(QDialog): 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") - layout = QVBoxLayout() + + 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) @@ -30,6 +41,8 @@ class ExportKontorDialog(QDialog): 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) @@ -42,11 +55,19 @@ class ExportKontorDialog(QDialog): 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: diff --git a/gui/kontor.py b/gui/kontor.py index 771c466..30bb3bd 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -114,6 +114,7 @@ class MainWindow(QMainWindow): export_dlg = ExportKontorDialog(self, self.kontor_db) if export_dlg.exec(): print(export_dlg.get_tables_to_export()) + print(f"export DB to {export_dlg.file_name}") self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) else: self.statusBar.showMessage("Export cancelled", 3000) -- 2.18.0 From e078f43cc6596c22a06110a178ce5e15157506ee Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 14:28:18 +0000 Subject: [PATCH 13/16] set correct project id in build.gradle --- springboot/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/springboot/build.gradle b/springboot/build.gradle index 8892faf..0624568 100644 --- a/springboot/build.gradle +++ b/springboot/build.gradle @@ -97,7 +97,7 @@ publishing { repositories { maven { name = "gitlabPackageRegistry" - url = uri("https://gitlab.com/api/v4/projects/62010300/packages/maven") + url = uri("https://gitlab.com/api/v4/projects/64726715/packages/maven") credentials(PasswordCredentials) } } -- 2.18.0 From fc4110b11d9456603686dfe7dff26df7e33c492c Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 15:34:11 +0100 Subject: [PATCH 14/16] fix problem in string formatting --- gui/dialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/dialogs.py b/gui/dialogs.py index f7e51d1..703baeb 100644 --- a/gui/dialogs.py +++ b/gui/dialogs.py @@ -57,13 +57,13 @@ class ExportKontorDialog(QDialog): def change_export_type(self, text): self.current_export_type = text - self.label.setText(f"Export DB to data.{self.export_options[text]["ext"]}") + 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"]}") + 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) -- 2.18.0 From 6923b3e5eeb76dac76ff8d0498fb73af2951f3cd Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 22:31:20 +0100 Subject: [PATCH 15/16] import from kontor-flask --- flask/config.py | 47 ++ flask/docs/Makefile | 20 + flask/docs/conf.py | 200 +++++++++ flask/docs/index.rst | 20 + flask/kontor/__init__.py | 74 ++++ flask/kontor/admin/__init__.py | 4 + flask/kontor/admin/forms.py | 17 + flask/kontor/admin/views.py | 63 +++ flask/kontor/auth/__init__.py | 4 + flask/kontor/auth/forms.py | 50 +++ flask/kontor/auth/views.py | 79 ++++ flask/kontor/comics/__init__.py | 3 + flask/kontor/comics/forms.py | 31 ++ flask/kontor/comics/models.py | 51 +++ flask/kontor/comics/views.py | 222 ++++++++++ flask/kontor/home/__init__.py | 3 + flask/kontor/home/views.py | 39 ++ flask/kontor/library/__init__.py | 3 + flask/kontor/library/forms.py | 35 ++ flask/kontor/library/models.py | 55 +++ flask/kontor/library/views.py | 228 ++++++++++ flask/kontor/models.py | 66 +++ flask/kontor/office/__init__.py | 3 + flask/kontor/static/css/style.css | 126 ++++++ flask/kontor/templates/admin/users/user.html | 21 + flask/kontor/templates/admin/users/users.html | 58 +++ flask/kontor/templates/auth/login.html | 18 + flask/kontor/templates/auth/register.html | 14 + flask/kontor/templates/base.html | 107 +++++ flask/kontor/templates/comics/artists.html | 58 +++ flask/kontor/templates/comics/comics.html | 66 +++ flask/kontor/templates/comics/publishers.html | 58 +++ .../templates/home/admin_dashboard.html | 31 ++ flask/kontor/templates/home/dashboard.html | 20 + flask/kontor/templates/home/index.html | 20 + flask/kontor/templates/library/authors.html | 58 +++ flask/kontor/templates/library/books.html | 74 ++++ .../kontor/templates/library/publishers.html | 58 +++ flask/kontor/templates/simpleform.html | 21 + .../kontor/templates/tysc/manufacturers.html | 58 +++ flask/kontor/templates/tysc/players.html | 60 +++ flask/kontor/templates/tysc/positions.html | 62 +++ flask/kontor/templates/tysc/sports.html | 58 +++ flask/kontor/templates/tysc/teams.html | 60 +++ flask/kontor/tysc/__init__.py | 377 ++++++++++++++++ flask/kontor/tysc/forms.py | 76 ++++ flask/kontor/tysc/models.py | 155 +++++++ flask/kontor/tysc/views.py | 405 ++++++++++++++++++ flask/kontor/version.py | 2 + flask/pylint.cfg | 382 +++++++++++++++++ flask/requirements.txt | 11 + flask/tests/__init__.py | 38 ++ flask/tests/test_comics_model.py | 51 +++ flask/tests/test_comics_view.py | 44 ++ flask/tests/test_config.txt | 4 + flask/tests/test_kontor_model.py | 14 + flask/tests/test_kontor_view.py | 58 +++ flask/tests/test_library_model.py | 49 +++ flask/tests/test_library_view.py | 44 ++ flask/tests/test_tysc_model.py | 53 +++ flask/tests/test_tysc_view.py | 110 +++++ 61 files changed, 4296 insertions(+) create mode 100644 flask/config.py create mode 100644 flask/docs/Makefile create mode 100644 flask/docs/conf.py create mode 100644 flask/docs/index.rst create mode 100644 flask/kontor/__init__.py create mode 100644 flask/kontor/admin/__init__.py create mode 100644 flask/kontor/admin/forms.py create mode 100644 flask/kontor/admin/views.py create mode 100644 flask/kontor/auth/__init__.py create mode 100644 flask/kontor/auth/forms.py create mode 100644 flask/kontor/auth/views.py create mode 100644 flask/kontor/comics/__init__.py create mode 100644 flask/kontor/comics/forms.py create mode 100644 flask/kontor/comics/models.py create mode 100644 flask/kontor/comics/views.py create mode 100644 flask/kontor/home/__init__.py create mode 100644 flask/kontor/home/views.py create mode 100644 flask/kontor/library/__init__.py create mode 100644 flask/kontor/library/forms.py create mode 100644 flask/kontor/library/models.py create mode 100644 flask/kontor/library/views.py create mode 100644 flask/kontor/models.py create mode 100644 flask/kontor/office/__init__.py create mode 100644 flask/kontor/static/css/style.css create mode 100644 flask/kontor/templates/admin/users/user.html create mode 100644 flask/kontor/templates/admin/users/users.html create mode 100644 flask/kontor/templates/auth/login.html create mode 100644 flask/kontor/templates/auth/register.html create mode 100644 flask/kontor/templates/base.html create mode 100644 flask/kontor/templates/comics/artists.html create mode 100644 flask/kontor/templates/comics/comics.html create mode 100644 flask/kontor/templates/comics/publishers.html create mode 100644 flask/kontor/templates/home/admin_dashboard.html create mode 100644 flask/kontor/templates/home/dashboard.html create mode 100644 flask/kontor/templates/home/index.html create mode 100644 flask/kontor/templates/library/authors.html create mode 100644 flask/kontor/templates/library/books.html create mode 100644 flask/kontor/templates/library/publishers.html create mode 100644 flask/kontor/templates/simpleform.html create mode 100644 flask/kontor/templates/tysc/manufacturers.html create mode 100644 flask/kontor/templates/tysc/players.html create mode 100644 flask/kontor/templates/tysc/positions.html create mode 100644 flask/kontor/templates/tysc/sports.html create mode 100644 flask/kontor/templates/tysc/teams.html create mode 100644 flask/kontor/tysc/__init__.py create mode 100644 flask/kontor/tysc/forms.py create mode 100644 flask/kontor/tysc/models.py create mode 100644 flask/kontor/tysc/views.py create mode 100644 flask/kontor/version.py create mode 100644 flask/pylint.cfg create mode 100644 flask/requirements.txt create mode 100644 flask/tests/__init__.py create mode 100644 flask/tests/test_comics_model.py create mode 100644 flask/tests/test_comics_view.py create mode 100644 flask/tests/test_config.txt create mode 100644 flask/tests/test_kontor_model.py create mode 100644 flask/tests/test_kontor_view.py create mode 100644 flask/tests/test_library_model.py create mode 100644 flask/tests/test_library_view.py create mode 100644 flask/tests/test_tysc_model.py create mode 100644 flask/tests/test_tysc_view.py diff --git a/flask/config.py b/flask/config.py new file mode 100644 index 0000000..4ea5e78 --- /dev/null +++ b/flask/config.py @@ -0,0 +1,47 @@ +# config.py + + +class Config(object): + """ + Common configurations + """ + + # Put any configurations here that are common across all environments + host = '127.0.0.1' + port = 8500 + database = 'kontor' + + +class DevelopmentConfig(Config): + """ + Development configurations + """ + + DEBUG = True + host = '0.0.0.0' + database = 'kontor_dev' + + +class ProductionConfig(Config): + """ + Production configurations + """ + + DEBUG = False + +class TestingConfig(Config): + """ + Testing configurations + """ + + TESTING = True + DEBUG = True + port = 8600 + database = 'kontor_test' + +app_config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig +} + diff --git a/flask/docs/Makefile b/flask/docs/Makefile new file mode 100644 index 0000000..d7218b4 --- /dev/null +++ b/flask/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = KontorFlask +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/flask/docs/conf.py b/flask/docs/conf.py new file mode 100644 index 0000000..7278f2d --- /dev/null +++ b/flask/docs/conf.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Kontor Flask documentation build configuration file, created by +# sphinx-quickstart on Mon Nov 6 08:53:04 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Kontor Flask' +copyright = '2017, Thomas Peetz' +author = 'Thomas Peetz' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.0.7' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'de' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'KontorFlaskdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'KontorFlask.tex', 'Kontor Flask Documentation', + 'Thomas Peetz', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'kontorflask', 'Kontor Flask Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'KontorFlask', 'Kontor Flask Documentation', + author, 'KontorFlask', 'One line description of project.', + 'Miscellaneous'), +] + + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/flask/docs/index.rst b/flask/docs/index.rst new file mode 100644 index 0000000..ca4df98 --- /dev/null +++ b/flask/docs/index.rst @@ -0,0 +1,20 @@ +.. Kontor Flask documentation master file, created by + sphinx-quickstart on Mon Nov 6 08:53:04 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Kontor Flask's documentation! +======================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/flask/kontor/__init__.py b/flask/kontor/__init__.py new file mode 100644 index 0000000..961e132 --- /dev/null +++ b/flask/kontor/__init__.py @@ -0,0 +1,74 @@ +""" +Module kontor implements Kontor application. +""" + +from flask import Flask +from flask_bootstrap import Bootstrap +from flask_login import LoginManager +from pymodm import connect + +# local imports +from config import app_config +from .version import __version__ + + +_LOGIN_MANAGER_ = LoginManager() + + +def get_host(config_name): + """ + Returns host address from configuration. + :param config_name: + :return: host address + """ + host = app_config[config_name].host + return host + + +def get_port(config_name): + """ + Returns port number from configuration. + :param config_name: + :return: port number + """ + port = app_config[config_name].port + return port + + +def create_app(config_name): + """ + Returns Flask application. + :param config_name: + :return: Flask application + """ + app = Flask(__name__, instance_relative_config=True) + app.config.from_object(app_config[config_name]) + app.config.from_pyfile('config.py') + + database = "mongodb://localhost:27017/{}".format(app_config[config_name].database) + connect(database, alias="kontor") + + Bootstrap(app) + _LOGIN_MANAGER_.init_app(app) + _LOGIN_MANAGER_.login_message = "You must be logged in to access this page." + _LOGIN_MANAGER_.login_view = "auth.login" + + from .admin.views import ADMIN as ADMIN_BLUEPRINT + app.register_blueprint(ADMIN_BLUEPRINT, url_prefix='/admin') + + from .auth.views import AUTH as AUTH_BLUEPRINT + app.register_blueprint(AUTH_BLUEPRINT) + + from .home.views import HOME as HOME_BLUEPRINT + app.register_blueprint(HOME_BLUEPRINT) + + from .comics.views import COMIC as COMIC_BLUEPRINT + app.register_blueprint(COMIC_BLUEPRINT, url_prefix='/comics') + + from .library.views import LIBRARY as LIBRARY_BLUEPRINT + app.register_blueprint(LIBRARY_BLUEPRINT, url_prefix='/library') + + from .tysc.views import TYSC as TYSC_BLUEPRINT + app.register_blueprint(TYSC_BLUEPRINT, url_prefix='/tysc') + + return app diff --git a/flask/kontor/admin/__init__.py b/flask/kontor/admin/__init__.py new file mode 100644 index 0000000..ca00a4b --- /dev/null +++ b/flask/kontor/admin/__init__.py @@ -0,0 +1,4 @@ +""" +Module admin implements administration functions. +""" +from . import views diff --git a/flask/kontor/admin/forms.py b/flask/kontor/admin/forms.py new file mode 100644 index 0000000..d05cfaf --- /dev/null +++ b/flask/kontor/admin/forms.py @@ -0,0 +1,17 @@ +""" +Define form to edit users +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField +from wtforms.validators import DataRequired + + +class UserEditForm(FlaskForm): + """ + Form for admin to edit users + """ + email = StringField('Email', validators=[DataRequired()]) + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First Name', validators=[DataRequired()]) + last_name = StringField('Last Name', validators=[DataRequired()]) + submit = SubmitField('Submit') diff --git a/flask/kontor/admin/views.py b/flask/kontor/admin/views.py new file mode 100644 index 0000000..e96d8f0 --- /dev/null +++ b/flask/kontor/admin/views.py @@ -0,0 +1,63 @@ +""" +Define routing rules for managing users +""" +from flask import abort, Blueprint, flash, redirect, render_template, url_for +from flask_login import current_user, login_required + +from .forms import UserEditForm +from ..models import User + +ADMIN = Blueprint('admin', __name__) + + +def check_admin(): + """ + Prevent non-admins from accessing the page + """ + if not current_user.is_admin: + abort(403) + + +# User Views +@ADMIN.route('/users') +@login_required +def list_users(): + """ + List all users + """ + check_admin() + + users = User.objects.all() + return render_template('admin/users/users.html', + users=users, title='Users') + + +@ADMIN.route('/users/edit/', methods=['GET', 'POST']) +@login_required +def edit_user(user_id): + """ + Edit an user. + """ + check_admin() + + user = User.objects.get({'username': user_id}) + + # prevent admin from being assigned a department or role + if user.is_admin: + abort(403) + + form = UserEditForm(obj=user) + if form.validate_on_submit(): + user.email = form.email.data + user.username = form.username.data + user.first_name = form.first_name.data + user.last_name = form.last_name.data + user.save() + flash('You have successfully edited an user.') + + # redirect to the roles page + return redirect(url_for('admin.list_users')) + + return render_template('admin/users/user.html', + user=user, form=form, + title='Edit User') diff --git a/flask/kontor/auth/__init__.py b/flask/kontor/auth/__init__.py new file mode 100644 index 0000000..2d9b1ca --- /dev/null +++ b/flask/kontor/auth/__init__.py @@ -0,0 +1,4 @@ +""" +Module auth implements authentication functions. +""" +from . import views diff --git a/flask/kontor/auth/forms.py b/flask/kontor/auth/forms.py new file mode 100644 index 0000000..6e4d6dd --- /dev/null +++ b/flask/kontor/auth/forms.py @@ -0,0 +1,50 @@ +""" +Contains forms for authentication. +""" +from flask_wtf import FlaskForm +from wtforms import PasswordField, StringField, SubmitField, ValidationError +from wtforms.validators import DataRequired, Email, EqualTo + +from ..models import User + + +class RegistrationForm(FlaskForm): + """ + Form for users to create new account + """ + email = StringField('Email', validators=[DataRequired(), Email()]) + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First Name', validators=[DataRequired()]) + last_name = StringField('Last Name', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired(), + EqualTo('confirm_password') + ]) + confirm_password = PasswordField('Confirm Password') + submit = SubmitField('Register') + + def validate_email(self, field): + """ + Check if email is already in use. + :param field: + :return: + """ + if User.objects.raw({'email': field.data}).count() > 0: + raise ValidationError('Email is already in use.') + + def validate_username(self, field): + """ + check if username is already in use. + :param field: + :return: + """ + if User.objects.raw({'username': field.data}).count() > 0: + raise ValidationError('Username is already in use.') + + +class LoginForm(FlaskForm): + """ + Form for users to login + """ + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') diff --git a/flask/kontor/auth/views.py b/flask/kontor/auth/views.py new file mode 100644 index 0000000..217ae18 --- /dev/null +++ b/flask/kontor/auth/views.py @@ -0,0 +1,79 @@ +""" +Define routing rules for registering users and login and logout. +""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required, login_user, logout_user +from .forms import LoginForm, RegistrationForm +from ..models import User + +AUTH = Blueprint('auth', __name__) + + +@AUTH.route('/register', methods=['GET', 'POST']) +def register(): + """ + Handle requests to the /register route + Add an employee to the database through the registration form + """ + form = RegistrationForm() + if form.validate_on_submit(): + user = User() + user.email = form.email.data + user.username = form.username.data + user.first_name = form.first_name.data + user.last_name = form.last_name.data + user.password = form.password.data + # add employee to the database + user.save() + flash('You have successfully registered! You may now login.') + + # redirect to the login page + return redirect(url_for('auth.login')) + + # load registration template + return render_template('auth/register.html', form=form, title='Register') + + +@AUTH.route('/login', methods=['GET', 'POST']) +def login(): + """ + Handle requests to the /login route + Validate given email with matching password + :return: + """ + form = LoginForm() + if form.validate_on_submit(): + + # check whether employee exists in the database and whether + # the password entered matches the password in the database + user = User.objects.get({'email': form.email.data}) + if user is not None and user.verify_password( + form.password.data): + # log employee in + login_user(user) + + # redirect to the appropriate dashboard page + if user.is_admin: + return redirect(url_for('home.admin_dashboard')) + else: + return redirect(url_for('home.dashboard')) + + # when login details are incorrect + else: + flash('Invalid email or password.') + + # load login template + return render_template('auth/login.html', form=form, title='Login') + +@AUTH.route('/logout') +@login_required +def logout(): + """ + Handle requests to the /logout route + Log an employee out through the logout link + """ + logout_user() + flash('You have successfully been logged out.') + + # redirect to the login page + return redirect(url_for('auth.login')) diff --git a/flask/kontor/comics/__init__.py b/flask/kontor/comics/__init__.py new file mode 100644 index 0000000..e947d82 --- /dev/null +++ b/flask/kontor/comics/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for comic related information +""" diff --git a/flask/kontor/comics/forms.py b/flask/kontor/comics/forms.py new file mode 100644 index 0000000..0bae45f --- /dev/null +++ b/flask/kontor/comics/forms.py @@ -0,0 +1,31 @@ +"""Define form to edit publisher, artists and comics""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, BooleanField, SelectField +from wtforms.validators import DataRequired +from bson import ObjectId + + +class PublisherForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class ArtistForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class ComicForm(FlaskForm): + """ + Form to add and edit Comics + """ + title = StringField('Title', validators=[DataRequired()]) + publisher = SelectField('Publisher', coerce=ObjectId) + current_order = BooleanField('Current Order') + submit = SubmitField('Submit') diff --git a/flask/kontor/comics/models.py b/flask/kontor/comics/models.py new file mode 100644 index 0000000..e39e744 --- /dev/null +++ b/flask/kontor/comics/models.py @@ -0,0 +1,51 @@ +"""This modules declares the model for Comic related information.""" + +from flask import current_app +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Publisher(MongoModel): + """Class Publisher represents a publisher of a comic.""" + name = fields.CharField() + + def __str__(self): + return "Publisher({})".format(self.name) + + @property + def comics(self): + """ + Return list of comics which has reference to this publisher + :return: + """ + comics = Comic.objects.raw({'publisher': self.pk}) + current_app.logger.debug(comics) + return comics + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Artist(MongoModel): + """Class Artist represents a comic artist.""" + name = fields.CharField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Comic(MongoModel): + """Class Comic represents a comic.""" + title = fields.CharField() + publisher = fields.ReferenceField(Publisher) + current_order = fields.BooleanField(default=False) + completed = fields.BooleanField(default=False) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/comics/views.py b/flask/kontor/comics/views.py new file mode 100644 index 0000000..090b032 --- /dev/null +++ b/flask/kontor/comics/views.py @@ -0,0 +1,222 @@ +""" +Define routing rules for comics, publisher and artists +""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import ComicForm, PublisherForm, ArtistForm +from .models import Comic, Publisher, Artist + + +COMIC = Blueprint('comic', __name__) + + +@COMIC.route('/artists') +@login_required +def list_artists(): + """ + List all artists + :return: + """ + artists = Artist.objects.all() + return render_template('comics/artists.html', + artists=artists, title="Artists") + + +@COMIC.route('/artists/edit/', methods=['GET', 'POST']) +@login_required +def edit_artist(artist_id): + """ + Edit a comic artist + """ + artist = Artist.objects.get({'_id': ObjectId(artist_id)}) + form = ArtistForm(obj=artist) + if form.validate_on_submit(): + artist.name = form.name.data + artist.save() + flash('You have successfully edited the artist.') + return redirect(url_for('comic.list_artists')) + form.name.data = artist.name + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Artist") + + +@COMIC.route('/artists/add', methods=['GET', 'POST']) +@login_required +def add_artist(): + """ + Add a artist + :return: + """ + form = ArtistForm() + if form.validate_on_submit(): + artist = Artist() + artist.name = form.name.data + try: + # add publisher to the database + artist.save() + flash('You have successfully added a new artist.') + except PyMongoError: + # in case publisher name already exists + flash('Error: artist name already exists.') + return redirect(url_for('comic.list_artists')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Artist") + + +@COMIC.route('/artists/delete/', methods=['GET', 'POST']) +@login_required +def delete_artist(artist_id): + """ + Delete a comic artist + :param artist_id: + :return: + """ + artist = Artist.objects.raw({'_id': ObjectId(artist_id)}) + if artist: + artist.delete() + flash('You have successfully deleted the comic artist.') + return redirect(url_for('comic.list_artists')) + + +@COMIC.route('/publishers') +@login_required +def list_publishers(): + """ + List all publishers + :return: + """ + publishers = Publisher.objects.all() + return render_template('comics/publishers.html', + publishers=publishers, title="Publishers") + + +@COMIC.route('/publishers/add', methods=['GET', 'POST']) +@login_required +def add_publisher(): + """ + Add a publisher to the database + :return: + """ + form = PublisherForm() + if form.validate_on_submit(): + publisher = Publisher() + publisher.name = form.name.data + try: + # add publisher to the database + publisher.save() + flash('You have successfully added a new publisher.') + except PyMongoError: + # in case publisher name already exists + flash('Error: publisher name already exists.') + return redirect(url_for('comic.list_publishers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Publisher") + + +@COMIC.route('/publishers/edit/', methods=['GET', 'POST']) +@login_required +def edit_publisher(publisher_id): + """ + Edit a publisher + """ + publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) + form = PublisherForm(obj=publisher) + if form.validate_on_submit(): + publisher.name = form.name.data + publisher.save() + flash('You have successfully edited the publisher.') + return redirect(url_for('comic.list_publishers')) + form.name.data = publisher.name + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Publisher") + + +@COMIC.route('/publishers/delete/', methods=['GET', 'POST']) +@login_required +def delete_publisher(publisher_id): + """ + Delete a publisher + :param publisher_id: ObjectId of publisher + :return: + """ + publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) + if publisher: + publisher.delete() + flash('You have successfully deleted the publisher.') + return redirect(url_for('comic.list_publishers')) + + +@COMIC.route('/comics') +@login_required +def list_comics(): + """ + List all comics + :return: + """ + comics = Comic.objects.all() + return render_template('comics/comics.html', + comics=comics, title="Comics") + + +@COMIC.route('/comics/edit/', methods=['GET', 'POST']) +@login_required +def edit_comic(comic_id): + """ + Edit a comic + """ + comic = Comic.objects.get({'_id': ObjectId(comic_id)}) + form = ComicForm(obj=comic) + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + form.publisher.default = comic.publisher.pk + form.publisher.process_data(comic.publisher.pk) + if form.validate_on_submit(): + comic.title = form.title.data + comic.current_order = form.current_order.data + comic.publisher = form.publisher.data + comic.save() + flash('You have successfully edited the comic.') + return redirect(url_for('comic.list_comics')) + form.title.data = comic.title + form.current_order.data = comic.current_order + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Comic") + + +@COMIC.route('/comics/add', methods=['GET', 'POST']) +@login_required +def add_comic(): + """ + Add a comic + :return: + """ + form = ComicForm() + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + if form.validate_on_submit(): + comic = Comic() + comic.title = form.title.data + comic.publisher = form.publisher.data + try: + comic.save() + flash('You have successfully added a new comic.') + except PyMongoError: + flash('Error: comic title already exists.') + return redirect(url_for('comic.list_comics')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Comic") + + +@COMIC.route('/comics/delete/', methods=['GET', 'POST']) +@login_required +def delete_comic(comic_id): + """ + Delete a comic + :param comic_id: + :return: + """ + comic = Comic.objects.raw({'_id': ObjectId(comic_id)}) + if comic: + comic.delete() + flash('You have successfully deleted the comic.') + return redirect(url_for('comic.list_comics')) diff --git a/flask/kontor/home/__init__.py b/flask/kontor/home/__init__.py new file mode 100644 index 0000000..d4468a9 --- /dev/null +++ b/flask/kontor/home/__init__.py @@ -0,0 +1,3 @@ +""" +Module for the Kontor homepage. +""" diff --git a/flask/kontor/home/views.py b/flask/kontor/home/views.py new file mode 100644 index 0000000..4e4d539 --- /dev/null +++ b/flask/kontor/home/views.py @@ -0,0 +1,39 @@ +""" +Define routing rules for homepage and dashboards +""" + +from flask import Blueprint, render_template, abort +from flask_login import login_required, current_user + +HOME = Blueprint('home', __name__) + + +@HOME.route('/') +def homepage(): + """ + Render the homepage template on the / route + """ + return render_template('home/index.html', title="Welcome") + + +@HOME.route('/dashboard') +@login_required +def dashboard(): + """ + Render the dashboard template on the /dashboard route + """ + return render_template('home/dashboard.html', title="Dashboard") + + +@HOME.route('/admin/dashboard') +@login_required +def admin_dashboard(): + """ + Render the admin_dashboard template on the /admin/dashboard route + :return: + """ + # prevent non-admins from accessing the page + if not current_user.is_admin: + abort(403) + + return render_template('home/admin_dashboard.html', title="Dashboard") diff --git a/flask/kontor/library/__init__.py b/flask/kontor/library/__init__.py new file mode 100644 index 0000000..31eb0bc --- /dev/null +++ b/flask/kontor/library/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for library related information +""" diff --git a/flask/kontor/library/forms.py b/flask/kontor/library/forms.py new file mode 100644 index 0000000..a080781 --- /dev/null +++ b/flask/kontor/library/forms.py @@ -0,0 +1,35 @@ +""" +Define form to edit publisher, artists and books +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, IntegerField +from wtforms.validators import DataRequired, Optional, NumberRange +from bson import ObjectId + + +class PublisherForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class AuthorForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class BookForm(FlaskForm): + """ + Form to add and edit Comics + """ + title = StringField('Title', validators=[DataRequired()]) + author = SelectField('Author', coerce=ObjectId) + publisher = SelectField('Publisher', coerce=ObjectId) + isbn = StringField('ISBN', validators=[Optional()]) + year = IntegerField('Year', validators=[NumberRange(min=1970, max=2050)]) + submit = SubmitField('Submit') diff --git a/flask/kontor/library/models.py b/flask/kontor/library/models.py new file mode 100644 index 0000000..7ca5c58 --- /dev/null +++ b/flask/kontor/library/models.py @@ -0,0 +1,55 @@ +""" +This module declares the model for the library related information. +""" +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Publisher(MongoModel): + """ + Class Publisher represents a publisher of a book. + """ + name = fields.CharField() + + def __str__(self): + return "Publisher({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Author(MongoModel): + """ + Class Author represents an author of a book. + """ + name = fields.CharField() + + def __str__(self): + return "Author({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Book(MongoModel): + """ + Class book represents a book. + """ + title = fields.CharField() + author = fields.ReferenceField(Author) + publisher = fields.ReferenceField(Publisher) + isbn = fields.CharField(blank=True) + year = fields.IntegerField(blank=True) + edition = fields.CharField() + + def __str__(self): + return "Book({})".format(self.title) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/library/views.py b/flask/kontor/library/views.py new file mode 100644 index 0000000..1b53eee --- /dev/null +++ b/flask/kontor/library/views.py @@ -0,0 +1,228 @@ +"""Define routing rules for book publishers""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import PublisherForm, AuthorForm, BookForm +from .models import Publisher, Author, Book + + +LIBRARY = Blueprint('library', __name__) + + +@LIBRARY.route('/publishers') +@login_required +def list_publishers(): + """ + List all publishers + :return: + """ + publishers = Publisher.objects.all() + return render_template('library/publishers.html', + publishers=publishers, title="Publishers") + + +@LIBRARY.route('/publishers/add', methods=['GET', 'POST']) +@login_required +def add_publisher(): + """ + Add a publisher to the database + :return: + """ + form = PublisherForm() + if form.validate_on_submit(): + publisher = Publisher() + publisher.name = form.name.data + try: + publisher.save() + flash('You have successfully added a new publisher.') + except PyMongoError: + flash('Error: publisher name already exists.') + return redirect(url_for('library.list_publishers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Publisher") + + +@LIBRARY.route('/publishers/edit/', methods=['GET', 'POST']) +@login_required +def edit_publisher(publisher_id): + """ + Edit a publisher + """ + publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) + form = PublisherForm(obj=publisher) + if form.validate_on_submit(): + publisher.name = form.name.data + publisher.save() + flash('You have successfully edited the publisher.') + return redirect(url_for('library.list_publishers')) + form.name.data = publisher.name + return render_template('simpleform.html', action="Edit", + form=form, publisher=publisher, title="Edit Publisher") + + +@LIBRARY.route('/publishers/delete/', methods=['GET', 'POST']) +@login_required +def delete_publisher(publisher_id): + """ + Delete a publisher + :param publisher_id: ObjectId of publisher + :return: + """ + publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) + if publisher: + publisher.delete() + flash('You have successfully deleted the publisher.') + return redirect(url_for('library.list_publishers')) + + +@LIBRARY.route('/authors') +@login_required +def list_authors(): + """ + List all authors + :return: + """ + authors = Author.objects.all() + return render_template('library/authors.html', + authors=authors, title="Authors") + + +@LIBRARY.route('/authors/edit/', methods=['GET', 'POST']) +@login_required +def edit_author(author_id): + """ + Edit a book artist + """ + author = Author.objects.get({'_id': ObjectId(author_id)}) + form = AuthorForm(obj=author) + if form.validate_on_submit(): + author.name = form.name.data + author.save() + flash('You have successfully edited the author.') + return redirect(url_for('library.list_authors')) + form.name.data = author.name + return render_template('simpleform.html', action="Edit", + form=form, author=author, title="Edit Author") + + +@LIBRARY.route('/authors/add', methods=['GET', 'POST']) +@login_required +def add_author(): + """ + Add a author + :return: + """ + form = AuthorForm() + if form.validate_on_submit(): + author = Author() + author.name = form.name.data + try: + author.save() + flash('You have successfully added a new author.') + except PyMongoError: + flash('Error: author name already exists.') + return redirect(url_for('library.list_authors')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Author") + + +@LIBRARY.route('/authors/delete/', methods=['GET', 'POST']) +@login_required +def delete_author(author_id): + """ + Delete a author + :param author_id: + :return: + """ + author = Author.objects.raw({'_id': ObjectId(author_id)}) + if author: + author.delete() + flash('You have successfully deleted the author.') + return redirect(url_for('library.list_authors')) + + +@LIBRARY.route('/books') +@login_required +def list_books(): + """ + List all comics + :return: + """ + books = Book.objects.all() + return render_template('library/books.html', + books=books, title="Books") + + +@LIBRARY.route('/books/edit/', methods=['GET', 'POST']) +@login_required +def edit_book(book_id): + """ + Edit a book + """ + book = Book.objects.get({'_id': ObjectId(book_id)}) + form = BookForm(obj=book) + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + if book.publisher: + form.publisher.default = book.publisher.pk + form.publisher.process_data(book.publisher.pk) + form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] + if book.author: + form.author.default = book.author.pk + form.author.process_data(book.author.pk) + if form.validate_on_submit(): + book.title = form.title.data + book.publisher = form.publisher.data + book.author = form.author.data + if form.isbn.data: + book.isbn = form.isbn.data + else: + book.isbn = None + book.year = form.year.data + book.save() + flash('You have successfully edited the book.') + return redirect(url_for('library.list_books')) + form.title.data = book.title + return render_template('simpleform.html', action="Edit", + form=form, book=book, title="Edit Book") + + +@LIBRARY.route('/books/add', methods=['GET', 'POST']) +@login_required +def add_book(): + """ + Add a book + :return: + """ + form = BookForm() + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] + if form.validate_on_submit(): + book = Book() + book.title = form.title.data + book.publisher = form.publisher.data + book.author = form.author.data + book.year = form.year.data + try: + book.save() + flash('You have successfully added a new book.') + except PyMongoError: + flash('Error: book title already exists.') + return redirect(url_for('library.list_books')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Book") + + +@LIBRARY.route('/books/delete/', methods=['GET', 'POST']) +@login_required +def delete_book(book_id): + """ + Delete a book + :param book_id: + :return: + """ + book = Book.objects.raw({'_id': ObjectId(book_id)}) + if book: + book.delete() + flash('You have successfully deleted the book.') + return redirect(url_for('library.list_books')) diff --git a/flask/kontor/models.py b/flask/kontor/models.py new file mode 100644 index 0000000..7150364 --- /dev/null +++ b/flask/kontor/models.py @@ -0,0 +1,66 @@ +""" +This modules declares the model for Kontor users. +""" +from pymodm import MongoModel, fields +from pymongo.write_concern import WriteConcern +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from kontor import _LOGIN_MANAGER_ + + +class User(UserMixin, MongoModel): + """ + This class represents an user for the Kontor application. + """ + email = fields.EmailField() + username = fields.CharField(max_length=60) + first_name = fields.CharField(max_length=60) + last_name = fields.CharField(max_length=60) + password_hash = fields.CharField(max_length=128) + is_admin = fields.BooleanField(default=False) + + @property + def password(self): + """ + Prevent pasword from being accessed + """ + raise AttributeError('password is not a readable attribute.') + + @password.setter + def password(self, password): + """ + Set password to a hashed password + """ + self.password_hash = generate_password_hash(password) + + def verify_password(self, password): + """ + Check if hashed password matches actual password + """ + return check_password_hash(self.password_hash, password) + + def get_id(self): + """ + Get username as id + :return: + """ + return self.username + + def __repr__(self): + return ''.format(self.username) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +# Set up user_loader +@_LOGIN_MANAGER_.user_loader +def load_user(user_name): + """ + Get list of users from database + :param user_name: + :return: + """ + return User.objects.get({'username': user_name}) diff --git a/flask/kontor/office/__init__.py b/flask/kontor/office/__init__.py new file mode 100644 index 0000000..d830d41 --- /dev/null +++ b/flask/kontor/office/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for office related information +""" diff --git a/flask/kontor/static/css/style.css b/flask/kontor/static/css/style.css new file mode 100644 index 0000000..35fe053 --- /dev/null +++ b/flask/kontor/static/css/style.css @@ -0,0 +1,126 @@ +/* app/static/css/style.css */ + +body, html { + width: 100%; + height: 100%; +} + +body, h1, h2, h3 { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 700; +} + +a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { + color: #aec251; +} + +a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover { + color: #687430; +} + +footer { + padding: 50px 0; + background-color: #f8f8f8; +} + +p.copyright { + margin: 15px 0 0; +} + +.alert-info { + width: 50%; + margin: auto; + color: #687430; + background-color: #e6ecca; + border-color: #aec251; +} + +.btn-default { + border-color: #aec251; + color: #aec251; +} + +.btn-default:hover { + background-color: #aec251; +} + +.center { + margin: auto; + width: 70%; + padding: 10px; +} + +.content-section { + padding: 50px 0; + border-top: 1px solid #e7e7e7; +} + +.footer, .push { + clear: both; + height: 4em; +} + +.intro-divider { + width: 400px; + border-top: 1px solid #f8f8f8; + border-bottom: 1px solid rgba(0,0,0,0.2); +} + +.intro-header { + padding-top: 50px; + padding-bottom: 50px; + text-align: center; + color: #f8f8f8; + background: url(../img/intro-bg.jpg) no-repeat center center; + background-size: cover; + height: 100%; +} + +.intro-message { + position: relative; + padding-top: 20%; + padding-bottom: 20%; +} + +.intro-message > h1 { + margin: 0; + text-shadow: 2px 2px 3px rgba(0,0,0,0.6); + font-size: 5em; +} + +.intro-message > h3 { + text-shadow: 2px 2px 3px rgba(0,0,0,0.6); +} + +.lead { + font-size: 18px; + font-weight: 400; +} + +.topnav { + font-size: 14px; +} + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -4em; +} + +.outer { + display: table; + position: absolute; + height: 70%; + width: 100%; +} + +.middle { + display: table-cell; + vertical-align: middle; +} + +.inner { + margin-left: auto; + margin-right: auto; +} diff --git a/flask/kontor/templates/admin/users/user.html b/flask/kontor/templates/admin/users/user.html new file mode 100644 index 0000000..50df2c4 --- /dev/null +++ b/flask/kontor/templates/admin/users/user.html @@ -0,0 +1,21 @@ + + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Edit User{% endblock %} +{% block body %} +
+
+
+
+
+

Edit User

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/admin/users/users.html b/flask/kontor/templates/admin/users/users.html new file mode 100644 index 0000000..b23f67c --- /dev/null +++ b/flask/kontor/templates/admin/users/users.html @@ -0,0 +1,58 @@ + + +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Users{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Users

+ {% if users %} +
+
+ + + + + + + + + + + {% for user in users %} + {% if user.is_admin %} + + + + + + + {% else %} + + + + + + + {% endif %} + {% endfor %} + +
Name Email Username Edit
Admin N/A N/A N/A
{{ user.first_name }} {{ user.last_name }} {{ user.email }} {{ user.username }} + + Edit + +
+
+ {% endif %} +
+
+
+
+ +{% endblock %} diff --git a/flask/kontor/templates/auth/login.html b/flask/kontor/templates/auth/login.html new file mode 100644 index 0000000..8b70e01 --- /dev/null +++ b/flask/kontor/templates/auth/login.html @@ -0,0 +1,18 @@ + + +{% import "bootstrap/utils.html" as utils %} +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Login{% endblock %} +{% block body %} +
+
+ {{ utils.flashed_messages() }} +
+
+

Login to your account

+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} diff --git a/flask/kontor/templates/auth/register.html b/flask/kontor/templates/auth/register.html new file mode 100644 index 0000000..55d4918 --- /dev/null +++ b/flask/kontor/templates/auth/register.html @@ -0,0 +1,14 @@ + + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block body %} +
+
+

Register for an account

+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} diff --git a/flask/kontor/templates/base.html b/flask/kontor/templates/base.html new file mode 100644 index 0000000..80bdc27 --- /dev/null +++ b/flask/kontor/templates/base.html @@ -0,0 +1,107 @@ + + + + + {{ title }} | Kontor + + + + + + + +
+
+ {% block body %} + {% endblock %} +
+
+
+
+
+

+

+ +
+
+ +
+ + diff --git a/flask/kontor/templates/comics/artists.html b/flask/kontor/templates/comics/artists.html new file mode 100644 index 0000000..a92415e --- /dev/null +++ b/flask/kontor/templates/comics/artists.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comic Artists{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comic Artists

+ {% if artists %} +
+
+ + + + + + + + + + {% for artist in artists %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ artist.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No artists have been added.

+
+ {% endif %} + + + Add Artist + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/comics/comics.html b/flask/kontor/templates/comics/comics.html new file mode 100644 index 0000000..c596ad5 --- /dev/null +++ b/flask/kontor/templates/comics/comics.html @@ -0,0 +1,66 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comics{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comics

+ {% if comics %} +
+
+ + + + + + + + + + + + {% for comic in comics %} + + + + + + + + {% endfor %} + +
Title Publisher Current Order Edit Delete
{{ comic.title }} + + {{ comic.publisher.name }} + + {{ comic.current_order }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No comics have been added.

+
+ {% endif %} + + + Add Comic + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/comics/publishers.html b/flask/kontor/templates/comics/publishers.html new file mode 100644 index 0000000..aa09f7b --- /dev/null +++ b/flask/kontor/templates/comics/publishers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comic Publishers{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comic Publishers

+ {% if publishers %} +
+
+ + + + + + + + + + {% for publisher in publishers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ publisher.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No comics have been added.

+
+ {% endif %} + + + Add Publisher + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/admin_dashboard.html b/flask/kontor/templates/home/admin_dashboard.html new file mode 100644 index 0000000..77a0c99 --- /dev/null +++ b/flask/kontor/templates/home/admin_dashboard.html @@ -0,0 +1,31 @@ + + +{% extends "base.html" %} +{% block title %}Admin Dashboard{% endblock %} +{% block body %} +
+
+
+
+
+

Admin Dashboard

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/dashboard.html b/flask/kontor/templates/home/dashboard.html new file mode 100644 index 0000000..6764375 --- /dev/null +++ b/flask/kontor/templates/home/dashboard.html @@ -0,0 +1,20 @@ + + +{% extends "base.html" %} +{% block title %}Dashboard{% endblock %} +{% block body %} +
+
+
+
+
+

The Dashboard

+

We made it here!

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/index.html b/flask/kontor/templates/home/index.html new file mode 100644 index 0000000..5b01225 --- /dev/null +++ b/flask/kontor/templates/home/index.html @@ -0,0 +1,20 @@ + + +{% extends "base.html" %} +{% block title %}Home{% endblock %} +{% block body %} +
+
+
+
+
+

Project Kontor

+

Store everything!

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/authors.html b/flask/kontor/templates/library/authors.html new file mode 100644 index 0000000..8c24f7b --- /dev/null +++ b/flask/kontor/templates/library/authors.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Library Authors{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Library Authors

+ {% if authors %} +
+
+ + + + + + + + + + {% for author in authors %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ author.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No artists have been added.

+
+ {% endif %} + + + Add Author + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/books.html b/flask/kontor/templates/library/books.html new file mode 100644 index 0000000..6a6ef76 --- /dev/null +++ b/flask/kontor/templates/library/books.html @@ -0,0 +1,74 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Books{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Books

+ {% if books %} +
+
+ + + + + + + + + + + + + + {% for book in books %} + + + + + + + + + + {% endfor %} + +
Title Author Publisher ISBN Year Edit Delete
{{ book.title }} + + {{ book.author.name }} + + + + {{ book.publisher.name }} + + {{ book.isbn }} {{ book.year }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No books have been added.

+
+ {% endif %} + + + Add Book + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/publishers.html b/flask/kontor/templates/library/publishers.html new file mode 100644 index 0000000..d0d363c --- /dev/null +++ b/flask/kontor/templates/library/publishers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Book Publishers{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Book Publishers

+ {% if publishers %} +
+
+ + + + + + + + + + {% for publisher in publishers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ publisher.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No publishers have been added.

+
+ {% endif %} + + + Add Publisher + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/simpleform.html b/flask/kontor/templates/simpleform.html new file mode 100644 index 0000000..6ce9d3b --- /dev/null +++ b/flask/kontor/templates/simpleform.html @@ -0,0 +1,21 @@ +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+

{{ title }}

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/manufacturers.html b/flask/kontor/templates/tysc/manufacturers.html new file mode 100644 index 0000000..f71ed75 --- /dev/null +++ b/flask/kontor/templates/tysc/manufacturers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if manufacturers %} +
+
+ + + + + + + + + + {% for manufacturer in manufacturers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ manufacturer.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No manufacturers have been added.

+
+ {% endif %} + + + Add Manufacturer + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/players.html b/flask/kontor/templates/tysc/players.html new file mode 100644 index 0000000..db25067 --- /dev/null +++ b/flask/kontor/templates/tysc/players.html @@ -0,0 +1,60 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if players %} +
+
+ + + + + + + + + + + {% for player in players %} + + + + + + + {% endfor %} + +
First Name Last Name Edit Delete
{{ player.first_name }} {{ player.last_name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No players have been added.

+
+ {% endif %} + + + Add Player + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/positions.html b/flask/kontor/templates/tysc/positions.html new file mode 100644 index 0000000..3b49e6e --- /dev/null +++ b/flask/kontor/templates/tysc/positions.html @@ -0,0 +1,62 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if positions %} +
+
+ + + + + + + + + + + + {% for position in positions %} + + + + + + + + {% endfor %} + +
Name Description Sport Edit Delete
{{ position.name }} {{ position.description }} {{ position.sport.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No positions have been added.

+
+ {% endif %} + + + Add Position + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/sports.html b/flask/kontor/templates/tysc/sports.html new file mode 100644 index 0000000..1efe09a --- /dev/null +++ b/flask/kontor/templates/tysc/sports.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Sports{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Sports

+ {% if sports %} +
+
+ + + + + + + + + + {% for sport in sports %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ sport.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No sports have been added.

+
+ {% endif %} + + + Add Sport + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/teams.html b/flask/kontor/templates/tysc/teams.html new file mode 100644 index 0000000..c51e9bf --- /dev/null +++ b/flask/kontor/templates/tysc/teams.html @@ -0,0 +1,60 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Teams{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Teams

+ {% if teams %} +
+
+ + + + + + + + + + + {% for team in teams %} + + + + + + + {% endfor %} + +
Name Shortname Edit Delete
{{ team.name }} {{ team.shortname }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No teams have been added.

+
+ {% endif %} + + + Add Team + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/tysc/__init__.py b/flask/kontor/tysc/__init__.py new file mode 100644 index 0000000..9f06f83 --- /dev/null +++ b/flask/kontor/tysc/__init__.py @@ -0,0 +1,377 @@ +""" +Define routing rules for TradeYourSportsCards related information +""" +from .models import Sport, Team, Position, Player +from .models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +def initialize_model(): + """ + Initialize collections for module TradeYourSportsCards. + """ + initialize_sport() + initialize_position() + initialize_teams() + initialize_players() + initialize_manufacturers() + initialize_card_sets() + initialize_parallel_sets() + initialize_insert_sets() + initialize_cards() + + +def initialize_sport(): + """ + Initialize collection Sport with american sports. + """ + for sport in Sport.objects.all(): + sport.delete() + sports = ["Baseball", "Basketball", "Football", "Hockey"] + for sport_name in sports: + sport = Sport() + sport.name = sport_name + sport.save() + + +def initialize_position(): + """ + Initialize collection Position with data. + """ + for position in Position.objects.all(): + position.delete() + positions = { + 'Quarterback': {'short': 'QB', 'sport': 'Football'}, + 'Wide Receiver': {'short': 'WR', 'sport': 'Football'}, + 'Runningback': {'short': 'RB', 'sport': 'Football'}, + 'Linebacker': {'short': 'LB', 'sport': 'Football'}, + 'Tight End': {'short': 'TE', 'sport': 'Football'}, + 'Fullback': {'short': 'FB', 'sport': 'Football'}, + 'Strong Safety': {'short': 'SS', 'sport': 'Football'}, + 'Defensive End': {'short': 'DE', 'sport': 'Football'}, + 'Kicker': {'short': 'K', 'sport': 'Football'}, + 'Punter': {'short': 'P', 'sport': 'Football'}, + 'Left Guard': {'short': 'LG', 'sport': 'Football'}, + 'Right Guard': {'short': 'RG', 'sport': 'Football'}, + 'Offensive Tackle': {'short': 'OT', 'sport': 'Football'}, + 'Defensive Back': {'short': 'DB', 'sport': 'Football'}, + 'Cornerback': {'short': 'CB', 'sport': 'Football'}, + 'Catcher': {'short': 'C', 'sport': 'Baseball'}, + 'First Base': {'short': '1B', 'sport': 'Baseball'}, + 'Second Base': {'short': '2B', 'sport': 'Baseball'}, + 'Third Base': {'short': '3B', 'sport': 'Baseball'}, + 'Shortstop': {'short': 'SS', 'sport': 'Baseball'}, + 'Left Field': {'short': 'LF', 'sport': 'Baseball'}, + 'Center Field': {'short': 'CF', 'sport': 'Baseball'}, + 'Right Field': {'short': 'RF', 'sport': 'Baseball'}, + 'Designated Hitter': {'short': 'DH', 'sport': 'Baseball'}, + 'Pitcher': {'short': 'P', 'sport': 'Baseball'} + } + for key in positions: + sport = Sport.objects.get({'name': positions.get(key)['sport']}) + position = Position() + position.description = key + position.name = positions.get(key)['short'] + position.sport = sport + position.save() + + +def initialize_teams(): + """ + Initialize collection Team with data. + """ + for team in Team.objects.all(): + team.delete() + teams = {'Buffalo Bills': {'short': 'Bills', 'sport': 'Football'}, + 'Indianapolis Colts': {'short': 'Colts', 'sport': 'Football'}, + 'Miami Dolphins': {'short': 'Dolphins', 'sport': 'Football'}, + 'New England Patriots': {'short': 'Patriots', 'sport': 'Football'}, + 'New York Jets': {'short': 'Jets', 'sport': 'Football'}, + 'Baltimore Ravens': {'short': 'Ravens', 'sport': 'Football'}, + 'Cincinnati Bengals': {'short': 'Bengals', 'sport': 'Football'}, + 'Cleveland Browns': {'short': 'Browns', 'sport': 'Football'}, + 'Jacksonville Jaguars': {'short': 'Jaguars', 'sport': 'Football'}, + 'Pittsburgh Steelers': {'short': 'Steelers', 'sport': 'Football'}, + 'Tennessee Titans': {'short': 'Titans', 'sport': 'Football'}, + 'Denver Broncos': {'short': 'Broncos', 'sport': 'Football'}, + 'Kansas City Chiefs': {'short': 'Chiefs', 'sport': 'Football'}, + 'Oakland Raiders': {'short': 'Raiders', 'sport': 'Football'}, + 'San Diego Chargers': {'short': 'Chargers', 'sport': 'Football'}, + 'Seattle Seahawks': {'short': 'Seahawks', 'sport': 'Football'}, + 'Arizona Cardinals': {'short': 'Cardinals', 'sport': 'Football'}, + 'Dallas Cowboys': {'short': 'Cowboys', 'sport': 'Football'}, + 'New York Giants': {'short': 'Giants', 'sport': 'Football'}, + 'Philadelphia Eagles': {'short': 'Eagles', 'sport': 'Football'}, + 'Washington Redskins': {'short': 'Redskins', 'sport': 'Football'}, + 'Chicago Bears': {'short': 'Bears', 'sport': 'Football'}, + 'Detroit Lions': {'short': 'Lions', 'sport': 'Football'}, + 'Green Bay Packers': {'short': 'Packers', 'sport': 'Football'}, + 'Minnesota Vikings': {'short': 'Vikings', 'sport': 'Football'}, + 'Tampa Bay Buccaneers': {'short': 'Buccaneers', 'sport': 'Football'}, + 'Atlanta Falcons': {'short': 'Falcons', 'sport': 'Football'}, + 'Carolina Panthers': {'short': 'Panthers', 'sport': 'Football'}, + 'New Orleans Saints': {'short': 'Saints', 'sport': 'Football'}, + 'St.Louis Rams': {'short': 'Rams', 'sport': 'Football'}, + 'San Francisco 49ers': {'short': '49ers', 'sport': 'Football'}, + 'Baltimore Orioles': {'short': 'Orioles', 'sport': 'Baseball'}, + 'Boston Red Sox': {'short': 'Red Sox', 'sport': 'Baseball'}, + 'New York Yankees': {'short': 'Yankees', 'sport': 'Baseball'}, + 'Tampa Bay Devil Rays': {'short': 'Devil Rays', 'sport': 'Baseball'}, + 'Toronto Blue Jays': {'short': 'Blue Jays', 'sport': 'Baseball'}, + 'Chicago White Sox': {'short': 'White Sox', 'sport': 'Baseball'}, + 'Cleveland Indians': {'short': 'Indians', 'sport': 'Baseball'}, + 'Detroit Tigers': {'short': 'Tigers', 'sport': 'Baseball'}, + 'Kansas City Royals': {'short': 'Royals', 'sport': 'Baseball'}, + 'Minnesota Twins': {'short': 'Twins', 'sport': 'Baseball'}, + 'Anaheim Angels': {'short': 'Angels', 'sport': 'Baseball'}, + 'Oakland Athletics': {'short': 'Athletics', 'sport': 'Baseball'}, + 'Seattle Mariners': {'short': 'Mariners', 'sport': 'Baseball'}, + 'Texas Rangers': {'short': 'Rangers', 'sport': 'Baseball'}, + 'Atlanta Braves': {'short': 'Braves', 'sport': 'Baseball'}, + 'Florida Marlins': {'short': 'Marlins', 'sport': 'Baseball'}, + 'Montreal Expos': {'short': 'Expos', 'sport': 'Baseball'}, + 'New York Mets': {'short': 'Mets', 'sport': 'Baseball'}, + 'Philadelphia Phillies': {'short': 'Phillies', 'sport': 'Baseball'}, + 'Chicago Cubs': {'short': 'Cubs', 'sport': 'Baseball'}, + 'Cincinnati Reds': {'short': 'Reds', 'sport': 'Baseball'}, + 'Houston Astros': {'short': 'Astros', 'sport': 'Baseball'}, + 'Milwaukee Brewers': {'short': 'Brewers', 'sport': 'Baseball'}, + 'Pittsburgh Pirates': {'short': 'Pirates', 'sport': 'Baseball'}, + 'St.Louis Cardinals': {'short': 'Cardinals', 'sport': 'Baseball'}, + 'Arizona Diamondbacks': {'short': 'Diamondbacks', 'sport': 'Baseball'}, + 'Colorado Rockies': {'short': 'Rockies', 'sport': 'Baseball'}, + 'Los Angeles Dodgers': {'short': 'Dodgers', 'sport': 'Baseball'}, + 'San Diego Padres': {'short': 'Padres', 'sport': 'Baseball'}, + 'San Francisco Giants': {'short': 'Giants', 'sport': 'Baseball'}, + 'Boston Celtics': {'short': 'Celtics', 'sport': 'Basketball'}, + 'Miami Heat': {'short': 'Heat', 'sport': 'Basketball'}, + 'New Jersey Nets': {'short': 'Mets', 'sport': 'Basketball'}, + 'New York Knicks': {'short': 'Knicks', 'sport': 'Basketball'}, + 'Orlando Magic': {'short': 'Magic', 'sport': 'Basketball'}, + 'Philadelphia 76ers': {'short': '76ers', 'sport': 'Basketball'}, + 'Washington Wizards': {'short': 'Wizards', 'sport': 'Basketball'}, + 'Atlanta Hawks': {'short': 'Hawks', 'sport': 'Basketball'}, + 'Charlotte Hornets': {'short': 'Hornets', 'sport': 'Basketball'}, + 'Chicago Bulls': {'short': 'Bulls', 'sport': 'Basketball'}, + 'Cleveland Cavaliers': {'short': 'Cavaliers', 'sport': 'Basketball'}, + 'Detroit Pistons': {'short': 'Pistons', 'sport': 'Basketball'}, + 'Indiana Pacers': {'short': 'Pacers', 'sport': 'Basketball'}, + 'Milwaukee Bucks': {'short': 'Bucks', 'sport': 'Basketball'}, + 'Toronto Raptors': {'short': 'Raptors', 'sport': 'Basketball'}, + 'Dallas Mavericks': {'short': 'Mavericks', 'sport': 'Basketball'}, + 'Denver Nuggets': {'short': 'Nuggets', 'sport': 'Basketball'}, + 'Houston Rockets': {'short': 'Rockets', 'sport': 'Basketball'}, + 'Minnesota Timberwolves': {'short': 'Timberwolves', 'sport': 'Basketball'}, + 'San Antonio Spurs': {'short': 'Spurs', 'sport': 'Basketball'}, + 'Utah Jazz': {'short': 'Jazz', 'sport': 'Basketball'}, + 'Vancouver Grizzlies': {'short': 'Grizzlies', 'sport': 'Basketball'}, + 'Golden State Warriors': {'short': 'Warriors', 'sport': 'Basketball'}, + 'Los Angeles Clippers': {'short': 'Clippers', 'sport': 'Basketball'}, + 'Los Angeles Lakers': {'short': 'Lakers', 'sport': 'Basketball'}, + 'Phoenix Suns': {'short': 'Suns', 'sport': 'Basketball'}, + 'Portland Trail Blazers': {'short': 'Blazers', 'sport': 'Basketball'}, + 'Sacramento Kings': {'short': 'Kings', 'sport': 'Basketball'}, + 'Seattle SuperSonics': {'short': 'SuperSonics', 'sport': 'Basketball'}, + 'Boston Bruins': {'short': 'Bruins', 'sport': 'Hockey'}, + 'Buffalo Sabres': {'short': 'Sabres', 'sport': 'Hockey'}, + 'Montreal Canadiens': {'short': 'Canadiens', 'sport': 'Hockey'}, + 'Ottawa Senators': {'short': 'Senators', 'sport': 'Hockey'}, + 'Toronto Maple Leafs': {'short': 'Maple Leafs', 'sport': 'Hockey'}, + 'New Jersey Devils': {'short': 'Devils', 'sport': 'Hockey'}, + 'New York Islander': {'short': 'Islander', 'sport': 'Hockey'}, + 'New York Rangers': {'short': 'Rangers', 'sport': 'Hockey'}, + 'Philadelphia Flyers': {'short': 'Flyers', 'sport': 'Hockey'}, + 'Pittsburgh Penguins': {'short': 'Penguins', 'sport': 'Hockey'}, + 'Atlanta Trashers': {'short': 'Trashers', 'sport': 'Hockey'}, + 'Carolina Hurricanes': {'short': 'Hurricanes', 'sport': 'Hockey'}, + 'Florida Panthers': {'short': 'Panthers', 'sport': 'Hockey'}, + 'Tampa Bay Lightnings': {'short': 'Lightnings', 'sport': 'Hockey'}, + 'Washington Capitals': {'short': 'Capitals', 'sport': 'Hockey'}, + 'Chicago Blackhawks': {'short': 'Blackhawks', 'sport': 'Hockey'}, + 'Columbo Blue Jackets': {'short': 'Blue Jackets', 'sport': 'Hockey'}, + 'Detroit Red Wings': {'short': 'Red Wings', 'sport': 'Hockey'}, + 'Nashville Predators': {'short': 'Predators', 'sport': 'Hockey'}, + 'St.Louis Blues': {'short': 'Blues', 'sport': 'Hockey'}, + 'Calgary Flames': {'short': 'Flames', 'sport': 'Hockey'}, + 'Colorado Avalanche': {'short': 'Avalanche', 'sport': 'Hockey'}, + 'Edmonton Oilers': {'short': 'Oilers', 'sport': 'Hockey'}, + 'Minnesota Wild': {'short': 'Wild', 'sport': 'Hockey'}, + 'Vancouver Canucks': {'short': 'Canucks', 'sport': 'Hockey'}, + 'Anaheim Mighty Ducks': {'short': 'Mighty Ducks', 'sport': 'Hockey'}, + 'Dallas Stars': {'short': 'Stars', 'sport': 'Hockey'}, + 'Los Angeles Kings': {'short': 'Kings', 'sport': 'Hockey'}, + 'Phoenix Coyotes': {'short': 'Coyotes', 'sport': 'Hockey'}, + 'San Jose Sharks': {'short': 'Sharks', 'sport': 'Hockey'}, + 'Houston Texans': {'short': 'Texans', 'sport': 'Football'}, + 'Houston Oilers': {'short': 'Oilers', 'sport': 'Football'} + } + for key in teams: + sport = Sport.objects.get({'name': teams.get(key)['sport']}) + team = Team() + team.name = key + team.shortname = teams.get(key)['short'] + team.sport = sport + team.save() + + +def initialize_players(): + """ + Initialize collection Manufacturer with data. + """ + for player in Player.objects.all(): + player.delete() + players = ['Pathon, Jerome', 'Bruschi, Tedy', 'Couch, Tim', 'Shea, Aaron', + 'Lewis, Jamal', 'Lewis, Jermaine', 'Banks, Tony', 'Fuamatu-Ma\'Afala, Chris', + 'Bettis, Jerome', 'Stewart, Kordell', 'Moon, Warren', 'Lockett, Kevin', + 'Gannon, Rich', 'Jett, James', 'Strong, Mack', 'Huard, Brock', + 'Watters, Ricky', 'Aikman, Troy', 'LaFleur, David', 'Brazzell, Chris', + 'Dayne, Ron', 'Brown, Na', 'Small, Torrance', 'Lewis, Chad', 'Murrell, Adrian', + 'Smith, Maurice', 'Chandler, Chris', 'Kanell, Danny', 'Williams, Ricky', + 'Garcia, Jeff', 'Streets, Tai', 'Garner, Charlie', 'Rice, Jerry', + 'Owens, Terrell', 'Bruce, Isaac', 'Canidate, Trung'] + for player_name in players: + player = Player() + (last_name, first_name) = player_name.split(',') + player.last_name = last_name.strip() + player.first_name = first_name.strip() + player.save() + + +def initialize_manufacturers(): + """ + Initialize collection Manufacturer with data. + """ + for manufacturer in Manufacturer.objects.all(): + manufacturer.delete() + manufacturers = ['Pacific', 'Fleer', 'Bowman', 'Topps', 'Donruss', + 'Score', 'Flair', 'Upper Deck'] + for manufacturer_name in manufacturers: + manufacturer = Manufacturer() + manufacturer.name = manufacturer_name + manufacturer.save() + + +def initialize_card_sets(): + """ + Initialize collection CardSet with data. + """ + for card_set in CardSet.objects.all(): + card_set.delete() + card_sets = {'Pacific': 'Pacific', + 'Fleer': 'Fleer', + 'Bowman': 'Bowman', + 'Leaf': 'Topps', + 'Ultra': 'Fleer', + 'Mystique': 'Fleer', + 'Finest Hour': 'Pacific', + 'SP': 'Upper Deck', + 'SPx': 'Upper Deck', + 'SP Authentic': 'Upper Deck', + 'Black Diamond': 'Upper Deck' + } + for set_name in card_sets: + card_set = CardSet() + card_set.name = set_name + card_set.manufacturer = Manufacturer.objects.get({'name': card_sets.get(set_name)}) + card_set.save() + + +def initialize_parallel_sets(): + """ + Initialize collection ParallelSet with data. + """ + for parallel_set in ParallelSet.objects.all(): + parallel_set.delete() + parallel_sets = {'Mystique Gold': 'Fleer', + 'Pacific Copper': 'Pacific', + 'Pacific Gold': 'Pacific' + } + for key in parallel_sets: + manufacturer = Manufacturer.objects.get({'name': parallel_sets.get(key)}) + parallel_set = ParallelSet() + parallel_set.name = key + parallel_set.manufacturer = manufacturer + parallel_set.save() + + +def initialize_insert_sets(): + """ + Initialize collection InsertSet with data. + """ + for insert_set in InsertSet.objects.all(): + insert_set.delete() + manufacturer = Manufacturer.objects.get({'name': 'Fleer'}) + insert_set = InsertSet() + insert_set.name = 'Mystique Big Buzz' + insert_set.manufacturer = manufacturer + insert_set.save() + + +def initialize_cards(): + """ + Initialize collection Card with data. + """ + for card in Card.objects.all(): + card.delete() + players = ['Pathon, Jerome', 'Bruschi, Tedy', 'Couch, Tim', 'Shea, Aaron', + 'Lewis, Jamal', 'Lewis, Jermaine', 'Banks, Tony', 'Fuamatu-Ma\'Afala, Chris', + 'Bettis, Jerome', 'Stewart, Kordell', 'Moon, Warren', 'Lockett, Kevin', + 'Gannon, Rich', 'Jett, James', 'Strong, Mack', 'Huard, Brock', + 'Watters, Ricky', 'Aikman, Troy', 'LaFleur, David', 'Brazzell, Chris', + 'Dayne, Ron', 'Brown, Na', 'Small, Torrance', 'Lewis, Chad', 'Murrell, Adrian', + 'Smith, Maurice', 'Chandler, Chris', 'Kanell, Danny', 'Williams, Ricky', + 'Garcia, Jeff', 'Streets, Tai', 'Garner, Charlie', 'Rice, Jerry', + 'Owens, Terrell', 'Bruce, Isaac', 'Canidate, Trung'] + cards = [ + [0, 'Indianapolis Colts', 'Pacific', 'Pacific', None, None, False, 2001, 185], + [1, 'Indianapolis Colts', 'Pacific', 'Pacific', None, None, False, 2001, 250], + [2, 'Cleveland Browns', 'Pacific', 'Pacific', None, None, False, 2001, 103], + [3, 'Cleveland Browns', 'Pacific', 'Pacific', None, None, False, 2001, 112], + [4, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 37], + [5, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 38], + [6, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 31], + [7, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 338], + [8, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 335], + [9, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 345], + [10, 'Kansas City Chiefs', 'Pacific', 'Pacific', None, None, False, 2001, 213], + [11, 'Kansas City Chiefs', 'Pacific', 'Pacific', None, None, False, 2001, 212], + [12, 'Oakland Raiders', 'Pacific', 'Pacific', None, None, False, 2001, 311], + [13, 'Oakland Raiders', 'Pacific', 'Pacific', None, None, False, 2001, 312], + [14, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 403], + [15, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 397], + [16, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 404], + [17, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 116], + [18, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 122], + [19, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 117], + [20, 'New York Giants', 'Pacific', 'Pacific', None, None, False, 2001, 281], + [21, 'New York Giants', 'Pacific', 'Pacific', None, None, False, 2001, 321], + [22, 'Philadelphia Eagles', 'Pacific', 'Pacific', None, None, False, 2001, 331], + [23, 'Philadelphia Eagles', 'Pacific', 'Pacific', None, None, False, 2001, 324], + [24, 'Washington Redskins', 'Pacific', 'Pacific', None, None, False, 2001, 445], + [25, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 28], + [26, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 17], + [27, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 23], + [28, 'New Orleans Saints', 'Pacific', 'Pacific', None, None, False, 2001, 273], + [29, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 380], + [30, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 390], + [31, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 381], + [32, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 387], + [33, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 386], + [34, 'St.Louis Rams', 'Pacific', 'Pacific', None, None, False, 2001, 349], + [35, 'St.Louis Rams', 'Pacific', 'Pacific', None, None, False, 2001, 350], + ] + for card_data in cards: + card = Card() + player_name = players[card_data[0]] + (last_name, first_name) = player_name.split(',') + card.player = Player.objects.get( + {'last_name': last_name.strip(), 'first_name': first_name.strip()} + ) + card.team = Team.objects.get({'name': card_data[1]}) + card.manufacturer = Manufacturer.objects.get({'name': card_data[2]}) + card.card_set = CardSet.objects.get({'name': card_data[3]}) + card.parallel_set = card_data[4] + card.insert_set = card_data[5] + card.rookie = card_data[6] + card.year = card_data[7] + card.number = card_data[8] + card.save() diff --git a/flask/kontor/tysc/forms.py b/flask/kontor/tysc/forms.py new file mode 100644 index 0000000..9cb5c3c --- /dev/null +++ b/flask/kontor/tysc/forms.py @@ -0,0 +1,76 @@ +""" +Define form to edit sport, teams, player, manufacturers and card types +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, IntegerField, BooleanField +from wtforms.validators import DataRequired, Optional, NumberRange +from bson import ObjectId + + +class SportForm(FlaskForm): + """ + Form to add and edit a Sport + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class TeamForm(FlaskForm): + """ + Form to add and edit a team + """ + name = StringField('Name', validators=[DataRequired()]) + shortname = StringField('Shortname', validators=[DataRequired()]) + sport = SelectField('Sport', coerce=ObjectId) + submit = SubmitField('Submit') + + +class PlayerForm(FlaskForm): + """ + Form to add and edit a player. + """ + first_name = StringField('First name', validators=[DataRequired()]) + last_name = StringField('Last name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class PositionForm(FlaskForm): + """ + Form to add and edit field positions for sports + """ + name = StringField('Name', validators=[DataRequired()]) + description = StringField('Description', validators=[DataRequired()]) + sport = SelectField('Sport', coerce=ObjectId) + submit = SubmitField('Submit') + + +class ManufacturerForm(FlaskForm): + """ + Form to add and edit a card manufacturer. + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class CardSetForm(FlaskForm): + """ + Form to add and edit a regular card set + """ + name = StringField('Name', validators=[DataRequired()]) + manufacturer = SelectField('Manufacturer', coerce=ObjectId) + submit = SubmitField('Submit') + + +class CardForm(FlaskForm): + """ + Form to add and edit a trading card + """ + player = SelectField('Player', coerce=ObjectId) + team = SelectField('Team', coerce=ObjectId) + manufacturer = SelectField('Manufacturer', coerce=ObjectId) + card_set = SelectField('Card Set', coerce=ObjectId) + parallel_set = SelectField('Parallel Set', coerce=ObjectId, validators=[Optional()]) + insert_set = SelectField('Inserts', coerce=ObjectId, validators=[Optional()]) + rookie = BooleanField('Rookie', validators=[DataRequired()]) + year = IntegerField('Year', validators=[NumberRange(min=1956, max=2020)]) + number = IntegerField('Number', validators=[DataRequired()]) diff --git a/flask/kontor/tysc/models.py b/flask/kontor/tysc/models.py new file mode 100644 index 0000000..02dc341 --- /dev/null +++ b/flask/kontor/tysc/models.py @@ -0,0 +1,155 @@ +"""This modules declares the model for TradingCards related information.""" +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Sport(MongoModel): + """Class Sport represents a sport.""" + name = fields.CharField() + + def __str__(self): + """Returns printable version of Sport object.""" + return self.name + + def __repr__(self): + """Returns printable version of Sport object.""" + return "Sport({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Position(MongoModel): + """Class Position represents the position of a player for a sport.""" + name = fields.CharField(max_length=4) + description = fields.CharField(max_length=30) + sport = fields.ReferenceField(Sport) + + def __str__(self): + """Returns printable version of Position object.""" + return "{0}({1})".format(self.name, self.description) + + def __repr__(self): + """Returns printable version of Position object.""" + return "Position({0}, {1})".format(self.name, self.description) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Team(MongoModel): + """Class Team represents a team for a sport.""" + name = fields.CharField(max_length=60) + shortname = fields.CharField(max_length=30) + sport = fields.ReferenceField(Sport, blank=True) + + def __str__(self): + """Returns printable version of Team object.""" + return self.name + + def __repr__(self): + """Returns printable version of Team object.""" + return "Team({0}{1})".format(self.name, self.shortname) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Player(MongoModel): + """ + Class Player represents a player. + """ + first_name = fields.CharField(max_length=60) + last_name = fields.CharField(max_length=60) + + def __str__(self): + """Returns printable version of Team object.""" + return "{0} {1}".format(self.first_name, self.last_name) + + def __repr__(self): + """Returns printable version of Team object.""" + return "Player({0} {1})".format(self.first_name, self.last_name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Manufacturer(MongoModel): + """ + Class Manufacturer represents a manufacturer of trading cards. + """ + name = fields.CharField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class CardSet(MongoModel): + """ + Class CardSet represents the regular card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class ParallelSet(MongoModel): + """ + Class CardSet represents the parallel card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class InsertSet(MongoModel): + """ + Class CardSet represents the inserts card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Card(MongoModel): + """ + Class CardSet represents the regular card set. + """ + # pylint: disable=too-many-instance-attributes + # Nine is reasonable in this case. + player = fields.ReferenceField(Player) + team = fields.ReferenceField(Team) + manufacturer = fields.ReferenceField(Manufacturer) + card_set = fields.ReferenceField(CardSet, blank=True) + parallel_set = fields.ReferenceField(ParallelSet, blank=True) + insert_set = fields.ReferenceField(InsertSet, blank=True) + rookie = fields.BooleanField(default=False) + year = fields.IntegerField(min_value=1956, max_value=2020) + number = fields.IntegerField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/tysc/views.py b/flask/kontor/tysc/views.py new file mode 100644 index 0000000..836b295 --- /dev/null +++ b/flask/kontor/tysc/views.py @@ -0,0 +1,405 @@ +""" +Define BLueprint and Views for TradeYourSportsCards +""" +from flask import Blueprint, url_for, render_template, redirect, flash +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import SportForm, TeamForm, PlayerForm, PositionForm +from .forms import ManufacturerForm +from .models import Sport, Team, Player, Position +from .models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +TYSC = Blueprint('tysc', __name__) + + +@TYSC.route('/sport') +@login_required +def list_sports(): + """ + List sports. + :return: + """ + sports = Sport.objects.all() + return render_template('tysc/sports.html', sports=sports, title="Sports") + + +@TYSC.route('/sport/edit/', methods=['GET', 'POST']) +@login_required +def edit_sport(sport_id): + """ + Edit a sport + """ + sport = Sport.objects.get({'_id': ObjectId(sport_id)}) + form = SportForm(obj=sport) + if form.validate_on_submit(): + sport.name = form.name.data + sport.save() + flash('You have successfully edited the sport.') + return redirect(url_for('tysc.list_sports')) + form.name.data = sport.name + return render_template('simpleform.html', action="Edit", + form=form, sport=sport, title="Edit Sport") + + +@TYSC.route('/sport/add', methods=['GET', 'POST']) +@login_required +def add_sport(): + """ + Add a sport + :return: + """ + form = SportForm() + if form.validate_on_submit(): + sport = Sport() + sport.name = form.name.data + try: + sport.save() + flash('You have successfully added a new sport.') + except PyMongoError: + flash('Error: sport name already exists.') + return redirect(url_for('tysc.list_sports')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Sport") + + +@TYSC.route('/sport/delete/', methods=['GET', 'POST']) +@login_required +def delete_sport(sport_id): + """ + Delete a sport + :param sport_id: + :return: + """ + sport = Sport.objects.raw({'_id': ObjectId(sport_id)}) + if sport: + sport.delete() + flash('You have successfully deleted the sport.') + return redirect(url_for('tysc.list_sports')) + + +@TYSC.route('/team') +@login_required +def list_teams(): + """ + List teams. + :return: + """ + teams = Team.objects.all() + return render_template('tysc/teams.html', teams=teams, title="Teams") + + +@TYSC.route('/team/edit/', methods=['GET', 'POST']) +@login_required +def edit_team(team_id): + """ + Edit a team + """ + team = Team.objects.get({'_id': ObjectId(team_id)}) + form = TeamForm(obj=team) + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if team.sport: + form.sport.default = team.sport.pk + form.sport.process_data(team.sport.pk) + if form.validate_on_submit(): + team.name = form.name.data + team.shortname = form.shortname.data + team.sport = form.sport.data + team.save() + flash('You have successfully edited the team.') + return redirect(url_for('tysc.list_teams')) + form.name.data = team.name + return render_template('simpleform.html', action="Edit", + form=form, team=team, title="Edit Team") + + +@TYSC.route('/team/add', methods=['GET', 'POST']) +@login_required +def add_team(): + """ + Add a team + :return: + """ + form = TeamForm() + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if form.validate_on_submit(): + team = Team() + team.name = form.name.data + team.shortname = form.shortname.data + team.sport = form.sport.data + try: + team.save() + flash('You have successfully added a new team.') + except PyMongoError: + flash('Error: team name already exists.') + return redirect(url_for('tysc.list_teams')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Team") + + +@TYSC.route('/team/delete/', methods=['GET', 'POST']) +@login_required +def delete_team(team_id): + """ + Delete a team + :param team_id: + :return: + """ + team = Team.objects.raw({'_id': ObjectId(team_id)}) + if team: + team.delete() + return redirect(url_for('tysc.list_teams')) + + +@TYSC.route('/position') +@login_required +def list_positions(): + """ + List positions. + :return: + """ + positions = Position.objects.all() + return render_template('tysc/positions.html', positions=positions, title="Positions") + + +@TYSC.route('/position/edit/', methods=['GET', 'POST']) +@login_required +def edit_position(position_id): + """ + Edit a position + """ + position = Position.objects.get({'_id': ObjectId(position_id)}) + form = PositionForm(obj=position) + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if position.sport: + form.sport.default = position.sport.pk + form.sport.process_data(position.sport.pk) + if form.validate_on_submit(): + position.name = form.name.data + position.description = form.description.data + position.sport = form.sport.data + position.save() + flash('You have successfully edited the position.') + return redirect(url_for('tysc.list_positions')) + form.name.data = position.name + form.description.data = position.description + return render_template('simpleform.html', action="Edit", + form=form, position=position, title="Edit position") + + +@TYSC.route('/position/add', methods=['GET', 'POST']) +@login_required +def add_position(): + """ + Add a position + :return: + """ + form = PositionForm() + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if form.validate_on_submit(): + position = Position() + position.name = form.name.data + position.description = form.description.data + position.sport = form.sport.data + try: + position.save() + flash('You have successfully added a new position.') + except PyMongoError: + flash('Error: team position already exists.') + return redirect(url_for('tysc.list_positions')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Position") + + +@TYSC.route('/position/delete/', methods=['GET', 'POST']) +@login_required +def delete_position(position_id): + """ + Delete a position + :param position_id: + :return: + """ + position = Position.objects.raw({'_id': ObjectId(position_id)}) + if position: + position.delete() + return redirect(url_for('tysc.list_positions')) + + +@TYSC.route('/player') +@login_required +def list_players(): + """ + List players. + :return: + """ + players = Player.objects.all() + return render_template('tysc/players.html', players=players, title="Players") + + +@TYSC.route('/player/edit/', methods=['GET', 'POST']) +@login_required +def edit_player(player_id): + """ + Edit a player + """ + player = Player.objects.get({'_id': ObjectId(player_id)}) + form = PlayerForm(obj=player) + if form.validate_on_submit(): + player.first_name = form.first_name.data + player.last_name = form.last_name.data + player.save() + flash('You have successfully edited the player.') + return redirect(url_for('tysc.list_players')) + form.first_name.data = player.first_name + form.last_name.data = player.last_name + return render_template('simpleform.html', action="Edit", + form=form, player=player, title="Edit player") + + +@TYSC.route('/player/add', methods=['GET', 'POST']) +@login_required +def add_player(): + """ + Add a player + :return: + """ + form = PlayerForm() + if form.validate_on_submit(): + player = Player() + player.first_name = form.first_name.data + player.last_name = form.last_name.data + try: + player.save() + flash('You have successfully added a new player.') + except PyMongoError: + flash('Error: player name already exists.') + return redirect(url_for('tysc.list_players')) + return render_template('simpleform.html', action="Add", + form=form, title="Add player") + + +@TYSC.route('/player/delete/', methods=['GET', 'POST']) +@login_required +def delete_player(player_id): + """ + Delete a player + :param player_id: + :return: + """ + player = Player.objects.raw({'_id': ObjectId(player_id)}) + if player: + player.delete() + return redirect(url_for('tysc.list_players')) + + +@TYSC.route('/manufacturer') +@login_required +def list_manufacturers(): + """ + List manufacturers. + :return: + """ + manufacturers = Manufacturer.objects.all() + return render_template('tysc/manufacturers.html', + manufacturers=manufacturers, + title="Manufacturers") + + +@TYSC.route('/manufacturer/edit/', methods=['GET', 'POST']) +@login_required +def edit_manufacturer(manufacturer_id): + """ + Edit a manufacturer + """ + manufacturer = Manufacturer.objects.get({'_id': ObjectId(manufacturer_id)}) + form = ManufacturerForm(obj=manufacturer) + if form.validate_on_submit(): + manufacturer.name = form.name.data + manufacturer.save() + flash('You have successfully edited the manufacturer.') + return redirect(url_for('tysc.list_manufacturers')) + form.name.data = manufacturer.name + return render_template('simpleform.html', action="Edit", + form=form, manufacturer=manufacturer, title="Edit Manufacturer") + + +@TYSC.route('/manufacturer/add', methods=['GET', 'POST']) +@login_required +def add_manufacturer(): + """ + Add a manufacturer + :return: + """ + form = ManufacturerForm() + if form.validate_on_submit(): + manufacturer = Manufacturer() + manufacturer.name = form.name.data + try: + manufacturer.save() + flash('You have successfully added a new manufacturer.') + except PyMongoError: + flash('Error: manufacturer name already exists.') + return redirect(url_for('tysc.list_manufacturers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Manufacturer") + + +@TYSC.route('/manufacturer/delete/', methods=['GET', 'POST']) +@login_required +def delete_manufacturer(manufacturer_id): + """ + Delete a manufacturer + :param manufacturer_id: + :return: + """ + manufacturer = Manufacturer.objects.raw({'_id': ObjectId(manufacturer_id)}) + if manufacturer: + manufacturer.delete() + flash('You have successfully deleted the manufacturer.') + return redirect(url_for('tysc.list_manufacturers')) + + +@TYSC.route('/cardsets') +@login_required +def list_card_sets(): + """ + List card sets. + :return: + """ + card_sets = CardSet.objects.all() + return render_template('tysc/cardsets.html', card_sets=card_sets, title="Card Sets") + + +@TYSC.route('/parallelsets') +@login_required +def list_parallel_sets(): + """ + List card sets. + :return: + """ + parallel_sets = ParallelSet.objects.all() + return render_template('tysc/parallelsets.html', + parallel_sets=parallel_sets, title="Parallel Sets") + + +@TYSC.route('/insertsets') +@login_required +def list_insert_sets(): + """ + List card sets. + :return: + """ + insert_sets = InsertSet.objects.all() + return render_template('tysc/insertsets.html', insert_sets=insert_sets, title="Inserts") + + +@TYSC.route('/cards') +@login_required +def list_cards(): + """ + List card sets. + :return: + """ + cards = Card.objects.all() + return render_template('tysc/cards.html', cards=cards, title="Cards") diff --git a/flask/kontor/version.py b/flask/kontor/version.py new file mode 100644 index 0000000..044c225 --- /dev/null +++ b/flask/kontor/version.py @@ -0,0 +1,2 @@ +"""This module declares the version of the Kontor application.""" +__version__ = '0.0.7' diff --git a/flask/pylint.cfg b/flask/pylint.cfg new file mode 100644 index 0000000..e08fe3a --- /dev/null +++ b/flask/pylint.cfg @@ -0,0 +1,382 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook='sys.path = list(); sys.path.append("./lib/python3.4/site-packages/")' + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=W1601,W1606,W1604,W1630,E1605,E1604,W1636,W1603,W1633,W1610,W1615,W1626,E1606,W0704,I0020,W1622,E1608,W1612,W1638,W1614,W1616,W1605,W1613,W1634,E1603,I0021,W1640,W1621,W1625,E1602,W1639,W1602,W1620,W1617,W1624,W1609,W1618,E1601,E1607,W1637,W1632,W1629,W1635,W1611,W1623,W1608,W1627,W1628,W1607,W1619,E1101,R0201,R0903,R0801,locally-disabled + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=parseable + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[BASIC] + +# Required attributes for module, separated by a comma +#required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=yes + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +#ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=stringprep,optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/flask/requirements.txt b/flask/requirements.txt new file mode 100644 index 0000000..f596076 --- /dev/null +++ b/flask/requirements.txt @@ -0,0 +1,11 @@ +flask +flask-login +flask-bootstrap +Flask-WTF +flask-testing +pymodm +nose +nose-htmloutput +coverage +pylint + diff --git a/flask/tests/__init__.py b/flask/tests/__init__.py new file mode 100644 index 0000000..8506c57 --- /dev/null +++ b/flask/tests/__init__.py @@ -0,0 +1,38 @@ +from flask_testing import TestCase +from kontor import create_app +from kontor.models import User + + +class TestBase(TestCase): + + def create_app(self): + + # pass in test configuration + config_name = 'testing' + app = create_app(config_name) + return app + + def setUp(self): + """ + Will be called before every test + """ + # create test admin user + admin = User() + admin.username="admin" + admin.password="admin2016" + admin.is_admin=True + admin.save() + + # create test non-admin user + employee = User() + employee.username="test_user" + employee.password="test2016" + employee.save() + + def tearDown(self): + """ + Will be called after every test + """ + users = User.objects.all() + for user in users: + user.delete() diff --git a/flask/tests/test_comics_model.py b/flask/tests/test_comics_model.py new file mode 100644 index 0000000..2a5dcc5 --- /dev/null +++ b/flask/tests/test_comics_model.py @@ -0,0 +1,51 @@ +""" +This module cantains tests for the comic data model +""" +import unittest +from . import TestBase +from kontor.comics.models import Artist, Comic, Publisher + + +class TestComicsModel(TestBase): + """This TestCase contains tests for comic data model.""" + + def setUp(self): + publisher = Publisher() + publisher.name = "Publisher1" + publisher.save() + artist = Artist() + artist.name = "Artist1" + artist.save() + comic = Comic() + comic.title = "Title1" + comic.save() + + def tearDown(self): + for comic in Comic.objects.all(): + comic.delete() + for artist in Artist.objects.all(): + artist.delete() + for publisher in Publisher.objects.all(): + publisher.delete() + + def test_comic_model(self): + comic_list = Comic.objects.all() + self.assertEqual(comic_list.count(), 1) + + def test_publisher_model(self): + self.assertEqual(Publisher.objects.all().count(), 1) + + def test_artist_model(self): + self.assertEqual(Artist.objects.all().count(), 1) + + def test_assign_publisher(self): + comic = Comic.objects.first() + publisher = Publisher.objects.first() + comic.publisher = publisher + comic.save() + self.assertEqual(publisher.name, comic.publisher.name) + self.assertEqual(publisher.comics.count(), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_comics_view.py b/flask/tests/test_comics_view.py new file mode 100644 index 0000000..e33fd79 --- /dev/null +++ b/flask/tests/test_comics_view.py @@ -0,0 +1,44 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestComicsViews(TestBase): + + def test_comics_index(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_comics') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_comics_artist(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_artists') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_comics_publisher(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_publishers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_config.txt b/flask/tests/test_config.txt new file mode 100644 index 0000000..2b847b0 --- /dev/null +++ b/flask/tests/test_config.txt @@ -0,0 +1,4 @@ +# instance/config.py + +SECRET_KEY = 'p9Bv<3Eid9%$i01' + diff --git a/flask/tests/test_kontor_model.py b/flask/tests/test_kontor_model.py new file mode 100644 index 0000000..0120475 --- /dev/null +++ b/flask/tests/test_kontor_model.py @@ -0,0 +1,14 @@ +""" +This module cantains tests for the general Kontor data model. +""" +from . import TestBase +from kontor.models import User + + +class TestKontorModel(TestBase): + """This TestCase contains tests for users.""" + def test_user_model(self): + """ + Test number of records in User collection + """ + self.assertEqual(User.objects.all().count(), 2) diff --git a/flask/tests/test_kontor_view.py b/flask/tests/test_kontor_view.py new file mode 100644 index 0000000..101ae2f --- /dev/null +++ b/flask/tests/test_kontor_view.py @@ -0,0 +1,58 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestKontorViews(TestBase): + + def test_homepage_view(self): + """ + Test that homepage is accessible without login + """ + response = self.client.get(url_for('home.homepage')) + self.assertEqual(response.status_code, 200) + + def test_login_view(self): + """ + Test that login page is accessible without login + """ + response = self.client.get(url_for('auth.login')) + self.assertEqual(response.status_code, 200) + + def test_logout_view(self): + """ + Test that logout link is inaccessible without login + and redirects to login page then to logout + """ + target_url = url_for('auth.logout') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_dashboard_view(self): + """ + Test that dashboard is inaccessible without login + and redirects to login page then to dashboard + """ + target_url = url_for('home.dashboard') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_admin_dashboard_view(self): + """ + Test that dashboard is inaccessible without login + and redirects to login page then to dashboard + """ + target_url = url_for('home.admin_dashboard') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_library_model.py b/flask/tests/test_library_model.py new file mode 100644 index 0000000..635bb58 --- /dev/null +++ b/flask/tests/test_library_model.py @@ -0,0 +1,49 @@ +""" +This module cantains tests for the comic data model +""" +import unittest +from . import TestBase +from kontor.library.models import Author, Publisher, Book + + +class TestComicsModel(TestBase): + """This TestCase contains tests for comic data model.""" + + def setUp(self): + publisher = Publisher() + publisher.name = "Publisher1" + publisher.save() + author = Author() + author.name = "Autor1" + author.save() + book = Book() + book.title = "Title1" + book.save() + + def tearDown(self): + for book in Book.objects.all(): + book.delete() + for author in Author.objects.all(): + author.delete() + for publisher in Publisher.objects.all(): + publisher.delete() + + def test_comic_model(self): + book_list = Book.objects.all() + self.assertEqual(book_list.count(), 1) + + def test_publisher_model(self): + self.assertEqual(Publisher.objects.all().count(), 1) + + def test_artist_model(self): + self.assertEqual(Author.objects.all().count(), 1) + + def test_assign_publisher(self): + book = Book.objects.first() + publisher = Publisher.objects.first() + book.publisher = publisher + book.save() + self.assertEqual(publisher.name, book.publisher.name) + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_library_view.py b/flask/tests/test_library_view.py new file mode 100644 index 0000000..51b2c30 --- /dev/null +++ b/flask/tests/test_library_view.py @@ -0,0 +1,44 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestLibraryViews(TestBase): + + def test_library_index(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_books') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_library_author(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_authors') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_library_publisher(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_publishers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_tysc_model.py b/flask/tests/test_tysc_model.py new file mode 100644 index 0000000..228cc49 --- /dev/null +++ b/flask/tests/test_tysc_model.py @@ -0,0 +1,53 @@ +""" +This module cantains tests for the TYSC data model +""" +import unittest +from . import TestBase +from kontor.tysc import initialize_model +from kontor.tysc.models import Sport, Position, Team, Player +from kontor.tysc.models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +class TestTYSCModel(TestBase): + """ + This TestCase contains tests for TYSC data model. + """ + _initialize_db_ = True + + def setUp(self): + if self._initialize_db_: + initialize_model() + self._initialize_db_ = False + + def tearDown(self): + pass + + def test_sport_model(self): + self.assertEqual(Sport.objects.all().count(), 4) + + def test_position_model(self): + self.assertEqual(Position.objects.all().count(), 25) + + def test_team_model(self): + self.assertEqual(Team.objects.all().count(), 122) + + def test_manufacturer_model(self): + self.assertEqual(Manufacturer.objects.all().count(), 8) + + def test_cardset_model(self): + self.assertEqual(CardSet.objects.all().count(), 11) + + def test_parallelset_model(self): + self.assertEqual(ParallelSet.objects.all().count(), 3) + + def test_insertset_model(self): + self.assertEqual(InsertSet.objects.all().count(), 1) + + def test_playerset_model(self): + self.assertEqual(Player.objects.all().count(), 36) + + def test_card_model(self): + self.assertEqual(Card.objects.all().count(), 36) + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_tysc_view.py b/flask/tests/test_tysc_view.py new file mode 100644 index 0000000..11fc222 --- /dev/null +++ b/flask/tests/test_tysc_view.py @@ -0,0 +1,110 @@ +"""This module contains tests for TradeYourSportsCards related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestTYSCViews(TestBase): + + def test_tysc_index(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_cards') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_sport(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_sports') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_team(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_teams') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_player(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_players') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_manufacturer(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_manufacturers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_card_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_card_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_parallel_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_parallel_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_insert_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_insert_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_card(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_cards') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() -- 2.18.0 From 5c261529fc4ec7cb15869b40a31efc7d9c48e935 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 22:34:21 +0100 Subject: [PATCH 16/16] import from kontor-flask branch develop/0.1.0 --- flask/.gitignore | 2 + flask/README.md | 2 + flask/app.py | 5 + flask/config.py | 47 -- flask/docs/Makefile | 20 - flask/docs/conf.py | 200 --------- flask/docs/index.rst | 20 - flask/kontor/__init__.py | 115 +++-- flask/kontor/admin/__init__.py | 4 - flask/kontor/admin/forms.py | 17 - flask/kontor/admin/views.py | 63 --- flask/kontor/api/__init__.py | 5 + flask/kontor/api/routes.py | 30 ++ flask/kontor/auth/__init__.py | 26 +- flask/kontor/auth/forms.py | 50 --- flask/kontor/auth/models.py | 45 ++ flask/kontor/auth/views.py | 79 ---- flask/kontor/comics/__init__.py | 12 +- flask/kontor/comics/api.py | 28 ++ flask/kontor/comics/forms.py | 31 -- flask/kontor/comics/models.py | 51 --- flask/kontor/comics/routes.py | 16 + .../kontor/comics/templates/comics/list.html | 28 ++ .../kontor/comics/templates/comics/view.html | 27 ++ flask/kontor/comics/views.py | 222 ---------- flask/kontor/config.py | 10 + flask/kontor/extensions.py | 5 + flask/kontor/home/__init__.py | 3 - flask/kontor/home/views.py | 39 -- flask/kontor/library/__init__.py | 3 - flask/kontor/library/forms.py | 35 -- flask/kontor/library/models.py | 55 --- flask/kontor/library/views.py | 228 ---------- flask/kontor/main/__init__.py | 5 + flask/kontor/main/routes.py | 18 + flask/kontor/media/__init__.py | 9 + flask/kontor/media/api.py | 18 + flask/kontor/media/models.py | 43 ++ flask/kontor/media/routes.py | 16 + .../templates/media/mediafile_detail.html | 29 ++ .../media/templates/media/mediafile_list.html | 30 ++ flask/kontor/models.py | 101 ++--- flask/kontor/office/__init__.py | 3 - flask/kontor/static/css/style.css | 126 ------ flask/kontor/templates/admin/users/user.html | 21 - flask/kontor/templates/admin/users/users.html | 58 --- flask/kontor/templates/auth/login.html | 18 - flask/kontor/templates/auth/register.html | 14 - flask/kontor/templates/base.html | 157 +++---- flask/kontor/templates/comics/artists.html | 58 --- flask/kontor/templates/comics/comics.html | 66 --- flask/kontor/templates/comics/publishers.html | 58 --- .../templates/home/admin_dashboard.html | 31 -- flask/kontor/templates/home/dashboard.html | 20 - flask/kontor/templates/home/index.html | 20 - flask/kontor/templates/index.html | 8 + flask/kontor/templates/library/authors.html | 58 --- flask/kontor/templates/library/books.html | 74 ---- .../kontor/templates/library/publishers.html | 58 --- flask/kontor/templates/simpleform.html | 21 - .../kontor/templates/tysc/manufacturers.html | 58 --- flask/kontor/templates/tysc/players.html | 60 --- flask/kontor/templates/tysc/positions.html | 62 --- flask/kontor/templates/tysc/sports.html | 58 --- flask/kontor/templates/tysc/teams.html | 60 --- flask/kontor/tysc/__init__.py | 377 ---------------- flask/kontor/tysc/forms.py | 76 ---- flask/kontor/tysc/models.py | 155 ------- flask/kontor/tysc/views.py | 405 ------------------ flask/kontor/version.py | 2 - flask/pylint.cfg | 382 ----------------- flask/requirements.txt | 11 - flask/tests/__init__.py | 38 -- flask/tests/test_comics_model.py | 51 --- flask/tests/test_comics_view.py | 44 -- flask/tests/test_config.txt | 4 - flask/tests/test_kontor_model.py | 14 - flask/tests/test_kontor_view.py | 58 --- flask/tests/test_library_model.py | 49 --- flask/tests/test_library_view.py | 44 -- flask/tests/test_tysc_model.py | 53 --- flask/tests/test_tysc_view.py | 110 ----- 82 files changed, 562 insertions(+), 4270 deletions(-) create mode 100644 flask/.gitignore create mode 100644 flask/README.md create mode 100644 flask/app.py delete mode 100644 flask/config.py delete mode 100644 flask/docs/Makefile delete mode 100644 flask/docs/conf.py delete mode 100644 flask/docs/index.rst delete mode 100644 flask/kontor/admin/__init__.py delete mode 100644 flask/kontor/admin/forms.py delete mode 100644 flask/kontor/admin/views.py create mode 100644 flask/kontor/api/__init__.py create mode 100644 flask/kontor/api/routes.py delete mode 100644 flask/kontor/auth/forms.py create mode 100644 flask/kontor/auth/models.py delete mode 100644 flask/kontor/auth/views.py create mode 100644 flask/kontor/comics/api.py delete mode 100644 flask/kontor/comics/forms.py delete mode 100644 flask/kontor/comics/models.py create mode 100644 flask/kontor/comics/routes.py create mode 100644 flask/kontor/comics/templates/comics/list.html create mode 100644 flask/kontor/comics/templates/comics/view.html delete mode 100644 flask/kontor/comics/views.py create mode 100644 flask/kontor/config.py create mode 100644 flask/kontor/extensions.py delete mode 100644 flask/kontor/home/__init__.py delete mode 100644 flask/kontor/home/views.py delete mode 100644 flask/kontor/library/__init__.py delete mode 100644 flask/kontor/library/forms.py delete mode 100644 flask/kontor/library/models.py delete mode 100644 flask/kontor/library/views.py create mode 100644 flask/kontor/main/__init__.py create mode 100644 flask/kontor/main/routes.py create mode 100644 flask/kontor/media/__init__.py create mode 100644 flask/kontor/media/api.py create mode 100644 flask/kontor/media/models.py create mode 100644 flask/kontor/media/routes.py create mode 100644 flask/kontor/media/templates/media/mediafile_detail.html create mode 100644 flask/kontor/media/templates/media/mediafile_list.html delete mode 100644 flask/kontor/office/__init__.py delete mode 100644 flask/kontor/static/css/style.css delete mode 100644 flask/kontor/templates/admin/users/user.html delete mode 100644 flask/kontor/templates/admin/users/users.html delete mode 100644 flask/kontor/templates/auth/login.html delete mode 100644 flask/kontor/templates/auth/register.html delete mode 100644 flask/kontor/templates/comics/artists.html delete mode 100644 flask/kontor/templates/comics/comics.html delete mode 100644 flask/kontor/templates/comics/publishers.html delete mode 100644 flask/kontor/templates/home/admin_dashboard.html delete mode 100644 flask/kontor/templates/home/dashboard.html delete mode 100644 flask/kontor/templates/home/index.html create mode 100644 flask/kontor/templates/index.html delete mode 100644 flask/kontor/templates/library/authors.html delete mode 100644 flask/kontor/templates/library/books.html delete mode 100644 flask/kontor/templates/library/publishers.html delete mode 100644 flask/kontor/templates/simpleform.html delete mode 100644 flask/kontor/templates/tysc/manufacturers.html delete mode 100644 flask/kontor/templates/tysc/players.html delete mode 100644 flask/kontor/templates/tysc/positions.html delete mode 100644 flask/kontor/templates/tysc/sports.html delete mode 100644 flask/kontor/templates/tysc/teams.html delete mode 100644 flask/kontor/tysc/__init__.py delete mode 100644 flask/kontor/tysc/forms.py delete mode 100644 flask/kontor/tysc/models.py delete mode 100644 flask/kontor/tysc/views.py delete mode 100644 flask/kontor/version.py delete mode 100644 flask/pylint.cfg delete mode 100644 flask/requirements.txt delete mode 100644 flask/tests/__init__.py delete mode 100644 flask/tests/test_comics_model.py delete mode 100644 flask/tests/test_comics_view.py delete mode 100644 flask/tests/test_config.txt delete mode 100644 flask/tests/test_kontor_model.py delete mode 100644 flask/tests/test_kontor_view.py delete mode 100644 flask/tests/test_library_model.py delete mode 100644 flask/tests/test_library_view.py delete mode 100644 flask/tests/test_tysc_model.py delete mode 100644 flask/tests/test_tysc_view.py diff --git a/flask/.gitignore b/flask/.gitignore new file mode 100644 index 0000000..259ed2a --- /dev/null +++ b/flask/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.idea diff --git a/flask/README.md b/flask/README.md new file mode 100644 index 0000000..6aad8a5 --- /dev/null +++ b/flask/README.md @@ -0,0 +1,2 @@ +# Kontor Flask + diff --git a/flask/app.py b/flask/app.py new file mode 100644 index 0000000..dc827b3 --- /dev/null +++ b/flask/app.py @@ -0,0 +1,5 @@ +from kontor import create_app + +if __name__ == '__main__': + app = create_app() + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/flask/config.py b/flask/config.py deleted file mode 100644 index 4ea5e78..0000000 --- a/flask/config.py +++ /dev/null @@ -1,47 +0,0 @@ -# config.py - - -class Config(object): - """ - Common configurations - """ - - # Put any configurations here that are common across all environments - host = '127.0.0.1' - port = 8500 - database = 'kontor' - - -class DevelopmentConfig(Config): - """ - Development configurations - """ - - DEBUG = True - host = '0.0.0.0' - database = 'kontor_dev' - - -class ProductionConfig(Config): - """ - Production configurations - """ - - DEBUG = False - -class TestingConfig(Config): - """ - Testing configurations - """ - - TESTING = True - DEBUG = True - port = 8600 - database = 'kontor_test' - -app_config = { - 'development': DevelopmentConfig, - 'production': ProductionConfig, - 'testing': TestingConfig -} - diff --git a/flask/docs/Makefile b/flask/docs/Makefile deleted file mode 100644 index d7218b4..0000000 --- a/flask/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = KontorFlask -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/flask/docs/conf.py b/flask/docs/conf.py deleted file mode 100644 index 7278f2d..0000000 --- a/flask/docs/conf.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Kontor Flask documentation build configuration file, created by -# sphinx-quickstart on Mon Nov 6 08:53:04 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'Kontor Flask' -copyright = '2017, Thomas Peetz' -author = 'Thomas Peetz' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.1' -# The full version, including alpha/beta/rc tags. -release = '0.0.7' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'de' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'KontorFlaskdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'KontorFlask.tex', 'Kontor Flask Documentation', - 'Thomas Peetz', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'kontorflask', 'Kontor Flask Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'KontorFlask', 'Kontor Flask Documentation', - author, 'KontorFlask', 'One line description of project.', - 'Miscellaneous'), -] - - - -# -- Options for Epub output ---------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project -epub_author = author -epub_publisher = author -epub_copyright = copyright - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/flask/docs/index.rst b/flask/docs/index.rst deleted file mode 100644 index ca4df98..0000000 --- a/flask/docs/index.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. Kontor Flask documentation master file, created by - sphinx-quickstart on Mon Nov 6 08:53:04 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Kontor Flask's documentation! -======================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/flask/kontor/__init__.py b/flask/kontor/__init__.py index 961e132..f8cb55c 100644 --- a/flask/kontor/__init__.py +++ b/flask/kontor/__init__.py @@ -1,74 +1,61 @@ -""" -Module kontor implements Kontor application. -""" +from flask import Flask, render_template +from flask_jwt_extended import JWTManager -from flask import Flask -from flask_bootstrap import Bootstrap -from flask_login import LoginManager -from pymodm import connect +from kontor import config +from kontor.extensions import db, ma +from logging.config import dictConfig -# local imports -from config import app_config -from .version import __version__ +dictConfig({ + 'version': 1, + 'formatters': {'default': { + 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + }}, + 'handlers': {'wsgi': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://flask.logging.wsgi_errors_stream', + 'formatter': 'default' + }}, + 'root': { + 'level': 'INFO', + 'handlers': ['wsgi'] + } +}) + +app = Flask(__name__) -_LOGIN_MANAGER_ = LoginManager() +def create_app(config_class=config.Config): + app.config.from_object(config_class) + db.init_app(app) + ma.init_app(app) + # Initialize Flask extensions here + app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! + jwt = JWTManager(app) -def get_host(config_name): - """ - Returns host address from configuration. - :param config_name: - :return: host address - """ - host = app_config[config_name].host - return host + with app.app_context(): + # db.create_all() + db.reflect() + # Register blueprints here + from kontor.main import bp as main_bp + app.register_blueprint(main_bp) + from kontor.comics import comics_bp + app.register_blueprint(comics_bp, url_prefix='/comics') + from kontor.api import api_bp + app.register_blueprint(api_bp, url_prefix='/api/v1') + from kontor.comics import comics_api + app.register_blueprint(comics_api, url_prefix='/api/v1/comics') -def get_port(config_name): - """ - Returns port number from configuration. - :param config_name: - :return: port number - """ - port = app_config[config_name].port - return port - - -def create_app(config_name): - """ - Returns Flask application. - :param config_name: - :return: Flask application - """ - app = Flask(__name__, instance_relative_config=True) - app.config.from_object(app_config[config_name]) - app.config.from_pyfile('config.py') - - database = "mongodb://localhost:27017/{}".format(app_config[config_name].database) - connect(database, alias="kontor") - - Bootstrap(app) - _LOGIN_MANAGER_.init_app(app) - _LOGIN_MANAGER_.login_message = "You must be logged in to access this page." - _LOGIN_MANAGER_.login_view = "auth.login" - - from .admin.views import ADMIN as ADMIN_BLUEPRINT - app.register_blueprint(ADMIN_BLUEPRINT, url_prefix='/admin') - - from .auth.views import AUTH as AUTH_BLUEPRINT - app.register_blueprint(AUTH_BLUEPRINT) - - from .home.views import HOME as HOME_BLUEPRINT - app.register_blueprint(HOME_BLUEPRINT) - - from .comics.views import COMIC as COMIC_BLUEPRINT - app.register_blueprint(COMIC_BLUEPRINT, url_prefix='/comics') - - from .library.views import LIBRARY as LIBRARY_BLUEPRINT - app.register_blueprint(LIBRARY_BLUEPRINT, url_prefix='/library') - - from .tysc.views import TYSC as TYSC_BLUEPRINT - app.register_blueprint(TYSC_BLUEPRINT, url_prefix='/tysc') + from kontor.media import media_bp + app.register_blueprint(media_bp, url_prefix='/media') + from kontor.media import media_api + app.register_blueprint(media_api, url_prefix='/api/v1/media') + # from kontor.auth.auth import auth_bp + # from kontor.cart.cart import cart_bp + # from kontor.general.general import general_bp + # app.register_blueprint(auth_bp) + # app.register_blueprint(cart_bp, url_prefix='/cart') + # app.register_blueprint(general_bp) return app diff --git a/flask/kontor/admin/__init__.py b/flask/kontor/admin/__init__.py deleted file mode 100644 index ca00a4b..0000000 --- a/flask/kontor/admin/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Module admin implements administration functions. -""" -from . import views diff --git a/flask/kontor/admin/forms.py b/flask/kontor/admin/forms.py deleted file mode 100644 index d05cfaf..0000000 --- a/flask/kontor/admin/forms.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Define form to edit users -""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField -from wtforms.validators import DataRequired - - -class UserEditForm(FlaskForm): - """ - Form for admin to edit users - """ - email = StringField('Email', validators=[DataRequired()]) - username = StringField('Username', validators=[DataRequired()]) - first_name = StringField('First Name', validators=[DataRequired()]) - last_name = StringField('Last Name', validators=[DataRequired()]) - submit = SubmitField('Submit') diff --git a/flask/kontor/admin/views.py b/flask/kontor/admin/views.py deleted file mode 100644 index e96d8f0..0000000 --- a/flask/kontor/admin/views.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Define routing rules for managing users -""" -from flask import abort, Blueprint, flash, redirect, render_template, url_for -from flask_login import current_user, login_required - -from .forms import UserEditForm -from ..models import User - -ADMIN = Blueprint('admin', __name__) - - -def check_admin(): - """ - Prevent non-admins from accessing the page - """ - if not current_user.is_admin: - abort(403) - - -# User Views -@ADMIN.route('/users') -@login_required -def list_users(): - """ - List all users - """ - check_admin() - - users = User.objects.all() - return render_template('admin/users/users.html', - users=users, title='Users') - - -@ADMIN.route('/users/edit/', methods=['GET', 'POST']) -@login_required -def edit_user(user_id): - """ - Edit an user. - """ - check_admin() - - user = User.objects.get({'username': user_id}) - - # prevent admin from being assigned a department or role - if user.is_admin: - abort(403) - - form = UserEditForm(obj=user) - if form.validate_on_submit(): - user.email = form.email.data - user.username = form.username.data - user.first_name = form.first_name.data - user.last_name = form.last_name.data - user.save() - flash('You have successfully edited an user.') - - # redirect to the roles page - return redirect(url_for('admin.list_users')) - - return render_template('admin/users/user.html', - user=user, form=form, - title='Edit User') diff --git a/flask/kontor/api/__init__.py b/flask/kontor/api/__init__.py new file mode 100644 index 0000000..b26fcb6 --- /dev/null +++ b/flask/kontor/api/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +api_bp = Blueprint('api_bp', __name__) + +from kontor.api import routes diff --git a/flask/kontor/api/routes.py b/flask/kontor/api/routes.py new file mode 100644 index 0000000..4e9b489 --- /dev/null +++ b/flask/kontor/api/routes.py @@ -0,0 +1,30 @@ +from flask import jsonify, request +from flask_jwt_extended import jwt_required, get_jwt_identity, create_access_token + +from kontor.api import api_bp + + +@api_bp.route('/') +def index(): + modules = ['comics'] + return jsonify(modules) + + +# Create a route to authenticate your users and return JWTs. The +# create_access_token() function is used to actually generate the JWT. +@api_bp.route("/login", methods=["POST"]) +def login(): + username = request.json.get("username", None) + password = request.json.get("password", None) + if username != "test" or password != "test": + return jsonify({"msg": "Bad username or password"}), 401 + + access_token = create_access_token(identity=username) + return jsonify(access_token=access_token) + + +@api_bp.route('/protected', methods=['GET']) +@jwt_required() +def protected(): + current_user = get_jwt_identity() + return {'message': f'Hello, {current_user}!'} diff --git a/flask/kontor/auth/__init__.py b/flask/kontor/auth/__init__.py index 2d9b1ca..9da7988 100644 --- a/flask/kontor/auth/__init__.py +++ b/flask/kontor/auth/__init__.py @@ -1,4 +1,22 @@ -""" -Module auth implements authentication functions. -""" -from . import views +import bcrypt +from flask import session +from flask_httpauth import HTTPBasicAuth + +from kontor import app +from kontor.auth.models import User + +auth = HTTPBasicAuth() + + +@auth.verify_password +def verify_password(username, password): + if username is None: + return False + # Add your authentication logic here + app.logger.info("login user %s", username) + user = User.query.filter_by(user_name=username).first() + app.logger.info("User: %s", user) + app.logger.info("Stored Password: '%s' Hashed Password: '%s'", user.password, bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())) + if 'user_name' in session and session['user_name'] == username: + return True + return False \ No newline at end of file diff --git a/flask/kontor/auth/forms.py b/flask/kontor/auth/forms.py deleted file mode 100644 index 6e4d6dd..0000000 --- a/flask/kontor/auth/forms.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Contains forms for authentication. -""" -from flask_wtf import FlaskForm -from wtforms import PasswordField, StringField, SubmitField, ValidationError -from wtforms.validators import DataRequired, Email, EqualTo - -from ..models import User - - -class RegistrationForm(FlaskForm): - """ - Form for users to create new account - """ - email = StringField('Email', validators=[DataRequired(), Email()]) - username = StringField('Username', validators=[DataRequired()]) - first_name = StringField('First Name', validators=[DataRequired()]) - last_name = StringField('Last Name', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired(), - EqualTo('confirm_password') - ]) - confirm_password = PasswordField('Confirm Password') - submit = SubmitField('Register') - - def validate_email(self, field): - """ - Check if email is already in use. - :param field: - :return: - """ - if User.objects.raw({'email': field.data}).count() > 0: - raise ValidationError('Email is already in use.') - - def validate_username(self, field): - """ - check if username is already in use. - :param field: - :return: - """ - if User.objects.raw({'username': field.data}).count() > 0: - raise ValidationError('Username is already in use.') - - -class LoginForm(FlaskForm): - """ - Form for users to login - """ - email = StringField('Email', validators=[DataRequired(), Email()]) - password = PasswordField('Password', validators=[DataRequired()]) - submit = SubmitField('Login') diff --git a/flask/kontor/auth/models.py b/flask/kontor/auth/models.py new file mode 100644 index 0000000..fc3b8d4 --- /dev/null +++ b/flask/kontor/auth/models.py @@ -0,0 +1,45 @@ +from kontor.extensions import db, ma +from sqlalchemy.sql import func + + +class User(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "user" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + enabled = db.Column(db.SmallInteger) + email = db.Column(db.String) + first_name = db.Column(db.String) + last_name = db.Column(db.String) + user_name = db.Column(db.String) + password = db.Column(db.String) + token = db.Column(db.String) + token_expired = db.Column(db.SmallInteger) + + def is_token_valid(self): + return self.review == 'b\x01' + + def is_user_enabled(self): + return self.should_download == 'b\x01' + +class Role(db.Model): + __tablename__ = "role" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + name = db.Column(db.String) + +class AuthorizationMatrix(db.Model): + __tablename__ = "authorization_matrix" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + role_id = db.Column(db.String, db.ForeignKey("role.id")) + user_id = db.Column(db.String, db.ForeignKey("user.id")) diff --git a/flask/kontor/auth/views.py b/flask/kontor/auth/views.py deleted file mode 100644 index 217ae18..0000000 --- a/flask/kontor/auth/views.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Define routing rules for registering users and login and logout. -""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required, login_user, logout_user -from .forms import LoginForm, RegistrationForm -from ..models import User - -AUTH = Blueprint('auth', __name__) - - -@AUTH.route('/register', methods=['GET', 'POST']) -def register(): - """ - Handle requests to the /register route - Add an employee to the database through the registration form - """ - form = RegistrationForm() - if form.validate_on_submit(): - user = User() - user.email = form.email.data - user.username = form.username.data - user.first_name = form.first_name.data - user.last_name = form.last_name.data - user.password = form.password.data - # add employee to the database - user.save() - flash('You have successfully registered! You may now login.') - - # redirect to the login page - return redirect(url_for('auth.login')) - - # load registration template - return render_template('auth/register.html', form=form, title='Register') - - -@AUTH.route('/login', methods=['GET', 'POST']) -def login(): - """ - Handle requests to the /login route - Validate given email with matching password - :return: - """ - form = LoginForm() - if form.validate_on_submit(): - - # check whether employee exists in the database and whether - # the password entered matches the password in the database - user = User.objects.get({'email': form.email.data}) - if user is not None and user.verify_password( - form.password.data): - # log employee in - login_user(user) - - # redirect to the appropriate dashboard page - if user.is_admin: - return redirect(url_for('home.admin_dashboard')) - else: - return redirect(url_for('home.dashboard')) - - # when login details are incorrect - else: - flash('Invalid email or password.') - - # load login template - return render_template('auth/login.html', form=form, title='Login') - -@AUTH.route('/logout') -@login_required -def logout(): - """ - Handle requests to the /logout route - Log an employee out through the logout link - """ - logout_user() - flash('You have successfully been logged out.') - - # redirect to the login page - return redirect(url_for('auth.login')) diff --git a/flask/kontor/comics/__init__.py b/flask/kontor/comics/__init__.py index e947d82..12bb371 100644 --- a/flask/kontor/comics/__init__.py +++ b/flask/kontor/comics/__init__.py @@ -1,3 +1,9 @@ -""" -Define routing rules for comic related information -""" +from flask import Blueprint + +comics_bp = Blueprint('comics_bp', __name__, + template_folder='templates', + static_folder='static', static_url_path='assets') +comics_api = Blueprint('comics_api', __name__) + +from kontor.comics import routes +from kontor.comics import api diff --git a/flask/kontor/comics/api.py b/flask/kontor/comics/api.py new file mode 100644 index 0000000..22bca28 --- /dev/null +++ b/flask/kontor/comics/api.py @@ -0,0 +1,28 @@ +from flask import Blueprint, render_template, jsonify + +from kontor.comics import comics_api +from kontor.models import Comic, comics_schema, Publisher, comic_schema, publisher_schema, publishers_schema + + +@comics_api.route('/') +def index(): + comics = Comic.query.all() + return comics_schema.dump(comics) + + +@comics_api.route('/comic/') +def view(comic_id): + comic = Comic.query.get(comic_id) + return comic_schema.dump(comic) + + +@comics_api.route('/publisher/') +def publisher_all(): + publishers = Publisher.query.all() + return publishers_schema.dump(publishers) + + +@comics_api.route('/publisher/') +def publisher_detail(publisher_id): + publisher = Publisher.query.get(publisher_id) + return publisher_schema.dump(publisher) \ No newline at end of file diff --git a/flask/kontor/comics/forms.py b/flask/kontor/comics/forms.py deleted file mode 100644 index 0bae45f..0000000 --- a/flask/kontor/comics/forms.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Define form to edit publisher, artists and comics""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, BooleanField, SelectField -from wtforms.validators import DataRequired -from bson import ObjectId - - -class PublisherForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class ArtistForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class ComicForm(FlaskForm): - """ - Form to add and edit Comics - """ - title = StringField('Title', validators=[DataRequired()]) - publisher = SelectField('Publisher', coerce=ObjectId) - current_order = BooleanField('Current Order') - submit = SubmitField('Submit') diff --git a/flask/kontor/comics/models.py b/flask/kontor/comics/models.py deleted file mode 100644 index e39e744..0000000 --- a/flask/kontor/comics/models.py +++ /dev/null @@ -1,51 +0,0 @@ -"""This modules declares the model for Comic related information.""" - -from flask import current_app -from pymongo.write_concern import WriteConcern -from pymodm import MongoModel, fields - - -class Publisher(MongoModel): - """Class Publisher represents a publisher of a comic.""" - name = fields.CharField() - - def __str__(self): - return "Publisher({})".format(self.name) - - @property - def comics(self): - """ - Return list of comics which has reference to this publisher - :return: - """ - comics = Comic.objects.raw({'publisher': self.pk}) - current_app.logger.debug(comics) - return comics - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Artist(MongoModel): - """Class Artist represents a comic artist.""" - name = fields.CharField() - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Comic(MongoModel): - """Class Comic represents a comic.""" - title = fields.CharField() - publisher = fields.ReferenceField(Publisher) - current_order = fields.BooleanField(default=False) - completed = fields.BooleanField(default=False) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) diff --git a/flask/kontor/comics/routes.py b/flask/kontor/comics/routes.py new file mode 100644 index 0000000..0b0f53e --- /dev/null +++ b/flask/kontor/comics/routes.py @@ -0,0 +1,16 @@ +from flask import Blueprint, render_template + +from kontor.comics import comics_bp +from kontor.models import Comic + + +@comics_bp.route('/') +def index(): + comics = Comic.query.all() + return render_template('comics/list.html', comics=comics) + + +@comics_bp.route('/comic/') +def view(comic_id): + comic = Comic.query.get(comic_id) + return render_template('comics/view.html', comic=comic) diff --git a/flask/kontor/comics/templates/comics/list.html b/flask/kontor/comics/templates/comics/list.html new file mode 100644 index 0000000..c0f4922 --- /dev/null +++ b/flask/kontor/comics/templates/comics/list.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} Comics {% endblock %}

+
+
+

Comics Blueprint

+ {% for comic in comics %} +
+ +

+ {{ comic.title }} +

+
+

{{ comic.publisher.name }}

+
+

Created

+

{{ comic.created_date }}

+

Modified

+

{{ comic.last_modified_date }}

+
+

Version: {{ comic.version }}

+

Review: {{ comic.review }}

+
+ {% endfor %} +
+{% endblock %} diff --git a/flask/kontor/comics/templates/comics/view.html b/flask/kontor/comics/templates/comics/view.html new file mode 100644 index 0000000..b70da14 --- /dev/null +++ b/flask/kontor/comics/templates/comics/view.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} Comics {% endblock %}

+
+
+

Comics Blueprint

+
+ +

+ {{ comic.title }} +

+
+ +

{{ comic.id }}

+
+
+

Created

+

{{ comic.created_date }}

+

Modified

+

{{ comic.last_modified_date }}

+
+

Version: {{ comic.version }}

+
+
+{% endblock %} diff --git a/flask/kontor/comics/views.py b/flask/kontor/comics/views.py deleted file mode 100644 index 090b032..0000000 --- a/flask/kontor/comics/views.py +++ /dev/null @@ -1,222 +0,0 @@ -""" -Define routing rules for comics, publisher and artists -""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required -from bson import ObjectId -from pymongo.errors import PyMongoError -from .forms import ComicForm, PublisherForm, ArtistForm -from .models import Comic, Publisher, Artist - - -COMIC = Blueprint('comic', __name__) - - -@COMIC.route('/artists') -@login_required -def list_artists(): - """ - List all artists - :return: - """ - artists = Artist.objects.all() - return render_template('comics/artists.html', - artists=artists, title="Artists") - - -@COMIC.route('/artists/edit/', methods=['GET', 'POST']) -@login_required -def edit_artist(artist_id): - """ - Edit a comic artist - """ - artist = Artist.objects.get({'_id': ObjectId(artist_id)}) - form = ArtistForm(obj=artist) - if form.validate_on_submit(): - artist.name = form.name.data - artist.save() - flash('You have successfully edited the artist.') - return redirect(url_for('comic.list_artists')) - form.name.data = artist.name - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Artist") - - -@COMIC.route('/artists/add', methods=['GET', 'POST']) -@login_required -def add_artist(): - """ - Add a artist - :return: - """ - form = ArtistForm() - if form.validate_on_submit(): - artist = Artist() - artist.name = form.name.data - try: - # add publisher to the database - artist.save() - flash('You have successfully added a new artist.') - except PyMongoError: - # in case publisher name already exists - flash('Error: artist name already exists.') - return redirect(url_for('comic.list_artists')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Artist") - - -@COMIC.route('/artists/delete/', methods=['GET', 'POST']) -@login_required -def delete_artist(artist_id): - """ - Delete a comic artist - :param artist_id: - :return: - """ - artist = Artist.objects.raw({'_id': ObjectId(artist_id)}) - if artist: - artist.delete() - flash('You have successfully deleted the comic artist.') - return redirect(url_for('comic.list_artists')) - - -@COMIC.route('/publishers') -@login_required -def list_publishers(): - """ - List all publishers - :return: - """ - publishers = Publisher.objects.all() - return render_template('comics/publishers.html', - publishers=publishers, title="Publishers") - - -@COMIC.route('/publishers/add', methods=['GET', 'POST']) -@login_required -def add_publisher(): - """ - Add a publisher to the database - :return: - """ - form = PublisherForm() - if form.validate_on_submit(): - publisher = Publisher() - publisher.name = form.name.data - try: - # add publisher to the database - publisher.save() - flash('You have successfully added a new publisher.') - except PyMongoError: - # in case publisher name already exists - flash('Error: publisher name already exists.') - return redirect(url_for('comic.list_publishers')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Publisher") - - -@COMIC.route('/publishers/edit/', methods=['GET', 'POST']) -@login_required -def edit_publisher(publisher_id): - """ - Edit a publisher - """ - publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) - form = PublisherForm(obj=publisher) - if form.validate_on_submit(): - publisher.name = form.name.data - publisher.save() - flash('You have successfully edited the publisher.') - return redirect(url_for('comic.list_publishers')) - form.name.data = publisher.name - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Publisher") - - -@COMIC.route('/publishers/delete/', methods=['GET', 'POST']) -@login_required -def delete_publisher(publisher_id): - """ - Delete a publisher - :param publisher_id: ObjectId of publisher - :return: - """ - publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) - if publisher: - publisher.delete() - flash('You have successfully deleted the publisher.') - return redirect(url_for('comic.list_publishers')) - - -@COMIC.route('/comics') -@login_required -def list_comics(): - """ - List all comics - :return: - """ - comics = Comic.objects.all() - return render_template('comics/comics.html', - comics=comics, title="Comics") - - -@COMIC.route('/comics/edit/', methods=['GET', 'POST']) -@login_required -def edit_comic(comic_id): - """ - Edit a comic - """ - comic = Comic.objects.get({'_id': ObjectId(comic_id)}) - form = ComicForm(obj=comic) - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - form.publisher.default = comic.publisher.pk - form.publisher.process_data(comic.publisher.pk) - if form.validate_on_submit(): - comic.title = form.title.data - comic.current_order = form.current_order.data - comic.publisher = form.publisher.data - comic.save() - flash('You have successfully edited the comic.') - return redirect(url_for('comic.list_comics')) - form.title.data = comic.title - form.current_order.data = comic.current_order - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Comic") - - -@COMIC.route('/comics/add', methods=['GET', 'POST']) -@login_required -def add_comic(): - """ - Add a comic - :return: - """ - form = ComicForm() - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - if form.validate_on_submit(): - comic = Comic() - comic.title = form.title.data - comic.publisher = form.publisher.data - try: - comic.save() - flash('You have successfully added a new comic.') - except PyMongoError: - flash('Error: comic title already exists.') - return redirect(url_for('comic.list_comics')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Comic") - - -@COMIC.route('/comics/delete/', methods=['GET', 'POST']) -@login_required -def delete_comic(comic_id): - """ - Delete a comic - :param comic_id: - :return: - """ - comic = Comic.objects.raw({'_id': ObjectId(comic_id)}) - if comic: - comic.delete() - flash('You have successfully deleted the comic.') - return redirect(url_for('comic.list_comics')) diff --git a/flask/kontor/config.py b/flask/kontor/config.py new file mode 100644 index 0000000..8382f66 --- /dev/null +++ b/flask/kontor/config.py @@ -0,0 +1,10 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') + # SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir, 'kontor.db') + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'mariadb+mariadbconnector://kontor:kontor@localhost/kontor' + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/flask/kontor/extensions.py b/flask/kontor/extensions.py new file mode 100644 index 0000000..461bd5d --- /dev/null +++ b/flask/kontor/extensions.py @@ -0,0 +1,5 @@ +import flask_sqlalchemy +from flask_marshmallow import Marshmallow + +db = flask_sqlalchemy.SQLAlchemy() +ma = Marshmallow() diff --git a/flask/kontor/home/__init__.py b/flask/kontor/home/__init__.py deleted file mode 100644 index d4468a9..0000000 --- a/flask/kontor/home/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Module for the Kontor homepage. -""" diff --git a/flask/kontor/home/views.py b/flask/kontor/home/views.py deleted file mode 100644 index 4e4d539..0000000 --- a/flask/kontor/home/views.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Define routing rules for homepage and dashboards -""" - -from flask import Blueprint, render_template, abort -from flask_login import login_required, current_user - -HOME = Blueprint('home', __name__) - - -@HOME.route('/') -def homepage(): - """ - Render the homepage template on the / route - """ - return render_template('home/index.html', title="Welcome") - - -@HOME.route('/dashboard') -@login_required -def dashboard(): - """ - Render the dashboard template on the /dashboard route - """ - return render_template('home/dashboard.html', title="Dashboard") - - -@HOME.route('/admin/dashboard') -@login_required -def admin_dashboard(): - """ - Render the admin_dashboard template on the /admin/dashboard route - :return: - """ - # prevent non-admins from accessing the page - if not current_user.is_admin: - abort(403) - - return render_template('home/admin_dashboard.html', title="Dashboard") diff --git a/flask/kontor/library/__init__.py b/flask/kontor/library/__init__.py deleted file mode 100644 index 31eb0bc..0000000 --- a/flask/kontor/library/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Define routing rules for library related information -""" diff --git a/flask/kontor/library/forms.py b/flask/kontor/library/forms.py deleted file mode 100644 index a080781..0000000 --- a/flask/kontor/library/forms.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Define form to edit publisher, artists and books -""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, SelectField, IntegerField -from wtforms.validators import DataRequired, Optional, NumberRange -from bson import ObjectId - - -class PublisherForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class AuthorForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class BookForm(FlaskForm): - """ - Form to add and edit Comics - """ - title = StringField('Title', validators=[DataRequired()]) - author = SelectField('Author', coerce=ObjectId) - publisher = SelectField('Publisher', coerce=ObjectId) - isbn = StringField('ISBN', validators=[Optional()]) - year = IntegerField('Year', validators=[NumberRange(min=1970, max=2050)]) - submit = SubmitField('Submit') diff --git a/flask/kontor/library/models.py b/flask/kontor/library/models.py deleted file mode 100644 index 7ca5c58..0000000 --- a/flask/kontor/library/models.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -This module declares the model for the library related information. -""" -from pymongo.write_concern import WriteConcern -from pymodm import MongoModel, fields - - -class Publisher(MongoModel): - """ - Class Publisher represents a publisher of a book. - """ - name = fields.CharField() - - def __str__(self): - return "Publisher({})".format(self.name) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Author(MongoModel): - """ - Class Author represents an author of a book. - """ - name = fields.CharField() - - def __str__(self): - return "Author({})".format(self.name) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Book(MongoModel): - """ - Class book represents a book. - """ - title = fields.CharField() - author = fields.ReferenceField(Author) - publisher = fields.ReferenceField(Publisher) - isbn = fields.CharField(blank=True) - year = fields.IntegerField(blank=True) - edition = fields.CharField() - - def __str__(self): - return "Book({})".format(self.title) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) diff --git a/flask/kontor/library/views.py b/flask/kontor/library/views.py deleted file mode 100644 index 1b53eee..0000000 --- a/flask/kontor/library/views.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Define routing rules for book publishers""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required -from bson import ObjectId -from pymongo.errors import PyMongoError -from .forms import PublisherForm, AuthorForm, BookForm -from .models import Publisher, Author, Book - - -LIBRARY = Blueprint('library', __name__) - - -@LIBRARY.route('/publishers') -@login_required -def list_publishers(): - """ - List all publishers - :return: - """ - publishers = Publisher.objects.all() - return render_template('library/publishers.html', - publishers=publishers, title="Publishers") - - -@LIBRARY.route('/publishers/add', methods=['GET', 'POST']) -@login_required -def add_publisher(): - """ - Add a publisher to the database - :return: - """ - form = PublisherForm() - if form.validate_on_submit(): - publisher = Publisher() - publisher.name = form.name.data - try: - publisher.save() - flash('You have successfully added a new publisher.') - except PyMongoError: - flash('Error: publisher name already exists.') - return redirect(url_for('library.list_publishers')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Publisher") - - -@LIBRARY.route('/publishers/edit/', methods=['GET', 'POST']) -@login_required -def edit_publisher(publisher_id): - """ - Edit a publisher - """ - publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) - form = PublisherForm(obj=publisher) - if form.validate_on_submit(): - publisher.name = form.name.data - publisher.save() - flash('You have successfully edited the publisher.') - return redirect(url_for('library.list_publishers')) - form.name.data = publisher.name - return render_template('simpleform.html', action="Edit", - form=form, publisher=publisher, title="Edit Publisher") - - -@LIBRARY.route('/publishers/delete/', methods=['GET', 'POST']) -@login_required -def delete_publisher(publisher_id): - """ - Delete a publisher - :param publisher_id: ObjectId of publisher - :return: - """ - publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) - if publisher: - publisher.delete() - flash('You have successfully deleted the publisher.') - return redirect(url_for('library.list_publishers')) - - -@LIBRARY.route('/authors') -@login_required -def list_authors(): - """ - List all authors - :return: - """ - authors = Author.objects.all() - return render_template('library/authors.html', - authors=authors, title="Authors") - - -@LIBRARY.route('/authors/edit/', methods=['GET', 'POST']) -@login_required -def edit_author(author_id): - """ - Edit a book artist - """ - author = Author.objects.get({'_id': ObjectId(author_id)}) - form = AuthorForm(obj=author) - if form.validate_on_submit(): - author.name = form.name.data - author.save() - flash('You have successfully edited the author.') - return redirect(url_for('library.list_authors')) - form.name.data = author.name - return render_template('simpleform.html', action="Edit", - form=form, author=author, title="Edit Author") - - -@LIBRARY.route('/authors/add', methods=['GET', 'POST']) -@login_required -def add_author(): - """ - Add a author - :return: - """ - form = AuthorForm() - if form.validate_on_submit(): - author = Author() - author.name = form.name.data - try: - author.save() - flash('You have successfully added a new author.') - except PyMongoError: - flash('Error: author name already exists.') - return redirect(url_for('library.list_authors')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Author") - - -@LIBRARY.route('/authors/delete/', methods=['GET', 'POST']) -@login_required -def delete_author(author_id): - """ - Delete a author - :param author_id: - :return: - """ - author = Author.objects.raw({'_id': ObjectId(author_id)}) - if author: - author.delete() - flash('You have successfully deleted the author.') - return redirect(url_for('library.list_authors')) - - -@LIBRARY.route('/books') -@login_required -def list_books(): - """ - List all comics - :return: - """ - books = Book.objects.all() - return render_template('library/books.html', - books=books, title="Books") - - -@LIBRARY.route('/books/edit/', methods=['GET', 'POST']) -@login_required -def edit_book(book_id): - """ - Edit a book - """ - book = Book.objects.get({'_id': ObjectId(book_id)}) - form = BookForm(obj=book) - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - if book.publisher: - form.publisher.default = book.publisher.pk - form.publisher.process_data(book.publisher.pk) - form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] - if book.author: - form.author.default = book.author.pk - form.author.process_data(book.author.pk) - if form.validate_on_submit(): - book.title = form.title.data - book.publisher = form.publisher.data - book.author = form.author.data - if form.isbn.data: - book.isbn = form.isbn.data - else: - book.isbn = None - book.year = form.year.data - book.save() - flash('You have successfully edited the book.') - return redirect(url_for('library.list_books')) - form.title.data = book.title - return render_template('simpleform.html', action="Edit", - form=form, book=book, title="Edit Book") - - -@LIBRARY.route('/books/add', methods=['GET', 'POST']) -@login_required -def add_book(): - """ - Add a book - :return: - """ - form = BookForm() - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] - if form.validate_on_submit(): - book = Book() - book.title = form.title.data - book.publisher = form.publisher.data - book.author = form.author.data - book.year = form.year.data - try: - book.save() - flash('You have successfully added a new book.') - except PyMongoError: - flash('Error: book title already exists.') - return redirect(url_for('library.list_books')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Book") - - -@LIBRARY.route('/books/delete/', methods=['GET', 'POST']) -@login_required -def delete_book(book_id): - """ - Delete a book - :param book_id: - :return: - """ - book = Book.objects.raw({'_id': ObjectId(book_id)}) - if book: - book.delete() - flash('You have successfully deleted the book.') - return redirect(url_for('library.list_books')) diff --git a/flask/kontor/main/__init__.py b/flask/kontor/main/__init__.py new file mode 100644 index 0000000..ebdaf6e --- /dev/null +++ b/flask/kontor/main/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('main', __name__) + +from kontor.main import routes diff --git a/flask/kontor/main/routes.py b/flask/kontor/main/routes.py new file mode 100644 index 0000000..0c2e5dd --- /dev/null +++ b/flask/kontor/main/routes.py @@ -0,0 +1,18 @@ +from flask import render_template, session + +from kontor import app +from kontor.main import bp +from kontor.auth import auth + + +@bp.route('/') +@auth.login_required +def index(): + return render_template('index.html') + +@bp.route('/logout') +def logout(): + app.logger.info("logout") + auth.current_user() + session['user_name'] = None + return render_template('index.html') diff --git a/flask/kontor/media/__init__.py b/flask/kontor/media/__init__.py new file mode 100644 index 0000000..581af5f --- /dev/null +++ b/flask/kontor/media/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +media_bp = Blueprint('media_bp', __name__, + template_folder='templates', + static_folder='static', static_url_path='assets') +media_api = Blueprint('media_api', __name__) + +from kontor.media import routes +from kontor.media import api diff --git a/flask/kontor/media/api.py b/flask/kontor/media/api.py new file mode 100644 index 0000000..be9b875 --- /dev/null +++ b/flask/kontor/media/api.py @@ -0,0 +1,18 @@ +from flask import Blueprint, render_template, jsonify + +from kontor import app +from kontor.media import media_api +from kontor.media.models import MediaFile, mediafile_schema, mediafiles_schema + + +@media_api.route('/') +def mediafile_list(): + app.logger.info("get all media files") + files = MediaFile.query.all() + return mediafiles_schema.dump(files) + + +@media_api.route('/mediafile/') +def mediafile_detail(file_id): + file = MediaFile.query.get(file_id) + return mediafile_schema.dump(file) diff --git a/flask/kontor/media/models.py b/flask/kontor/media/models.py new file mode 100644 index 0000000..4e54978 --- /dev/null +++ b/flask/kontor/media/models.py @@ -0,0 +1,43 @@ +from kontor.extensions import db, ma +from sqlalchemy.sql import func + + +class MediaFile(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "media_file" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + title = db.Column(db.String) + file_name = db.Column(db.String) + url = db.Column(db.String) + path = db.Column(db.String) + cloud_link = db.Column(db.String) + review = db.Column(db.SmallInteger) + should_download = db.Column(db.Boolean) + + def is_review(self): + return self.review == 'b\x00' + + def is_download(self): + return self.should_download == 'b\x00' + + +class MediaFileSchema(ma.SQLAlchemySchema): + class Meta: + model = MediaFile + fields = ("id", "created_date", "last_modified_date", "version", "title", "file_name", "url", "path", "cloud_link", "review", "should_download", "_links") + + # Smart hyperlinking + _links = ma.Hyperlinks( + { + "self": ma.URLFor("media_api.mediafile_detail", values=dict(mediafile_id="")), + "collection": ma.URLFor("media_api.mediafile_list"), + } + ) + + +mediafile_schema = MediaFileSchema() +mediafiles_schema = MediaFileSchema(many=True) diff --git a/flask/kontor/media/routes.py b/flask/kontor/media/routes.py new file mode 100644 index 0000000..a32f5e3 --- /dev/null +++ b/flask/kontor/media/routes.py @@ -0,0 +1,16 @@ +from flask import Blueprint, render_template + +from kontor.media import media_bp +from kontor.media.models import MediaFile + + +@media_bp.route('/') +def mediafile_list(): + files = MediaFile.query.all() + return render_template('media/mediafile_list.html', mediafiles=files) + + +@media_bp.route('/mediafile/') +def mediafile_detail(mediafile_id): + mediafile = MediaFile.query.get(mediafile_id) + return render_template('media/mediafile_detail.html', mediafile=mediafile) diff --git a/flask/kontor/media/templates/media/mediafile_detail.html b/flask/kontor/media/templates/media/mediafile_detail.html new file mode 100644 index 0000000..e3474ae --- /dev/null +++ b/flask/kontor/media/templates/media/mediafile_detail.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} MediaFile {% endblock %}

+
+
+

MediaFile Blueprint

+
+ +

+ {{ mediafile.title }} +

+
+ +

{{ mediafile.id }}

+
+
+

Created

+

{{ mediafile.created_date }}

+

Modified

+

{{ mediafile.last_modified_date }}

+
+

Version: {{ mediafile.version }}

+

Review: {{ mediafile.is_review() }}

+

Should Download: {{mediafile.is_download() }}

+
+
+{% endblock %} diff --git a/flask/kontor/media/templates/media/mediafile_list.html b/flask/kontor/media/templates/media/mediafile_list.html new file mode 100644 index 0000000..2d7d29f --- /dev/null +++ b/flask/kontor/media/templates/media/mediafile_list.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} MediaFile {% endblock %}

+
+
+

MediaFile Blueprint

+ {% for mediafile in mediafiles %} +
+ +

+ {{ mediafile.title }} +

+
+

{{ mediafile.id }}

+
+

Created

+

{{ mediafile.created_date }}

+

Modified

+

{{ mediafile.last_modified_date }}

+
+

Version: {{ mediafile.version }}

+

Review: {{ mediafile.is_review() }}

+

Should Download: {{ mediafile.is_download() }}

+

Links: {{ mediafile._links }}

+
+ {% endfor %} +
+{% endblock %} diff --git a/flask/kontor/models.py b/flask/kontor/models.py index 7150364..eba153c 100644 --- a/flask/kontor/models.py +++ b/flask/kontor/models.py @@ -1,66 +1,49 @@ -""" -This modules declares the model for Kontor users. -""" -from pymodm import MongoModel, fields -from pymongo.write_concern import WriteConcern -from flask_login import UserMixin -from werkzeug.security import generate_password_hash, check_password_hash -from kontor import _LOGIN_MANAGER_ +from kontor.extensions import db, ma +from sqlalchemy.sql import func -class User(UserMixin, MongoModel): - """ - This class represents an user for the Kontor application. - """ - email = fields.EmailField() - username = fields.CharField(max_length=60) - first_name = fields.CharField(max_length=60) - last_name = fields.CharField(max_length=60) - password_hash = fields.CharField(max_length=128) - is_admin = fields.BooleanField(default=False) +class Publisher(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "publisher" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + name = db.Column(db.String) + comics = db.relationship('Comic', back_populates='publisher', lazy='dynamic') - @property - def password(self): - """ - Prevent pasword from being accessed - """ - raise AttributeError('password is not a readable attribute.') - - @password.setter - def password(self, password): - """ - Set password to a hashed password - """ - self.password_hash = generate_password_hash(password) - - def verify_password(self, password): - """ - Check if hashed password matches actual password - """ - return check_password_hash(self.password_hash, password) - - def get_id(self): - """ - Get username as id - :return: - """ - return self.username - - def __repr__(self): - return ''.format(self.username) +class PublisherSchema(ma.SQLAlchemySchema): class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) + model = Publisher + + comics = ma.List(ma.HyperlinkRelated("comics_api.index")) -# Set up user_loader -@_LOGIN_MANAGER_.user_loader -def load_user(user_name): - """ - Get list of users from database - :param user_name: - :return: - """ - return User.objects.get({'username': user_name}) +publisher_schema = PublisherSchema() +publishers_schema = PublisherSchema(many=True) + + +class Comic(db.Model): + # __table__ = db.metadata.tables["comic"] + __tablename__ = "comic" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + title = db.Column(db.String) + publisher_id = db.Column(db.String, db.ForeignKey("publisher.id")) + publisher = db.relationship("Publisher", back_populates="comics") + + +class ComicSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Comic + include_fk = True + publisher = ma.HyperlinkRelated("comics_api.publisher_detail") + + +comic_schema = ComicSchema() +comics_schema = ComicSchema(many=True) diff --git a/flask/kontor/office/__init__.py b/flask/kontor/office/__init__.py deleted file mode 100644 index d830d41..0000000 --- a/flask/kontor/office/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Define routing rules for office related information -""" diff --git a/flask/kontor/static/css/style.css b/flask/kontor/static/css/style.css deleted file mode 100644 index 35fe053..0000000 --- a/flask/kontor/static/css/style.css +++ /dev/null @@ -1,126 +0,0 @@ -/* app/static/css/style.css */ - -body, html { - width: 100%; - height: 100%; -} - -body, h1, h2, h3 { - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: 700; -} - -a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { - color: #aec251; -} - -a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover { - color: #687430; -} - -footer { - padding: 50px 0; - background-color: #f8f8f8; -} - -p.copyright { - margin: 15px 0 0; -} - -.alert-info { - width: 50%; - margin: auto; - color: #687430; - background-color: #e6ecca; - border-color: #aec251; -} - -.btn-default { - border-color: #aec251; - color: #aec251; -} - -.btn-default:hover { - background-color: #aec251; -} - -.center { - margin: auto; - width: 70%; - padding: 10px; -} - -.content-section { - padding: 50px 0; - border-top: 1px solid #e7e7e7; -} - -.footer, .push { - clear: both; - height: 4em; -} - -.intro-divider { - width: 400px; - border-top: 1px solid #f8f8f8; - border-bottom: 1px solid rgba(0,0,0,0.2); -} - -.intro-header { - padding-top: 50px; - padding-bottom: 50px; - text-align: center; - color: #f8f8f8; - background: url(../img/intro-bg.jpg) no-repeat center center; - background-size: cover; - height: 100%; -} - -.intro-message { - position: relative; - padding-top: 20%; - padding-bottom: 20%; -} - -.intro-message > h1 { - margin: 0; - text-shadow: 2px 2px 3px rgba(0,0,0,0.6); - font-size: 5em; -} - -.intro-message > h3 { - text-shadow: 2px 2px 3px rgba(0,0,0,0.6); -} - -.lead { - font-size: 18px; - font-weight: 400; -} - -.topnav { - font-size: 14px; -} - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -4em; -} - -.outer { - display: table; - position: absolute; - height: 70%; - width: 100%; -} - -.middle { - display: table-cell; - vertical-align: middle; -} - -.inner { - margin-left: auto; - margin-right: auto; -} diff --git a/flask/kontor/templates/admin/users/user.html b/flask/kontor/templates/admin/users/user.html deleted file mode 100644 index 50df2c4..0000000 --- a/flask/kontor/templates/admin/users/user.html +++ /dev/null @@ -1,21 +0,0 @@ - - -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Edit User{% endblock %} -{% block body %} -
-
-
-
-
-

Edit User

-
-
- {{ wtf.quick_form(form) }} -
-
-
-
-
-{% endblock %} diff --git a/flask/kontor/templates/admin/users/users.html b/flask/kontor/templates/admin/users/users.html deleted file mode 100644 index b23f67c..0000000 --- a/flask/kontor/templates/admin/users/users.html +++ /dev/null @@ -1,58 +0,0 @@ - - -{% import "bootstrap/utils.html" as utils %} -{% extends "base.html" %} -{% block title %}Users{% endblock %} -{% block body %} -
-
-
-
-
- {{ utils.flashed_messages() }} -
-

Users

- {% if users %} -
-
- - - - - - - - - - - {% for user in users %} - {% if user.is_admin %} - - - - - - - {% else %} - - - - - - - {% endif %} - {% endfor %} - -
Name Email Username Edit
Admin N/A N/A N/A
{{ user.first_name }} {{ user.last_name }} {{ user.email }} {{ user.username }} - - Edit - -
-
- {% endif %} -
-
-
-
-
-{% endblock %} diff --git a/flask/kontor/templates/auth/login.html b/flask/kontor/templates/auth/login.html deleted file mode 100644 index 8b70e01..0000000 --- a/flask/kontor/templates/auth/login.html +++ /dev/null @@ -1,18 +0,0 @@ - - -{% import "bootstrap/utils.html" as utils %} -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Login{% endblock %} -{% block body %} -
-
- {{ utils.flashed_messages() }} -
-
-

Login to your account

-
- {{ wtf.quick_form(form) }} -
-
-{% endblock %} diff --git a/flask/kontor/templates/auth/register.html b/flask/kontor/templates/auth/register.html deleted file mode 100644 index 55d4918..0000000 --- a/flask/kontor/templates/auth/register.html +++ /dev/null @@ -1,14 +0,0 @@ - - -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Register{% endblock %} -{% block body %} -
-
-

Register for an account

-
- {{ wtf.quick_form(form) }} -
-
-{% endblock %} diff --git a/flask/kontor/templates/base.html b/flask/kontor/templates/base.html index 80bdc27..0d7af2d 100644 --- a/flask/kontor/templates/base.html +++ b/flask/kontor/templates/base.html @@ -1,107 +1,68 @@ - - {{ title }} | Kontor - - - - - + + {% block title %} {% endblock %} - FlaskApp + -