Vorbereitung Release 0.2.0
This commit is contained in:
Binary file not shown.
@@ -1 +1,4 @@
|
||||
.env
|
||||
.coverage
|
||||
app.log
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
## ------------------------------- Builder Stage ------------------------------ ##
|
||||
FROM python:3.13-bookworm AS builder
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
build-essential libmariadb-dev && \
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y build-essential && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Download the latest installer, install it and then remove it
|
||||
@@ -35,6 +34,8 @@ FROM python:3.13-slim-bookworm AS production
|
||||
|
||||
#RUN --mount=type=secret,id=secret-key,target=secrets.json
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y curl
|
||||
|
||||
RUN useradd --create-home appuser
|
||||
USER appuser
|
||||
|
||||
@@ -42,7 +43,6 @@ WORKDIR /app
|
||||
|
||||
COPY /src src
|
||||
COPY --from=builder /app/.venv .venv
|
||||
COPY --from=builder /usr/lib/x86_64-linux-gnu/libmariadb.so.3 /usr/lib/x86_64-linux-gnu
|
||||
|
||||
# Set up environment variables for production
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
@@ -51,5 +51,5 @@ ENV PATH="/app/.venv/bin:$PATH"
|
||||
EXPOSE $PORT
|
||||
|
||||
# Start the application with Uvicorn in production mode, using environment variable references
|
||||
CMD ["uvicorn", "src.main:kontor", "--log-level", "info", "--host", "0.0.0.0" , "--port", "8800"]
|
||||
CMD ["uvicorn", "src.main:kontor", "--log-level", "info", "--host", "0.0.0.0" , "--port", "8500"]
|
||||
|
||||
|
||||
+3
-3
@@ -4,11 +4,11 @@ clean:
|
||||
find . -name '*.py[co]' -delete
|
||||
|
||||
test:
|
||||
DB_HOST=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
|
||||
DB_SERVER=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
|
||||
|
||||
docker: clean
|
||||
docker build --target=production -t kontor-api -t kontor-api:0.1.0 -t kontor-api .
|
||||
docker build --target=production -t kontor-api:0.2.0-SNAPSHOT .
|
||||
|
||||
dev:
|
||||
MARIADB_SERVER=localhost uv run fastapi dev src/main.py --port 8008
|
||||
DB_SERVER=127.0.0.1 uv run fastapi dev src/main.py --port 8008
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ dependencies = [
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"fastapi[standard]>=0.115.12",
|
||||
"httpx==0.24.1",
|
||||
"mariadb>=1.1.12",
|
||||
"sqlalchemy>=2.0.40",
|
||||
"platformdirs>=4.3.7",
|
||||
"pathlib>=1.0.1",
|
||||
@@ -19,7 +18,15 @@ dependencies = [
|
||||
"sqlalchemy>=2.0.40",
|
||||
"sqlmodel>=0.0.24",
|
||||
"python-dotenv>=1.1.0",
|
||||
"python-jose>=3.4.0",
|
||||
"python-jose[cryptography]>=3.4.0",
|
||||
"python-multipart>=0.0.20",
|
||||
"natsort>=8.4.0",
|
||||
"psycopg2-binary>=2.9.10",
|
||||
"pytest-cov>=6.1.1",
|
||||
"databases[sqlite]>=0.9.0",
|
||||
"pydantic[email]>=2.11.3",
|
||||
"jinja2>=3.1.6",
|
||||
"asyncpg>=0.30.0",
|
||||
"bcrypt>=4.3.0",
|
||||
"fastapi-jwt-auth>=0.5.0",
|
||||
]
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.apis.version1 import comic, media, tysc
|
||||
from src.apis.version1 import comic, mediaactor, mediafile, mediaactorfile, tysc, admin
|
||||
|
||||
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(mediafile.router, prefix="/media", tags=["media"])
|
||||
api_router.include_router(mediaactor.router, prefix="/media", tags=["media"])
|
||||
api_router.include_router(mediaactorfile.router, prefix="/media", tags=["media"])
|
||||
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
|
||||
api_router.include_router(admin.router, prefix="/login", tags=["login"])
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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)]
|
||||
@@ -0,0 +1,52 @@
|
||||
from datetime import timedelta
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, status, Depends, Response
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from src.core.config import settings
|
||||
from src.core.security import create_access_token, authenticate_user, get_current_active_user
|
||||
from src.db.models.admin import Profile
|
||||
from src.schema.admin import Token, ProfileModel
|
||||
from src.webapps.auth.forms import LoginForm
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/token")
|
||||
def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
|
||||
user = authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email, "scope": " ".join(form_data.scopes)}, expires_delta=access_token_expires
|
||||
)
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
|
||||
|
||||
# @router.post("/token-cookie", response_model=Token)
|
||||
def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()):
|
||||
user = authenticate_user(form_data.username, form_data.password) # type: ignore
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
)
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email}, expires_delta=access_token_expires
|
||||
)
|
||||
response.set_cookie(
|
||||
key="access_token", value=f"Bearer {access_token}", httponly=True
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.get("/profile", response_model=ProfileModel)
|
||||
async def read_profile(current_user: Annotated[Profile, Depends(get_current_active_user)]):
|
||||
return current_user
|
||||
@@ -1,53 +1,68 @@
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
|
||||
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",
|
||||
tags=["comics"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.comic import Artist, Comic, Issue, Publisher
|
||||
from src.db.repository.comics.artist import get_artist_details
|
||||
from src.db.repository.comics.comic import (
|
||||
get_comic_details,
|
||||
get_issue_details,
|
||||
get_short_info,
|
||||
list_comics,
|
||||
)
|
||||
from src.db.repository.comics.publisher import get_publisher_details
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.comics.artist import ArtistCreation, ArtistResponse
|
||||
from src.schema.comics.artist_details import ArtistDetailResponse
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.comic_details import ComicDetailsResponse
|
||||
from src.schema.comics.issue_details import IssueDetailsResponse
|
||||
from src.schema.comics.publisher import PublisherResponse
|
||||
from src.schema.comics.publisher_details import PublisherDetailsResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/comics")
|
||||
def get_all_comics(db: SessionDep) -> List[ComicResponse]:
|
||||
results: List[ComicResponse] = []
|
||||
comics = db.scalars(select(Comic)).all()
|
||||
comics = list_comics(db)
|
||||
for comic in comics:
|
||||
response = get_short_info(comic)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
|
||||
def get_comic(comic_id: UUID, db: SessionDep) -> ComicDetailsResponse:
|
||||
def get_comic(comic_id: str, db: SessionDep) -> ComicDetailsResponse:
|
||||
comic = db.get(Comic, comic_id)
|
||||
if comic is None:
|
||||
raise HTTPException(status_code=404, detail="Comic could not be found")
|
||||
logger.info(f"create ComicDetailsResponse for {comic}")
|
||||
response: ComicDetailsResponse = get_comic_details(comic)
|
||||
logger.info(f"ComicDetailsResponse: {response}")
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/artists", response_model=List[ArtistResponse])
|
||||
def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
|
||||
results: List[ArtistResponse] = []
|
||||
artists = db.query(Artist).all()
|
||||
for artist in artists:
|
||||
results.append(ArtistResponse(id=artist.id, name=artist.name))
|
||||
results.append(ArtistResponse(id=artist.id, name=str(artist.name))) # type: ignore
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
|
||||
def get_artist(artist_id: UUID, db: SessionDep) -> ArtistDetailResponse:
|
||||
def get_artist(artist_id: str, db: SessionDep) -> ArtistDetailResponse:
|
||||
artist = db.get(Artist, artist_id)
|
||||
if artist is None:
|
||||
raise HTTPException(status_code=404, detail="Artist could not be found")
|
||||
response: ArtistDetailResponse = get_artist_details(artist)
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/artists", status_code=status.HTTP_201_CREATED)
|
||||
def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse:
|
||||
artist: Artist = Artist()
|
||||
@@ -57,6 +72,32 @@ def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistRespons
|
||||
db.commit()
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Artist already added")
|
||||
response = ArtistResponse(id=artist.id, name=artist.name)
|
||||
response = ArtistResponse(id=artist.id, name=str(artist.name))
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/publishers", response_model=List[PublisherResponse])
|
||||
def get_all_publishers(db: SessionDep) -> List[PublisherResponse]:
|
||||
results: List[PublisherResponse] = []
|
||||
publishers = db.query(Publisher).all()
|
||||
for publisher in publishers:
|
||||
results.append(PublisherResponse(id=publisher.id, name=str(publisher.name)))
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/publishers/{publisher_id}", response_model=PublisherDetailsResponse)
|
||||
def get_publisher(publisher_id: str, db: SessionDep) -> PublisherDetailsResponse:
|
||||
publisher = db.get(Publisher, publisher_id)
|
||||
if publisher is None:
|
||||
raise HTTPException(status_code=404, detail="Publisher could not be found")
|
||||
response: PublisherDetailsResponse = get_publisher_details(publisher)
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/issues", response_model=List[IssueDetailsResponse])
|
||||
def get_issues(db: SessionDep) -> List[IssueDetailsResponse]:
|
||||
results: List[IssueDetailsResponse] = []
|
||||
issues = db.query(Issue).all()
|
||||
for issue in issues:
|
||||
results.append(get_issue_details(issue))
|
||||
return results
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from src.schema.admin import HealthCheck
|
||||
|
||||
health_router = APIRouter()
|
||||
|
||||
|
||||
@health_router.get(
|
||||
"/health",
|
||||
tags=["healthcheck"],
|
||||
summary="Perform a health check",
|
||||
response_description="Return HTTP status code 200 (OK)",
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
def health_check() -> HealthCheck:
|
||||
"""
|
||||
## Perform a health check
|
||||
Endpoint to perform a healthcheck on. This endpoint can primarily be used Docker
|
||||
to ensure a robust container orchestration and management is in place. Other
|
||||
services which rely on proper functioning of the API service will not deploy if this
|
||||
endpoint returns any other HTTP status code except 200 (OK).
|
||||
:return:
|
||||
HealthCheck: Returns a JSON response with the health status
|
||||
"""
|
||||
return HealthCheck(status="ok")
|
||||
@@ -0,0 +1,41 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.core.config import settings
|
||||
from src.core.log_conf import logger
|
||||
from src.core.security import authenticate_user, create_access_token
|
||||
from src.schema.admin import Token
|
||||
|
||||
login_router = APIRouter()
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: str | None = None
|
||||
password: str | None = None
|
||||
|
||||
|
||||
@login_router.post(
|
||||
"/login",
|
||||
tags=["login"],
|
||||
summary="Login and get token",
|
||||
response_description="Return HTTP status code 200 (OK)",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def login(request: LoginRequest) -> Token:
|
||||
logger.info(f"login with {request.email}")
|
||||
user = authenticate_user(request.email, request.password) # type: ignore
|
||||
scopes = ["admin", "read"]
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email, "scope": " ".join(scopes)},
|
||||
expires_delta=access_token_expires,
|
||||
)
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
@@ -1,76 +0,0 @@
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, status, HTTPException
|
||||
from sqlalchemy import select, Sequence
|
||||
|
||||
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",
|
||||
tags=["media"]
|
||||
)
|
||||
|
||||
@router.get("/update-titles")
|
||||
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
|
||||
results: list[MediaFileResponse] = []
|
||||
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
|
||||
for mediafile in files:
|
||||
mediafile.update_title()
|
||||
db.add(mediafile)
|
||||
response = get_file_details(mediafile)
|
||||
results.append(response)
|
||||
db.commit()
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/files", response_model=List[MediaFileResponse])
|
||||
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaFileResponse]:
|
||||
results: list[MediaFileResponse] = []
|
||||
files: Sequence[MediaFile]
|
||||
if review:
|
||||
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
|
||||
elif download:
|
||||
files = db.query(MediaFile).filter(MediaFile.should_download == 1).all()
|
||||
else:
|
||||
files = db.scalars(select(MediaFile)).all()
|
||||
for mediafile in files:
|
||||
response = get_file_details(mediafile)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def get_file(file_id: UUID, db: SessionDep) -> MediaFileResponse:
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
response = get_file_details(mediafile)
|
||||
return response
|
||||
|
||||
@router.put("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def update_file(file_id: UUID, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
|
||||
mediaFile = db.get(MediaFile, file_id)
|
||||
if not mediaFile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
set_file(info, mediaFile)
|
||||
db.add(mediaFile)
|
||||
db.commit()
|
||||
return info
|
||||
|
||||
|
||||
@router.post("/files", status_code=status.HTTP_201_CREATED)
|
||||
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse:
|
||||
print(new_link.url)
|
||||
try:
|
||||
mediaFile: MediaFile = MediaFile()
|
||||
setattr(mediaFile, "url", new_link.url)
|
||||
setattr(mediaFile, "review", 1)
|
||||
setattr(mediaFile, "should_download", 1)
|
||||
db.add(mediaFile)
|
||||
db.commit()
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Link duplicate")
|
||||
response = get_file_details(mediaFile)
|
||||
return response
|
||||
@@ -0,0 +1,48 @@
|
||||
from fastapi import APIRouter, status, HTTPException
|
||||
from sqlalchemy import select
|
||||
from src.core.log_conf import logger
|
||||
from src.db.repository.media import create_new_mediaactor, delete_mediaactor
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.media.actor import Actor, MediaActorResponse, get_actor_details
|
||||
from src.db.models.media import MediaActor
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/actors", response_model=list[MediaActorResponse])
|
||||
# def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
|
||||
def get_all_actors(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaActorResponse]: # type: ignore
|
||||
results: list[MediaActorResponse] = []
|
||||
actors = db.scalars(select(MediaActor)).all()
|
||||
for mediaactor in actors:
|
||||
response = MediaActorResponse(id=mediaactor.id, name=str(mediaactor.name), url=str(mediaactor.url))
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/actors/{actor_id}", response_model=MediaActorResponse)
|
||||
def get_actor(actor_id: str, db: SessionDep) -> MediaActorResponse: # type: ignore
|
||||
media_actor = db.get(MediaActor, actor_id)
|
||||
if not media_actor:
|
||||
raise HTTPException(status_code=404, detail="MediaActor could not be found")
|
||||
response = get_actor_details(media_actor)
|
||||
return response
|
||||
|
||||
@router.delete("/actors/{actor_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_actor(actor_id: str, db: SessionDep): # type: ignore
|
||||
media_actor = db.get(MediaActor, actor_id)
|
||||
if not media_actor:
|
||||
raise HTTPException(status_code=404, detail="MediaActor could not be found")
|
||||
logger.info(f"delete MediaActor: {actor_id}")
|
||||
actor_files = media_actor.media_actor_files
|
||||
logger.info(f"MediaActorFiles links {len(actor_files)}")
|
||||
if len(actor_files) == 0:
|
||||
delete_mediaactor(db, media_actor.id)
|
||||
|
||||
@router.post("/actors", status_code=status.HTTP_201_CREATED)
|
||||
def add_actor(new_actor: Actor, db: SessionDep) -> MediaActorResponse: # type: ignore
|
||||
logger.info(f"add actor {new_actor.url}")
|
||||
try:
|
||||
mediaActor: MediaActor = create_new_mediaactor(new_actor, db)
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Link duplicate")
|
||||
response = get_actor_details(mediaActor)
|
||||
return response
|
||||
@@ -0,0 +1,33 @@
|
||||
from fastapi import APIRouter, status, HTTPException
|
||||
from sqlalchemy import select
|
||||
from src.db.models.media import MediaActorFile
|
||||
from src.db.repository.media import delete_mediaactorfile
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.media.actorfile import MediaActorFileResponse, get_actorfile_details
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/actorfiles", response_model=list[MediaActorFileResponse])
|
||||
def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]: # type: ignore
|
||||
results: list[MediaActorFileResponse] = []
|
||||
actorfiles = db.scalars(select(MediaActorFile)).all()
|
||||
for mediaactorfile in actorfiles:
|
||||
response = MediaActorFileResponse(id=mediaactorfile.id, actor_id=str(mediaactorfile.media_actor_id), file_id=str(mediaactorfile.media_file_id))
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/actorfiles/{actorfile_id}", response_model=MediaActorFileResponse)
|
||||
def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse: # type: ignore
|
||||
media_actorfile = db.get(MediaActorFile, actorfile_id)
|
||||
if not media_actorfile:
|
||||
raise HTTPException(status_code=404, detail="MediaActor could not be found")
|
||||
response = get_actorfile_details(media_actorfile)
|
||||
return response
|
||||
|
||||
@router.delete("/actorfiles/{actorfile_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_actorfile(actorfile_id: str, db: SessionDep): # type: ignore
|
||||
media_actorfile = db.get(MediaActorFile, actorfile_id)
|
||||
if not media_actorfile:
|
||||
raise HTTPException(status_code=404, detail="MediaActor could not be found")
|
||||
delete_mediaactorfile(db, media_actorfile.id)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
from fastapi import APIRouter, status, HTTPException, Depends
|
||||
from sqlalchemy import select, Sequence
|
||||
from src.core.log_conf import logger
|
||||
from src.db.repository.media import create_new_mediaactorfile, create_new_mediafile, delete_mediafile
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.media.actor import MediaActorResponse
|
||||
from src.schema.media.actorfile import MediaActorFileResponse
|
||||
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
|
||||
from src.db.models.media import MediaFile
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/update-titles")
|
||||
def update_titles(db: SessionDep) -> list[MediaFileResponse]: # type: ignore
|
||||
results: list[MediaFileResponse] = []
|
||||
files = db.query(MediaFile).filter(MediaFile.review == True).all()
|
||||
for mediafile in files:
|
||||
mediafile.update_title()
|
||||
db.add(mediafile)
|
||||
response = get_file_details(mediafile)
|
||||
results.append(response)
|
||||
db.commit()
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/files", response_model=list[MediaFileResponse])
|
||||
# def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
|
||||
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaFileResponse]: # type: ignore
|
||||
results: list[MediaFileResponse] = []
|
||||
files: Sequence[MediaFile]
|
||||
if review:
|
||||
files = db.query(MediaFile).filter(MediaFile.review == True).all() # type: ignore
|
||||
elif download:
|
||||
files = db.query(MediaFile).filter(MediaFile.should_download == True).all() # type: ignore
|
||||
else:
|
||||
files = db.scalars(select(MediaFile)).all() # type: ignore
|
||||
for mediafile in files: # type: ignore
|
||||
response = get_file_details(mediafile)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse: # type: ignore
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
response = get_file_details(mediafile)
|
||||
return response
|
||||
|
||||
@router.delete("/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_file(file_id: str, db: SessionDep): # type: ignore
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
logger.info(f"delete MediaFile: {file_id}")
|
||||
actor_files = mediafile.media_actor_files
|
||||
logger.info(f"MediaActorFiles links {len(actor_files)}")
|
||||
if len(actor_files) == 0:
|
||||
delete_mediafile(db, mediafile.id)
|
||||
|
||||
@router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse])
|
||||
def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]: # type: ignore
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
actor_files = mediafile.media_actor_files
|
||||
logger.info(f"already known actors: {actor_files}")
|
||||
results: list[MediaActorResponse] = []
|
||||
for actor_file in actor_files:
|
||||
response = MediaActorResponse(id=actor_file.media_actor.id, name=actor_file.media_actor.name, url=actor_file.media_actor.url)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.put("/files/{file_id}/actors", response_model=list[MediaActorFileResponse])
|
||||
def update_file_actors(file_id: str, db: SessionDep, actors: list[MediaActorResponse]) -> list[MediaActorFileResponse]: # type: ignore
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
actor_files = mediafile.media_actor_files
|
||||
logger.info(f"already known actors: {actor_files}")
|
||||
for actor in actors:
|
||||
already_associated = False
|
||||
for actor_file in actor_files:
|
||||
if actor.id == actor_file.media_actor_id:
|
||||
logger.info("alreay associated - do nothing")
|
||||
already_associated = True
|
||||
break
|
||||
if not already_associated:
|
||||
create_new_mediaactorfile(db, actor.id, mediafile.id)
|
||||
db.refresh(mediafile)
|
||||
actor_files = mediafile.media_actor_files
|
||||
results: list[MediaActorFileResponse] = []
|
||||
for actor_file in actor_files:
|
||||
response = MediaActorFileResponse(id=actor_file.id, actor_id=actor_file.media_actor_id, file_id=actor_file.media_file_id)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.put("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def update_file(file_id: str, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse: # type: ignore
|
||||
mediaFile = db.get(MediaFile, file_id)
|
||||
if not mediaFile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
set_file(info, mediaFile)
|
||||
db.add(mediaFile)
|
||||
db.commit()
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be updated")
|
||||
response = get_file_details(mediafile)
|
||||
return response
|
||||
|
||||
@router.post("/files", status_code=status.HTTP_201_CREATED)
|
||||
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse: # type: ignore
|
||||
logger.info(f"add url {new_link.url}")
|
||||
try:
|
||||
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Link duplicate")
|
||||
response = get_file_details(mediaFile)
|
||||
return response
|
||||
@@ -1,15 +1,11 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.tysc.sport import SportResponse
|
||||
from src.db.models.tysc import Sport
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/tysc",
|
||||
tags=["tysc"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/sports")
|
||||
def get_all_sports(db: SessionDep) -> List[SportResponse]:
|
||||
|
||||
@@ -9,15 +9,17 @@ 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}"
|
||||
PROJECT_VERSION: str = "0.2.0"
|
||||
|
||||
DB_USER: str = os.getenv("DB_USER", "kontor")
|
||||
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor")
|
||||
DB_SERVER: str = os.getenv("DB_SERVER", "postgres")
|
||||
DB_PORT: int = int(os.getenv("DB_PORT", 5432))
|
||||
DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor")
|
||||
DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}"
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=")
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60*24*7 # one week in mins
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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",
|
||||
},
|
||||
"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": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
"error": {
|
||||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"root": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"kontor": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"uvicorn.error": {"level": "INFO"},
|
||||
"uvicorn.access": {
|
||||
"handlers": ["default"],
|
||||
"level": "WARNING",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("kontor")
|
||||
@@ -0,0 +1,156 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated, Dict, List, Optional
|
||||
|
||||
import bcrypt
|
||||
from fastapi import Depends, HTTPException, Request, Security, status
|
||||
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.security import OAuth2, OAuth2PasswordBearer, SecurityScopes
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from jose import JWTError, jwt
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.core.config import settings
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.admin import Profile
|
||||
from src.db.repository.admin import get_profile
|
||||
from src.db.session import SessionLocal
|
||||
from src.schema.admin import ProfileModel, TokenData
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(
|
||||
tokenUrl="/api/login/token",
|
||||
scopes={"me": "read", "admin": "read"},
|
||||
)
|
||||
|
||||
|
||||
class OAuth2PasswordBearerWithCookie(OAuth2):
|
||||
def __init__(
|
||||
self,
|
||||
tokenUrl: str,
|
||||
scheme_name: Optional[str] = None,
|
||||
scopes: Optional[Dict[str, str]] = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
if not scopes:
|
||||
scopes = {}
|
||||
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) # type: ignore
|
||||
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.cookies.get("access_token") # type: ignore # changed to accept access token from httpOnly Cookie
|
||||
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return param
|
||||
|
||||
|
||||
def authenticate_user(username: str, password: str) -> Optional[Profile]:
|
||||
with SessionLocal() as db:
|
||||
user = get_profile(username=username, db=db)
|
||||
logger.debug(user)
|
||||
if not user:
|
||||
return None
|
||||
if bcrypt.checkpw(password.encode(), user.password.encode()):
|
||||
print("User successful authenticated")
|
||||
else:
|
||||
logger.info("Authentication failed!")
|
||||
return user
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(
|
||||
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
|
||||
)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)]
|
||||
):
|
||||
if security_scopes.scopes:
|
||||
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
|
||||
else:
|
||||
authenticate_value = "Bearer"
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": authenticate_value},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
username: str = payload.get("sub") # type: ignore
|
||||
logger.info("username/email extracted is ", username)
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
scope: str = payload.get("scope", "")
|
||||
token_scopes: List[str] = scope.split(" ")
|
||||
token_data = TokenData(scopes=token_scopes, username=username)
|
||||
except (JWTError, ValidationError):
|
||||
raise credentials_exception
|
||||
with SessionLocal() as db:
|
||||
user = get_profile(username=token_data.username, db=db) # type: ignore
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
for scope in security_scopes.scopes:
|
||||
if scope not in token_scopes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="not enough permissions",
|
||||
headers={"WWW-Authenticate": authenticate_value},
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])],
|
||||
) -> ProfileModel:
|
||||
if not current_user.enabled: # type: ignore
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
user_model = ProfileModel(
|
||||
username=current_user.user_name,
|
||||
email=current_user.email, # type: ignore
|
||||
first_name=current_user.first_name,
|
||||
last_name=current_user.last_name, # type: ignore
|
||||
active=current_user.enabled,
|
||||
) # type: ignore
|
||||
return user_model
|
||||
|
||||
|
||||
def get_current_user_from_token(token: str = Depends(oauth2_scheme)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
username: str = payload.get("sub") # type: ignore
|
||||
logger.info("username/email extracted is ", username)
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
with SessionLocal() as db:
|
||||
user = get_profile(username=username, db=db)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
@@ -1,7 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
|
||||
from sqlalchemy.orm import relationship, mapped_column, Mapped
|
||||
|
||||
from src.db.models.base import Base, BaseMixin
|
||||
@@ -9,12 +8,12 @@ from src.db.models.base import Base, BaseMixin
|
||||
|
||||
class Profile(Base, BaseMixin):
|
||||
__tablename__ = 'profile'
|
||||
first_name = Column(String(255))
|
||||
last_name = Column(String(255))
|
||||
user_name = Column(String(255), nullable=False)
|
||||
email = Column(String(255))
|
||||
password = Column(String(255))
|
||||
enabled = Column(BIT(1))
|
||||
first_name = Column(String)
|
||||
last_name = Column(String)
|
||||
user_name = Column(String, nullable=False)
|
||||
email = Column(String)
|
||||
password = Column(String)
|
||||
enabled = Column(Boolean)
|
||||
assignments = relationship("Assignment")
|
||||
tokens = relationship("Token")
|
||||
|
||||
@@ -28,20 +27,23 @@ class Profile(Base, BaseMixin):
|
||||
full_name += self.last_name
|
||||
return full_name
|
||||
|
||||
def __str__(self):
|
||||
return f"Profile({self.id} {self.user_name}, {self.email})"
|
||||
|
||||
|
||||
class Token(Base, BaseMixin):
|
||||
__tablename__ = "token"
|
||||
token = Column(String(255), nullable=False, unique=True)
|
||||
name = Column(String(255))
|
||||
token = Column(String, nullable=False, unique=True)
|
||||
name = Column(String)
|
||||
last_used_date: Mapped[datetime] = mapped_column()
|
||||
enabled = Column(BIT(1))
|
||||
profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False)
|
||||
enabled = Column(Boolean)
|
||||
profile_id = Column(String, ForeignKey("profile.id"), nullable=False)
|
||||
profile = relationship("Profile", back_populates="tokens")
|
||||
|
||||
|
||||
class Permission(Base, BaseMixin):
|
||||
__tablename__ = "permission"
|
||||
name = Column(String(255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
assignments = relationship("Assignment")
|
||||
|
||||
|
||||
@@ -53,20 +55,14 @@ class Assignment(Base, BaseMixin):
|
||||
permission = relationship("Permission", back_populates="assignments")
|
||||
|
||||
|
||||
class ModuleData(Base, BaseMixin):
|
||||
__tablename__ = "module_data"
|
||||
module_name = Column(String(255), nullable=False)
|
||||
import_data = Column(BIT(1))
|
||||
|
||||
|
||||
class MailAccount(Base, BaseMixin):
|
||||
__tablename__ = "mail_account"
|
||||
host = Column(String(255))
|
||||
host = Column(String)
|
||||
port = Column(Integer)
|
||||
protocol = Column(String(255))
|
||||
user_name = Column(String(255))
|
||||
password = Column(String(255))
|
||||
start_tls = Column(BIT(1))
|
||||
protocol = Column(String)
|
||||
user_name = Column(String)
|
||||
password = Column(String)
|
||||
start_tls = Column(Boolean)
|
||||
|
||||
|
||||
class Mail(Base, BaseMixin):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import func, Column, String
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy import func, Column, String, Boolean
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
@@ -11,8 +10,8 @@ class Base(DeclarativeBase):
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
id = Column(String(255), primary_key=True, default=uuid.uuid4())
|
||||
# id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4())
|
||||
#id = Column(String, primary_key=True, default=uuid.uuid4)
|
||||
id: Mapped[str] = mapped_column(primary_key=True, default=str(uuid.uuid4()))
|
||||
# created_date = Column(DateTime)
|
||||
created_date: Mapped[datetime] = mapped_column(default=func.now())
|
||||
# last_modified_date = Column(DateTime)
|
||||
@@ -22,10 +21,10 @@ class BaseMixin:
|
||||
|
||||
|
||||
class BaseVideoMixin:
|
||||
cloud_link = Column(String(255))
|
||||
file_name = Column(String(255))
|
||||
path = Column(String(255))
|
||||
review = Column(BIT(1))
|
||||
title = Column(String(255))
|
||||
url = Column(String(255), unique=True)
|
||||
should_download = Column(BIT(1))
|
||||
cloud_link = Column(String, nullable=True)
|
||||
file_name = Column(String, nullable=True)
|
||||
path = Column(String)
|
||||
review = Column(Boolean)
|
||||
title = Column(String)
|
||||
url = Column(String, nullable=True)
|
||||
should_download = Column(Boolean)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
from typing import Dict, List
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import AnyStr, Dict, List, Optional, Any
|
||||
from natsort import natsorted
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
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"
|
||||
name = Column(String(length=255), unique=True)
|
||||
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)
|
||||
weblink = Column(String, nullable=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):
|
||||
@@ -21,11 +30,12 @@ class Publisher(Base, BaseMixin):
|
||||
|
||||
class Comic(Base, BaseMixin):
|
||||
__tablename__ = 'comic'
|
||||
title = Column(String(length=255), unique=True)
|
||||
title = Column(String, unique=True)
|
||||
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
|
||||
publisher = relationship("Publisher", back_populates="comics")
|
||||
current_order = Column(BIT(1))
|
||||
completed = Column(BIT(1))
|
||||
current_order = Column(Boolean)
|
||||
completed = Column(Boolean)
|
||||
weblink = Column(String, nullable=True)
|
||||
issues = relationship("Issue", order_by="Issue.issue_number")
|
||||
story_arcs = relationship("StoryArc")
|
||||
trade_paperbacks = relationship("TradePaperback")
|
||||
@@ -38,10 +48,10 @@ class Comic(Base, BaseMixin):
|
||||
def __str__(self):
|
||||
return f'{self.title}({self.id})'
|
||||
|
||||
def get_artists(self) -> Dict[str, List[str]]:
|
||||
works: Dict[str, List[str]] = {}
|
||||
def get_artists(self) -> Dict[Any, List[Any]]:
|
||||
works: Dict[Any, List[Any]] = {}
|
||||
for work in self.comic_works:
|
||||
work_type = work.work_type.name
|
||||
work_type = work.work_type
|
||||
artist = work.artist
|
||||
if work_type in works:
|
||||
works[work_type].append(artist)
|
||||
@@ -56,15 +66,16 @@ class Comic(Base, BaseMixin):
|
||||
|
||||
class Volume(Base, BaseMixin):
|
||||
__tablename__ = "volume"
|
||||
name = Column(String(length=255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||
comic = relationship("Comic", back_populates="volumes")
|
||||
story_arcs = relationship("StoryArc")
|
||||
issues = relationship("Issue")
|
||||
|
||||
|
||||
class TradePaperback(Base, BaseMixin):
|
||||
__tablename__ = "trade_paperback"
|
||||
name = Column(String(length=255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
issue_start = Column(Integer)
|
||||
issue_end = Column(Integer)
|
||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||
@@ -73,31 +84,58 @@ class TradePaperback(Base, BaseMixin):
|
||||
|
||||
class StoryArc(Base, BaseMixin):
|
||||
__tablename__ = "story_arc"
|
||||
name = Column(String(length=255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||
comic = relationship("Comic", back_populates="story_arcs")
|
||||
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
|
||||
volume = relationship("Volume", back_populates="story_arcs")
|
||||
issues = relationship("Issue")
|
||||
|
||||
|
||||
class Issue(Base, BaseMixin):
|
||||
__tablename__ = "issue"
|
||||
issue_number = Column(String(255))
|
||||
in_stock = Column(BIT(1))
|
||||
is_read = Column(BIT(1))
|
||||
issue_number = Column(String)
|
||||
title = Column(String, nullable=True)
|
||||
published_on: Mapped[datetime] = mapped_column(nullable=True)
|
||||
in_stock = Column(Boolean)
|
||||
is_read = Column(Boolean)
|
||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||
comic = relationship("Comic", back_populates="issues")
|
||||
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
|
||||
volume = relationship("Volume", back_populates="issues")
|
||||
story_arc_id = Column(String, ForeignKey("story_arc.id"), nullable=True)
|
||||
story_arc = relationship("StoryArc", back_populates="issues")
|
||||
issue_works = relationship("IssueWork")
|
||||
|
||||
def get_full_title(self) -> str:
|
||||
full_title: str = str(self.issue_number)
|
||||
if self.title:
|
||||
full_title += str(": " + self.title)
|
||||
return full_title
|
||||
|
||||
def get_artists(self) -> Dict[Any, List[Any]]:
|
||||
works: Dict[Any, List[Any]] = {}
|
||||
for work in self.issue_works:
|
||||
work_type = work.work_type
|
||||
artist = work.artist
|
||||
if work_type in works:
|
||||
works[work_type].append(artist)
|
||||
else:
|
||||
works[work_type] = [artist]
|
||||
return works
|
||||
|
||||
|
||||
class Artist(Base, BaseMixin):
|
||||
__tablename__ = "artist"
|
||||
name = Column(String(length=255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
weblink = Column(String, nullable=True)
|
||||
comic_works = relationship("ComicWork")
|
||||
issue_works = relationship("IssueWork")
|
||||
|
||||
def get_comics(self) -> Dict[str, List[str]]:
|
||||
works: Dict[str, List[str]] = {}
|
||||
def get_comics(self) -> Dict[Any, List[Comic]]:
|
||||
works: Dict[Any, List[Comic]] = {}
|
||||
for work in self.comic_works:
|
||||
work_type = work.work_type.name
|
||||
work_type = work.work_type
|
||||
comic = work.comic
|
||||
if work_type in works:
|
||||
works[work_type].append(comic)
|
||||
@@ -108,8 +146,20 @@ class Artist(Base, BaseMixin):
|
||||
|
||||
class WorkType(Base, BaseMixin):
|
||||
__tablename__ = "worktype"
|
||||
name = Column(String(length=255), nullable=False, unique=True)
|
||||
name = Column(String, nullable=False, unique=True)
|
||||
comic_works = relationship("ComicWork")
|
||||
issue_works = relationship("IssueWork")
|
||||
|
||||
def get_artists(self) -> Dict[str, List[str]]:
|
||||
works: Dict[str, List[str]] = {}
|
||||
for work in self.comic_works:
|
||||
comic = work.comic.title
|
||||
artist = work.artist
|
||||
if comic in works:
|
||||
works[comic].append(artist)
|
||||
else:
|
||||
works[comic] = [artist]
|
||||
return works
|
||||
|
||||
def __repr__(self):
|
||||
return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})'
|
||||
@@ -126,3 +176,13 @@ class ComicWork(Base, BaseMixin):
|
||||
artist = relationship("Artist", back_populates="comic_works")
|
||||
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
|
||||
work_type = relationship("WorkType", back_populates="comic_works")
|
||||
|
||||
|
||||
class IssueWork(Base, BaseMixin):
|
||||
__tablename__ = "issue_work"
|
||||
issue_id = Column(String, ForeignKey("issue.id"), nullable=False)
|
||||
issue = relationship("Issue", back_populates="issue_works")
|
||||
artist_id = Column(String, ForeignKey("artist.id"), nullable=False)
|
||||
artist = relationship("Artist", back_populates="issue_works")
|
||||
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
|
||||
work_type = relationship("WorkType", back_populates="issue_works")
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, List
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@@ -13,7 +12,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
from src.db.models.tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport
|
||||
from src.db.models.comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType
|
||||
from src.db.models.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
|
||||
from src.db.models.admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix
|
||||
from src.db.models.admin import Mail, MailAccount, ModuleData, Token, Assignment, Permission, Profile
|
||||
from src.db.models.metadata import MetaDataTable, MetaDataColumn
|
||||
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
|
||||
|
||||
@@ -79,10 +78,10 @@ class KontorDB:
|
||||
self.registry[MediaVideo.__tablename__] = MediaVideo
|
||||
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
|
||||
self.registry[MetaDataTable.__tablename__] = MetaDataTable
|
||||
self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix
|
||||
self.registry[Assignment.__tablename__] = Assignment
|
||||
self.registry[Token.__tablename__] = Token
|
||||
self.registry[User.__tablename__] = User
|
||||
self.registry[Role.__tablename__] = Role
|
||||
self.registry[Profile.__tablename__] = Profile
|
||||
self.registry[Permission.__tablename__] = Permission
|
||||
self.registry[ModuleData.__tablename__] = ModuleData
|
||||
self.registry[MailAccount.__tablename__] = MailAccount
|
||||
self.registry[Mail.__tablename__] = Mail
|
||||
@@ -360,7 +359,7 @@ class KontorDB:
|
||||
update_list[link.id] = link.title
|
||||
return update_list
|
||||
|
||||
def get_download_list(self) -> list[uuid.UUID]:
|
||||
def get_download_list(self) -> List[str]:
|
||||
download_list = []
|
||||
__session__ = sessionmaker(self.engine)
|
||||
_filter = { 'should_download': True}
|
||||
|
||||
@@ -6,8 +6,7 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy import Column, String, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
|
||||
@@ -26,31 +25,31 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
||||
def update_title(self) -> None:
|
||||
logging.info(f"update title for {self.url}")
|
||||
try:
|
||||
r = requests.get(self.url)
|
||||
r = requests.get(str(self.url))
|
||||
soup = BeautifulSoup(r.content, "html.parser")
|
||||
title = soup.title.string
|
||||
title = soup.title.get_text() # type: ignore
|
||||
self.title = title
|
||||
self.review = 0
|
||||
self.review = False
|
||||
except:
|
||||
self.title = None
|
||||
self.review = 1
|
||||
self.review = True
|
||||
self.last_modified_date = datetime.now()
|
||||
|
||||
def download_file(self, download_dir: str, dl_tool: str):
|
||||
logging.info(f"download file for {self.url} to {download_dir}")
|
||||
result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True)
|
||||
result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) # type: ignore
|
||||
if result.returncode == 0:
|
||||
output = result.stdout
|
||||
output = re.sub(' +', ' ', output)
|
||||
lines_list = output.splitlines()
|
||||
file_name = self.__parse_output__(lines_list)
|
||||
if file_name is None:
|
||||
self.review = 1
|
||||
self.should_download = 1
|
||||
self.review = True
|
||||
self.should_download = True
|
||||
self.file_name = None
|
||||
else:
|
||||
download_file = Path(file_name)
|
||||
self.should_download = 0
|
||||
self.should_download = False
|
||||
self.file_name = download_file.name
|
||||
self.cloud_link = str(download_file.absolute())
|
||||
self.last_modified_date = datetime.now()
|
||||
@@ -71,31 +70,52 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
||||
|
||||
class MediaActor(Base, BaseMixin):
|
||||
__tablename__ = 'media_actor'
|
||||
name = Column(String(255))
|
||||
name = Column(String)
|
||||
url = Column(String, unique=True, nullable=True)
|
||||
media_actor_files = relationship("MediaActorFile")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'MediaActor({self.id} {self.name} {self.url})'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.url}({self.id})'
|
||||
|
||||
|
||||
class MediaActorFile(Base, BaseMixin):
|
||||
__tablename__ = 'media_actor_file'
|
||||
media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False)
|
||||
media_actor_id = Column(String, ForeignKey("media_actor.id"), nullable=False)
|
||||
media_actor = relationship("MediaActor", back_populates="media_actor_files")
|
||||
media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=True)
|
||||
media_file_id = Column(String, ForeignKey("media_file.id"), nullable=True)
|
||||
media_file = relationship("MediaFile", back_populates="media_actor_files")
|
||||
|
||||
def __repr__(self):
|
||||
return f'MediaActorFile({self.id} {self.media_actor_id} {self.media_file_id})'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.id} {self.media_actor_id} {self.media_file_id}'
|
||||
|
||||
class MediaArticle(Base, BaseMixin):
|
||||
__tablename__ = 'media_article'
|
||||
review = Column(BIT(1))
|
||||
title = Column(String(255))
|
||||
url = Column(String(255), unique=True)
|
||||
review = Column(Boolean)
|
||||
title = Column(String)
|
||||
url = Column(String, unique=True)
|
||||
|
||||
|
||||
class MediaVideo(Base, BaseMixin):
|
||||
__tablename__ = 'media_video'
|
||||
cloud_link = Column(String(255))
|
||||
file_name = Column(String(255))
|
||||
path = Column(String(255))
|
||||
review = Column(BIT(1))
|
||||
title = Column(String(255))
|
||||
url = Column(String(255), unique=True)
|
||||
should_download = Column(BIT(1))
|
||||
cloud_link = Column(String)
|
||||
file_name = Column(String)
|
||||
path = Column(String)
|
||||
review = Column(Boolean)
|
||||
title = Column(String)
|
||||
url = Column(String, unique=True)
|
||||
should_download = Column(Boolean)
|
||||
|
||||
def __repr__(self):
|
||||
return f'MediaFile({self.id} {self.title} {self.url})'
|
||||
|
||||
def __str__(self):
|
||||
if self.title is None:
|
||||
return f'{self.url}({self.id})'
|
||||
else:
|
||||
return f'{self.title}({self.id})'
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
from sqlalchemy import Column, String, ForeignKey, Integer
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.db.models.base import Base, BaseMixin
|
||||
|
||||
|
||||
class MetaDataTable(Base, BaseMixin):
|
||||
__tablename__ = 'meta_data_table'
|
||||
table_name = Column(String(255), unique=True)
|
||||
table_columns = relationship("MetaDataColumn")
|
||||
|
||||
def __repr__(self):
|
||||
return f'MetaDataTable({self.id} {self.table_name})'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.table_name}({self.id})'
|
||||
|
||||
|
||||
class MetaDataColumn(Base, BaseMixin):
|
||||
__tablename__ = 'meta_data_column'
|
||||
column_name = Column(String(255), nullable=False)
|
||||
column_sync_name = Column(String(255))
|
||||
column_type = Column(String(255))
|
||||
column_modifier = Column(String(255), nullable=True)
|
||||
column_order = Column(Integer)
|
||||
table_id = Column(String, ForeignKey('meta_data_table.id'))
|
||||
table = relationship("MetaDataTable", back_populates="table_columns")
|
||||
column_label = Column(String(255))
|
||||
filter_label = Column(String(255))
|
||||
is_shown = Column(BIT(1))
|
||||
show_filter = Column(BIT(1))
|
||||
ref_column = Column(String, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
if self.column_name is None:
|
||||
return f'MetaDataColumn({self.id} {self.table.table_name}.__)'
|
||||
else:
|
||||
return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.column_name}({self.id})'
|
||||
@@ -1,5 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.db.models.base import Base, BaseMixin
|
||||
@@ -10,15 +9,15 @@ class Sport(Base, BaseMixin):
|
||||
__table_args__ = (
|
||||
UniqueConstraint("name"),
|
||||
)
|
||||
name = Column(String(255), nullable=False, index=True, unique=True)
|
||||
name = Column(String, nullable=False, index=True, unique=True)
|
||||
teams = relationship("Team")
|
||||
positions = relationship("FieldPosition")
|
||||
|
||||
|
||||
class Team(Base, BaseMixin):
|
||||
__tablename__ = "team"
|
||||
name = Column(String(255), nullable=False, index=True, unique=True)
|
||||
short_name = Column(String(255), nullable=False, )
|
||||
name = Column(String, nullable=False, index=True, unique=True)
|
||||
short_name = Column(String, nullable=False, )
|
||||
sport_id = Column(String, ForeignKey("sport.id"), nullable=False)
|
||||
sport = relationship("Sport", back_populates="teams")
|
||||
roosters = relationship("Rooster")
|
||||
@@ -30,8 +29,8 @@ class FieldPosition(Base, BaseMixin):
|
||||
UniqueConstraint("name", "sport_id"),
|
||||
UniqueConstraint("short_name", "sport_id"),
|
||||
)
|
||||
name = Column(String(255), nullable=False, index=True)
|
||||
short_name = Column(String(255), nullable=False)
|
||||
name = Column(String, nullable=False, index=True)
|
||||
short_name = Column(String, nullable=False)
|
||||
sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True)
|
||||
sport = relationship("Sport", back_populates="positions")
|
||||
roosters = relationship("Rooster")
|
||||
@@ -42,8 +41,8 @@ class Player(Base, BaseMixin):
|
||||
__table_args__ = (
|
||||
UniqueConstraint("first_name", "last_name"),
|
||||
)
|
||||
first_name = Column(String(255), nullable=False, index=True)
|
||||
last_name = Column(String(255), nullable=False, index=True)
|
||||
first_name = Column(String, nullable=False, index=True)
|
||||
last_name = Column(String, nullable=False, index=True)
|
||||
roosters = relationship("Rooster")
|
||||
|
||||
def get_full_name(self) -> str:
|
||||
@@ -67,7 +66,7 @@ class Rooster(Base, BaseMixin):
|
||||
|
||||
class Vendor(Base, BaseMixin):
|
||||
__tablename__ = "vendor"
|
||||
name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
name = Column(String, nullable=False, unique=True, index=True)
|
||||
card_sets = relationship("CardSet")
|
||||
cards = relationship("Card")
|
||||
|
||||
@@ -77,9 +76,9 @@ class CardSet(Base, BaseMixin):
|
||||
__table_args__ = (
|
||||
UniqueConstraint("name", "vendor_id"),
|
||||
)
|
||||
name = Column(String(255), index=True)
|
||||
parallel_set = Column(BIT(1))
|
||||
insert_set = Column(BIT(1))
|
||||
name = Column(String, index=True)
|
||||
parallel_set = Column(Boolean)
|
||||
insert_set = Column(Boolean)
|
||||
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
|
||||
vendor = relationship("Vendor", back_populates="card_sets")
|
||||
cards = relationship("Card")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import AnyStr, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.db.models.admin import Profile
|
||||
|
||||
|
||||
def get_profile(username: AnyStr, db: Session) -> Optional[Profile]:
|
||||
profile = db.query(Profile).filter(Profile.email == username).first()
|
||||
return profile
|
||||
@@ -0,0 +1,46 @@
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.comic import Artist
|
||||
from src.schema.comics.artist import AddArtist
|
||||
from src.schema.comics.artist_details import ArtistDetailResponse, ArtistWorktypeComicResponse, ArtistWorktypeIssueResponse
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.worktype import WorktypeResponse
|
||||
|
||||
|
||||
def get_artist_details(artist: Artist) -> ArtistDetailResponse:
|
||||
comic_works: List[ArtistWorktypeComicResponse] = []
|
||||
comic_works_map = {}
|
||||
for work in artist.comic_works:
|
||||
worktype_id = work.work_type.id
|
||||
if worktype_id in comic_works_map:
|
||||
comic = ComicResponse(id=work.comic.id, title=work.comic.title, completed=work.comic.completed)
|
||||
comic_works_map[worktype_id].comics.append(comic)
|
||||
else:
|
||||
comic_works_map[worktype_id] = ArtistWorktypeComicResponse(
|
||||
worktype=WorktypeResponse(id=worktype_id, name=work.work_type.name),
|
||||
comics=[ComicResponse(id=work.comic.id, title=work.comic.title, completed=work.comic.completed)]
|
||||
)
|
||||
for value in comic_works_map.values():
|
||||
comic_works.append(value)
|
||||
issue_works: List[ArtistWorktypeIssueResponse] = []
|
||||
response = ArtistDetailResponse(
|
||||
id=artist.id,
|
||||
name=str(artist.name),
|
||||
weblink=str(artist.weblink),
|
||||
comic_works=comic_works,
|
||||
issue_works=issue_works,
|
||||
)
|
||||
return response
|
||||
|
||||
def update_artist(add_artist: AddArtist, artist_id: str, db: Session) -> Artist:
|
||||
logger.info("update artist")
|
||||
artist: Optional[Artist] = db.get(Artist, artist_id)
|
||||
artist.name = add_artist.name
|
||||
db.add(artist)
|
||||
db.commit()
|
||||
db.refresh(artist)
|
||||
return artist
|
||||
@@ -0,0 +1,87 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.comic import Comic, Issue
|
||||
from src.schema.comics.artist import ArtistResponse
|
||||
from src.schema.comics.comic import ComicResponse, ComicSchema
|
||||
from src.schema.comics.comic_details import ComicDetailsResponse, ComicWorktypeArtistResponse
|
||||
from src.schema.comics.issue import IssueResponse
|
||||
from src.schema.comics.issue_details import IssueDetailsResponse
|
||||
from src.schema.comics.publisher import PublisherResponse
|
||||
from src.schema.comics.volume import VolumeResponse
|
||||
from src.schema.comics.worktype import WorktypeResponse
|
||||
|
||||
|
||||
def list_comics(db: Session) -> List[Comic]:
|
||||
comics = db.query(Comic).all()
|
||||
return comics
|
||||
|
||||
|
||||
def get_issue_details(issue: Issue) -> IssueDetailsResponse:
|
||||
response = IssueDetailsResponse(
|
||||
id=issue.id,
|
||||
issue_number=str(issue.issue_number),
|
||||
in_stock=bool(issue.in_stock),
|
||||
is_read=bool(issue.is_read),
|
||||
comic=ComicResponse(id=issue.comic.id, title=issue.comic.title, completed=issue.comic.completed),
|
||||
volume=VolumeResponse(id=issue.volume.id, name=issue.volume.name)
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def update_comic(comic: ComicSchema, comic_id: str, db: Session) -> type[Comic] | None:
|
||||
logger.info(f"update_comic: {comic} with {comic_id}")
|
||||
comic = db.get(Comic, comic_id) # type: ignore
|
||||
return comic # type: ignore
|
||||
|
||||
def get_short_info(comic: Comic) -> ComicResponse:
|
||||
response = ComicResponse(
|
||||
id=comic.id,
|
||||
title=str(comic.title),
|
||||
completed=bool(comic.completed == 1)
|
||||
)
|
||||
return response
|
||||
|
||||
def get_comic_details(comic: Comic) -> ComicDetailsResponse:
|
||||
volumes: List[VolumeResponse] = []
|
||||
for volume in comic.volumes:
|
||||
volumes.append(VolumeResponse(id=volume.id, name=volume.name))
|
||||
issues: List[IssueResponse] = []
|
||||
for issue in comic.issues:
|
||||
issues.append(IssueResponse(
|
||||
id=issue.id,
|
||||
issue_number=issue.issue_number,
|
||||
in_stock=issue.in_stock,
|
||||
is_read=issue.is_read
|
||||
))
|
||||
works: List[ComicWorktypeArtistResponse] = []
|
||||
works_map: Dict[str, ComicWorktypeArtistResponse] = {}
|
||||
for work in comic.comic_works:
|
||||
worktype_id = work.work_type.id
|
||||
if worktype_id in works_map:
|
||||
artist = ArtistResponse(id=work.artist.id, name=work.artist.name)
|
||||
works_map[worktype_id].artists.append(artist)
|
||||
logger.info(f"add artist to response map: {artist} -> {works_map}")
|
||||
print(f"add artist to response map: {artist} -> {works_map}")
|
||||
else:
|
||||
works_map[worktype_id] = ComicWorktypeArtistResponse(
|
||||
worktype=WorktypeResponse(id=worktype_id, name=work.work_type.name),
|
||||
artists=[ArtistResponse(id=work.artist.id, name=work.artist.name)]
|
||||
)
|
||||
for value in works_map.values():
|
||||
works.append(value)
|
||||
response = ComicDetailsResponse(
|
||||
id=str(comic.id),
|
||||
created=str(comic.created_date),
|
||||
title=str(comic.title),
|
||||
completed=bool(comic.completed),
|
||||
current_order=bool(comic.current_order),
|
||||
weblink=str(comic.weblink),
|
||||
publisher=PublisherResponse(id=comic.publisher.id, name=comic.publisher.name),
|
||||
issues=issues,
|
||||
volumes=volumes,
|
||||
works=works
|
||||
)
|
||||
return response
|
||||
@@ -0,0 +1,24 @@
|
||||
from typing import List
|
||||
|
||||
from src.db.models.comic import Publisher
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.publisher import PublisherResponse
|
||||
from src.schema.comics.publisher_details import PublisherDetailsResponse
|
||||
|
||||
|
||||
def get_publisher_details(publisher: Publisher) -> PublisherDetailsResponse:
|
||||
imprints: List[PublisherResponse] = []
|
||||
for imprint in publisher.imprints:
|
||||
imprints.append(PublisherResponse(id=imprint.id, name=str(imprint.name)))
|
||||
comics: List[ComicResponse] = []
|
||||
for comic in publisher.comics:
|
||||
comics.append(
|
||||
ComicResponse(id=comic.id, title=comic.title, completed=comic.completed)
|
||||
)
|
||||
parent_publisher: PublisherResponse | None = None
|
||||
if publisher.parent_publisher:
|
||||
parent_publisher = PublisherResponse(id=publisher.parent_publisher.id, name=str(publisher.parent_publisher.name))
|
||||
response: PublisherDetailsResponse = PublisherDetailsResponse(
|
||||
id=publisher.id, name=str(publisher.name), parent_publisher=parent_publisher, imprints=imprints, comics=comics
|
||||
)
|
||||
return response
|
||||
@@ -0,0 +1,32 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import AnyStr
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.comic import WorkType
|
||||
from src.schema.comics.worktype import AddWorkType
|
||||
|
||||
|
||||
def create_new_worktype(work: AddWorkType, db: Session) -> WorkType:
|
||||
worktype = WorkType()
|
||||
worktype.id = str(uuid.uuid4())
|
||||
worktype.created_date = datetime.now()
|
||||
worktype.last_modified_date = datetime.now()
|
||||
worktype.name = work.worktype
|
||||
db.add(worktype)
|
||||
db.commit()
|
||||
db.refresh(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
|
||||
@@ -0,0 +1,86 @@
|
||||
from sqlalchemy.orm import Session
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.media import MediaActor, MediaActorFile, MediaFile, MediaVideo
|
||||
from src.schema.media.actor import Actor
|
||||
from src.webapps.media.forms import AddLinkForm
|
||||
|
||||
|
||||
def create_new_video(video: AddLinkForm, db: Session) -> MediaVideo:
|
||||
print(video.url)
|
||||
media_video = MediaVideo()
|
||||
media_video.id = str(uuid.uuid4())
|
||||
media_video.url = video.url # type: ignore
|
||||
media_video.created_date = datetime.now()
|
||||
media_video.last_modified_date = datetime.now()
|
||||
media_video.review = True # type: ignore
|
||||
media_video.should_download = True # type: ignore
|
||||
db.add(media_video)
|
||||
db.commit()
|
||||
db.refresh(media_video)
|
||||
print(media_video)
|
||||
return media_video
|
||||
|
||||
def create_new_mediafile(link: str, db: Session) -> MediaFile:
|
||||
logger.info("create MediaFile with url {link}")
|
||||
media_file: MediaFile = MediaFile()
|
||||
media_file.id = str(uuid.uuid4())
|
||||
media_file.url = link # type: ignore
|
||||
media_file.created_date = datetime.now()
|
||||
media_file.last_modified_date = datetime.now()
|
||||
media_file.version = 0
|
||||
media_file.review = True
|
||||
media_file.should_download = True
|
||||
db.add(media_file)
|
||||
db.commit()
|
||||
db.refresh(media_file)
|
||||
logger.info(f"created {media_file}")
|
||||
return media_file
|
||||
|
||||
def delete_mediafile(db: Session, media_file_id: str):
|
||||
logger.info(f"delete MediaFile with id {media_file_id}")
|
||||
media_file = db.get(MediaFile, media_file_id)
|
||||
db.delete(media_file)
|
||||
db.commit()
|
||||
|
||||
def create_new_mediaactor(new_actor: Actor, db: Session) -> MediaActor:
|
||||
logger.info(f"create MediaActor with url {new_actor.url}")
|
||||
media_actor: MediaActor = MediaActor()
|
||||
media_actor.id = str(uuid.uuid4())
|
||||
media_actor.name = str(new_actor.name) # type: ignore
|
||||
media_actor.url = str(new_actor.url) # type: ignore
|
||||
media_actor.created_date = datetime.now()
|
||||
media_actor.last_modified_date = datetime.now()
|
||||
media_actor.version = 0
|
||||
db.add(media_actor)
|
||||
db.commit()
|
||||
db.refresh(media_actor)
|
||||
logger.info(f"created {media_actor}")
|
||||
return media_actor
|
||||
|
||||
def delete_mediaactor(db: Session, actor_id: str):
|
||||
logger.info(f"delete MediaActor with id {actor_id}")
|
||||
media_actor = db.get(MediaActor, actor_id)
|
||||
db.delete(media_actor)
|
||||
db.commit()
|
||||
|
||||
def create_new_mediaactorfile(db: Session, actor_id: str, file_id: str) -> MediaActorFile:
|
||||
logger.info(f"create MediaActorFile with actor {actor_id} and file {file_id}")
|
||||
media_actor_file: MediaActorFile = MediaActorFile()
|
||||
media_actor_file.id = str(uuid.uuid4())
|
||||
media_actor_file.created_date = datetime.now()
|
||||
media_actor_file.last_modified_date = datetime.now()
|
||||
media_actor_file.version = 0
|
||||
media_actor_file.media_actor_id = actor_id # type: ignore
|
||||
media_actor_file.media_file_id = file_id # type: ignore
|
||||
db.add(media_actor_file)
|
||||
db.commit()
|
||||
db.refresh(media_actor_file)
|
||||
return media_actor_file
|
||||
|
||||
def delete_mediaactorfile(db: Session, actorfile_id: str):
|
||||
logger.info(f"delete MediaActorFile with id {actorfile_id}")
|
||||
media_actorfile = db.get(MediaActorFile, actorfile_id)
|
||||
db.delete(media_actorfile)
|
||||
db.commit()
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Generator, Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
|
||||
@@ -10,6 +11,9 @@ engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
||||
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
|
||||
|
||||
def get_db() -> Generator:
|
||||
with SessionLocal() as db:
|
||||
yield db
|
||||
|
||||
SessionDep: type[Session] = Annotated[Session, Depends(get_db)]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import databases
|
||||
|
||||
from src.core.log_conf import logger
|
||||
from src.db.session import SQLALCHEMY_DATABASE_URL
|
||||
|
||||
|
||||
async def check_db_connected():
|
||||
try:
|
||||
if not str(SQLALCHEMY_DATABASE_URL).__contains__("sqlite"):
|
||||
database = databases.Database(SQLALCHEMY_DATABASE_URL)
|
||||
if not database.is_connected:
|
||||
await database.connect()
|
||||
await database.execute("SELECT 1")
|
||||
logger.info("Database is connected (^_^)")
|
||||
except Exception as e:
|
||||
print(
|
||||
"Looks like db is missing or is there is some problem in connection,see below traceback"
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
async def check_db_disconnected():
|
||||
try:
|
||||
if not str(SQLALCHEMY_DATABASE_URL).__contains__("sqlite"):
|
||||
database = databases.Database(SQLALCHEMY_DATABASE_URL)
|
||||
if database.is_connected:
|
||||
await database.disconnect()
|
||||
logger.info("Database is Disconnected (-_-) zZZ")
|
||||
except Exception as e:
|
||||
raise e
|
||||
+39
-6
@@ -1,29 +1,62 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
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.apis.version1.healthcheck import health_router
|
||||
from src.apis.version1.login import login_router
|
||||
from src.core.config import settings
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.base import Base
|
||||
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
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await check_db_connected()
|
||||
yield
|
||||
await check_db_disconnected()
|
||||
|
||||
|
||||
def include_router(app: FastAPI):
|
||||
app.include_router(api_router)
|
||||
app.include_router(web_app_router)
|
||||
app.include_router(health_router)
|
||||
app.include_router(login_router)
|
||||
|
||||
|
||||
def configure_static(app: FastAPI):
|
||||
app.mount("/static", StaticFiles(directory="src/static"), name="static")
|
||||
|
||||
|
||||
def add_middle_ware(app: FastAPI):
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def create_tables():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
def start_application():
|
||||
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)
|
||||
|
||||
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)
|
||||
add_middle_ware(app)
|
||||
create_tables()
|
||||
return app
|
||||
|
||||
|
||||
kontor = start_application()
|
||||
|
||||
kontor = start_application(logger)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
||||
scopes: List[str] = []
|
||||
|
||||
|
||||
class ProfileModel(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
active: bool
|
||||
|
||||
class HealthCheck(BaseModel):
|
||||
"""
|
||||
Health check model
|
||||
"""
|
||||
status: str = "ok"
|
||||
@@ -1,36 +1,14 @@
|
||||
from typing import List, Dict
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.db.models.comic import Artist
|
||||
|
||||
|
||||
class ArtistCreation(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
class ArtistResponse(BaseModel):
|
||||
id: UUID
|
||||
id: str
|
||||
name: str
|
||||
|
||||
class ArtistDetailResponse(BaseModel):
|
||||
id: UUID
|
||||
class AddArtist(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
works: Dict[str, List[str]]
|
||||
|
||||
def get_artist_details(artist: Artist) -> ArtistDetailResponse:
|
||||
works = {}
|
||||
for work in artist.comic_works:
|
||||
work_type = work.work_type.name
|
||||
comic_title = work.comic.title
|
||||
if work_type in works:
|
||||
works[work_type].append(comic_title)
|
||||
else:
|
||||
works[work_type] = [comic_title]
|
||||
response = ArtistDetailResponse(
|
||||
id=artist.id,
|
||||
name=artist.name,
|
||||
works=works
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.issue_details import IssueDetailsResponse
|
||||
from src.schema.comics.worktype import WorktypeResponse
|
||||
|
||||
|
||||
class ArtistWorktypeComicResponse(BaseModel):
|
||||
worktype: WorktypeResponse
|
||||
comics: List[ComicResponse]
|
||||
|
||||
class ArtistWorktypeIssueResponse(BaseModel):
|
||||
worktype: WorktypeResponse
|
||||
issues: List[IssueDetailsResponse]
|
||||
|
||||
class ArtistDetailResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
weblink: str
|
||||
comic_works: List[ArtistWorktypeComicResponse]
|
||||
issue_works: List[ArtistWorktypeIssueResponse]
|
||||
@@ -1,56 +1,17 @@
|
||||
from typing import List, Dict
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.db.models.comic import Comic
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, AnyUrl
|
||||
from src.core.log_conf import logger
|
||||
|
||||
|
||||
class ComicResponse(BaseModel):
|
||||
id: UUID
|
||||
id: str
|
||||
title: str
|
||||
completed: bool
|
||||
|
||||
class ComicDetailsResponse(BaseModel):
|
||||
id: UUID
|
||||
created: str
|
||||
|
||||
class ComicSchema(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
completed : bool
|
||||
current_order : bool
|
||||
publisher: str
|
||||
volumes: List[str]
|
||||
works: Dict[str, List[str]]
|
||||
|
||||
def get_short_info(comic: Comic) -> ComicResponse:
|
||||
response = ComicResponse(
|
||||
id=comic.id,
|
||||
title=comic.title,
|
||||
completed=(comic.completed == 1)
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
|
||||
volumes = []
|
||||
for volume in comic.volumes:
|
||||
volumes.append(volume.name)
|
||||
works = {}
|
||||
for work in comic.comic_works:
|
||||
work_type = work.work_type.name
|
||||
artist_name = work.artist.name
|
||||
if work_type in works:
|
||||
works[work_type].append(artist_name)
|
||||
else:
|
||||
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),
|
||||
publisher=comic.publisher.name,
|
||||
volumes=volumes,
|
||||
works=works
|
||||
)
|
||||
return response
|
||||
|
||||
weblink: Optional[AnyUrl]
|
||||
completed: Optional[bool]
|
||||
current_order: Optional[bool]
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.schema.comics.artist import ArtistResponse
|
||||
from src.schema.comics.issue import IssueResponse
|
||||
from src.schema.comics.publisher import PublisherResponse
|
||||
from src.schema.comics.volume import VolumeResponse
|
||||
from src.schema.comics.worktype import WorktypeResponse
|
||||
|
||||
|
||||
class ComicWorktypeArtistResponse(BaseModel):
|
||||
worktype: WorktypeResponse
|
||||
artists: List[ArtistResponse]
|
||||
|
||||
|
||||
class ComicDetailsResponse(BaseModel):
|
||||
id: str
|
||||
created: str
|
||||
title: str
|
||||
completed : bool
|
||||
current_order : bool
|
||||
weblink: str
|
||||
publisher: PublisherResponse
|
||||
issues: List[IssueResponse]
|
||||
volumes: List[VolumeResponse]
|
||||
works: List[ComicWorktypeArtistResponse]
|
||||
@@ -0,0 +1,8 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class IssueResponse(BaseModel):
|
||||
id: str
|
||||
issue_number: str
|
||||
in_stock: bool
|
||||
is_read: bool
|
||||
@@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.volume import VolumeResponse
|
||||
|
||||
|
||||
class IssueDetailsResponse(BaseModel):
|
||||
id: str
|
||||
issue_number: str
|
||||
in_stock: bool
|
||||
is_read: bool
|
||||
comic: ComicResponse
|
||||
volume: VolumeResponse | None
|
||||
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PublisherResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
@@ -0,0 +1,14 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.publisher import PublisherResponse
|
||||
|
||||
|
||||
class PublisherDetailsResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
parent_publisher: PublisherResponse | None
|
||||
imprints: List[PublisherResponse]
|
||||
comics: List[ComicResponse]
|
||||
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class VolumeResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
@@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AddWorkType(BaseModel):
|
||||
worktype: str
|
||||
|
||||
class WorktypeResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
|
||||
from src.db.models.media import MediaActor
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MediaActorResponse(BaseModel):
|
||||
id: str
|
||||
name: str | None
|
||||
url: str
|
||||
|
||||
class Actor(BaseModel):
|
||||
name: str | None
|
||||
url: str
|
||||
|
||||
def get_actor_details(media_actor: MediaActor) -> MediaActorResponse:
|
||||
reponse: MediaActorResponse = MediaActorResponse(id=media_actor.id, name=str(media_actor.name), url=str(media_actor.url))
|
||||
return reponse
|
||||
@@ -0,0 +1,16 @@
|
||||
from datetime import datetime
|
||||
|
||||
from src.db.models.media import MediaActorFile, MediaFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MediaActorFileResponse(BaseModel):
|
||||
id: str
|
||||
file_id: str
|
||||
actor_id: str
|
||||
|
||||
def get_actorfile_details(media_actorfile: MediaActorFile) -> MediaActorFileResponse:
|
||||
response: MediaActorFileResponse = MediaActorFileResponse(id=media_actorfile.id,
|
||||
file_id=str(media_actorfile.media_file_id),
|
||||
actor_id=str(media_actorfile.media_actor_id))
|
||||
return response
|
||||
@@ -1,45 +1,38 @@
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from src.db.models.media import MediaFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MediaFileResponse(BaseModel):
|
||||
id: UUID
|
||||
id: str
|
||||
title: str | None = None
|
||||
file_name: str | None = None
|
||||
cloud_link: str | None = None
|
||||
url: str
|
||||
url: str | None = None
|
||||
review: bool = False
|
||||
should_download: bool = False
|
||||
|
||||
class Link(BaseModel):
|
||||
url: str
|
||||
|
||||
def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None:
|
||||
def get_file_details(mediafile: MediaFile) -> MediaFileResponse:
|
||||
response = MediaFileResponse(id=mediafile.id,
|
||||
title=mediafile.title,
|
||||
file_name=mediafile.file_name,
|
||||
cloud_link=mediafile.cloud_link,
|
||||
url=str(mediafile.url),
|
||||
review=(mediafile.review == 1),
|
||||
should_download=(mediafile.should_download == 1))
|
||||
review=mediafile.review,
|
||||
should_download=mediafile.should_download)
|
||||
#print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}")
|
||||
#print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}")
|
||||
return response
|
||||
|
||||
def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None:
|
||||
mediafile.file_name = model.file_name
|
||||
mediafile.cloud_link = model.cloud_link
|
||||
mediafile.url = model.url
|
||||
mediafile.cloud_link = model.cloud_link # type: ignore
|
||||
mediafile.url = model.url # type: ignore
|
||||
mediafile.title = model.title
|
||||
mediafile.last_modified_date = datetime.now()
|
||||
if model.review:
|
||||
mediafile.review = 1
|
||||
else:
|
||||
mediafile.review = 0
|
||||
if model.should_download:
|
||||
mediafile.should_download = 1
|
||||
else:
|
||||
mediafile.should_download = 0
|
||||
mediafile.review = model.review
|
||||
mediafile.should_download = model.should_download
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AddLink(BaseModel):
|
||||
url: str
|
||||
@@ -1,8 +1,8 @@
|
||||
from uuid import UUID
|
||||
|
||||
from typing import AnyStr
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SportResponse(BaseModel):
|
||||
id: UUID
|
||||
id: AnyStr
|
||||
name: str
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial;
|
||||
padding: 10px;
|
||||
background: lightgrey;
|
||||
}
|
||||
|
||||
/* Header/Blog Title */
|
||||
.header {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
/* Style the top navigation bar */
|
||||
.topnav {
|
||||
overflow: hidden;
|
||||
background-color: darkgrey;
|
||||
}
|
||||
|
||||
/* Style the topnav links */
|
||||
.topnav a {
|
||||
float: left;
|
||||
display: block;
|
||||
color: #f2f2f2;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Change color on hover */
|
||||
.topnav a:hover {
|
||||
background-color: #ddd;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.subnav {
|
||||
overflow: hidden;
|
||||
background-color: grey;
|
||||
}
|
||||
|
||||
.subnav a {
|
||||
float: left;
|
||||
display: block;
|
||||
color: #f2f2f2;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.form-inline label {
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.form-inline input {
|
||||
padding-right: 50px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.form-inline button {
|
||||
padding: 10px 20px;
|
||||
background-color: dodgerblue;
|
||||
border: 1px solid #ddd;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.form-inline button:hover {
|
||||
background-color: royalblue;
|
||||
}
|
||||
|
||||
.pill-nav a {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background-color: dodgerblue;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* Change the color of links on mouse-over */
|
||||
.pill-nav a:hover {
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Add a color to the active/current link */
|
||||
.pill-nav a.active {
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.maincolumn {
|
||||
float:left;
|
||||
width: 100%;
|
||||
}
|
||||
/* Create two unequal columns that floats next to each other */
|
||||
/* Left column */
|
||||
.leftcolumn {
|
||||
float: left;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
/* Right column */
|
||||
.rightcolumn {
|
||||
float: left;
|
||||
width: 25%;
|
||||
background-color: #f1f1f1;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* Fake image */
|
||||
.fakeimg {
|
||||
background-color: #aaa;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Add a card effect for articles */
|
||||
.card {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Clear floats after the columns */
|
||||
.row::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #ddd;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Responsive layout - when the screen is less than 800px wide, make the two columns stack on top of each other instead of next to each other */
|
||||
@media screen and (max-width: 800px) {
|
||||
.leftcolumn, .rightcolumn {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive layout - when the screen is less than 400px wide, make the navigation links stack on top of each other instead of next to each other */
|
||||
@media screen and (max-width: 400px) {
|
||||
.topnav a {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>Permissions</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">Permissions..</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Name</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for permission in permissions %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/admins/permissions/{{permission.id}}">{{permission.name}}</a></th>
|
||||
<td><a href="/admin/permission/edit/{{permission.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||
<td><a href="/admin/permission/delete/{{permission.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<a href="/admin/permission/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add Permission</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>Profiles</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">Profiles..</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Username</th>
|
||||
<th scope="col">First Name</th>
|
||||
<th scope="col">Last Name</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for profile in profiles %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/admins/profiles/{{profile.id}}">{{profile.user_name}}</a></th>
|
||||
<th scope="row">{{profile.first_name}}</th>
|
||||
<th scope="row">{{profile.last_name}}</th>
|
||||
<th scope="row">{{profile.email}}</th>
|
||||
<td><a href="/admin/profile/edit/{{profile.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||
<td><a href="/admin/profile/delete/{{profile.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<a href="/admin/profile/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add Profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Login</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<h5 class="display-5">Login to Kontor</h5>
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-success font-weight-bold">
|
||||
{% if msg %}
|
||||
<div class="badge bg-success text-wrap font-weight-bold" style="font-size: large;">
|
||||
{{msg}}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label>Email</label>
|
||||
<input type="text" required placeholder="Your email" name="email" value="{{email}}" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Password</label>
|
||||
<input type="password" required placeholder="Choose a secure password" value="{{password}}" name="password" class="form-control">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+1
@@ -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>
|
||||
@@ -20,12 +20,16 @@
|
||||
<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">
|
||||
{% for work in artist.get_comics() %}
|
||||
<p>
|
||||
{{work}}:
|
||||
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||
<ul>
|
||||
{% for comic in artist.get_comics()[work] %}
|
||||
<li><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></li>
|
||||
@@ -43,8 +47,20 @@
|
||||
<th scope="row">Data Modified</th>
|
||||
<td colspan="2">{{artist.last_modified_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{artist.version}}</td>
|
||||
</tr>
|
||||
</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>
|
||||
<a href="/comic/artist/edit/{{artist.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/comic/artist/delete/{{artist.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Edit Artist</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<h3 class="text-center display-4">Edit an Artist entry</h3>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<input type="text" required class="form-control" name="artist.name" value="{{artist_name}}" placeholder="Artist name here">
|
||||
<input type="text" required class="form-control" name="artist.link" value="{{artist_link}}" placeholder="Web link for artist here">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary" name="action" value="submit">Submit</button>
|
||||
<button type="cancel" class="btn btn-primary" name="action" value="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -18,7 +18,7 @@
|
||||
{% 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" %}
|
||||
{% include "comic/artist_cards.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% if loop.index %3 %}
|
||||
|
||||
@@ -27,12 +27,17 @@
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Link</th>
|
||||
<td colspan="2">{{comic.weblink}}</td>
|
||||
</tr>
|
||||
{% if comic.get_artists()|length > 0 %}
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
{% for work in comic.get_artists() %}
|
||||
<p>
|
||||
{{work}}:
|
||||
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||
<ul>
|
||||
{% for artist in comic.get_artists()[work] %}
|
||||
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||
@@ -42,6 +47,29 @@
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if comic.volumes|length > 0 %}
|
||||
<tr>
|
||||
<th scope="row">Volumes</th>
|
||||
<td colspan="2">
|
||||
<ul>
|
||||
{% for volume in comic.volumes %}
|
||||
<li><a href="/comic/volumes/{{volume.id}}">{{volume.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Issues</th>
|
||||
<td colspan="2">
|
||||
<ul>
|
||||
{% for issue in comic.sorted_issues() %}
|
||||
<li><a href="/comic/issues/{{issue.id}}">{{issue.get_full_title()}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Created</th>
|
||||
<td colspan="2">{{comic.created_date}}</td>
|
||||
@@ -54,18 +82,15 @@
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{comic.version}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Issues</th>
|
||||
<td colspan="2">
|
||||
<ul>
|
||||
{% for issue in comic.sorted_issues() %}
|
||||
<li><a href="comic/issues/{{issue.id}}">{{issue.issue_number}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/comic/comics" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/comic/comic/edit/{{comic.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/comic/comic/delete/{{comic.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Edit Comic</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<h3 class="text-center display-4">Edit an Comic entry</h3>
|
||||
<form class="form-horizontal" method="POST">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="title">Title</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{comic_title}}" placeholder="Comic title here">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="weblink">Link</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="weblink" name="weblink" value="{{comic_weblink}}" placeholder="Web link for comic here">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" id="completed" name="completed" value="{{comic_completed}}"> Completed</label>
|
||||
</div
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" id="current_order" name="current_order" value="{{comic_current_order}}"> Current Order</label>
|
||||
</div
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary" name="action" value="submit">Submit</button>
|
||||
<button type="cancel" class="btn btn-primary" name="action" value="cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -4,28 +4,58 @@
|
||||
<title>Comic List</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Comics..</Comics></h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block subnav %}
|
||||
<div class="subnav">
|
||||
<a href="/comic/artists/">Artists</a>
|
||||
<a href="/comic/publishers/">Publishers</a>
|
||||
<a href="/comic/worktypes">WorkTypes</a>
|
||||
</div>
|
||||
{% 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>
|
||||
<form class="form-inline" action="/comic/comics">
|
||||
<input type="text" placeholder="Search" name="query">
|
||||
<label>
|
||||
<input type="checkbox" name="completed" {% if request.query_params.get("completed")=="on" %}checked{% endif %}>Completed
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="order" {% if request.query_params.get("order")=="on" %}checked{% endif %}>Order
|
||||
</label>
|
||||
<button type="submit">Search</button>
|
||||
<div class="pill-nav">
|
||||
<a href="/comic/comic/add">Add Comic</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="maincolumn">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Publisher</th>
|
||||
<th scope="col">Completed</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for comic in comics %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></th>
|
||||
<td><a href="/comic/publishers/{{comic.publisher.id}}">{{comic.publisher.name}}</a></td>
|
||||
<td>{% with check=comic.completed %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
<td><a href="/comic/comics/edit/{{comic.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||
<td><a href="/comic/comics/delete/{{comic.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</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,92 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Issue Detail</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-5">Issue Detail</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Issue Number</th>
|
||||
<td colspan="2">{{issue.issue_number}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Full Title</th>
|
||||
<td colspan="2">{{issue.title}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Published</th>
|
||||
<td colspan="2">{{issue.published_on}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Auf Lager</th>
|
||||
<td colspan="2">
|
||||
{% with check=issue.in_stock %}
|
||||
{% include "components/check.html" %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Gelesen</th>
|
||||
<td colspan="2">
|
||||
{% with check=issue.is_read %}
|
||||
{% include "components/check.html" %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Comic</th>
|
||||
<td colspan="2">
|
||||
<a href="/comic/comics/{{issue.comic_id}}">{{issue.comic.title}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% if issue.volume %}
|
||||
<tr>
|
||||
<th scope="row">Volume</th>
|
||||
<td colspan="2">
|
||||
<a href="/comic/comics/{{issue.volume_id}}">{{issue.volume.name}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
{% for work in issue.get_artists() %}
|
||||
<p>
|
||||
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||
<ul>
|
||||
{% for artist in issue.get_artists()[work] %}
|
||||
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Created</th>
|
||||
<td colspan="2">{{issue.created_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Modified</th>
|
||||
<td colspan="2">{{issue.last_modified_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{issue.version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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">
|
||||
@@ -41,5 +59,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/comic/publishers" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/comic/publisher/edit/{{publisher.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/comic/publisher/delete/{{publisher.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{% for publisher in publishers %}
|
||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||
{% with obj=publisher %}
|
||||
{% include "components/publisher_cards.html" %}
|
||||
{% include "comic/publisher_cards.html" %}
|
||||
{% endwith %}
|
||||
{% if loop.index %3 %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>WorkType Detail</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">WorkType Name</th>
|
||||
<td colspan="2">{{worktype.name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
{% for comic in worktype.get_artists() %}
|
||||
<p>
|
||||
{{comic}}:
|
||||
<ul>
|
||||
{% for artist in worktype.get_artists()[comic] %}
|
||||
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
{% 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>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Modified</th>
|
||||
<td colspan="2">{{worktype.last_modified_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{worktype.version}}</td>
|
||||
</tr>
|
||||
</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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Edit WorkType</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<h3 class="text-center display-4">Add a WorkType</h3>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<input type="text" required class="form-control" name="worktype" value="{{worktype}}" placeholder="WorkType here">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>WorkTypes</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Name</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for worktype in worktypes %}
|
||||
<tr>
|
||||
<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/worktype/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add WorkType</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if check == 1 %}
|
||||
{% if check %}
|
||||
<img src="{{ url_for('static', path='images/tick.png') }}" alt="" width="24" height="24">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', path='images/cross.png') }}" alt="" width="24" height="24">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<h2>Footer</h2>
|
||||
@@ -1,8 +1,14 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
|
||||
<div class="topnav">
|
||||
<a href="/">Kontor</a>
|
||||
<a href="/comic/comics">Comics</a>
|
||||
<a href="/media/files">Media</a>
|
||||
<a style="float:right" href="/login/">Login</a></li>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <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>
|
||||
<a class="navbar-brand" href="#">Kontor</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>
|
||||
@@ -18,7 +24,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="#">Something else here</a></li>
|
||||
<li><a class="dropdown-item" href="/comic/worktypes">WorkTypes</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
@@ -26,6 +32,7 @@
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<li><a class="dropdown-item" href="/media/files/">MediaFiles</a></li>
|
||||
<li><a class="dropdown-item" href="/media/actors/">MediaActors</a></li>
|
||||
<li><a class="dropdown-item" href="/media/videos/">MediaVideos</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
@@ -42,18 +49,19 @@
|
||||
<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>
|
||||
<li><a class="dropdown-item" href="/admin/profiles">Profiles</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/permissions">Permissions</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
|
||||
Media
|
||||
</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>
|
||||
<li><a class="dropdown-item" href="/media/add-link/">Add Link</a></li>
|
||||
<li><a class="dropdown-item" href="/media/add-file">Add MediaFile</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -63,4 +71,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav> -->
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<title>Kontor</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Kontor</h1>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
<th scope="row">Actor Name</th>
|
||||
<td colspan="2">{{actor.name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">URL</th>
|
||||
<td colspan="2">{{actor.url}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
@@ -41,5 +45,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/media/actors" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/media/actor/edit/{{actor.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/media/actor/delete/{{actor.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{% for actor in actors %}
|
||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||
{% with obj=actor %}
|
||||
{% include "components/actor_cards.html" %}
|
||||
{% include "media/actor_cards.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% if loop.index %3 %}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Add a Video Link</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<h3 class="text-center display-4">Add a Video Link</h3>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<input type="text" required class="form-control" name="url" value="{{url}}" placeholder="Video Link here">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -61,5 +61,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/media/files" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/media/file/edit/{{mediafile.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/media/file/delete/{{mediafile.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,26 +4,54 @@
|
||||
<title>MediaFiles List</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h1>MediaFiles..</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block subnav %}
|
||||
{% include "media/media_nav.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Titel</th>
|
||||
<th scope="col">URL</th>
|
||||
<th scope="col">Cloudlink</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for mediafile in mediafiles %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/media/files/{{mediafile.id}}">{{mediafile.title}}</a></th>
|
||||
<td>{{mediafile.url}}</td>
|
||||
<td>{{mediafile.cloud_link}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row">
|
||||
<form class="form-inline" action="/media/files">
|
||||
<input type="text" placeholder="Search" name="query">
|
||||
<label>
|
||||
<input type="checkbox" name="review" {% if request.query_params.get("review")=="on" %}checked{% endif %}>Review
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="download" {% if request.query_params.get("download")=="on" %}checked{% endif %}>Download
|
||||
</label>
|
||||
<button type="submit">Search</button>
|
||||
<div class="pill-nav">
|
||||
<a href="/media/files/add">Add MediaFile</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="maincolumn">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Titel</th>
|
||||
<th scope="col">Review</th>
|
||||
<th scope="col">Download</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for mediafile in mediafiles %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/media/files/{{mediafile.id}}">{{mediafile.title}}</a></th>
|
||||
<td>{% with check=mediafile.review %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
<td>{% with check=mediafile.should_download %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
<td><a href="/media/files/edit/{{mediafile.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||
<td><a href="/media/files/delete/{{mediafile.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="subnav">
|
||||
<a href="/media/files/">MediaFiles</a>
|
||||
<a href="/media/actors/">MediaActors</a>
|
||||
<a href="/media/videos/">MediaVideos</a>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>MediaVideo Detail</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-5">MediaVideo Detail</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo Title</th>
|
||||
<td colspan="2">{{mediavideo.title}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo URL</th>
|
||||
<td colspan="2">{{mediavideo.url}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo Cloudlink</th>
|
||||
<td colspan="2">{{mediavideo.cloud_link}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo Download?</th>
|
||||
<td colspan="2">{{mediavideo.should_download}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo Review?</th>
|
||||
<td colspan="2">{{mediavideo.review}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Created</th>
|
||||
<td colspan="2">{{mediavideo.created_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Modified</th>
|
||||
<td colspan="2">{{mediavideo.last_modified_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{mediavideo.version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>Video Links</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Titel</th>
|
||||
<th scope="col">Review</th>
|
||||
<th scope="col">Download</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for mediavideo in mediavideos %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/media/videos/{{mediavideo.id}}">{{mediavideo.title}}</a></th>
|
||||
<td>{% with check=mediavideo.review %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
<td>{% with check=mediavideo.should_download %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,26 +1,75 @@
|
||||
<!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>
|
||||
<html lang="de-DE">
|
||||
<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="{{ url_for('static', path='style.css') }}">
|
||||
{% block title %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% include "components/navbar.html" %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<body>
|
||||
<div class="header">
|
||||
{% block header %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% include "components/navbar.html" %}
|
||||
{% block subnav %}
|
||||
{% endblock %}
|
||||
<!-- <div class="topnav">
|
||||
<a href="#">Link</a>
|
||||
<a href="#">Link</a>
|
||||
<a href="#">Link</a>
|
||||
<a href="#" style="float:right">Link</a>
|
||||
</div> -->
|
||||
|
||||
<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 %}
|
||||
{% 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="{{ url_for('static', path='js/autocomplete.js') }}"></script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
<!-- <div class="row">
|
||||
<div class="leftcolumn">
|
||||
<div class="card">
|
||||
<h2>TITLE HEADING</h2>
|
||||
<h5>Title description, Dec 7, 2017</h5>
|
||||
<div class="fakeimg" style="height:200px;">Image</div>
|
||||
<p>Some text..</p>
|
||||
<p>Sunt in culpa qui officia deserunt mollit anim id est laborum consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>TITLE HEADING</h2>
|
||||
<h5>Title description, Sep 2, 2017</h5>
|
||||
<div class="fakeimg" style="height:200px;">Image</div>
|
||||
<p>Some text..</p>
|
||||
<p>Sunt in culpa qui officia deserunt mollit anim id est laborum consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rightcolumn">
|
||||
<div class="card">
|
||||
<h2>About Me</h2>
|
||||
<div class="fakeimg" style="height:100px;">Image</div>
|
||||
<p>Some text about me in culpa qui officia deserunt mollit anim..</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Popular Post</h3>
|
||||
<div class="fakeimg"><p>Image</p></div>
|
||||
<div class="fakeimg"><p>Image</p></div>
|
||||
<div class="fakeimg"><p>Image</p></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Follow Me</h3>
|
||||
<p>Some text..</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="footer">
|
||||
{% include "components/footer.html" %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from src.db.models.admin import Permission, Profile
|
||||
from src.db.session import SessionDep
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/admin")
|
||||
|
||||
@router.get("/profiles")
|
||||
def get_profiles(db: SessionDep, request: Request, msg: Optional[str] = None):
|
||||
profiles = db.query(Profile).all()
|
||||
return templates.TemplateResponse("admin/profiles.html", {"request": request, "msg": msg, "profiles": profiles})
|
||||
|
||||
@router.get("/profiles/{profile_id}")
|
||||
def comic_details(profile_id: str, request: Request, db: SessionDep):
|
||||
profile = db.get(Profile, profile_id)
|
||||
return templates.TemplateResponse("admin/profile_detail.html", {"request": request, "profile":profile})
|
||||
|
||||
@router.get("/permissions")
|
||||
def get_permissions(db: SessionDep, request: Request, msg: Optional[str] = None):
|
||||
permissions = db.query(Permission).all()
|
||||
return templates.TemplateResponse("admin/permissions.html", {"request": request, "msg": msg, "permissions": permissions})
|
||||
|
||||
@router.get("/permissions/{permission_id}")
|
||||
def artist_detail(permission_id: str, request: Request, db: SessionDep):
|
||||
permission= db.get(Permission, str(permission_id))
|
||||
return templates.TemplateResponse("comic/permission_detail.html", {"request": request, "permission": permission})
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
|
||||
class LoginForm:
|
||||
def __init__(self, request: Request):
|
||||
self.request: Request = request
|
||||
self.errors: List = []
|
||||
self.username: Optional[str] = None
|
||||
self.password: Optional[str] = None
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
# since auth works on username field we are considering email as username
|
||||
self.username = form.get("email")
|
||||
self.password = form.get("password")
|
||||
|
||||
async def is_valid(self):
|
||||
if not self.username or not (self.username.__contains__("@")):
|
||||
self.errors.append("Email is required")
|
||||
if not self.password or not len(self.password) >= 4:
|
||||
self.errors.append("A valid password is required")
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,36 @@
|
||||
# from src.apis.version1.admin import login_for_access_token
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from src.apis.version1.admin import login_for_token_cookie
|
||||
from src.db.session import SessionDep
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from src.webapps.auth.forms import LoginForm
|
||||
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False)
|
||||
|
||||
|
||||
@router.get("/login/")
|
||||
def login(request: Request):
|
||||
return templates.TemplateResponse("auth/login.html", {"request": request})
|
||||
|
||||
|
||||
@router.post("/login/")
|
||||
async def login(request: Request):
|
||||
form = LoginForm(request)
|
||||
await form.load_data()
|
||||
if await form.is_valid():
|
||||
try:
|
||||
form.__dict__.update(msg="Login Successful :)")
|
||||
response = templates.TemplateResponse("auth/login.html", form.__dict__)
|
||||
login_for_token_cookie(response=response, form_data=form)
|
||||
return response
|
||||
except HTTPException:
|
||||
form.__dict__.update(msg="")
|
||||
form.__dict__.get("errors").append("Incorrect Email or Password")
|
||||
return templates.TemplateResponse("auth/login.html", form.__dict__)
|
||||
return templates.TemplateResponse("auth/login.html", form.__dict__)
|
||||
@@ -1,15 +1,23 @@
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from src.webapps.comic import route_comics
|
||||
from src.webapps.media import route_media
|
||||
from src.webapps.admin import route_admin
|
||||
from src.webapps.auth import route_login
|
||||
from src.webapps.comic import route_comics, route_worktype, route_artists
|
||||
from src.webapps.media import route_actors, route_media, route_videos
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(route_comics.router)
|
||||
api_router.include_router(route_artists.router)
|
||||
api_router.include_router(route_worktype.router)
|
||||
api_router.include_router(route_media.router)
|
||||
api_router.include_router(route_actors.router)
|
||||
api_router.include_router(route_videos.router)
|
||||
api_router.include_router(route_login.router)
|
||||
api_router.include_router(route_admin.router)
|
||||
|
||||
@api_router.get("/")
|
||||
def home(request: Request, msg: str = None):
|
||||
def home(request: Request, msg: str | None = None):
|
||||
return templates.TemplateResponse("index.html", {"request": request, "msg": msg})
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from fastapi import Request
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class AddArtistForm:
|
||||
def __init__(self, request: Request, artist_id: str, artist_name: str, artist_link: str):
|
||||
self.request = request
|
||||
self.errors: List = []
|
||||
self.id: str = artist_id
|
||||
self.name: Optional[str] = artist_name
|
||||
self.link: Optional[str] = artist_link
|
||||
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
print(f"{form.keys()}")
|
||||
self.name = form.get("artist.name")
|
||||
self.link = form.get("artist.link")
|
||||
|
||||
def is_valid(self):
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name=}, {self.link=}"
|
||||
@@ -0,0 +1,27 @@
|
||||
from fastapi import Request
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class ValidateComicForm:
|
||||
def __init__(self, request: Request, comic_id: str, completed: bool, current_order: bool):
|
||||
self.request = request
|
||||
self.errors: List = []
|
||||
self.id = comic_id
|
||||
self.title: Optional[str] = None
|
||||
self.weblink: Optional[str] = None
|
||||
self.completed = completed
|
||||
self.current_order = current_order
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
print(f"{form.keys()}")
|
||||
self.title = form.get("title")
|
||||
self.weblink = form.get("weblink")
|
||||
|
||||
def is_valid(self):
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title=}, {self.weblink=}"
|
||||
@@ -0,0 +1,20 @@
|
||||
from fastapi import Request
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class AddWorktypeForm:
|
||||
def __init__(self, request: Request):
|
||||
self.request = request
|
||||
self.errors: List = []
|
||||
self.worktype: Optional[str] = None
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
self.worktype = form.get("worktype")
|
||||
|
||||
def is_valid(self):
|
||||
if not self.worktype or (len(self.worktype) == 0):
|
||||
self.errors.append("WorkType cannot be empty")
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,59 @@
|
||||
from fastapi import APIRouter, Request, status, Form
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from src.db.models.comic import Artist
|
||||
from typing import AnyStr
|
||||
|
||||
from src.db.repository.comics.artist import update_artist
|
||||
from src.db.session import SessionDep
|
||||
#from src.db.repository.comic import create_new_worktype, update_worktype
|
||||
from src.main import logger
|
||||
from src.schema.comics.artist import AddArtist
|
||||
from src.webapps.comic.forms.artist import AddArtistForm
|
||||
|
||||
#from src.schema.comics.worktype import AddWorkType
|
||||
#from src.webapps.comic.forms import AddWorktypeForm
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/comic")
|
||||
|
||||
@router.get("/artists")
|
||||
def get_artists(db: SessionDep, request: Request, msg: str | None = None):
|
||||
artists = db.query(Artist).all()
|
||||
return templates.TemplateResponse("comic/artists.html", {"request": request, "msg": msg, "artists": artists})
|
||||
|
||||
@router.get("/artists/{artist_id}")
|
||||
def artist_detail(artist_id: AnyStr, request: Request, db: SessionDep):
|
||||
artist = db.get(Artist, str(artist_id))
|
||||
return templates.TemplateResponse("comic/artist_detail.html", {"request": request, "artist": artist})
|
||||
|
||||
|
||||
@router.get("/artist/edit/{artist_id}")
|
||||
def edit_artist(db: SessionDep, request: Request, artist_id: str):
|
||||
artist = db.get(Artist, artist_id)
|
||||
return templates.TemplateResponse("comic/artist_edit.html", {"request": request, "artist_name": artist.name, "artist_link": artist.weblink})
|
||||
|
||||
@router.post("/artist/edit/{artist_id}")
|
||||
async def edit_artist(request: Request, db: SessionDep, artist_id: str, action: str = Form(...), artist_name: str = Form(...), artist_link: str = Form(...)):
|
||||
if action == "cancel":
|
||||
return RedirectResponse(f"/comic/artists/{artist_id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
form = AddArtistForm(request, artist_id, artist_name, artist_link)
|
||||
await form.load_data()
|
||||
if form.is_valid():
|
||||
try:
|
||||
artist = AddArtist(**form.__dict__)
|
||||
artist = update_artist(add_artist=artist, artist_id=artist_id, db=db)
|
||||
return RedirectResponse(f"/comic/artists/{artist.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
form.__dict__.get("errors").append("artist already added")
|
||||
return templates.TemplateResponse("comic/artist_edit.html", form.__dict__)
|
||||
return templates.TemplateResponse("comic/artist_edit.html", form.__dict__)
|
||||
|
||||
@router.get("/artist/delete/{artist_id}")
|
||||
async def delete_artist(db: SessionDep, request: Request, artist_id: str):
|
||||
artist = db.get(Artist, artist_id)
|
||||
db.delete(artist)
|
||||
db.commit()
|
||||
return RedirectResponse("/comic/artists", status_code=status.HTTP_303_SEE_OTHER)
|
||||
@@ -1,42 +1,90 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi import APIRouter, Form, Request, 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
|
||||
from src.db.models.comic import Comic, Publisher, Issue
|
||||
from typing import AnyStr
|
||||
from src.core.log_conf import logger
|
||||
from src.db.repository.comics.comic import update_comic
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.comics.comic import ComicSchema
|
||||
from src.webapps.comic.forms.comic import ValidateComicForm
|
||||
|
||||
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()
|
||||
def get_comics(db: SessionDep, request: Request, msg: str | None = None):
|
||||
params = request.query_params
|
||||
query = params.get("query")
|
||||
filter = {}
|
||||
completed = params.get('completed') == "on"
|
||||
if completed:
|
||||
filter['completed'] = True
|
||||
order = params.get("order") == "on"
|
||||
if order:
|
||||
filter['current_order'] = True
|
||||
if query is not None and len(query) > 0:
|
||||
filter['title'] = query
|
||||
if len(filter) > 0:
|
||||
if "title" in filter:
|
||||
comics = db.query(Comic).filter(Comic.title.ilike(f'%{query}%'))
|
||||
else:
|
||||
comics = db.query(Comic).filter_by(**filter).all()
|
||||
else:
|
||||
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):
|
||||
def comic_details(comic_id: AnyStr, request: Request, db: SessionDep):
|
||||
comic = db.get(Comic, comic_id)
|
||||
return templates.TemplateResponse("comic/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})
|
||||
@router.get("/comic/edit/{comic_id}")
|
||||
def edit_comic(db: SessionDep, request: Request, comic_id: str):
|
||||
comic = db.get(Comic, comic_id)
|
||||
return templates.TemplateResponse("comic/comic_edit.html", {"request": request, "comic_title": comic.title, "comic_weblink": comic.weblink})
|
||||
|
||||
@router.get("/artists/{artist_id}")
|
||||
def artist_detail(artist_id: UUID, request: Request, db: SessionDep):
|
||||
artist = db.get(Artist, artist_id)
|
||||
return templates.TemplateResponse("comic/artist_detail.html", {"request": request, "artist": artist})
|
||||
|
||||
|
||||
@router.post("/comic/edit/{comic_id}")
|
||||
async def validate_comic(request: Request, db: SessionDep, comic_id: str, action: str = Form(...), completed: bool = Form(False), current_order: bool = Form(False)):
|
||||
if action == "cancel":
|
||||
return RedirectResponse(f"/comic/comics/{comic_id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
form = ValidateComicForm(request, comic_id, completed, current_order)
|
||||
logger.info(f"request: {repr(request)}")
|
||||
await form.load_data()
|
||||
logger.info(f"form: {form}")
|
||||
if form.is_valid():
|
||||
try:
|
||||
comic = ComicSchema(**form.__dict__)
|
||||
comic = update_comic(comic=comic, comic_id=comic_id, db=db)
|
||||
return RedirectResponse(f"/comic/comics/{comic.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
form.__dict__.get("errors").append("comic already added")
|
||||
return templates.TemplateResponse("comic/comic_edit.html", form.__dict__)
|
||||
return templates.TemplateResponse("comic/comic_edit.html", form.__dict__)
|
||||
|
||||
@router.get("/publishers")
|
||||
def get_publishers(db: SessionDep, request: Request, msg: str = None):
|
||||
def get_publishers(db: SessionDep, request: Request, msg: str | None = None):
|
||||
publishers = db.query(Publisher).all()
|
||||
return templates.TemplateResponse("comic/publishers.html", {"request": request, "publishers": publishers})
|
||||
|
||||
@router.get("/publishers/{publisher_id}")
|
||||
def publisher_details(publisher_id: UUID, request: Request, db: SessionDep, msg: str = None):
|
||||
def publisher_details(publisher_id: AnyStr, request: Request, db: SessionDep, msg: str = None):
|
||||
publisher = db.get(Publisher, publisher_id)
|
||||
if publisher is None:
|
||||
msg = "Could not find Publisher"
|
||||
return templates.TemplateResponse("comic/publisher_detail.html", {"request": request, "msg": msg, "publisher": publisher})
|
||||
|
||||
@router.get("/issues")
|
||||
def get_issues(db: SessionDep, request: Request, msg: str | None = None):
|
||||
issues = db.query(Issue).all()
|
||||
return templates.TemplateResponse("comic/issues.html", {"request": request, "msg": msg, "issues": issues})
|
||||
|
||||
@router.get("/issues/{issue_id}")
|
||||
def issue_details(issue_id: AnyStr, request: Request, db: SessionDep):
|
||||
issue = db.get(Issue, issue_id)
|
||||
return templates.TemplateResponse("comic/issue_detail.html", {"request": request, "issue": issue})
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
from fastapi import APIRouter, Request, status
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from src.db.models.comic import WorkType
|
||||
from typing import AnyStr
|
||||
|
||||
from src.db.repository.comics.worktype import create_new_worktype, update_worktype
|
||||
from src.db.session import SessionDep
|
||||
from src.main import logger
|
||||
from src.schema.comics.worktype import AddWorkType
|
||||
from src.webapps.comic.forms.worktype import AddWorktypeForm
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/comic")
|
||||
|
||||
@router.get("/worktypes")
|
||||
def get_worktypes(db: SessionDep, request: Request, msg: str | None = None):
|
||||
worktypes = db.query(WorkType).all()
|
||||
return templates.TemplateResponse("comic/worktypes.html", {"request": request, "msg": msg, "worktypes": worktypes})
|
||||
|
||||
@router.get("/worktypes/{worktype_id}")
|
||||
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("/worktype/add")
|
||||
def add_worktype(request: Request, db: SessionDep):
|
||||
return templates.TemplateResponse("comic/worktype_edit.html", {"request": request})
|
||||
|
||||
@router.post("/worktype/add")
|
||||
async def add_worktype_post(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)
|
||||
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/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: WorkType | None = 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_post(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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user