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_FILE=/run/secrets/db-password
- POSTGRES_PASSWORD=kontor - POSTGRES_PASSWORD=kontor
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready", "-U", "kontor"] test: ["CMD-SHELL", "pg_isready -U kontor"]
interval: 1s interval: 1s
timeout: 5s timeout: 5s
retries: 10 retries: 10
@@ -43,7 +43,7 @@ services:
volumes: volumes:
- mariadb-storage:/var/lib/mysql:rw - mariadb-storage:/var/lib/mysql:rw
kontor: kontor:
image: kontor:0.2.0-SNAPSHOT image: kontor:0.1.0
restart: unless-stopped restart: unless-stopped
networks: networks:
- database - database
@@ -54,7 +54,7 @@ services:
postgres: postgres:
condition: service_healthy condition: service_healthy
kontor-api: kontor-api:
image: kontor-api:0.2.0-SNAPSHOT image: kontor-api:0.1.0
restart: unless-stopped restart: unless-stopped
networks: networks:
- database - database
Binary file not shown.
+1
View File
@@ -1 +1,2 @@
.env .env
.coverage
+1 -1
View File
@@ -4,7 +4,7 @@ clean:
find . -name '*.py[co]' -delete find . -name '*.py[co]' -delete
test: 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: clean
docker build --target=production -t kontor-api:0.2.0-SNAPSHOT . 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-jose>=3.4.0",
"python-multipart>=0.0.20", "python-multipart>=0.0.20",
"natsort>=8.4.0", "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 from src.apis.version1 import comic, media, tysc
api_router = APIRouter(prefix="/api") api_router = APIRouter(prefix="/api")
api_router.include_router(comic.router, prefix="/comics", tags=["comics"]) api_router.include_router(comic.router, tags=["comics"])
api_router.include_router(media.router, prefix="/media", tags=["media"]) api_router.include_router(media.router, tags=["media"])
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"]) 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_USER: str = os.getenv("DB_USER", "kontor")
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "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_PORT: str = os.getenv("DB_PORT", 5432)
DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor") DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor")
DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}" 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 datetime import datetime
from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship, mapped_column, Mapped from sqlalchemy.orm import relationship, mapped_column, Mapped
from src.db.models.base import Base, BaseMixin from src.db.models.base import Base, BaseMixin
@@ -14,7 +13,7 @@ class Profile(Base, BaseMixin):
user_name = Column(String(255), nullable=False) user_name = Column(String(255), nullable=False)
email = Column(String(255)) email = Column(String(255))
password = Column(String(255)) password = Column(String(255))
enabled = Column(BIT(1)) enabled = Column(Boolean)
assignments = relationship("Assignment") assignments = relationship("Assignment")
tokens = relationship("Token") tokens = relationship("Token")
@@ -34,7 +33,7 @@ class Token(Base, BaseMixin):
token = Column(String(255), nullable=False, unique=True) token = Column(String(255), nullable=False, unique=True)
name = Column(String(255)) name = Column(String(255))
last_used_date: Mapped[datetime] = mapped_column() 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_id = Column(String(255), ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="tokens") profile = relationship("Profile", back_populates="tokens")
@@ -56,7 +55,7 @@ class Assignment(Base, BaseMixin):
class ModuleData(Base, BaseMixin): class ModuleData(Base, BaseMixin):
__tablename__ = "module_data" __tablename__ = "module_data"
module_name = Column(String(255), nullable=False) module_name = Column(String(255), nullable=False)
import_data = Column(BIT(1)) import_data = Column(Boolean)
class MailAccount(Base, BaseMixin): class MailAccount(Base, BaseMixin):
@@ -66,7 +65,7 @@ class MailAccount(Base, BaseMixin):
protocol = Column(String(255)) protocol = Column(String(255))
user_name = Column(String(255)) user_name = Column(String(255))
password = Column(String(255)) password = Column(String(255))
start_tls = Column(BIT(1)) start_tls = Column(Boolean)
class Mail(Base, BaseMixin): class Mail(Base, BaseMixin):
+3 -4
View File
@@ -1,8 +1,7 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import func, Column, String from sqlalchemy import func, Column, String, Boolean
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -25,7 +24,7 @@ class BaseVideoMixin:
cloud_link = Column(String(255)) cloud_link = Column(String(255))
file_name = Column(String(255)) file_name = Column(String(255))
path = Column(String(255)) path = Column(String(255))
review = Column(BIT(1)) review = Column(Boolean)
title = Column(String(255)) title = Column(String(255))
url = Column(String(255), unique=True) 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 typing import Dict, List
from natsort import natsorted from natsort import natsorted
from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin from src.db.models.base import Base, BaseMixin
@@ -24,8 +23,8 @@ class Comic(Base, BaseMixin):
title = Column(String(length=255), unique=True) title = Column(String(length=255), unique=True)
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
publisher = relationship("Publisher", back_populates="comics") publisher = relationship("Publisher", back_populates="comics")
current_order = Column(BIT(1)) current_order = Column(Boolean)
completed = Column(BIT(1)) completed = Column(Boolean)
issues = relationship("Issue", order_by="Issue.issue_number") issues = relationship("Issue", order_by="Issue.issue_number")
story_arcs = relationship("StoryArc") story_arcs = relationship("StoryArc")
trade_paperbacks = relationship("TradePaperback") trade_paperbacks = relationship("TradePaperback")
@@ -81,8 +80,8 @@ class StoryArc(Base, BaseMixin):
class Issue(Base, BaseMixin): class Issue(Base, BaseMixin):
__tablename__ = "issue" __tablename__ = "issue"
issue_number = Column(String(255)) issue_number = Column(String(255))
in_stock = Column(BIT(1)) in_stock = Column(Boolean)
is_read = Column(BIT(1)) is_read = Column(Boolean)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False) comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="issues") comic = relationship("Comic", back_populates="issues")
volume_id = Column(String, ForeignKey("volume.id"), nullable=True) 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.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.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.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.metadata import MetaDataTable, MetaDataColumn
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
@@ -79,10 +79,10 @@ class KontorDB:
self.registry[MediaVideo.__tablename__] = MediaVideo self.registry[MediaVideo.__tablename__] = MediaVideo
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
self.registry[MetaDataTable.__tablename__] = MetaDataTable self.registry[MetaDataTable.__tablename__] = MetaDataTable
self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix self.registry[Assignment.__tablename__] = Assignment
self.registry[Token.__tablename__] = Token self.registry[Token.__tablename__] = Token
self.registry[User.__tablename__] = User self.registry[Profile.__tablename__] = Profile
self.registry[Role.__tablename__] = Role self.registry[Permission.__tablename__] = Permission
self.registry[ModuleData.__tablename__] = ModuleData self.registry[ModuleData.__tablename__] = ModuleData
self.registry[MailAccount.__tablename__] = MailAccount self.registry[MailAccount.__tablename__] = MailAccount
self.registry[Mail.__tablename__] = Mail self.registry[Mail.__tablename__] = Mail
+9 -10
View File
@@ -6,8 +6,7 @@ from pathlib import Path
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from sqlalchemy import Column, String, ForeignKey from sqlalchemy import Column, String, ForeignKey, Boolean
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin, BaseVideoMixin from src.db.models.base import Base, BaseMixin, BaseVideoMixin
@@ -30,10 +29,10 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
soup = BeautifulSoup(r.content, "html.parser") soup = BeautifulSoup(r.content, "html.parser")
title = soup.title.string title = soup.title.string
self.title = title self.title = title
self.review = 0 self.review = False
except: except:
self.title = None self.title = None
self.review = 1 self.review = True
self.last_modified_date = datetime.now() self.last_modified_date = datetime.now()
def download_file(self, download_dir: str, dl_tool: str): def download_file(self, download_dir: str, dl_tool: str):
@@ -45,12 +44,12 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
lines_list = output.splitlines() lines_list = output.splitlines()
file_name = self.__parse_output__(lines_list) file_name = self.__parse_output__(lines_list)
if file_name is None: if file_name is None:
self.review = 1 self.review = True
self.should_download = 1 self.should_download = True
self.file_name = None self.file_name = None
else: else:
download_file = Path(file_name) download_file = Path(file_name)
self.should_download = 0 self.should_download = False
self.file_name = download_file.name self.file_name = download_file.name
self.cloud_link = str(download_file.absolute()) self.cloud_link = str(download_file.absolute())
self.last_modified_date = datetime.now() self.last_modified_date = datetime.now()
@@ -85,7 +84,7 @@ class MediaActorFile(Base, BaseMixin):
class MediaArticle(Base, BaseMixin): class MediaArticle(Base, BaseMixin):
__tablename__ = 'media_article' __tablename__ = 'media_article'
review = Column(BIT(1)) review = Column(Boolean)
title = Column(String(255)) title = Column(String(255))
url = Column(String(255), unique=True) url = Column(String(255), unique=True)
@@ -95,7 +94,7 @@ class MediaVideo(Base, BaseMixin):
cloud_link = Column(String(255)) cloud_link = Column(String(255))
file_name = Column(String(255)) file_name = Column(String(255))
path = Column(String(255)) path = Column(String(255))
review = Column(BIT(1)) review = Column(Boolean)
title = Column(String(255)) title = Column(String(255))
url = Column(String(255), unique=True) 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 import Column, String, ForeignKey, Integer, Boolean
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin from src.db.models.base import Base, BaseMixin
@@ -28,8 +27,8 @@ class MetaDataColumn(Base, BaseMixin):
table = relationship("MetaDataTable", back_populates="table_columns") table = relationship("MetaDataTable", back_populates="table_columns")
column_label = Column(String(255)) column_label = Column(String(255))
filter_label = Column(String(255)) filter_label = Column(String(255))
is_shown = Column(BIT(1)) is_shown = Column(Boolean)
show_filter = Column(BIT(1)) show_filter = Column(Boolean)
ref_column = Column(String, nullable=True) ref_column = Column(String, nullable=True)
def __repr__(self): def __repr__(self):
+3 -4
View File
@@ -1,5 +1,4 @@
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin from src.db.models.base import Base, BaseMixin
@@ -78,8 +77,8 @@ class CardSet(Base, BaseMixin):
UniqueConstraint("name", "vendor_id"), UniqueConstraint("name", "vendor_id"),
) )
name = Column(String(255), index=True) name = Column(String(255), index=True)
parallel_set = Column(BIT(1)) parallel_set = Column(Boolean)
insert_set = Column(BIT(1)) insert_set = Column(Boolean)
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
vendor = relationship("Vendor", back_populates="card_sets") vendor = relationship("Vendor", back_populates="card_sets")
cards = relationship("Card") cards = relationship("Card")
+2 -2
View File
@@ -46,8 +46,8 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
id=comic.id, id=comic.id,
created=str(comic.created_date), created=str(comic.created_date),
title=comic.title, title=comic.title,
completed=(comic.completed == 1), completed=comic.completed,
current_order=(comic.current_order == 1), current_order=comic.current_order,
publisher=comic.publisher.name, publisher=comic.publisher.name,
volumes=volumes, volumes=volumes,
works=works works=works
+4 -10
View File
@@ -23,8 +23,8 @@ def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None:
file_name=mediafile.file_name, file_name=mediafile.file_name,
cloud_link=mediafile.cloud_link, cloud_link=mediafile.cloud_link,
url=str(mediafile.url), url=str(mediafile.url),
review=(mediafile.review == 1), review=mediafile.review,
should_download=(mediafile.should_download == 1)) should_download=mediafile.should_download)
#print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}") #print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}")
#print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}") #print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}")
return response return response
@@ -35,11 +35,5 @@ def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None:
mediafile.url = model.url mediafile.url = model.url
mediafile.title = model.title mediafile.title = model.title
mediafile.last_modified_date = datetime.now() mediafile.last_modified_date = datetime.now()
if model.review: mediafile.review = model.review
mediafile.review = 1 mediafile.should_download = model.should_download
else:
mediafile.review = 0
if model.should_download:
mediafile.should_download = 1
else:
mediafile.should_download = 0
@@ -1,4 +1,4 @@
{% if check == 1 %} {% if check %}
<img src="{{ url_for('static', path='images/tick.png') }}" alt="" width="24" height="24"> <img src="{{ url_for('static', path='images/tick.png') }}" alt="" width="24" height="24">
{% else %} {% else %}
<img src="{{ url_for('static', path='images/cross.png') }}" alt="" width="24" height="24"> <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 from fastapi.testclient import TestClient
import pytest import pytest
from src.main import app from src.main import kontor
@pytest.fixture(name="client") @pytest.fixture(name="client")
def client_fixture(): def client_fixture():
client = TestClient(app) client = TestClient(kontor)
yield client yield client
def test_get_artists(client: TestClient): def test_get_artists(client: TestClient):
response = client.get("/comic/artists") response = client.get("/api/comic/artists")
assert response.status_code == 200 assert response.status_code == 200
assert len(response.json()) == 5 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" }, { 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]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.7.0" version = "2.7.0"
@@ -287,8 +316,9 @@ dependencies = [
{ name = "natsort" }, { name = "natsort" },
{ name = "pathlib" }, { name = "pathlib" },
{ name = "platformdirs" }, { name = "platformdirs" },
{ name = "psycopg2" }, { name = "psycopg2-binary" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "python-jose" }, { name = "python-jose" },
{ name = "python-multipart" }, { name = "python-multipart" },
@@ -307,8 +337,9 @@ requires-dist = [
{ name = "natsort", specifier = ">=8.4.0" }, { name = "natsort", specifier = ">=8.4.0" },
{ name = "pathlib", specifier = ">=1.0.1" }, { name = "pathlib", specifier = ">=1.0.1" },
{ name = "platformdirs", specifier = ">=4.3.7" }, { 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", specifier = "==7.4.0" },
{ name = "pytest-cov", specifier = ">=6.1.1" },
{ name = "python-dotenv", specifier = ">=1.1.0" }, { name = "python-dotenv", specifier = ">=1.1.0" },
{ name = "python-jose", specifier = ">=3.4.0" }, { name = "python-jose", specifier = ">=3.4.0" },
{ name = "python-multipart", specifier = ">=0.0.20" }, { name = "python-multipart", specifier = ">=0.0.20" },
@@ -426,12 +457,22 @@ wheels = [
] ]
[[package]] [[package]]
name = "psycopg2" name = "psycopg2-binary"
version = "2.9.10" version = "2.9.10"
source = { registry = "https://pypi.org/simple" } 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 = [ 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]] [[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" }, { 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]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.1.0" version = "1.1.0"
+3 -1
View File
@@ -3,6 +3,7 @@ Setup database connections
""" """
import sqlite3 import sqlite3
import mariadb import mariadb
import psycopg2
import logging.config import logging.config
from platformdirs import PlatformDirs from platformdirs import PlatformDirs
from pathlib import Path from pathlib import Path
@@ -24,7 +25,8 @@ def get_database_cursors(log, config: str):
password=db_config['mariadb']['password'], password=db_config['mariadb']['password'],
database=db_config['mariadb']['database'] 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): 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 FROM alpine/java:21-jdk
WORKDIR / 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 EXPOSE 8000
CMD ["java", "-jar", "-Dspring.profiles.active=prod", "-Dvaadin.productionMode=true", "app.jar"] 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; package de.thpeetz.kontor.admin.services;
import de.thpeetz.kontor.admin.data.*; import de.thpeetz.kontor.admin.data.Assignment;
import de.thpeetz.kontor.admin.repository.*; 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
@@ -21,7 +25,7 @@ import java.util.stream.Collectors;
@Service("userDetailsService") @Service("userDetailsService")
public class KontorUserDetailsService implements UserDetailsService { public class KontorUserDetailsService implements UserDetailsService {
private static SecureRandom random = new SecureRandom(); private static final SecureRandom random = new SecureRandom();
@Autowired @Autowired
private ProfileRepository profileRepository; 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: hibernate:
ddl-auto: update ddl-auto: update
#ddl-auto: create-drop #ddl-auto: create-drop
show-sql: true show-sql: false
properties: properties:
hibernate: hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect #dialect: org.hibernate.dialect.PostgreSQLDialect
sql: sql:
init: init:
mode: never mode: never