6 Commits
v0.1 ... v0.5

Author SHA1 Message Date
5b92bc8465 Ask for age instead of birth date
All checks were successful
release-tag / release-image (push) Successful in 36s
2025-05-26 20:48:47 +02:00
83ec86b027 update 2025-05-26 20:43:35 +02:00
726767c950 fix? hopefully ..
All checks were successful
release-tag / release-image (push) Successful in 33s
2025-05-26 11:02:21 +02:00
1a1179a9ee fix broken commit
All checks were successful
release-tag / release-image (push) Successful in 32s
2025-05-26 10:58:36 +02:00
df2d8f33a5 include time restriction
All checks were successful
release-tag / release-image (push) Successful in 42s
2025-05-26 10:48:10 +02:00
4170eb14fb move db to volume 2025-05-25 22:24:40 +02:00
16 changed files with 163 additions and 45 deletions

View File

@@ -2,6 +2,8 @@ FROM python:3.13-alpine
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
VOLUME /data
WORKDIR /app/ WORKDIR /app/
# Install uv # Install uv

View File

@@ -6,8 +6,10 @@ from sqlmodel import Session
from app.core.db import engine from app.core.db import engine
def get_db() -> Generator[Session, None, None]: def get_db() -> Generator[Session, None, None]:
with Session(engine) as session: with Session(engine) as session:
yield session yield session
SessionDep = Annotated[Session, Depends(get_db)] SessionDep = Annotated[Session, Depends(get_db)]

View File

@@ -5,27 +5,28 @@ from app.models import RegistrationCreate
from app import crud from app import crud
from app.api.deps import SessionDep from app.api.deps import SessionDep
from app.core.utils import is_registration_open
from fastapi.responses import RedirectResponse
router = APIRouter(prefix="/registration") router = APIRouter(prefix="/registration")
@router.post( @router.post(
"/register", "/register_form",
) )
def register(*, session: SessionDep, registration_create: Annotated[RegistrationCreate, Form()]): def register(
print(registration_create) *, session: SessionDep, registration_create: Annotated[RegistrationCreate, Form()]
):
""" """
Register Register
""" """
registration = crud.create_registration(session=session, registration_create=registration_create) if is_registration_open():
#if settings.emails_enabled and user_in.email: crud.create_registration(
# email_data = generate_new_account_email( session=session, registration_create=registration_create
# email_to=user_in.email, username=user_in.email, password=user_in.password )
# )
# send_email( return RedirectResponse("/success.html", status_code=303)
# email_to=user_in.email, else:
# subject=email_data.subject, return RedirectResponse("/", status_code=303)
# html_content=email_data.html_content,
# )
return registration

View File

@@ -1,10 +1,24 @@
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
import os import os
from datetime import datetime
from zoneinfo import ZoneInfo
date_format = "%Y-%m-%dT%H:%M:%S%z"
class Settings(BaseSettings): class Settings(BaseSettings):
API_V1_STR: str = "" API_V1_STR: str = ""
PROJECT_NAME: str = "Choriosity Anmeldung" PROJECT_NAME: str = "Choriosity Anmeldung"
SQLALCHEMY_DATABASE_URI: str = "sqlite:///" + os.environ.get("DATABASE_URL", "./db.sqlite") SQLALCHEMY_DATABASE_URI: str = "sqlite:///" + os.environ.get(
"DATABASE_URL", "/data/db.sqlite"
)
NOT_BEFORE: datetime = datetime.strptime(
os.environ.get("NOT_BEFORE", "2000-01-01T00:00:01+02:00"), date_format
)
NOT_AFTER: datetime = datetime.strptime(
os.environ.get("NOT_AFTER", "2100-01-01T00:00:01+02:00"), date_format
)
TZ: ZoneInfo = ZoneInfo(os.environ.get("TZ", "Europe/Berlin"))
settings = Settings() settings = Settings()

View File

@@ -9,6 +9,7 @@ engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
# otherwise, SQLModel might fail to initialize relationships properly # otherwise, SQLModel might fail to initialize relationships properly
# for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28 # for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28
def init_db(session: Session) -> None: def init_db(session: Session) -> None:
# Tables should be created with Alembic migrations # Tables should be created with Alembic migrations
# But if you don't want to use migrations, create # But if you don't want to use migrations, create
@@ -16,4 +17,4 @@ def init_db(session: Session) -> None:
from sqlmodel import SQLModel from sqlmodel import SQLModel
# This works because the models are already imported and registered from app.models # This works because the models are already imported and registered from app.models
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)

9
app/core/utils.py Normal file
View File

@@ -0,0 +1,9 @@
from app.core.config import settings
from datetime import datetime
def is_registration_open():
now = datetime.now(tz=settings.TZ)
not_before = now >= settings.NOT_BEFORE
not_after = now <= settings.NOT_AFTER
return not_before and not_after, not_before, not_after

View File

