add CRUD for WorkType

This commit is contained in:
Thomas Peetz
2025-05-13 00:42:41 +02:00
parent 06a48a03ac
commit 3537642df9
49 changed files with 514 additions and 215 deletions
+2
View File
@@ -1,2 +1,4 @@
.env .env
.coverage .coverage
app.log
+53
View File
@@ -0,0 +1,53 @@
import logging
import logging.config
from typing import Any
LOGGING_CONFIG: dict[str, Any] = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(asctime)s - %(name)s - %(levelprefix)s %(message)s",
"use_colors": None,
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
},
"access_file": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
"use_colors": False,
},
},
"handlers": {
"file_handler": {
"formatter": "access_file",
"class": "logging.handlers.RotatingFileHandler",
"filename" : "./app.log",
"mode" : "a+",
"maxBytes" : 10*1024*1024,
"backupCount": 0,
},
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
"uvicorn.error": {"level": "INFO"},
"uvicorn.access": {"handlers": ["access", "file_handler"], "level": "INFO", "propagate": False},
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)
+1 -1
View File
@@ -10,7 +10,7 @@ class Base(DeclarativeBase):
class BaseMixin: class BaseMixin:
#id = Column(String(255), primary_key=True, default=uuid.uuid4) #id = Column(String, primary_key=True, default=uuid.uuid4)
id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4) id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4)
# created_date = Column(DateTime) # created_date = Column(DateTime)
created_date: Mapped[datetime] = mapped_column(default=func.now()) created_date: Mapped[datetime] = mapped_column(default=func.now())
+6 -6
View File
@@ -6,28 +6,28 @@ from src.db.models.base import Base, BaseMixin
class Article(Base, BaseMixin): class Article(Base, BaseMixin):
__tablename__ = 'article' __tablename__ = 'article'
title = Column(String(length=255), unique=True) title = Column(String, unique=True)
article_authors = relationship("ArticleAuthor") article_authors = relationship("ArticleAuthor")
class Author(Base, BaseMixin): class Author(Base, BaseMixin):
__tablename__ = 'author' __tablename__ = 'author'
first_name = Column(String(255)) first_name = Column(String)
last_name = Column(String(255)) last_name = Column(String)
article_authors = relationship("ArticleAuthor") article_authors = relationship("ArticleAuthor")
book_authors = relationship("BookAuthor") book_authors = relationship("BookAuthor")
class BookshelfPublisher(Base, BaseMixin): class BookshelfPublisher(Base, BaseMixin):
__tablename__ = 'bookshelf_publisher' __tablename__ = 'bookshelf_publisher'
name = Column(String(length=255), unique=True) name = Column(String, unique=True)
books = relationship("Book") books = relationship("Book")
class Book(Base, BaseMixin): class Book(Base, BaseMixin):
__tablename__ = 'book' __tablename__ = 'book'
isbn = Column(String(255), unique=True) isbn = Column(String, unique=True)
title = Column(String(255)) title = Column(String)
year = Column(Integer, nullable=False) year = Column(Integer, nullable=False)
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False) publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
publisher = relationship('BookshelfPublisher', back_populates="books") publisher = relationship('BookshelfPublisher', back_populates="books")
+15 -4
View File
@@ -1,14 +1,23 @@
from typing import Dict, List import uuid
from datetime import datetime
from typing import Dict, List, Optional
from natsort import natsorted from natsort import natsorted
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship, Mapped, mapped_column
from src.db.models.base import Base, BaseMixin from src.db.models.base import Base, BaseMixin
class Publisher(Base, BaseMixin): class Publisher(Base):
__tablename__ = "publisher" __tablename__ = "publisher"
id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4)
created_date: Mapped[datetime] = mapped_column(default=func.now())
last_modified_date: Mapped[datetime] = mapped_column(default=func.now())
version: Mapped[int] = mapped_column(default=0)
name = Column(String, unique=True) name = Column(String, unique=True)
parent_publisher_id: Mapped[Optional[str]] = mapped_column(ForeignKey('publisher.id'))
parent_publisher: Mapped[Optional['Publisher']] = relationship("Publisher", back_populates="imprints", remote_side=[id])
imprints: Mapped[List['Publisher']] = relationship('Publisher', back_populates="parent_publisher")
comics = relationship("Comic") comics = relationship("Comic")
def __repr__(self): def __repr__(self):
@@ -25,6 +34,7 @@ class Comic(Base, BaseMixin):
publisher = relationship("Publisher", back_populates="comics") publisher = relationship("Publisher", back_populates="comics")
current_order = Column(Boolean) current_order = Column(Boolean)
completed = Column(Boolean) completed = Column(Boolean)
weblink = Column(String, nullable=True)
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")
@@ -91,6 +101,7 @@ class Issue(Base, BaseMixin):
class Artist(Base, BaseMixin): class Artist(Base, BaseMixin):
__tablename__ = "artist" __tablename__ = "artist"
name = Column(String, nullable=False) name = Column(String, nullable=False)
weblink = Column(String, nullable=True)
comic_works = relationship("ComicWork") comic_works = relationship("ComicWork")
def get_comics(self) -> Dict[str, List[str]]: def get_comics(self) -> Dict[str, List[str]]:
+15 -4
View File
@@ -1,13 +1,14 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import List, Type from typing import List, Type, AnyStr
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.core.log_conf import logger
from src.db.models.comic import Artist, Comic, Issue, WorkType from src.db.models.comic import Artist, Comic, Issue, WorkType
from src.schema.comics.artist import ArtistDetailResponse from src.schema.comics.artist import ArtistDetailResponse
from src.schema.comics.issue import IssueDetailsResponse from src.schema.comics.issue import IssueDetailsResponse
from src.webapps.comic.forms import AddWorktypeForm from src.schema.comics.worktype import AddWorkType
def get_artist_details(artist: Artist) -> ArtistDetailResponse: def get_artist_details(artist: Artist) -> ArtistDetailResponse:
@@ -42,7 +43,7 @@ def get_issue_details(issue: Issue) -> IssueDetailsResponse:
) )
return response return response
def create_new_worktype(work: AddWorktypeForm, db: Session) -> WorkType: def create_new_worktype(work: AddWorkType, db: Session) -> WorkType:
worktype = WorkType() worktype = WorkType()
worktype.id = str(uuid.uuid4()) worktype.id = str(uuid.uuid4())
worktype.created_date = datetime.now() worktype.created_date = datetime.now()
@@ -51,5 +52,15 @@ def create_new_worktype(work: AddWorktypeForm, db: Session) -> WorkType:
db.add(worktype) db.add(worktype)
db.commit() db.commit()
db.refresh(worktype) db.refresh(worktype)
print(worktype) logger.info(f"create_new_worktype: {worktype}")
return worktype
def update_worktype(work: AddWorkType, worktype_id: AnyStr, db: Session) -> WorkType:
logger.info("update worktype")
worktype = db.get(WorkType, worktype_id)
worktype.name = work.worktype
db.add(worktype)
db.commit()
db.refresh(worktype)
return worktype return worktype
+5 -7
View File
@@ -1,19 +1,18 @@
import logging import logging
import logging.config
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from src.apis.base import api_router from src.apis.base import api_router
from src.core.log_conf import LOGGING_CONFIG, logger
from src.db.session import engine from src.db.session import engine
from src.db.utils import check_db_connected, check_db_disconnected from src.db.utils import check_db_connected, check_db_disconnected
from src.webapps.base import api_router as web_app_router from src.webapps.base import api_router as web_app_router
from src.core.config import settings from src.core.config import settings
from src.db.models.base import Base from src.db.models.base import Base
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]) # Logs to console
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@@ -31,13 +30,12 @@ def configure_static(app: FastAPI):
def create_tables(): def create_tables():
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
def start_application(): def start_application(log):
logging.info(f"using database: {settings.DATABASE_URL}") log.info(f"using database: {settings.DATABASE_URL}")
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan) app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan)
include_router(app) include_router(app)
configure_static(app) configure_static(app)
create_tables() create_tables()
return app return app
kontor = start_application(logger)
kontor = start_application()
+1
View File
@@ -15,4 +15,5 @@ class ArtistResponse(BaseModel):
class ArtistDetailResponse(BaseModel): class ArtistDetailResponse(BaseModel):
id: str id: str
name: str name: str
weblink: str
works: Dict[str, List[str]] works: Dict[str, List[str]]
+2 -1
View File
@@ -16,6 +16,7 @@ class ComicDetailsResponse(BaseModel):
title: str title: str
completed : bool completed : bool
current_order : bool current_order : bool
weblink: str
publisher: str publisher: str
volumes: List[str] volumes: List[str]
works: Dict[str, List[str]] works: Dict[str, List[str]]
@@ -47,9 +48,9 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
title=comic.title, title=comic.title,
completed=comic.completed, completed=comic.completed,
current_order=comic.current_order, current_order=comic.current_order,
weblink=comic.weblink,
publisher=comic.publisher.name, publisher=comic.publisher.name,
volumes=volumes, volumes=volumes,
works=works works=works
) )
return response return response
@@ -20,6 +20,10 @@
<th scope="row">Artist Name</th> <th scope="row">Artist Name</th>
<td colspan="2">{{artist.name}}</td> <td colspan="2">{{artist.name}}</td>
</tr> </tr>
<tr>
<th scope="row">Link</th>
<td colspan="2">{{artist.weblink}}</td>
</tr>
<tr> <tr>
<th scope="row">Works</th> <th scope="row">Works</th>
<td colspan="2"> <td colspan="2">
@@ -46,5 +50,9 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="row">
<div><a href="/comic/artists" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a></div>
</div>
</div> </div>
{% endblock %} {% endblock %}
@@ -27,6 +27,10 @@
{% endwith %} {% endwith %}
</td> </td>
</tr> </tr>
<tr>
<th scope="row">Link</th>
<td colspan="2">{{comic.weblink}}</td>
</tr>
<tr> <tr>
<th scope="row">Works</th> <th scope="row">Works</th>
<td colspan="2"> <td colspan="2">
@@ -20,6 +20,24 @@
<th scope="row">Publisher Name</th> <th scope="row">Publisher Name</th>
<td colspan="2">{{publisher.name}}</td> <td colspan="2">{{publisher.name}}</td>
</tr> </tr>
{% if publisher.parent_publisher_id %}
<tr>
<th scope="row">Parent Company</th>
<td colspan="2"><a href="/comic/publishers/{{publisher.parent_publisher_id}}">{{publisher.parent_publisher.name}}</a></td>
</tr>
{% endif %}
{% if publisher.imprints|length > 0 %}
<tr>
<th scope="row">Imprints</th>
<td colspan="2">
<ul>
{% for imprint in publisher.imprints %}
<li><a href="/comic/publishers/{{imprint.id}}">{{imprint.name}}</a></li>
{% endfor %}
</ul>
</td>
</tr>
{% endif %}
<tr> <tr>
<th scope="row">Comics</th> <th scope="row">Comics</th>
<td colspan="2"> <td colspan="2">
@@ -30,6 +30,10 @@
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
<tr>
<th scope="row">ID</th>
<td colspan="2">{{worktype.id}}</td>
</tr>
<tr> <tr>
<th scope="row">Data Created</th> <th scope="row">Data Created</th>
<td colspan="2">{{worktype.created_date}}</td> <td colspan="2">{{worktype.created_date}}</td>
@@ -45,5 +49,23 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="row">
<div>
<a href="/comic/worktypes" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
<a href="/comic/worktype/edit/{{worktype.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
<a href="/comic/worktype/delete/{{worktype.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %}
<script type="text/javascript">
function delete_job(id){
fetch('/comics/worktypes/'+id+'/delete/'+id,{
method:'DELETE',})
.then(response => response.json())
.then(document.getElementById('result').innerHTML = "Refreshing...")
.then(data => document.getElementById('result').innerHTML = data.detail);
}
</script>
{% endblock %}
@@ -16,11 +16,13 @@
<tbody> <tbody>
{% for worktype in worktypes %} {% for worktype in worktypes %}
<tr> <tr>
<th scope="row"><a href="/comic/worktypes/{{worktype.id}}">{{worktype.name}}</a></th> <th scope="row"><a href="/comic/worktypes/{{worktype.id}}">{{worktype.name}}</a></th>
<td><a href="/comic/worktype/edit/{{worktype.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
<td><a href="/comic/worktype/delete/{{worktype.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="/comic/add-worktype" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add WorkType</a> <a href="/comic/worktype/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add WorkType</a>
</div> </div>
{% endblock %} {% endblock %}
@@ -1,6 +1,7 @@
<div class="card shadow p-3 mb-2 bg-body rounded" style="width: 18rem;"> <div class="card shadow p-3 mb-2 bg-body rounded" style="width: 18rem;">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{{obj.name}}</h5> <h5 class="card-title">{{obj.name}}</h5>
<p class="card-text">Link : {{obj.weblink}}</p>
<a href="/comic/artists/{{obj.id}}" class="btn btn-primary">Read more</a> <a href="/comic/artists/{{obj.id}}" class="btn btn-primary">Read more</a>
</div> </div>
</div> </div>
@@ -16,7 +16,7 @@
<li><a class="dropdown-item" href="/comic/artists/">Artists</a></li> <li><a class="dropdown-item" href="/comic/artists/">Artists</a></li>
<li><a class="dropdown-item" href="/comic/publishers/">Publishers</a></li> <li><a class="dropdown-item" href="/comic/publishers/">Publishers</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/comic/worktypes/">WorkTypes</a></li> <li><a class="dropdown-item" href="/comic/worktypes">WorkTypes</a></li>
</ul> </ul>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
+4 -4
View File
@@ -16,13 +16,13 @@
<th scope="col">Cloudlink</th> <th scope="col">Cloudlink</th>
</tr></thead> </tr></thead>
<tbody> <tbody>
{% for mediavideo in mediavideos %} {% for mediavideo in mediavideos %}
<tr> <tr>
<th scope="row"><a href="/media/videos/{{mediavideo.id}}">{{mediavideo.title}}</a></th> <th scope="row"><a href="/media/videos/{{mediavideo.id}}">{{mediavideo.title}}</a></th>
<td>{{mediavideo.url}}</td> <td>{{mediavideo.url}}</td>
<td>{{mediavideo.cloud_link}}</td> <td>{{mediavideo.cloud_link}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
+40 -9
View File
@@ -1,10 +1,13 @@
from fastapi import APIRouter, Request, responses, status from fastapi import APIRouter, Request, responses, status
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
from src.apis.utils import SessionDep from src.apis.utils import SessionDep
from src.db.models.comic import Comic, Artist, Publisher, Issue, WorkType from src.db.models.comic import Comic, Artist, Publisher, Issue, WorkType
from typing import AnyStr from typing import AnyStr
from src.db.repository.comic import create_new_worktype from src.db.repository.comic import create_new_worktype, update_worktype
from src.main import logger
from src.schema.comics.worktype import AddWorkType from src.schema.comics.worktype import AddWorkType
from src.webapps.comic.forms import AddWorktypeForm from src.webapps.comic.forms import AddWorktypeForm
@@ -59,26 +62,54 @@ def get_worktypes(db: SessionDep, request: Request, msg: str = None):
return templates.TemplateResponse("comic/worktypes.html", {"request": request, "msg": msg, "worktypes": worktypes}) return templates.TemplateResponse("comic/worktypes.html", {"request": request, "msg": msg, "worktypes": worktypes})
@router.get("/worktypes/{worktype_id}") @router.get("/worktypes/{worktype_id}")
def worktype_detail(worktype_id: AnyStr, request: Request, db: SessionDep): def worktype_detail(db: SessionDep, request: Request, worktype_id: AnyStr):
worktype = db.get(WorkType, worktype_id) worktype = db.get(WorkType, worktype_id)
return templates.TemplateResponse("comic/worktype_detail.html", {"request": request, "worktype": worktype}) return templates.TemplateResponse("comic/worktype_detail.html", {"request": request, "worktype": worktype})
@router.get("/add-worktype") @router.get("/worktype/add")
def add_worktype(request: Request, db: SessionDep): def add_worktype(request: Request, db: SessionDep):
return templates.TemplateResponse("comic/add_worktype.html", {"request": request}) return templates.TemplateResponse("comic/worktype_edit.html", {"request": request})
@router.post("/add-worktype") @router.post("/worktype/add")
async def post_worktype(request: Request, db: SessionDep): async def add_worktype(db: SessionDep, request: Request):
form = AddWorktypeForm(request) form = AddWorktypeForm(request)
await form.load_data() await form.load_data()
if form.is_valid(): if form.is_valid():
try: try:
work = AddWorkType(**form.__dict__) work = AddWorkType(**form.__dict__)
worktype = create_new_worktype(work=work, db=db) worktype = create_new_worktype(work=work, db=db)
return responses.RedirectResponse(f"/comic/worktypes/{worktype.id}", status_code=status.HTTP_302_FOUND) logger.info(f"add_worktype: redirect to /comic/worktypes/{worktype.id}")
return RedirectResponse(f"/comic/worktypes/{worktype.id}", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e: except Exception as e:
print(e) print(e)
form.__dict__.get("errors").append("worktype already added") form.__dict__.get("errors").append("worktype already added")
return templates.TemplateResponse("comic/add_worktype.html", form.__dict__) return templates.TemplateResponse("comic/worktype_edit.html", form.__dict__)
return templates.TemplateResponse("comic/add_worktype.html", form.__dict__) print("form is not valid")
return templates.TemplateResponse("comic/worktype_edit.html", form.__dict__)
@router.get("/worktype/edit/{worktype_id}")
def edit_worktype(db: SessionDep, request: Request, worktype_id: str):
worktype = db.get(WorkType, worktype_id)
return templates.TemplateResponse("comic/worktype_edit.html", {"request": request, "worktype": worktype.name})
@router.post("/worktype/edit/{worktype_id}")
async def edit_worktype(request: Request, db: SessionDep, worktype_id: str):
form = AddWorktypeForm(request)
await form.load_data()
if form.is_valid():
try:
work = AddWorkType(**form.__dict__)
worktype = update_worktype(work=work, worktype_id=worktype_id, db=db)
return RedirectResponse(f"/comic/worktypes/{worktype.id}", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(e)
form.__dict__.get("errors").append("worktype already added")
return templates.TemplateResponse("comic/worktype_edit.html", form.__dict__)
return templates.TemplateResponse("comic/worktype_edit.html", form.__dict__)
@router.get("/worktype/delete/{worktype_id}")
async def delete_worktype(db: SessionDep, request: Request, worktype_id: str):
worktype = db.get(WorkType, worktype_id)
db.delete(worktype)
db.commit()
return RedirectResponse("/comic/worktypes", status_code=status.HTTP_303_SEE_OTHER)
+86
View File
@@ -0,0 +1,86 @@
from db.models.admin import (
Assignment,
Token,
Profile,
Permission,
MailAccount,
ModuleData,
Mail,
)
from db.models.bookshelf import (
ArticleAuthor,
BookAuthor,
BookshelfPublisher,
Article,
Book,
Author,
)
from db.models.comic import (
Issue,
StoryArc,
TradePaperback,
Volume,
ComicWork,
Artist,
Comic,
Publisher,
WorkType,
)
from db.models.media import (
MediaFile,
MediaActor,
MediaActorFile,
MediaArticle,
MediaVideo,
)
from db.models.metadata import MetaDataColumn, MetaDataTable
from db.models.tysc import (
Card,
CardSet,
Rooster,
Team,
FieldPosition,
Player,
Vendor,
Sport,
)
registry = {
Card.__tablename__: Card,
CardSet.__tablename__: CardSet,
Rooster.__tablename__: Rooster,
Team.__tablename__: Team,
FieldPosition.__tablename__: FieldPosition,
Player.__tablename__: Player,
Vendor.__tablename__: Vendor,
Sport.__tablename__: Sport,
Issue.__tablename__: Issue,
TradePaperback.__tablename__: TradePaperback,
StoryArc.__tablename__: StoryArc,
Volume.__tablename__: Volume,
ComicWork.__tablename__: ComicWork,
Artist.__tablename__: Artist,
Comic.__tablename__: Comic,
Publisher.__tablename__: Publisher,
WorkType.__tablename__: WorkType,
ArticleAuthor.__tablename__: ArticleAuthor,
BookAuthor.__tablename__: BookAuthor,
BookshelfPublisher.__tablename__: BookshelfPublisher,
Article.__tablename__: Article,
Book.__tablename__: Book,
Author.__tablename__: Author,
MediaFile.__tablename__: MediaFile,
MediaActor.__tablename__: MediaActor,
MediaActorFile.__tablename__: MediaActorFile,
MediaArticle.__tablename__: MediaArticle,
MediaVideo.__tablename__: MediaVideo,
MetaDataColumn.__tablename__: MetaDataColumn,
MetaDataTable.__tablename__: MetaDataTable,
Assignment.__tablename__: Assignment,
Token.__tablename__: Token,
Profile.__tablename__: Profile,
Permission.__tablename__: Permission,
ModuleData.__tablename__: ModuleData,
MailAccount.__tablename__: MailAccount,
Mail.__tablename__: Mail
}
@@ -3,16 +3,16 @@ from datetime import datetime
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship, mapped_column, Mapped from sqlalchemy.orm import relationship, mapped_column, Mapped
from .base import Base, BaseMixin from db.models.base import Base, BaseMixin
class Profile(Base, BaseMixin): class Profile(Base, BaseMixin):
__tablename__ = 'profile' __tablename__ = 'profile'
first_name = Column(String(255)) first_name = Column(String)
last_name = Column(String(255)) last_name = Column(String)
user_name = Column(String(255), nullable=False) user_name = Column(String, nullable=False)
email = Column(String(255)) email = Column(String)
password = Column(String(255)) password = Column(String)
enabled = Column(Boolean) enabled = Column(Boolean)
assignments = relationship("Assignment") assignments = relationship("Assignment")
tokens = relationship("Token") tokens = relationship("Token")
@@ -30,11 +30,11 @@ class Profile(Base, BaseMixin):
class Token(Base, BaseMixin): class Token(Base, BaseMixin):
__tablename__ = "token" __tablename__ = "token"
token = Column(String(255), nullable=False, unique=True) token = Column(String, nullable=False, unique=True)
name = Column(String(255)) name = Column(String)
last_used_date: Mapped[datetime] = mapped_column() last_used_date: Mapped[datetime] = mapped_column()
enabled = Column(Boolean) enabled = Column(Boolean)
profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False) profile_id = Column(String, ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="tokens") profile = relationship("Profile", back_populates="tokens")
@@ -1,7 +1,7 @@
from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base, BaseMixin from db.models.base import Base, BaseMixin
class Article(Base, BaseMixin): class Article(Base, BaseMixin):
@@ -1,12 +1,22 @@
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean import uuid
from sqlalchemy.orm import relationship from datetime import datetime
from typing import List, Optional
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
from sqlalchemy.orm import relationship, Mapped, mapped_column
from .base import Base, BaseMixin from db.models.base import Base, BaseMixin
class Publisher(Base, BaseMixin): class Publisher(Base):
__tablename__ = "publisher" __tablename__ = "publisher"
id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4)
created_date: Mapped[datetime] = mapped_column(default=func.now())
last_modified_date: Mapped[datetime] = mapped_column(default=func.now())
version: Mapped[int] = mapped_column(default=0)
name = Column(String, unique=True) name = Column(String, unique=True)
parent_publisher_id: Mapped[Optional[str]] = mapped_column(ForeignKey('publisher.id'))
parent_publisher: Mapped[Optional['Publisher']] = relationship("Publisher", back_populates="imprints", remote_side=[id])
imprints: Mapped[List['Publisher']] = relationship('Publisher', back_populates="parent_publisher")
comics = relationship("Comic") comics = relationship("Comic")
def __repr__(self): def __repr__(self):
@@ -23,6 +33,7 @@ class Comic(Base, BaseMixin):
publisher = relationship("Publisher", back_populates="comics") publisher = relationship("Publisher", back_populates="comics")
current_order = Column(Boolean) current_order = Column(Boolean)
completed = Column(Boolean) completed = Column(Boolean)
weblink = Column(String, nullable=True)
issues = relationship("Issue") issues = relationship("Issue")
story_arcs = relationship("StoryArc") story_arcs = relationship("StoryArc")
trade_paperbacks = relationship("TradePaperback") trade_paperbacks = relationship("TradePaperback")
@@ -74,6 +85,7 @@ class Issue(Base, BaseMixin):
class Artist(Base, BaseMixin): class Artist(Base, BaseMixin):
__tablename__ = "artist" __tablename__ = "artist"
name = Column(String, nullable=False) name = Column(String, nullable=False)
weblink = Column(String, nullable=True)
comic_works = relationship("ComicWork") comic_works = relationship("ComicWork")
@@ -97,3 +109,4 @@ class ComicWork(Base, BaseMixin):
artist = relationship("Artist", back_populates="comic_works") artist = relationship("Artist", back_populates="comic_works")
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
work_type = relationship("WorkType", back_populates="comic_works") work_type = relationship("WorkType", back_populates="comic_works")
@@ -6,16 +6,17 @@ from logging import Logger
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from sqlalchemy import UUID, select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from .tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport from db.models import registry
from .comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType from db.models.tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport
from .bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author from db.models.comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType
from .admin import Mail, MailAccount, ModuleData, Permission, Profile, Token, Assignment from db.models.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
from .metadata import MetaDataTable, MetaDataColumn from db.models.admin import Mail, MailAccount, ModuleData, Permission, Profile, Token, Assignment
from .media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile from db.models.metadata import MetaDataTable, MetaDataColumn
from db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
class ColumnEntry(Enum): class ColumnEntry(Enum):
@@ -46,57 +47,8 @@ class KontorDB:
def __init__(self, db_engine: Any, log: Logger): def __init__(self, db_engine: Any, log: Logger):
self.engine = db_engine self.engine = db_engine
self.registry = {}
self.init_registry()
self.log = log self.log = log
def init_registry(self):
self.registry[Card.__tablename__] = Card
self.registry[CardSet.__tablename__] = CardSet
self.registry[Rooster.__tablename__] = Rooster
self.registry[Team.__tablename__] = Team
self.registry[FieldPosition.__tablename__] = FieldPosition
self.registry[Player.__tablename__] = Player
self.registry[Vendor.__tablename__] = Vendor
self.registry[Sport.__tablename__] = Sport
self.registry[Issue.__tablename__] = Issue
self.registry[TradePaperback.__tablename__] = TradePaperback
self.registry[StoryArc.__tablename__] = StoryArc
self.registry[Volume.__tablename__] = Volume
self.registry[ComicWork.__tablename__] = ComicWork
self.registry[Artist.__tablename__] = Artist
self.registry[Comic.__tablename__] = Comic
self.registry[Publisher.__tablename__] = Publisher
self.registry[WorkType.__tablename__] = WorkType
self.registry[ArticleAuthor.__tablename__] = ArticleAuthor
self.registry[BookAuthor.__tablename__] = BookAuthor
self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher
self.registry[Article.__tablename__] = Article
self.registry[Book.__tablename__] = Book
self.registry[Author.__tablename__] = Author
self.registry[MediaFile.__tablename__] = MediaFile
self.registry[MediaActor.__tablename__] = MediaActor
self.registry[MediaActorFile.__tablename__] = MediaActorFile
self.registry[MediaArticle.__tablename__] = MediaArticle
self.registry[MediaVideo.__tablename__] = MediaVideo
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
self.registry[MetaDataTable.__tablename__] = MetaDataTable
self.registry[Assignment.__tablename__] = Assignment
self.registry[Token.__tablename__] = Token
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
def get_table_names(self) -> list:
result = []
__session__ = sessionmaker(self.engine)
with __session__() as session:
tables = session.scalars(select(MetaDataTable)).all()
result = [table.table_name for table in tables]
return result
def get_table_by_name(self, table_name: str) -> dict: def get_table_by_name(self, table_name: str) -> dict:
result = {} result = {}
__session__ = sessionmaker(self.engine) __session__ = sessionmaker(self.engine)
@@ -130,19 +82,6 @@ class KontorDB:
order += 1 order += 1
return meta_data return meta_data
def get_columns(self, table_name: str) -> dict:
columns = {}
__session__ = sessionmaker(self.engine)
table_info = self.get_table_by_name(table_name)
_filters = {'table_id': table_info['id']}
with __session__() as session:
for column in session.query(MetaDataColumn).filter_by(**_filters).all():
columns[column.column_name] = {
ColumnEntry.COLUMN_ORDER: column.column_order,
ColumnEntry.COLUMN_TYPE: column.column_type
}
return columns
def get_filters(self, table_name: str) -> dict: def get_filters(self, table_name: str) -> dict:
_filter_map = {} _filter_map = {}
__session__ = sessionmaker(self.engine) __session__ = sessionmaker(self.engine)
@@ -159,7 +98,7 @@ class KontorDB:
def data(self, table_name: str, columns: dict, filters: dict) -> list: def data(self, table_name: str, columns: dict, filters: dict) -> list:
data = [] data = []
__session__ = sessionmaker(self.engine) __session__ = sessionmaker(self.engine)
table = self.registry[table_name] table = registry[table_name]
with __session__() as session: with __session__() as session:
entries = [] entries = []
if len(filters) == 0: if len(filters) == 0:
@@ -363,7 +302,7 @@ class KontorDB:
update_list[link.id] = link.title update_list[link.id] = link.title
return update_list return update_list
def get_download_list(self) -> list[UUID]: def get_download_list(self) -> list[str]:
download_list = [] download_list = []
__session__ = sessionmaker(self.engine) __session__ = sessionmaker(self.engine)
_filter = {'should_download': True} _filter = {'should_download': True}
@@ -8,7 +8,7 @@ from bs4 import BeautifulSoup
from sqlalchemy import Boolean, Column, False_, String, ForeignKey from sqlalchemy import Boolean, Column, False_, String, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base, BaseMixin, BaseVideoMixin from db.models.base import Base, BaseMixin, BaseVideoMixin
class MediaFile(Base, BaseMixin, BaseVideoMixin): class MediaFile(Base, BaseMixin, BaseVideoMixin):
@@ -1,7 +1,7 @@
from sqlalchemy import Column, String, ForeignKey, Integer, Boolean from sqlalchemy import Column, String, ForeignKey, Integer, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base, BaseMixin from db.models.base import Base, BaseMixin
class MetaDataTable(Base, BaseMixin): class MetaDataTable(Base, BaseMixin):
@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base, BaseMixin from db.models.base import Base, BaseMixin
class Sport(Base, BaseMixin): class Sport(Base, BaseMixin):
+28
View File
@@ -0,0 +1,28 @@
from typing import List
from sqlalchemy.orm import Session
from db.models.metadata import MetaDataColumn, MetaDataTable
from db.schemas.metadata import MetaDataTableResponse, MetaDataColumnResponse
def get_tables(db: Session) -> List[MetaDataTableResponse]:
tables = db.query(MetaDataTable).all()
results: List[MetaDataTableResponse] = [MetaDataTableResponse(id=table.id, name=table.table_name) for table in tables]
return results
def get_columns_for_table(db: Session, table: MetaDataTableResponse)-> List[MetaDataColumnResponse]:
columns = db.query(MetaDataColumn).filter_by(table_id = table.id).all()
results: List[MetaDataColumnResponse] = []
for column in columns:
result: MetaDataColumnResponse = MetaDataColumnResponse(
id=str(column.id),
name=column.column_name,
label=column.column_label,
order=column.column_order,
ref_column=column.ref_column,
column_type=column.column_type)
results.append(result)
return results
+15
View File
@@ -0,0 +1,15 @@
from pydantic import BaseModel, PositiveInt
class MetaDataTableResponse(BaseModel):
id: str
name: str
class MetaDataColumnResponse(BaseModel):
id: str
name: str
label: str
order: PositiveInt
ref_column: str | None
column_type: str
+2 -2
View File
@@ -80,7 +80,7 @@ def is_file_downloaded(media_file: dict, dir: Path) -> FileStatus:
def update_status(item_id: UUID, file_info: dict): def update_status(item_id: UUID, file_info: dict):
update = requests.put(f"http://127.0.0.1:8800/media/files/{item_id}", json=file_info) update = requests.put(f"http://127.0.0.1:8800/api/media/files/{item_id}", json=file_info)
log.info(f"update status: {update.status_code}") log.info(f"update status: {update.status_code}")
log.info(f"update result: {update.json()}") log.info(f"update result: {update.json()}")
@@ -97,7 +97,7 @@ def rename_file(file_info: dict):
if __name__ == '__main__': if __name__ == '__main__':
log = get_logger(args.verbose, args.config) log = get_logger(args.verbose, args.config)
log.info('kontor.download started') log.info('kontor.download started')
response = requests.get("http://127.0.0.1:8800/media/files?download=true") response = requests.get("http://127.0.0.1:8800/api/media/files?download=true")
log.info(f"Status: {response.status_code}") log.info(f"Status: {response.status_code}")
data = response.json() data = response.json()
log.info(f"data: {len(data)}") log.info(f"data: {len(data)}")
+35 -7
View File
@@ -2,17 +2,17 @@
import data from json file to MariaDB import data from json file to MariaDB
""" """
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from datetime import datetime
import json
import yaml import yaml
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from platformdirs import PlatformDirs from platformdirs import PlatformDirs
from pathlib import Path from pathlib import Path
from db.models import registry
from schema.base import Base from db.models.base import Base
from schema.database import KontorDB
from config import get_logger from config import get_logger
from schema.database import ExportType from db.repository.metadata import get_tables, get_columns_for_table
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--verbose', '-v', action='count', default=0) parser.add_argument('--verbose', '-v', action='count', default=0)
@@ -38,6 +38,34 @@ if __name__ == '__main__':
engine = create_engine(connect_string) engine = create_engine(connect_string)
Base.metadata.create_all(bind=engine, checkfirst=True) Base.metadata.create_all(bind=engine, checkfirst=True)
__session__ = sessionmaker(bind=engine) __session__ = sessionmaker(bind=engine)
kontor_db = KontorDB(engine, logger) with __session__() as db:
kontor_db.export_db(ExportType.JSON, args.file) data = {}
tables = get_tables(db)
for table in tables:
# logger.info(f"Table {table.name} with {table.id}")
columns = get_columns_for_table(db, table)
model = registry[table.name]
rows = db.query(model).all()
entries = []
for row in rows:
entry = {}
for column in columns:
# logger.info(f" Column {column.order} {column.name} with {column.id}")
try:
value = getattr(row, column.name)
if isinstance(value, datetime):
entry[column.name] = str(value)
else:
entry[column.name] = value
except AttributeError as error:
logger.info(f"{error}")
entries.append(entry)
data[table.name] = entries
logger.info(f"{table.name}: {len(entries)} exported")
json_dump = json.dumps(data, indent=4)
with open(args.file, "w") as dump_file:
dump_file.write(json_dump)
logger.info(f"{len(data)} tables exported")
#kontor_db = KontorDB(engine, logger)
#kontor_db.export_db(ExportType.JSON, args.file)
logger.info('kontor.export finished') logger.info('kontor.export finished')
-1
View File
@@ -7,7 +7,6 @@ from typing import Dict, List
from config import get_logger, get_database_cursors from config import get_logger, get_database_cursors
import json import json
import psycopg2
from psycopg2.sql import SQL from psycopg2.sql import SQL
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
+4 -4
View File
@@ -37,7 +37,7 @@ def get_logger(level: int, config: str):
if __name__ == '__main__': if __name__ == '__main__':
log = get_logger(args.verbose, args.config) log = get_logger(args.verbose, args.config)
log.info('kontor.update_titles started') log.info('kontor.update_titles started')
response = requests.get("http://127.0.0.1:8800/media/files?review=true") response = requests.get("http://127.0.0.1:8800/api/media/files?review=true")
log.info(f"Status: {response.status_code}") log.info(f"Status: {response.status_code}")
data = response.json() data = response.json()
log.info(f"data: {len(data)}") log.info(f"data: {len(data)}")
@@ -49,11 +49,11 @@ if __name__ == '__main__':
soup = BeautifulSoup(r.content, "html.parser") soup = BeautifulSoup(r.content, "html.parser")
title = soup.title.string title = soup.title.string
item['title'] = title item['title'] = title
item['review'] = 0 item['review'] = False
except: except:
item['title'] = None item['title'] = None
item['review'] = 1 item['review'] = True
update = requests.put(f"http://127.0.0.1:8800/media/files/{item['id']}", json=item) update = requests.put(f"http://127.0.0.1:8800/api/media/files/{item['id']}", json=item)
log.info(f"update status: {update.status_code}") log.info(f"update status: {update.status_code}")
log.info(f"update result: {update.json()}") log.info(f"update result: {update.json()}")
log.info('kontor.update_titles finished') log.info('kontor.update_titles finished')
@@ -84,6 +84,7 @@ public class ComicConstants {
comics.addItem(new SideNavItem(TPB, TradePaperbackView.class)); comics.addItem(new SideNavItem(TPB, TradePaperbackView.class));
comics.addItem(new SideNavItem(STORYARC, StoryArcView.class)); comics.addItem(new SideNavItem(STORYARC, StoryArcView.class));
comics.addItem(new SideNavItem(VOLUME, VolumeView.class)); comics.addItem(new SideNavItem(VOLUME, VolumeView.class));
comics.addItem(new SideNavItem(WORKTYPE, WorktypeView.class));
return comics; return comics;
} }
@@ -124,7 +124,6 @@ public class SetupModuleComics implements ApplicationListener<ContextRefreshedEv
Comic brath = createComicIfNotFound(crossgen, "Brath", false, false); Comic brath = createComicIfNotFound(crossgen, "Brath", false, false);
Comic catwomanrome = createComicIfNotFound(dc, "Catwoman When In Rome", false, false); Comic catwomanrome = createComicIfNotFound(dc, "Catwoman When In Rome", false, false);
Comic crimson = createComicIfNotFound(wildstorm, "Crimson", false, false); Comic crimson = createComicIfNotFound(wildstorm, "Crimson", false, false);
Comic crossgencomic = createComicIfNotFound(crossgen, "Crossgen", false, false);
Comic dangergirl = createComicIfNotFound(cliffhanger, "Danger Girl", false, false); Comic dangergirl = createComicIfNotFound(cliffhanger, "Danger Girl", false, false);
Comic dangergirlbackinblack = createComicIfNotFound(wildstorm, "Danger Girl Back in Black", false, false); Comic dangergirlbackinblack = createComicIfNotFound(wildstorm, "Danger Girl Back in Black", false, false);
Comic daringescapes = createComicIfNotFound(image, "Daring Escapes", false, true); Comic daringescapes = createComicIfNotFound(image, "Daring Escapes", false, true);
@@ -275,23 +274,10 @@ public class SetupModuleComics implements ApplicationListener<ContextRefreshedEv
createComicIfNotFound(darkhorse, "Star Wars: Knights of the Old Republic", false, false); createComicIfNotFound(darkhorse, "Star Wars: Knights of the Old Republic", false, false);
createComicIfNotFound(darkhorse, "Star Wars: Legacy", false, false); createComicIfNotFound(darkhorse, "Star Wars: Legacy", false, false);
createComicIfNotFound(darkhorse, "Star Wars: Dark Times", false, false); createComicIfNotFound(darkhorse, "Star Wars: Dark Times", false, false);
createComicWorkIfNotFound(crossgencomic, michaelturner, writer); createComicWorkIfNotFound(ultimatefantasticfour, brianbendis, writer);
createComicWorkIfNotFound(dangergirl, michaelturner, writer); createComicWorkIfNotFound(ultimatespidermanannual, brianbendis, writer);
createComicWorkIfNotFound(ultimatefantasticfour, michaelturner, writer);
createComicWorkIfNotFound(ultimatespidermanannual, michaelturner, writer);
createComicWorkIfNotFound(uncannyxmen, michaelturner, writer); createComicWorkIfNotFound(uncannyxmen, michaelturner, writer);
createComicWorkIfNotFound(starwars, michaelturner, writer); createComicWorkIfNotFound(newavengers, brianbendis, writer);
createComicWorkIfNotFound(shehulk, michaelturner, writer);
createComicWorkIfNotFound(shehulk2, michaelturner, writer);
createComicWorkIfNotFound(scion, michaelturner, writer);
createComicWorkIfNotFound(newavengers, michaelturner, writer);
createComicWorkIfNotFound(newmutants, michaelturner, writer);
createComicWorkIfNotFound(midnightnation, michaelturner, writer);
createComicWorkIfNotFound(monsterwar, michaelturner, writer);
createComicWorkIfNotFound(monsterwar2005, michaelturner, writer);
createComicWorkIfNotFound(mystic, michaelturner, writer);
createComicWorkIfNotFound(holidayspecial2004, michaelturner, writer);
createComicWorkIfNotFound(hackslashgirlsgonedead, michaelturner, writer);
createComicWorkIfNotFound(uncannyxmen, brianbendis, writer); createComicWorkIfNotFound(uncannyxmen, brianbendis, writer);
createStoryArcIfNotFound("Higher Learning", emmafrost); createStoryArcIfNotFound("Higher Learning", emmafrost);
createStoryArcIfNotFound("Mind Games", emmafrost); createStoryArcIfNotFound("Mind Games", emmafrost);
@@ -31,6 +31,8 @@ public class Artist extends AbstractEntity {
@Column(unique = true) @Column(unique = true)
private String name; private String name;
private String weblink;
@OneToMany(fetch = FetchType.EAGER, mappedBy = "artist", cascade = CascadeType.REFRESH, orphanRemoval = true) @OneToMany(fetch = FetchType.EAGER, mappedBy = "artist", cascade = CascadeType.REFRESH, orphanRemoval = true)
@Nullable @Nullable
List<ComicWork> comicWorks = new LinkedList<>(); List<ComicWork> comicWorks = new LinkedList<>();
@@ -6,7 +6,7 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import de.thpeetz.kontor.common.data.AbstractEntity; import de.thpeetz.kontor.common.data.AbstractEntity;
import io.micrometer.common.lang.Nullable; import jakarta.annotation.Nullable;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -19,7 +19,6 @@ import jakarta.validation.constraints.NotNull;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString;
/** /**
* Represents a comic entity. * Represents a comic entity.
@@ -44,6 +43,9 @@ public class Comic extends AbstractEntity {
private Boolean completed = false; private Boolean completed = false;
@Nullable
private String weblink;
@OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true)
@Nullable @Nullable
List<ComicWork> comicWorks; List<ComicWork> comicWorks;
@@ -3,18 +3,21 @@ package de.thpeetz.kontor.comics.data;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import de.thpeetz.kontor.common.data.AbstractEntity; import de.thpeetz.kontor.common.data.AbstractEntity;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Column;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.validation.constraints.NotEmpty; import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.*;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString;
@Getter @Getter
@Setter @Setter
@@ -26,10 +29,30 @@ public class Publisher extends AbstractEntity {
@Column(unique = true) @Column(unique = true)
private String name; private String name;
private String weblink;
@JsonBackReference
@ManyToOne
@JoinColumn(name = "parent_publisher_id")
@Nullable
@JsonIgnoreProperties({ "comics" })
private Publisher parentCompany;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parentCompany", cascade = CascadeType.ALL, orphanRemoval = true)
@Nullable
private List<Publisher> imprints = new LinkedList<>();
@OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true)
@Nullable @Nullable
private List<Comic> comics = new LinkedList<>(); private List<Comic> comics = new LinkedList<>();
public String getParentCompanyName() {
if (parentCompany != null) {
return parentCompany.name;
}
return null;
}
@Override @Override
public String toString() { public String toString() {
final StringBuffer sb = new StringBuffer("Publisher{"); final StringBuffer sb = new StringBuffer("Publisher{");
@@ -16,12 +16,14 @@ import com.vaadin.flow.data.binder.Binder;
import de.thpeetz.kontor.comics.data.Artist; import de.thpeetz.kontor.comics.data.Artist;
import de.thpeetz.kontor.comics.data.ComicWork; import de.thpeetz.kontor.comics.data.ComicWork;
import lombok.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class ArtistForm extends FormLayout { public class ArtistForm extends FormLayout {
TextField name = new TextField("Name"); TextField name = new TextField("Name");
TextField weblink = new TextField("Link");
Grid<ComicWork> comicWorks = new Grid<>(ComicWork.class); Grid<ComicWork> comicWorks = new Grid<>(ComicWork.class);
Button save = new Button("Save"); Button save = new Button("Save");
@@ -38,7 +40,7 @@ public class ArtistForm extends FormLayout {
comicWorks.getColumnByKey("workType.name").setHeader("Work type"); comicWorks.getColumnByKey("workType.name").setHeader("Work type");
comicWorks.getColumnByKey("comic.title").setHeader("Comic"); comicWorks.getColumnByKey("comic.title").setHeader("Comic");
comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); comicWorks.getColumns().forEach(col -> col.setAutoWidth(true));
add(name, comicWorks, createButtonsLayout()); add(name, weblink, comicWorks, createButtonsLayout());
} }
private HorizontalLayout createButtonsLayout() { private HorizontalLayout createButtonsLayout() {
@@ -72,17 +74,15 @@ public class ArtistForm extends FormLayout {
this.comicWorks.setItems(works); this.comicWorks.setItems(works);
} }
@Getter
public abstract static class ArtistFormEvent extends ComponentEvent<ArtistForm> { public abstract static class ArtistFormEvent extends ComponentEvent<ArtistForm> {
private Artist artist; private final Artist artist;
protected ArtistFormEvent(ArtistForm source, Artist artist) { protected ArtistFormEvent(ArtistForm source, Artist artist) {
super(source, false); super(source, false);
this.artist = artist; this.artist = artist;
} }
public Artist getArtist() {
return artist;
}
} }
public static class SaveEvent extends ArtistFormEvent { public static class SaveEvent extends ArtistFormEvent {
@@ -2,6 +2,7 @@ package de.thpeetz.kontor.comics.views;
import java.util.List; import java.util.List;
import lombok.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -29,6 +30,7 @@ public class ComicForm extends FormLayout {
TextField title = new TextField("Title"); TextField title = new TextField("Title");
ComboBox<Publisher> publisher = new ComboBox<>("Publisher"); ComboBox<Publisher> publisher = new ComboBox<>("Publisher");
TextField weblink = new TextField("Link");
Checkbox currentOrder = new Checkbox("Current order"); Checkbox currentOrder = new Checkbox("Current order");
Checkbox completed = new Checkbox("Completed"); Checkbox completed = new Checkbox("Completed");
Grid<ComicWork> comicWorks = new Grid<>(ComicWork.class); Grid<ComicWork> comicWorks = new Grid<>(ComicWork.class);
@@ -51,7 +53,7 @@ public class ComicForm extends FormLayout {
comicWorks.getColumnByKey("workType.name").setHeader("Work type"); comicWorks.getColumnByKey("workType.name").setHeader("Work type");
comicWorks.getColumnByKey("artist.name").setHeader("Artist"); comicWorks.getColumnByKey("artist.name").setHeader("Artist");
comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); comicWorks.getColumns().forEach(col -> col.setAutoWidth(true));
add(title, publisher, currentOrder, completed, comicWorks, createButtonsLayout()); add(title, publisher, weblink, currentOrder, completed, comicWorks, createButtonsLayout());
} }
private HorizontalLayout createButtonsLayout() { private HorizontalLayout createButtonsLayout() {
@@ -85,17 +87,15 @@ public class ComicForm extends FormLayout {
comicWorks.setItems(works); comicWorks.setItems(works);
} }
@Getter
public abstract static class ComicFormEvent extends ComponentEvent<ComicForm> { public abstract static class ComicFormEvent extends ComponentEvent<ComicForm> {
private Comic comic; private final Comic comic;
protected ComicFormEvent(ComicForm source, Comic comic) { protected ComicFormEvent(ComicForm source, Comic comic) {
super(source, false); super(source, false);
this.comic = comic; this.comic = comic;
} }
public Comic getComic() {
return comic;
}
} }
public static class SaveEvent extends ComicFormEvent { public static class SaveEvent extends ComicFormEvent {
@@ -50,6 +50,8 @@ public class ComicView extends VerticalLayout {
.setHeader("Bestellung").setWidth("6rem").setSortable(true); .setHeader("Bestellung").setWidth("6rem").setSortable(true);
Grid.Column<Comic> completedColumn = grid.addComponentColumn(comic -> StatusIcon.create(comic.getCompleted())) Grid.Column<Comic> completedColumn = grid.addComponentColumn(comic -> StatusIcon.create(comic.getCompleted()))
.setHeader("Abgeschlossen").setWidth("6rem").setSortable(true); .setHeader("Abgeschlossen").setWidth("6rem").setSortable(true);
Grid.Column<Comic> weblinkColumn = grid.addColumn(Comic::getWeblink)
.setHeader("Link").setResizable(true).setSortable(true);
TextField filterText = new TextField(); TextField filterText = new TextField();
@Getter @Getter
ComicForm form; ComicForm form;
@@ -123,6 +125,7 @@ public class ComicView extends VerticalLayout {
columnToggleContextMenu.addColumnToggleItem(publisherColumn); columnToggleContextMenu.addColumnToggleItem(publisherColumn);
columnToggleContextMenu.addColumnToggleItem(currentOrderColumn); columnToggleContextMenu.addColumnToggleItem(currentOrderColumn);
columnToggleContextMenu.addColumnToggleItem(completedColumn); columnToggleContextMenu.addColumnToggleItem(completedColumn);
columnToggleContextMenu.addColumnToggleItem(weblinkColumn);
HorizontalLayout toolbar = new HorizontalLayout(filterText, addComicButton, menuButton); HorizontalLayout toolbar = new HorizontalLayout(filterText, addComicButton, menuButton);
toolbar.addClassName("toolbar"); toolbar.addClassName("toolbar");
return toolbar; return toolbar;
@@ -5,6 +5,7 @@ import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.Key; import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.combobox.*;
import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
@@ -12,9 +13,14 @@ import com.vaadin.flow.data.binder.BeanValidationBinder;
import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.Binder;
import de.thpeetz.kontor.comics.data.Publisher; import de.thpeetz.kontor.comics.data.Publisher;
import lombok.*;
import java.util.*;
public class PublisherForm extends FormLayout { public class PublisherForm extends FormLayout {
public TextField name = new TextField("Name"); public TextField name = new TextField("Name");
public TextField weblink = new TextField("Link");
ComboBox<Publisher> parentCompany = new ComboBox<>("Parent Company");
Button save = new Button("Save"); Button save = new Button("Save");
Button delete = new Button("Delete"); Button delete = new Button("Delete");
@@ -22,11 +28,13 @@ public class PublisherForm extends FormLayout {
Binder<Publisher> binder = new BeanValidationBinder<>(Publisher.class); Binder<Publisher> binder = new BeanValidationBinder<>(Publisher.class);
public PublisherForm() { public PublisherForm(List<Publisher> publishers) {
addClassName("publisher-form"); addClassName("publisher-form");
binder.bindInstanceFields(this); binder.bindInstanceFields(this);
add(name, createButtonsLayout()); parentCompany.setItems(publishers);
parentCompany.setItemLabelGenerator(Publisher::getName);
add(name, weblink, parentCompany, createButtonsLayout());
} }
private HorizontalLayout createButtonsLayout() { private HorizontalLayout createButtonsLayout() {
@@ -55,17 +63,15 @@ public class PublisherForm extends FormLayout {
binder.setBean(publisher); binder.setBean(publisher);
} }
@Getter
public abstract static class PublisherFormEvent extends ComponentEvent<PublisherForm> { public abstract static class PublisherFormEvent extends ComponentEvent<PublisherForm> {
private Publisher publisher; private final Publisher publisher;
protected PublisherFormEvent(PublisherForm source, Publisher publisher) { protected PublisherFormEvent(PublisherForm source, Publisher publisher) {
super(source, false); super(source, false);
this.publisher = publisher; this.publisher = publisher;
} }
public Publisher getPublisher() {
return publisher;
}
} }
public static class SaveEvent extends PublisherFormEvent { public static class SaveEvent extends PublisherFormEvent {
@@ -1,9 +1,12 @@
package de.thpeetz.kontor.comics.views; package de.thpeetz.kontor.comics.views;
import com.vaadin.flow.component.button.*;
import de.thpeetz.kontor.comics.data.*;
import de.thpeetz.kontor.common.views.*;
import lombok.*;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import com.vaadin.flow.component.Component; 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.grid.Grid;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
@@ -14,9 +17,7 @@ import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.annotation.SpringComponent; import com.vaadin.flow.spring.annotation.SpringComponent;
import de.thpeetz.kontor.comics.ComicConstants; import de.thpeetz.kontor.comics.ComicConstants;
import de.thpeetz.kontor.comics.data.Publisher;
import de.thpeetz.kontor.comics.services.ComicService; import de.thpeetz.kontor.comics.services.ComicService;
import de.thpeetz.kontor.common.views.MainLayout;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
@SpringComponent @SpringComponent
@@ -26,8 +27,24 @@ import jakarta.annotation.security.PermitAll;
@PageTitle("Publisher | Comics | Kontor") @PageTitle("Publisher | Comics | Kontor")
public class PublisherView extends VerticalLayout { public class PublisherView extends VerticalLayout {
Grid<Publisher> grid = new Grid<>(Publisher.class); @Getter
Grid<Publisher> grid = new Grid<>(Publisher.class, false);
Grid.Column<Publisher> idColumn = grid.addColumn(Publisher::getId)
.setHeader("ID").setResizable(true).setSortable(true);
Grid.Column<Publisher> createdColumn = grid.addColumn(Publisher::getCreatedDate)
.setHeader("Erstellt").setResizable(true).setSortable(true);
Grid.Column<Publisher> modifiedColumn = grid.addColumn(Publisher::getLastModifiedDate)
.setHeader("Geändert").setResizable(true).setSortable(true);
Grid.Column<Publisher> nameColumn = grid.addColumn(Publisher::getName)
.setHeader("Titel").setResizable(true).setSortable(true);
Grid.Column<Publisher> parentCompanyColumn = grid.addColumn(Publisher::getParentCompanyName)
.setHeader("Parent Company").setResizable(true).setSortable(true);
Grid.Column<Publisher> imprintColumn = grid.addComponentColumn(publisher -> StatusIcon.create(publisher.getParentCompany() != null))
.setHeader("Imprint").setWidth("6rem").setSortable(true);
Grid.Column<Publisher> weblinkColumn = grid.addColumn(Publisher::getWeblink)
.setHeader("Link").setResizable(true).setSortable(true);
TextField filterText = new TextField(); TextField filterText = new TextField();
@Getter
PublisherForm form; PublisherForm form;
ComicService service; ComicService service;
@@ -46,13 +63,12 @@ public class PublisherView extends VerticalLayout {
private void configureGrid() { private void configureGrid() {
grid.addClassName("publisher-grid"); grid.addClassName("publisher-grid");
grid.setSizeFull(); grid.setSizeFull();
grid.setColumns("name");
grid.getColumns().forEach(col -> col.setAutoWidth(true)); grid.getColumns().forEach(col -> col.setAutoWidth(true));
grid.asSingleSelect().addValueChangeListener(event -> editPublisher(event.getValue())); grid.asSingleSelect().addValueChangeListener(event -> editPublisher(event.getValue()));
} }
private void configureForm() { private void configureForm() {
form = new PublisherForm(); form = new PublisherForm(service.findAllPublishers(null));
form.setWidth("25em"); form.setWidth("25em");
form.setVisible(false); form.setVisible(false);
form.addSaveListener(this::savePublisher); form.addSaveListener(this::savePublisher);
@@ -72,14 +88,6 @@ public class PublisherView extends VerticalLayout {
closeEditor(); closeEditor();
} }
public Grid<Publisher> getGrid() {
return grid;
}
public PublisherForm getForm() {
return form;
}
private Component getContent() { private Component getContent() {
HorizontalLayout content = new HorizontalLayout(grid, form); HorizontalLayout content = new HorizontalLayout(grid, form);
content.setFlexGrow(2, grid); content.setFlexGrow(2, grid);
@@ -98,7 +106,17 @@ public class PublisherView extends VerticalLayout {
Button addPublisherButton = new Button("Add publisher"); Button addPublisherButton = new Button("Add publisher");
addPublisherButton.addClickListener(click -> addPublisher()); addPublisherButton.addClickListener(click -> addPublisher());
HorizontalLayout toolbar = new HorizontalLayout(filterText, addPublisherButton); Button menuButton = new Button("Show/Hide Columns");
menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
ColumnToggleContextMenu<Publisher> columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton);
columnToggleContextMenu.addColumnToggleItem(idColumn);
columnToggleContextMenu.addColumnToggleItem(createdColumn);
columnToggleContextMenu.addColumnToggleItem(modifiedColumn);
columnToggleContextMenu.addColumnToggleItem(nameColumn);
columnToggleContextMenu.addColumnToggleItem(parentCompanyColumn);
columnToggleContextMenu.addColumnToggleItem(imprintColumn);
columnToggleContextMenu.addColumnToggleItem(weblinkColumn);
HorizontalLayout toolbar = new HorizontalLayout(filterText, addPublisherButton, menuButton);
toolbar.addClassName("toolbar"); toolbar.addClassName("toolbar");
return toolbar; return toolbar;
} }
@@ -1,5 +1,6 @@
package de.thpeetz.kontor.comics.views; package de.thpeetz.kontor.comics.views;
import lombok.*;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Component;
@@ -26,8 +27,10 @@ import jakarta.annotation.security.PermitAll;
@PageTitle("Worktype | Comics | Kontor") @PageTitle("Worktype | Comics | Kontor")
public class WorktypeView extends VerticalLayout { public class WorktypeView extends VerticalLayout {
@Getter
Grid<Worktype> grid = new Grid<>(Worktype.class); Grid<Worktype> grid = new Grid<>(Worktype.class);
TextField filterText = new TextField(); TextField filterText = new TextField();
@Getter
WorktypeForm form; WorktypeForm form;
ComicService service; ComicService service;
@@ -42,10 +45,6 @@ public class WorktypeView extends VerticalLayout {
updateList(); updateList();
} }
public Grid<Worktype> getGrid() {
return grid;
}
private void configureGrid() { private void configureGrid() {
grid.addClassName("worktype-grid"); grid.addClassName("worktype-grid");
grid.setSizeFull(); grid.setSizeFull();
@@ -54,13 +53,9 @@ public class WorktypeView extends VerticalLayout {
grid.asSingleSelect().addValueChangeListener(event -> editWorktype(event.getValue())); grid.asSingleSelect().addValueChangeListener(event -> editWorktype(event.getValue()));
} }
public WorktypeForm getForm() {
return form;
}
private void configureForm() { private void configureForm() {
form = new WorktypeForm(); form = new WorktypeForm();
form.setWidth("25em"); form.setWidth("45em");
form.setVisible(false); form.setVisible(false);
form.addSaveListener(this::saveWorktype); form.addSaveListener(this::saveWorktype);
form.addDeleteListener(this::deleteWorktype); form.addDeleteListener(this::deleteWorktype);
@@ -19,17 +19,19 @@ import de.thpeetz.kontor.bookshelf.BookshelfConstants;
import de.thpeetz.kontor.comics.ComicConstants; import de.thpeetz.kontor.comics.ComicConstants;
import de.thpeetz.kontor.security.SecurityService; import de.thpeetz.kontor.security.SecurityService;
import de.thpeetz.kontor.tysc.TyscConstants; import de.thpeetz.kontor.tysc.TyscConstants;
import lombok.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class KontorLayoutUtil { public class KontorLayoutUtil {
private final AppLayout appLayout; private final AppLayout appLayout;
@Setter
private HorizontalLayout secondaryNavigation; private HorizontalLayout secondaryNavigation;
private AdminService adminService; private final AdminService adminService;
private SecurityService securityService; private final SecurityService securityService;
public KontorLayoutUtil(AppLayout layout, AdminService adminService, SecurityService securityService) { public KontorLayoutUtil(AppLayout layout, AdminService adminService, SecurityService securityService) {
this.adminService = adminService; this.adminService = adminService;
@@ -37,10 +39,6 @@ public class KontorLayoutUtil {
this.appLayout = layout; this.appLayout = layout;
} }
public void setSecondaryNavigation(HorizontalLayout secondaryNavigation) {
this.secondaryNavigation = secondaryNavigation;
}
public void createHeader(String titleName) { public void createHeader(String titleName) {
appLayout.addToDrawer(createTitle(), getScroller()); appLayout.addToDrawer(createTitle(), getScroller());
appLayout.addToNavbar(getHeader(titleName)); appLayout.addToNavbar(getHeader(titleName));
@@ -9,13 +9,7 @@ import de.thpeetz.kontor.security.SecurityService;
public class SeparateMainLayout extends AppLayout { public class SeparateMainLayout extends AppLayout {
private final AdminService adminService;
private final SecurityService securityService;
public SeparateMainLayout(AdminService adminService, SecurityService securityService) { public SeparateMainLayout(AdminService adminService, SecurityService securityService) {
this.adminService = adminService;
this.securityService = securityService;
KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService);
layout.setSecondaryNavigation(getSecondaryNavigation()); layout.setSecondaryNavigation(getSecondaryNavigation());