Vorbereitung Release 0.2.0 #83

Merged
tpeetz merged 178 commits from develop/0.2.0 into main 2026-01-29 22:50:42 +00:00
36 changed files with 184 additions and 977 deletions
Showing only changes of commit 72c1a7d265 - Show all commits
+3 -3
View File
@@ -9,7 +9,7 @@ services:
#- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
- POSTGRES_PASSWORD=kontor
healthcheck:
test: ["CMD-SHELL", "pg_isready", "-U", "kontor"]
test: ["CMD-SHELL", "pg_isready -U kontor"]
interval: 1s
timeout: 5s
retries: 10
@@ -43,7 +43,7 @@ services:
volumes:
- mariadb-storage:/var/lib/mysql:rw
kontor:
image: kontor:0.2.0-SNAPSHOT
image: kontor:0.1.0
restart: unless-stopped
networks:
- database
@@ -54,7 +54,7 @@ services:
postgres:
condition: service_healthy
kontor-api:
image: kontor-api:0.2.0-SNAPSHOT
image: kontor-api:0.1.0
restart: unless-stopped
networks:
- database
Binary file not shown.
+1
View File
@@ -1 +1,2 @@
.env
.coverage
+1 -1
View File
@@ -4,7 +4,7 @@ clean:
find . -name '*.py[co]' -delete
test:
DB_HOST=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
DB_SERVER=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
docker: clean
docker build --target=production -t kontor-api:0.2.0-SNAPSHOT .
+2 -1
View File
@@ -22,5 +22,6 @@ dependencies = [
"python-jose>=3.4.0",
"python-multipart>=0.0.20",
"natsort>=8.4.0",
"psycopg2>=2.9.10",
"psycopg2-binary>=2.9.10",
"pytest-cov>=6.1.1",
]
+3 -3
View File
@@ -3,6 +3,6 @@ from fastapi import APIRouter
from src.apis.version1 import comic, media, tysc
api_router = APIRouter(prefix="/api")
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
api_router.include_router(media.router, prefix="/media", tags=["media"])
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
api_router.include_router(comic.router, tags=["comics"])
api_router.include_router(media.router, tags=["media"])
api_router.include_router(tysc.router, tags=["tysc"])
+1 -1
View File
@@ -13,7 +13,7 @@ class Settings:
DB_USER: str = os.getenv("DB_USER", "kontor")
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor")
DB_SERVER: str = os.getenv("DB_SERVER", "mariadb")
DB_SERVER: str = os.getenv("DB_SERVER", "postgres")
DB_PORT: str = os.getenv("DB_PORT", 5432)
DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor")
DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}"
+5 -6
View File
@@ -1,7 +1,6 @@
from datetime import datetime
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship, mapped_column, Mapped
from src.db.models.base import Base, BaseMixin
@@ -14,7 +13,7 @@ class Profile(Base, BaseMixin):
user_name = Column(String(255), nullable=False)
email = Column(String(255))
password = Column(String(255))
enabled = Column(BIT(1))
enabled = Column(Boolean)
assignments = relationship("Assignment")
tokens = relationship("Token")
@@ -34,7 +33,7 @@ class Token(Base, BaseMixin):
token = Column(String(255), nullable=False, unique=True)
name = Column(String(255))
last_used_date: Mapped[datetime] = mapped_column()
enabled = Column(BIT(1))
enabled = Column(Boolean)
profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="tokens")
@@ -56,7 +55,7 @@ class Assignment(Base, BaseMixin):
class ModuleData(Base, BaseMixin):
__tablename__ = "module_data"
module_name = Column(String(255), nullable=False)
import_data = Column(BIT(1))
import_data = Column(Boolean)
class MailAccount(Base, BaseMixin):
@@ -66,7 +65,7 @@ class MailAccount(Base, BaseMixin):
protocol = Column(String(255))
user_name = Column(String(255))
password = Column(String(255))
start_tls = Column(BIT(1))
start_tls = Column(Boolean)
class Mail(Base, BaseMixin):
+3 -4
View File
@@ -1,8 +1,7 @@
import uuid
from datetime import datetime
from sqlalchemy import func, Column, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy import func, Column, String, Boolean
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -25,7 +24,7 @@ class BaseVideoMixin:
cloud_link = Column(String(255))
file_name = Column(String(255))
path = Column(String(255))
review = Column(BIT(1))
review = Column(Boolean)
title = Column(String(255))
url = Column(String(255), unique=True)
should_download = Column(BIT(1))
should_download = Column(Boolean)
+5 -6
View File
@@ -1,7 +1,6 @@
from typing import Dict, List
from natsort import natsorted
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin
@@ -24,8 +23,8 @@ class Comic(Base, BaseMixin):
title = Column(String(length=255), unique=True)
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
publisher = relationship("Publisher", back_populates="comics")
current_order = Column(BIT(1))
completed = Column(BIT(1))
current_order = Column(Boolean)
completed = Column(Boolean)
issues = relationship("Issue", order_by="Issue.issue_number")
story_arcs = relationship("StoryArc")
trade_paperbacks = relationship("TradePaperback")
@@ -81,8 +80,8 @@ class StoryArc(Base, BaseMixin):
class Issue(Base, BaseMixin):
__tablename__ = "issue"
issue_number = Column(String(255))
in_stock = Column(BIT(1))
is_read = Column(BIT(1))
in_stock = Column(Boolean)
is_read = Column(Boolean)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="issues")
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
+4 -4
View File
@@ -13,7 +13,7 @@ from sqlalchemy.orm import sessionmaker
from src.db.models.tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport
from src.db.models.comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType
from src.db.models.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
from src.db.models.admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix
from src.db.models.admin import Mail, MailAccount, ModuleData, Token, Assignment, Permission, Profile
from src.db.models.metadata import MetaDataTable, MetaDataColumn
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
@@ -79,10 +79,10 @@ class KontorDB:
self.registry[MediaVideo.__tablename__] = MediaVideo
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
self.registry[MetaDataTable.__tablename__] = MetaDataTable
self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix
self.registry[Assignment.__tablename__] = Assignment
self.registry[Token.__tablename__] = Token
self.registry[User.__tablename__] = User
self.registry[Role.__tablename__] = Role
self.registry[Profile.__tablename__] = Profile
self.registry[Permission.__tablename__] = Permission
self.registry[ModuleData.__tablename__] = ModuleData
self.registry[MailAccount.__tablename__] = MailAccount
self.registry[Mail.__tablename__] = Mail
+9 -10
View File
@@ -6,8 +6,7 @@ from pathlib import Path
import requests
from bs4 import BeautifulSoup
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy import Column, String, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
@@ -30,10 +29,10 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
soup = BeautifulSoup(r.content, "html.parser")
title = soup.title.string
self.title = title
self.review = 0
self.review = False
except:
self.title = None
self.review = 1
self.review = True
self.last_modified_date = datetime.now()
def download_file(self, download_dir: str, dl_tool: str):
@@ -45,12 +44,12 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
lines_list = output.splitlines()
file_name = self.__parse_output__(lines_list)
if file_name is None:
self.review = 1
self.should_download = 1
self.review = True
self.should_download = True
self.file_name = None
else:
download_file = Path(file_name)
self.should_download = 0
self.should_download = False
self.file_name = download_file.name
self.cloud_link = str(download_file.absolute())
self.last_modified_date = datetime.now()
@@ -85,7 +84,7 @@ class MediaActorFile(Base, BaseMixin):
class MediaArticle(Base, BaseMixin):
__tablename__ = 'media_article'
review = Column(BIT(1))
review = Column(Boolean)
title = Column(String(255))
url = Column(String(255), unique=True)
@@ -95,7 +94,7 @@ class MediaVideo(Base, BaseMixin):
cloud_link = Column(String(255))
file_name = Column(String(255))
path = Column(String(255))
review = Column(BIT(1))
review = Column(Boolean)
title = Column(String(255))
url = Column(String(255), unique=True)
should_download = Column(BIT(1))
should_download = Column(Boolean)
+3 -4
View File
@@ -1,5 +1,4 @@
from sqlalchemy import Column, String, ForeignKey, Integer
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy import Column, String, ForeignKey, Integer, Boolean
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin
@@ -28,8 +27,8 @@ class MetaDataColumn(Base, BaseMixin):
table = relationship("MetaDataTable", back_populates="table_columns")
column_label = Column(String(255))
filter_label = Column(String(255))
is_shown = Column(BIT(1))
show_filter = Column(BIT(1))
is_shown = Column(Boolean)
show_filter = Column(Boolean)
ref_column = Column(String, nullable=True)
def __repr__(self):
+3 -4
View File
@@ -1,5 +1,4 @@
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin
@@ -78,8 +77,8 @@ class CardSet(Base, BaseMixin):
UniqueConstraint("name", "vendor_id"),
)
name = Column(String(255), index=True)
parallel_set = Column(BIT(1))
insert_set = Column(BIT(1))
parallel_set = Column(Boolean)
insert_set = Column(Boolean)
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
vendor = relationship("Vendor", back_populates="card_sets")
cards = relationship("Card")
+2 -2
View File
@@ -46,8 +46,8 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
id=comic.id,
created=str(comic.created_date),
title=comic.title,
completed=(comic.completed == 1),
current_order=(comic.current_order == 1),
completed=comic.completed,
current_order=comic.current_order,
publisher=comic.publisher.name,
volumes=volumes,
works=works
+4 -10
View File
@@ -23,8 +23,8 @@ def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None:
file_name=mediafile.file_name,
cloud_link=mediafile.cloud_link,
url=str(mediafile.url),
review=(mediafile.review == 1),
should_download=(mediafile.should_download == 1))
review=mediafile.review,
should_download=mediafile.should_download)
#print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}")
#print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}")
return response
@@ -35,11 +35,5 @@ def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None:
mediafile.url = model.url
mediafile.title = model.title
mediafile.last_modified_date = datetime.now()
if model.review:
mediafile.review = 1
else:
mediafile.review = 0
if model.should_download:
mediafile.should_download = 1
else:
mediafile.should_download = 0
mediafile.review = model.review
mediafile.should_download = model.should_download
@@ -1,4 +1,4 @@
{% if check == 1 %}
{% if check %}
<img src="{{ url_for('static', path='images/tick.png') }}" alt="" width="24" height="24">
{% else %}
<img src="{{ url_for('static', path='images/cross.png') }}" alt="" width="24" height="24">
+3 -3
View File
@@ -1,15 +1,15 @@
from fastapi.testclient import TestClient
import pytest
from src.main import app
from src.main import kontor
@pytest.fixture(name="client")
def client_fixture():
client = TestClient(app)
client = TestClient(kontor)
yield client
def test_get_artists(client: TestClient):
response = client.get("/comic/artists")
response = client.get("/api/comic/artists")
assert response.status_code == 200
assert len(response.json()) == 5
+59 -5
View File
@@ -89,6 +89,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload_time = "2025-03-30T20:36:45.376Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload_time = "2025-03-30T20:35:47.417Z" },
{ url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload_time = "2025-03-30T20:35:49.002Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload_time = "2025-03-30T20:35:51.073Z" },
{ url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload_time = "2025-03-30T20:35:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload_time = "2025-03-30T20:35:54.658Z" },
{ url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload_time = "2025-03-30T20:35:56.221Z" },
{ url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload_time = "2025-03-30T20:35:57.801Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload_time = "2025-03-30T20:35:59.378Z" },
{ url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload_time = "2025-03-30T20:36:01.005Z" },
{ url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload_time = "2025-03-30T20:36:03.006Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload_time = "2025-03-30T20:36:04.638Z" },
{ url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload_time = "2025-03-30T20:36:06.503Z" },
{ url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload_time = "2025-03-30T20:36:08.137Z" },
{ url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload_time = "2025-03-30T20:36:09.781Z" },
{ url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload_time = "2025-03-30T20:36:11.409Z" },
{ url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload_time = "2025-03-30T20:36:13.86Z" },
{ url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload_time = "2025-03-30T20:36:16.074Z" },
{ url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload_time = "2025-03-30T20:36:18.033Z" },
{ url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload_time = "2025-03-30T20:36:19.644Z" },
{ url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload_time = "2025-03-30T20:36:21.282Z" },
{ url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload_time = "2025-03-30T20:36:43.61Z" },
]
[[package]]
name = "dnspython"
version = "2.7.0"
@@ -287,8 +316,9 @@ dependencies = [
{ name = "natsort" },
{ name = "pathlib" },
{ name = "platformdirs" },
{ name = "psycopg2" },
{ name = "psycopg2-binary" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "python-dotenv" },
{ name = "python-jose" },
{ name = "python-multipart" },
@@ -307,8 +337,9 @@ requires-dist = [
{ name = "natsort", specifier = ">=8.4.0" },
{ name = "pathlib", specifier = ">=1.0.1" },
{ name = "platformdirs", specifier = ">=4.3.7" },
{ name = "psycopg2", specifier = ">=2.9.10" },
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
{ name = "pytest", specifier = "==7.4.0" },
{ name = "pytest-cov", specifier = ">=6.1.1" },
{ name = "python-dotenv", specifier = ">=1.1.0" },
{ name = "python-jose", specifier = ">=3.4.0" },
{ name = "python-multipart", specifier = ">=0.0.20" },
@@ -426,12 +457,22 @@ wheels = [
]
[[package]]
name = "psycopg2"
name = "psycopg2-binary"
version = "2.9.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/62/51/2007ea29e605957a17ac6357115d0c1a1b60c8c984951c19419b3474cdfd/psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11", size = 385672, upload_time = "2024-10-16T11:24:54.832Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload_time = "2024-10-16T11:24:58.126Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/49/a6cfc94a9c483b1fa401fbcb23aca7892f60c7269c5ffa2ac408364f80dc/psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2", size = 2569060, upload_time = "2025-01-04T20:09:15.28Z" },
{ url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload_time = "2024-10-16T11:21:42.841Z" },
{ url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload_time = "2024-10-16T11:21:51.989Z" },
{ url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload_time = "2024-10-16T11:21:57.584Z" },
{ url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload_time = "2024-10-16T11:22:02.005Z" },
{ url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload_time = "2024-10-16T11:22:06.412Z" },
{ url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload_time = "2024-10-16T11:22:11.583Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload_time = "2024-10-16T11:22:16.406Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload_time = "2024-10-16T11:22:21.366Z" },
{ url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload_time = "2024-10-16T11:22:25.684Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload_time = "2024-10-16T11:22:30.562Z" },
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload_time = "2025-01-04T20:09:19.234Z" },
]
[[package]]
@@ -510,6 +551,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/b2/741130cbcf2bbfa852ed95a60dc311c9e232c7ed25bac3d9b8880a8df4ae/pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", size = 323580, upload_time = "2023-06-23T11:17:25.738Z" },
]
[[package]]
name = "pytest-cov"
version = "6.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload_time = "2025-04-05T14:07:51.592Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload_time = "2025-04-05T14:07:49.641Z" },
]
[[package]]
name = "python-dotenv"
version = "1.1.0"
+3 -1
View File
@@ -3,6 +3,7 @@ Setup database connections
"""
import sqlite3
import mariadb
import psycopg2
import logging.config
from platformdirs import PlatformDirs
from pathlib import Path
@@ -24,7 +25,8 @@ def get_database_cursors(log, config: str):
password=db_config['mariadb']['password'],
database=db_config['mariadb']['database']
)
return sqlite_conn, mariadb_conn
postgres_conn = psycopg2.connect(f"host={db_config['postgres']['host']} port={db_config['postgres']['port']} user={db_config['postgres']['user']} password={db_config['postgres']['password']} dbname={db_config['postgres']['']}")
return sqlite_conn, mariadb_conn, postgres_conn
def create_tables(sqlite_conn, logger, recreate_db, scripts):
+59
View File
@@ -0,0 +1,59 @@
"""
copy data from JSON to Postgres
"""
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from pathlib import Path
from config import get_logger, get_database_cursors
import json
import psycopg2
from psycopg2.sql import SQL
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--verbose', '-v', action='count', default=0)
parser.add_argument('--config', '-c', default='kontor-docker')
parser.add_argument('--file', '-f', default='~/.sync/media/data.json')
args = parser.parse_args()
def copy_data(postgres_conn, data_file: Path, log):
postgres_cursor = postgres_conn.cursor()
import_file = Path(data_file)
if not import_file.exists():
log.info(f"File {data_file} does not exist. Do nothing.")
return
log.info("read json file")
with open(data_file, 'r') as json_file:
json_load = json.load(json_file)
for table in json_load:
log.info(f"{table}: {len(json_load[table])}")
# result[table] = import_table(table, json_load[table])
truncate_statement = 'TRUNCATE {}'.format(table)
#log.info(f"truncate: {truncate_statement}")
postgres_cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
postgres_cursor.execute(truncate_statement)
items = json_load[table]
for item in items:
#log.info(f"item: {item}")
values = []
columns = []
for (key, value) in item.items():
columns.append(key)
values.append(value)
row = tuple(values)
log.info(f"values: {row}")
insert_statement = 'INSERT INTO {}({}) VALUES({})'.format(table, ', '.join(columns), ', '.join(['%s']*len(columns)))
#log.info(f"statement: {insert_statement}")
postgres_cursor.execute(SQL(insert_statement), row)
try:
postgres_conn.commit()
except psycopg2.Error as error:
log.info('insert failed with %s', error)
if __name__ == '__main__':
logger = get_logger(args.verbose, args.config)
logger.info('kontor.json_to_postgres started')
_, _, p_conn = get_database_cursors(logger, args.config)
copy_data(p_conn, args.file, logger)
p_conn.close()
logger.info('kontor.json_to_postgres finished')
+1 -1
View File
@@ -1,5 +1,5 @@
FROM alpine/java:21-jdk
WORKDIR /
ADD build/libs/kontor-spring-0.1.0-SNAPSHOT.jar app.jar
ADD build/libs/kontor-spring-0.2.0-SNAPSHOT.jar app.jar
EXPOSE 8000
CMD ["java", "-jar", "-Dspring.profiles.active=prod", "-Dvaadin.productionMode=true", "app.jar"]
@@ -1,35 +0,0 @@
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();
}
}
@@ -1,30 +0,0 @@
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<AuthorizationMatrix> matrix;
}
@@ -1,62 +0,0 @@
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;
@OneToMany(fetch = FetchType.EAGER, mappedBy = "user")
@Nullable
private List<AuthorizationMatrix> matrix = new LinkedList<>();
@OneToMany(fetch = FetchType.EAGER, mappedBy = "user")
@Nullable
private List<Token> tokens = 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();
}
}
@@ -1,15 +0,0 @@
package de.thpeetz.kontor.admin.repository;
import java.util.List;
import de.thpeetz.kontor.admin.data.AuthorizationMatrix;
import de.thpeetz.kontor.admin.data.Role;
import de.thpeetz.kontor.admin.data.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AuthorizationMatrixRepository extends JpaRepository<AuthorizationMatrix, String> {
List<AuthorizationMatrix> findByUser(User user);
List<AuthorizationMatrix> findByRole(Role role);
}
@@ -1,19 +0,0 @@
package de.thpeetz.kontor.admin.repository;
import java.util.List;
import de.thpeetz.kontor.admin.data.Role;
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<Role, String> {
@Query("select r from Role r " +
"where lower(r.name) like lower(concat('%', :searchTerm, '%')) ")
List<Role> search(@Param("searchTerm") String searchTerm);
@Query("select r from Role r " +
"where lower(r.name) like lower(:name) ")
Role findByName(@Param("name") String name);
}
@@ -1,17 +0,0 @@
package de.thpeetz.kontor.admin.repository;
import java.util.List;
import de.thpeetz.kontor.admin.data.User;
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<User, String> {
@Query("select u from User u " +
"where lower(u.lastName) like lower(concat('%', :searchTerm, '%')) ")
List<User> search(@Param("searchTerm") String searchTerm);
User findByUserName(String userName);
}
@@ -1,7 +1,11 @@
package de.thpeetz.kontor.admin.services;
import de.thpeetz.kontor.admin.data.*;
import de.thpeetz.kontor.admin.repository.*;
import de.thpeetz.kontor.admin.data.Assignment;
import de.thpeetz.kontor.admin.data.Profile;
import de.thpeetz.kontor.admin.data.Permission;
import de.thpeetz.kontor.admin.repository.AssignmentRepository;
import de.thpeetz.kontor.admin.repository.ProfileRepository;
import de.thpeetz.kontor.admin.repository.PermissionRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
@@ -21,7 +25,7 @@ import java.util.stream.Collectors;
@Service("userDetailsService")
public class KontorUserDetailsService implements UserDetailsService {
private static SecureRandom random = new SecureRandom();
private static final SecureRandom random = new SecureRandom();
@Autowired
private ProfileRepository profileRepository;
@@ -1,112 +0,0 @@
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> user = new ComboBox<>("User");
ComboBox<Role> role = new ComboBox<>("Role");
Button save = new Button("Save");
Button delete = new Button("Delete");
Button close = new Button("Cancel");
Binder<AuthorizationMatrix> binder = new BeanValidationBinder<>(AuthorizationMatrix.class);
public AuthorizationForm(List<User> users, List<Role> 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<AuthorizationForm> {
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<DeleteEvent> listener) {
addListener(DeleteEvent.class, listener);
}
public void addSaveListener(ComponentEventListener<SaveEvent> listener) {
addListener(SaveEvent.class, listener);
}
public void addCloseListener(ComponentEventListener<CloseEvent> listener) {
addListener(CloseEvent.class, listener);
}
}
@@ -1,114 +0,0 @@
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<AuthorizationMatrix> 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());
}
}
@@ -1,102 +0,0 @@
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<Role> 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<RoleForm> {
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<DeleteEvent> listener) {
addListener(DeleteEvent.class, listener);
}
public void addSaveListener(ComponentEventListener<SaveEvent> listener) {
addListener(SaveEvent.class, listener);
}
public void addCloseListener(ComponentEventListener<CloseEvent> listener) {
addListener(CloseEvent.class, listener);
}
}
@@ -1,118 +0,0 @@
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<Role> 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()));
}
}
@@ -1,143 +0,0 @@
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<Role> permissions = new CheckboxGroup<>("Permissions");
Button save = new Button("Save");
Button delete = new Button("Delete");
Button close = new Button("Cancel");
Binder<User> binder = new BeanValidationBinder<>(User.class);
public UserForm() {
addClassName("user-form");
binder.bindInstanceFields(this);
add(userName, password, email, firstName, lastName, enabled, configurePermissionsGroup(), createButtonsLayout());
}
private CheckboxGroup<Role> 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<Role> 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<UserForm> {
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<DeleteEvent> listener) {
addListener(DeleteEvent.class, listener);
}
public void addSaveListener(ComponentEventListener<SaveEvent> listener) {
addListener(SaveEvent.class, listener);
}
public void addCloseListener(ComponentEventListener<CloseEvent> listener) {
addListener(CloseEvent.class, listener);
}
}
@@ -1,135 +0,0 @@
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<User> 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<Role> 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()));
}
}
@@ -11,10 +11,10 @@ spring:
hibernate:
ddl-auto: update
#ddl-auto: create-drop
show-sql: true
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
#dialect: org.hibernate.dialect.PostgreSQLDialect
sql:
init:
mode: never