@@ -3,9 +3,11 @@ from sqlmodel import Session
from app.models import Registration, RegistrationCreate from app.models import Registration, RegistrationCreate
def create_registration(*, session: Session, registration_create: RegistrationCreate) -> Registration: def create_registration(
*, session: Session, registration_create: RegistrationCreate
) -> Registration:
db_obj = Registration.model_validate(registration_create) db_obj = Registration.model_validate(registration_create)
session.add(db_obj) session.add(db_obj)
session.commit() session.commit()
session.refresh(db_obj) session.refresh(db_obj)
return db_obj return db_obj

View File

@@ -1,7 +1,10 @@
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.templating import Jinja2Templates
from app.api.main import api_router from app.api.main import api_router
from app.core.config import settings from app.core.config import settings
@@ -9,6 +12,8 @@ from app.core.db import init_db, engine
from sqlmodel import Session from sqlmodel import Session
from app.core.utils import is_registration_open
with Session(engine) as session: with Session(engine) as session:
init_db(session) init_db(session)
@@ -16,10 +21,42 @@ app = FastAPI(
title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json" title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json"
) )
@api_router.get("/") templates = Jinja2Templates(directory="app/templates")
def index():
return FileResponse("app/index.html")
@api_router.get("/")
def index(request: Request):
reg_open, not_before, not_after = is_registration_open()
print(reg_open, not_before, not_after)
if reg_open:
return templates.TemplateResponse(
request=request, name="registration-open.html", context={}
)
elif not_before:
return templates.TemplateResponse(
request=request, name="registration-closed.html", context={
"not_before": settings.NOT_BEFORE,
"not_after": settings.NOT_AFTER,
}
)
elif not_after:
return templates.TemplateResponse(
request=request, name="registration-not-open.html", context={
"not_before": settings.NOT_BEFORE,
"not_after": settings.NOT_AFTER,
}
)
@api_router.get("/success.html")
def success(request: Request):
return templates.TemplateResponse(
request=request, name="registration-success.html", context={
"not_before": settings.NOT_BEFORE,
"not_after": settings.NOT_AFTER,
}
)
app.include_router(api_router, prefix=settings.API_V1_STR) app.include_router(api_router, prefix=settings.API_V1_STR)
app.mount("/static", StaticFiles(directory="app/static"), name="static")

View File

