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
+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:
#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)
# created_date = Column(DateTime)
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):
__tablename__ = 'article'
title = Column(String(length=255), unique=True)
title = Column(String, unique=True)
article_authors = relationship("ArticleAuthor")
class Author(Base, BaseMixin):
__tablename__ = 'author'
first_name = Column(String(255))
last_name = Column(String(255))
first_name = Column(String)
last_name = Column(String)
article_authors = relationship("ArticleAuthor")
book_authors = relationship("BookAuthor")
class BookshelfPublisher(Base, BaseMixin):
__tablename__ = 'bookshelf_publisher'
name = Column(String(length=255), unique=True)
name = Column(String, unique=True)
books = relationship("Book")
class Book(Base, BaseMixin):
__tablename__ = 'book'
isbn = Column(String(255), unique=True)
title = Column(String(255))
isbn = Column(String, unique=True)
title = Column(String)
year = Column(Integer, nullable=False)
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
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 sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
from sqlalchemy.orm import relationship, Mapped, mapped_column
from src.db.models.base import Base, BaseMixin
class Publisher(Base, BaseMixin):
class Publisher(Base):
__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)
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")
def __repr__(self):
@@ -25,6 +34,7 @@ class Comic(Base, BaseMixin):
publisher = relationship("Publisher", back_populates="comics")
current_order = Column(Boolean)
completed = Column(Boolean)
weblink = Column(String, nullable=True)
issues = relationship("Issue", order_by="Issue.issue_number")
story_arcs = relationship("StoryArc")
trade_paperbacks = relationship("TradePaperback")
@@ -91,6 +101,7 @@ class Issue(Base, BaseMixin):
class Artist(Base, BaseMixin):
__tablename__ = "artist"
name = Column(String, nullable=False)
weblink = Column(String, nullable=True)
comic_works = relationship("ComicWork")
def get_comics(self) -> Dict[str, List[str]]:
+15 -4
View File
@@ -1,13 +1,14 @@
import uuid
from datetime import datetime
from typing import List, Type
from typing import List, Type, AnyStr
from sqlalchemy.orm import Session
from src.core.log_conf import logger
from src.db.models.comic import Artist, Comic, Issue, WorkType
from src.schema.comics.artist import ArtistDetailResponse
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:
@@ -42,7 +43,7 @@ def get_issue_details(issue: Issue) -> IssueDetailsResponse:
)
return response
def create_new_worktype(work: AddWorktypeForm, db: Session) -> WorkType:
def create_new_worktype(work: AddWorkType, db: Session) -> WorkType:
worktype = WorkType()
worktype.id = str(uuid.uuid4())
worktype.created_date = datetime.now()
@@ -51,5 +52,15 @@ def create_new_worktype(work: AddWorktypeForm, db: Session) -> WorkType:
db.add(worktype)
db.commit()
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
+5 -7
View File
@@ -1,19 +1,18 @@
import logging
import logging.config
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
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.utils import check_db_connected, check_db_disconnected
from src.webapps.base import api_router as web_app_router
from src.core.config import settings
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
async def lifespan(app: FastAPI):
@@ -31,13 +30,12 @@ def configure_static(app: FastAPI):
def create_tables():
Base.metadata.create_all(bind=engine)
def start_application():
logging.info(f"using database: {settings.DATABASE_URL}")
def start_application(log):
log.info(f"using database: {settings.DATABASE_URL}")
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan)
include_router(app)
configure_static(app)
create_tables()
return app
kontor = start_application()
kontor = start_application(logger)
+1
View File
@@ -15,4 +15,5 @@ class ArtistResponse(BaseModel):
class ArtistDetailResponse(BaseModel):
id: str
name: str
weblink: str
works: Dict[str, List[str]]
+2 -1
View File
@@ -16,6 +16,7 @@ class ComicDetailsResponse(BaseModel):
title: str
completed : bool
current_order : bool
weblink: str
publisher: str
volumes: List[str]
works: Dict[str, List[str]]
@@ -47,9 +48,9 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
title=comic.title,
completed=comic.completed,
current_order=comic.current_order,
weblink=comic.weblink,
publisher=comic.publisher.name,
volumes=volumes,
works=works
)
return response
@@ -20,6 +20,10 @@
<th scope="row">Artist Name</th>
<td colspan="2">{{artist.name}}</td>
</tr>
<tr>
<th scope="row">Link</th>
<td colspan="2">{{artist.weblink}}</td>
</tr>
<tr>
<th scope="row">Works</th>
<td colspan="2">
@@ -46,5 +50,9 @@
</tbody>
</table>
</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>
{% endblock %}
@@ -27,6 +27,10 @@
{% endwith %}
</td>
</tr>
<tr>
<th scope="row">Link</th>
<td colspan="2">{{comic.weblink}}</td>
</tr>
<tr>
<th scope="row">Works</th>
<td colspan="2">
@@ -20,6 +20,24 @@
<th scope="row">Publisher Name</th>
<td colspan="2">{{publisher.name}}</td>
</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>
<th scope="row">Comics</th>
<td colspan="2">
@@ -30,6 +30,10 @@
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">ID</th>
<td colspan="2">{{worktype.id}}</td>
</tr>
<tr>
<th scope="row">Data Created</th>
<td colspan="2">{{worktype.created_date}}</td>
@@ -45,5 +49,23 @@
</tbody>
</table>
</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>
{% 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>
{% for worktype in worktypes %}
<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>
{% endfor %}
</tbody>
</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>
{% endblock %}
@@ -1,6 +1,7 @@
<div class="card shadow p-3 mb-2 bg-body rounded" style="width: 18rem;">
<div class="card-body">
<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>
</div>
</div>
@@ -16,7 +16,7 @@
<li><a class="dropdown-item" href="/comic/artists/">Artists</a></li>
<li><a class="dropdown-item" href="/comic/publishers/">Publishers</a></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>
</li>
<li class="nav-item dropdown">
+4 -4
View File
@@ -16,13 +16,13 @@
<th scope="col">Cloudlink</th>
</tr></thead>
<tbody>
{% for mediavideo in mediavideos %}
<tr>
{% for mediavideo in mediavideos %}
<tr>
<th scope="row"><a href="/media/videos/{{mediavideo.id}}">{{mediavideo.title}}</a></th>
<td>{{mediavideo.url}}</td>
<td>{{mediavideo.cloud_link}}</td>
</tr>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
+40 -9
View File
@@ -1,10 +1,13 @@
from fastapi import APIRouter, Request, responses, status
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
from src.apis.utils import SessionDep
from src.db.models.comic import Comic, Artist, Publisher, Issue, WorkType
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.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})
@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)
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):
return templates.TemplateResponse("comic/add_worktype.html", {"request": request})
return templates.TemplateResponse("comic/worktype_edit.html", {"request": request})
@router.post("/add-worktype")
async def post_worktype(request: Request, db: SessionDep):
@router.post("/worktype/add")
async def add_worktype(db: SessionDep, request: Request):
form = AddWorktypeForm(request)
await form.load_data()
if form.is_valid():
try:
work = AddWorkType(**form.__dict__)
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:
print(e)
form.__dict__.get("errors").append("worktype already added")
return templates.TemplateResponse("comic/add_worktype.html", form.__dict__)
return templates.TemplateResponse("comic/add_worktype.html", form.__dict__)
return templates.TemplateResponse("comic/worktype_edit.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)