import templates from bottle and django. Use bootstrap for HTML content

This commit is contained in:
Thomas Peetz
2025-04-27 14:14:56 +02:00
parent 13faff8e01
commit 8ec0784490
215 changed files with 671 additions and 10178 deletions
+8
View File
@@ -0,0 +1,8 @@
from fastapi import APIRouter
from src.apis.version1 import comic, media, tysc
api_router = APIRouter(prefix="/api")
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
api_router.include_router(media.router, prefix="/media", tags=["media"])
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
+8
View File
@@ -0,0 +1,8 @@
from typing import Annotated
from fastapi import Depends
from sqlalchemy.orm import Session
from src.db.session import get_db
SessionDep = Annotated[Session, Depends(get_db)]
@@ -3,10 +3,10 @@ from typing import List
from fastapi import APIRouter, HTTPException, status
from sqlalchemy import select
from src.models.comics.comic import ComicResponse, ComicDetailsResponse, get_comic_details, get_short_info
from src.models.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse, get_artist_details
from src.routers import SessionDep
from src.schema.comic import Comic, Artist
from src.apis.utils import SessionDep
from src.schema.comics.comic import ComicResponse, ComicDetailsResponse, get_comic_details, get_short_info
from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse, get_artist_details
from src.db.models.comic import Comic, Artist
router = APIRouter(
prefix="/comic",
@@ -4,9 +4,9 @@ from uuid import UUID
from fastapi import APIRouter, status, HTTPException
from sqlalchemy import select, Sequence
from src.models.media.file import MediaFileResponse, Link, get_file_details, set_file
from src.routers import SessionDep
from src.schema.media import MediaFile
from src.apis.utils import SessionDep
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
from src.db.models.media import MediaFile
router = APIRouter(
prefix="/media",
@@ -1,9 +1,9 @@
from typing import List
from fastapi import APIRouter
from src.models.tysc.sport import SportResponse
from src.routers import SessionDep
from src.schema.tysc import Sport
from src.apis.utils import SessionDep
from src.schema.tysc.sport import SportResponse
from src.db.models.tysc import Sport
router = APIRouter(
prefix="/tysc",
+23
View File
@@ -0,0 +1,23 @@
import os
from pathlib import Path
from dotenv import load_dotenv
env_path = Path(".") / ".env"
load_dotenv(dotenv_path=env_path)
class Settings:
PROJECT_NAME: str = "Kontor"
PROJECT_VERSION: str = "0.1.0"
MARIADB_USER: str = os.getenv("MARIADB_USER", "kontor")
MARIADB_PASSWORD: str = os.getenv("MARIADB_PASSWORD", "kontor")
MARIADB_SERVER: str = os.getenv("MARIADB_SERVER", "mariadb")
MARIADB_PORT: str = os.getenv("MARIADB_PORT", 3306)
MARIADB_DB: str = os.getenv("MARIADB_DB", "kontor")
DATABASE_URL: str = f"mariadb+mariadbconnector://{MARIADB_USER}:{MARIADB_PASSWORD}@{MARIADB_SERVER}:{MARIADB_PORT}/{MARIADB_DB}"
settings = Settings()
@@ -4,7 +4,7 @@ from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship, mapped_column, Mapped
from .base import Base, BaseMixin
from src.db.models.base import Base, BaseMixin
class User(Base, BaseMixin):
@@ -1,7 +1,7 @@
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from .base import Base, BaseMixin
from src.db.models.base import Base, BaseMixin
class Article(Base, BaseMixin):
@@ -2,7 +2,7 @@ from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from .base import Base, BaseMixin
from src.db.models.base import Base, BaseMixin
class Publisher(Base, BaseMixin):
@@ -3,7 +3,6 @@ import logging
import uuid
from datetime import datetime
from enum import Enum, auto
from logging import Logger
from pathlib import Path
from typing import Any
@@ -11,12 +10,12 @@ from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker
from .tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport
from .comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType
from .bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
from .admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix
from .metadata import MetaDataTable, MetaDataColumn
from .media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
from src.db.models.tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport
from src.db.models.comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType
from src.db.models.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
from src.db.models.admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix
from src.db.models.metadata import MetaDataTable, MetaDataColumn
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
class ColumnEntry(Enum):
@@ -10,7 +10,7 @@ from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from .base import Base, BaseMixin, BaseVideoMixin
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
class MediaFile(Base, BaseMixin, BaseVideoMixin):
@@ -2,7 +2,7 @@ from sqlalchemy import Column, String, ForeignKey, Integer
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from .base import Base, BaseMixin
from src.db.models.base import Base, BaseMixin
class MetaDataTable(Base, BaseMixin):
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from .base import Base, BaseMixin
from src.db.models.base import Base, BaseMixin
class Sport(Base, BaseMixin):
+15
View File
@@ -0,0 +1,15 @@
from typing import Generator, Annotated
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from src.core.config import settings
SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
def get_db() -> Generator:
with SessionLocal() as db:
yield db
+24 -8
View File
@@ -1,13 +1,29 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from src.routers import comic, media, tysc
from src.apis.base import api_router
from src.db.session import engine
from src.webapps.base import api_router as web_app_router
from src.core.config import settings
from src.db.models.base import Base
app = FastAPI()
def include_router(app: FastAPI):
app.include_router(api_router)
app.include_router(web_app_router)
app.include_router(comic.router)
app.include_router(media.router)
app.include_router(tysc.router)
def configure_static(app: FastAPI):
app.mount("/static", StaticFiles(directory="src/static"), name="static")
def create_tables():
Base.metadata.create_all(bind=engine)
def start_application():
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)
include_router(app)
configure_static(app)
create_tables()
return app
kontor = start_application()
@app.get("/")
def read_root():
return {"Hello": "World"}
-26
View File
@@ -1,26 +0,0 @@
import logging
import os
from typing import Annotated
from fastapi import Depends
from src.schema.base import Base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format(
os.environ.get('DB_USER', 'kontor'),
os.environ.get('DB_PASSWORD', 'kontor'),
os.environ.get('DB_HOST', 'mariadb'),
os.environ.get('DB_PORT', 3306),
os.environ.get('DB_NAME', 'kontor')
))
engine = create_engine(connect_string)
SessionLocal = sessionmaker(bind=engine)
Base.metadata.create_all(bind=engine, checkfirst=True)
def get_db():
logging.info("get_db")
with SessionLocal() as db:
yield db
SessionDep = Annotated[Session, Depends(get_db)]
-9
View File
@@ -1,9 +0,0 @@
from .admin import User, Token, Role, AuthorizationMatrix, ModuleData, MailAccount, Mail
from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor
from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType
from .metadata import MetaDataTable, MetaDataColumn
from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor
from .media import MediaFile, MediaArticle, MediaVideo
from .base import Base
from .database import KontorDB, ColumnEntry
@@ -1,10 +1,9 @@
from typing import List, Dict
from uuid import UUID
from src.schema.comic import Artist
from pydantic import BaseModel
from src.schema import Artist
from src.db.models.comic import Artist
class ArtistCreation(BaseModel):
@@ -1,10 +1,9 @@
from typing import List, Dict
from uuid import UUID
from src.schema.comic import Comic
from pydantic import BaseModel
from src.schema import Comic
from src.db.models.comic import Comic
class ComicResponse(BaseModel):
@@ -14,6 +13,7 @@ class ComicResponse(BaseModel):
class ComicDetailsResponse(BaseModel):
id: UUID
created: str
title: str
completed : bool
current_order : bool
@@ -22,12 +22,12 @@ class ComicDetailsResponse(BaseModel):
works: Dict[str, List[str]]
def get_short_info(comic: Comic) -> ComicResponse:
reponse = ComicResponse(
response = ComicResponse(
id=comic.id,
title=comic.title,
completed=(comic.completed == 1)
)
return reponse
return response
def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
@@ -44,6 +44,7 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
works[work_type] = [artist_name]
response = ComicDetailsResponse(
id=comic.id,
created=str(comic.created_date),
title=comic.title,
completed=(comic.completed == 1),
current_order=(comic.current_order == 1),
@@ -1,7 +1,7 @@
from datetime import datetime
from uuid import UUID
from src.schema.media import MediaFile
from src.db.models.media import MediaFile
from pydantic import BaseModel
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+5
View File
@@ -0,0 +1,5 @@
$( function() {
$( "#autocomplete" ).autocomplete({
source: "/jobs/autocomplete"
});
} );
@@ -0,0 +1,32 @@
{% extends "shared/base.html" %}
{% block title %}
<title>Comic Artists</title>
{% endblock %}
{% block content %}
{% with msg=msg %}
{% include "components/alerts.html" %}
{% endwith %}
<div class="container">
<div class="row">
<div class="col">
<h1 class="display-5">Find Artists..</h1>
</div>
</div>
<div class="row">
{% for artist in artists %}
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
{% with obj=artist %}
{% include "components/artist_cards.html" %}
{% endwith %}
{% if loop.index %3 %}
</div>
{% else %}
</div></div><br><div class="row">
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,32 @@
{% extends "shared/base.html" %}
{% block title %}
<title>Comic List</title>
{% endblock %}
{% block content %}
{% with msg=msg %}
{% include "components/alerts.html" %}
{% endwith %}
<div class="container">
<div class="row">
<div class="col">
<h1 class="display-5">Find Jobs..</h1>
</div>
</div>
<div class="row">
{% for comic in comics %}
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
{% with obj=comic %}
{% include "components/comic_cards.html" %}
{% endwith %}
{% if loop.index %3 %}
</div>
{% else %}
</div></div><br><div class="row">
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,53 @@
{% extends "shared/base.html" %}
{% block title %}
<title>Comic Detail</title>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col">
<h1 class="display-5">Comic Detail</h1>
</div>
</div>
<div class="row">
<table class="table table-striped table-hover">
<tbody>
<tr>
<th scope="row">Comic Title</th>
<td colspan="2">{{comic.title}}</td>
</tr>
<tr>
<th scope="row">Publisher</th>
<td colspan="2"><a href="/comics/publishers/{{comic.publisher.id}}">{{comic.publisher.name}}</a></td>
</tr>
<tr>
<th scope="row">Completed</th>
<td colspan="2">{{comic.completed}}</td>
</tr>
<tr>
<th scope="row">Works</th>
<td colspan="2">
{% for work in comic.comic_works %}
<p>
{{work.work_type.name}}: <a href="/comics/artists/{{work.artist.id}}">{{work.artist.name}}</a>
</p>
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">Data Created</th>
<td colspan="2">{{comic.created_date}}</td>
</tr>
<tr>
<th scope="row">Data Modified</th>
<td colspan="2">{{comic.last_modified_date}}</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,5 @@
{% if msg %}
<div class="alert alert-info" role="alert">
{{msg}}
</div>
{% endif %}
@@ -0,0 +1,6 @@
<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>
<a href="/comic/artists/{{obj.id}}" class="btn btn-primary">Read more</a>
</div>
</div>
@@ -0,0 +1,8 @@
<div class="card shadow p-3 mb-2 bg-body rounded" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">{{obj.title}}</h5>
<p class="card-text">Publisher : {{obj.publisher.name}}</p>
<p class="card-text">Completed : {{obj.completed}}</p>
<a href="/comic/comics/{{obj.id}}" class="btn btn-primary">Read more</a>
</div>
</div>
@@ -0,0 +1,60 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<img src="{{ url_for('static', path='images/logo.png') }}" alt="" width="30" height="24">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Comics
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="/comic/comics/">Comics</a></li>
<li><a class="dropdown-item" href="/comic/artists/">Artists</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="/docs/">Docs</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/redoc/">Redoc</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Account
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="/register/">Signup</a></li>
<li><a class="dropdown-item" href="/login/">Login</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
</ul>
<ul class="navbar-nav ml-auto mb-2 mb-lg-0">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Jobs
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="/comics/">Comics</a></li>
<li><a class="dropdown-item" href="/comics/">Media</a></li>
</ul>
</li>
</ul>
<form class="d-flex" action="/search/">
<input class="form-control me-2" name="query" id="autocomplete" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>
+14
View File
@@ -0,0 +1,14 @@
{% extends "shared/base.html" %}
{% block title %}
<title>Kontor</title>
{% endblock %}
{% block content %}
{% with msg=msg %}
{% include "components/alerts.html" %}
{% endwith %}
<div class="container">
</div>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
{% block title %}
{% endblock %}
</head>
<body>
{% include "components/navbar.html" %}
{% block content %}
{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script src="{{ url_for('static', path='js/autocomplete.js') }}"></script>
{% block scripts %}
{% endblock %}
</body>
</html>
View File
+13
View File
@@ -0,0 +1,13 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from src.webapps.comic import route_comics
templates = Jinja2Templates(directory="src/templates")
api_router = APIRouter()
api_router.include_router(route_comics.router)
@api_router.get("/")
def home(request: Request, msg: str = None):
return templates.TemplateResponse("index.html", {"request": request, "msg": msg})
@@ -0,0 +1,26 @@
from uuid import UUID
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from src.apis.utils import SessionDep
from src.db.models.comic import Comic, Artist
from src.schema.comics.comic import get_comic_details
templates = Jinja2Templates(directory="src/templates")
router = APIRouter(include_in_schema=False, prefix="/comic")
@router.get("/comics")
def get_comics(db: SessionDep, request: Request, msg: str = None):
comics = db.query(Comic).all()
return templates.TemplateResponse("comic/comics.html", {"request": request, "msg": msg, "comics": comics})
@router.get("/comics/{comic_id}")
def comic_details(comic_id: UUID, request: Request, db: SessionDep):
comic = db.get(Comic, comic_id)
return templates.TemplateResponse("comic/detail.html", {"request": request, "comic":comic})
@router.get("/artists")
def get_artists(db: SessionDep, request: Request, msg: str = None):
artists = db.query(Artist).all()
return templates.TemplateResponse("comic/artists.html", {"request": request, "msg": msg, "artists": artists})