@@ -4,10 +4,9 @@ from pydantic import EmailStr
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from enum import Enum from enum import Enum
from datetime import date, datetime from datetime import datetime
from app.core.config import settings
from sqlalchemy.sql import func
class VoiceEnum(str, Enum): class VoiceEnum(str, Enum):
@@ -25,18 +24,22 @@ class PeriodEnum(str, Enum):
twoyears = "1 - 2 Jahre" twoyears = "1 - 2 Jahre"
longterm = "mehr als zwei Jahre" longterm = "mehr als zwei Jahre"
class RegistrationBase(SQLModel): class RegistrationBase(SQLModel):
email: EmailStr = Field(unique=True, index=True, max_length=255) email: EmailStr = Field(max_length=255)
first_name: str first_name: str
last_name: str last_name: str
birthday: date age: int
voice: VoiceEnum voice: VoiceEnum
duration: PeriodEnum duration: PeriodEnum
number_of_attempts: int
class RegistrationCreate(RegistrationBase): class RegistrationCreate(RegistrationBase):
pass pass
class Registration(RegistrationBase, table=True): class Registration(RegistrationBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
timestamp: datetime = Field(default_factory=func.now) timestamp: datetime = Field(default_factory=lambda : datetime.now(tz=settings.TZ))

0
app/static/styles.css Normal file
View File

View File

@@ -0,0 +1,11 @@
<html>
<head>
<title>Choriosity Anmeldung</title>
</head>
<body>
Die Anmeldung ist leider bereits geschlossen. Folge uns, um vom nächsten Anmeldezeitraum zu erfahren.
</body>
</html>

View File

@@ -0,0 +1,11 @@
<html>
<head>
<title>Choriosity Anmeldung</title>
</head>
<body>
Die Anmeldung ist noch nicht geöffnet. Sie öffnet {{ not_before.strftime('am %d.%m.%Y um %H:%M Uhr') }} und schließt {{ not_after.strftime('am %d.%m.%Y um %H:%M Uhr') }}.
</body>
</html>

View File

@@ -5,44 +5,44 @@
</head> </head>
<body> <body>
<form method="POST" action="/registration/register"> <form method="POST" action="/registration/register_form">
<label for="email">E-Mail-Adresse:</label> <input type="email" id="email" name="email" /> <label for="email">Deine E-Mail-Adresse:</label> <input type="email" id="email" name="email" /><br />
<label for="first_name">Dein Vorname:</label> <input type="text" id="first_name" name="first_name" /> <label for="first_name">Dein Vorname:</label> <input type="text" id="first_name" name="first_name" /><br />
<label for="last_name">Dein Nachname:</label> <input type="text" id="last_name" name="last_name" /> <label for="last_name">Dein Nachname:</label> <input type="text" id="last_name" name="last_name" /><br />
<label for="birthday">Dein Geburtstag:</label> <input type="date" id="birthday" name="birthday" /> <label for="age">Dein Alter:</label> <input type="number" id="age" name="age" /><br />
<fieldset> <fieldset>
<legend>Welche Stimme singst du?</legend> <legend>Welche Stimme singst du?</legend>
<div> <div>
<input type="radio" id="base" name="voice" value="Bass" checked /> <input type="radio" id="base" name="voice" value="Bass" />
<label for="base">Bass</label> <label for="base">Bass</label>
</div> </div>
<div> <div>
<input type="radio" id="tenor" name="voice" value="Tenor" checked /> <input type="radio" id="tenor" name="voice" value="Tenor" />
<label for="tenor">Tenor</label> <label for="tenor">Tenor</label>
</div> </div>
<div> <div>
<input type="radio" id="alto" name="voice" value="Alt" checked /> <input type="radio" id="alto" name="voice" value="Alt" />
<label for="alto">Alt</label> <label for="alto">Alt</label>
</div> </div>
<div> <div>
<input type="radio" id="soprano" name="voice" value="Sopran" checked /> <input type="radio" id="soprano" name="voice" value="Sopran" />
<label for="soprano">Sopran</label> <label for="soprano">Sopran</label>
</div> </div>
<div> <div>
<input type="radio" id="women" name="voice" value="Alt oder Sopran" checked /> <input type="radio" id="women" name="voice" value="Alt oder Sopran" />
<label for="women">Sopran oder Alt</label> <label for="women">Sopran oder Alt</label>
</div> </div>
<div> <div>
<input type="radio" id="men" name="voice" value="Bass oder Tenor" checked /> <input type="radio" id="men" name="voice" value="Bass oder Tenor" />
<label for="men">Bass oder Tenor</label> <label for="men">Bass oder Tenor</label>
</div> </div>
</fieldset> </fieldset>
@@ -51,26 +51,28 @@
<legend>Wie lange bleibst du in Ulm?</legend> <legend>Wie lange bleibst du in Ulm?</legend>
<div> <div>
<input type="radio" id="halfyear" name="duration" value="1/2 Jahr" checked /> <input type="radio" id="halfyear" name="duration" value="1/2 Jahr" />
<label for="halfyear">1/2 Jahr</label> <label for="halfyear">1/2 Jahr</label>
</div> </div>
<div> <div>
<input type="radio" id="halfyear" name="duration" value="1/2 - 1 Jahr" checked /> <input type="radio" id="halfyear" name="duration" value="1/2 - 1 Jahr" />
<label for="halfyear">1/2 - 1 Jahr</label> <label for="halfyear">1/2 - 1 Jahr</label>
</div> </div>
<div> <div>
<input type="radio" id="twoyears" name="duration" value="1 - 2 Jahre" checked /> <input type="radio" id="twoyears" name="duration" value="1 - 2 Jahre" />
<label for="twoyears">1 - 2 jahre</label> <label for="twoyears">1 - 2 jahre</label>
</div> </div>
<div> <div>
<input type="radio" id="longterm" name="duration" value="mehr als zwei Jahre" checked /> <input type="radio" id="longterm" name="duration" value="mehr als zwei Jahre" />
<label for="longterm">länger als 2 Jahre</label> <label for="longterm">länger als 2 Jahre</label>
</div> </div>
</fieldset> </fieldset>
<label for="number_of_attempts">Wie oft hast du schon versucht, dich anzumelden?</label><input type="number" id="number_of_attempts" name="number_of_attempts" /><br />
<input type="submit" value="Anmelden" /> <input type="submit" value="Anmelden" />
</form> </form>

View File

@@ -0,0 +1,11 @@
<html>
<head>
<title>Choriosity Anmeldung</title>
</head>
<body>
Danke für deine Anmeldung
</body>
</html>

View File

@@ -9,6 +9,7 @@ dependencies = [
"pydantic>=2.11.5", "pydantic>=2.11.5",
"pydantic-settings>=2.9.1", "pydantic-settings>=2.9.1",
"sqlmodel>=0.0.24", "sqlmodel>=0.0.24",
"tzdata>=2025.2",
] ]
[tool.uv] [tool.uv]

11
uv.lock generated
View File

@@ -43,6 +43,7 @@ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "sqlmodel" }, { name = "sqlmodel" },
{ name = "tzdata" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -58,6 +59,7 @@ requires-dist = [
{ name = "pydantic", specifier = ">=2.11.5" }, { name = "pydantic", specifier = ">=2.11.5" },
{ name = "pydantic-settings", specifier = ">=2.9.1" }, { name = "pydantic-settings", specifier = ">=2.9.1" },
{ name = "sqlmodel", specifier = ">=0.0.24" }, { name = "sqlmodel", specifier = ">=0.0.24" },
{ name = "tzdata", specifier = ">=2025.2" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -718,6 +720,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
] ]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.34.2" version = "0.34.2"