Compare commits

...

15 Commits

Author SHA1 Message Date
c8500a4337 Move meal manager into it's own package 2025-10-12 21:42:28 +02:00
03d823c713 Rename new-registration-app to meal-manager 2025-10-12 21:25:23 +02:00
773f8ad2b6 remove old signup page 2025-10-12 21:19:37 +02:00
3291fbf6a0 Add comments to registrations 2025-10-12 21:12:45 +02:00
84f128806c Set up migration environment with alembic 2025-10-12 20:51:10 +02:00
494170e2ab Drop Sqlmodel and use plain Sqlalchemy 2025-10-11 22:14:04 +02:00
a190471b44 fix: Display issue on subscription page 2025-10-11 15:02:32 +02:00
02ecfa2209 Add info text to subscription 2025-10-11 14:37:34 +02:00
112459964a Minor tweaks
* display date on event page
  * Filter existing subscriptions for new subscriptions dropdown
  * Add Typeahead for Team registration
2025-10-09 12:15:46 +02:00
457418c271 Fix python version to 3.13 2025-10-08 14:07:03 +02:00
1926382021 Add uvicorn and remove Preise link 2025-10-08 14:02:19 +02:00
65b2abdad6 Add subscription functionality 2025-10-08 13:53:03 +02:00
81daf2aa0c Add rudimentary green theme 2025-10-08 10:19:37 +02:00
3812dd5d47 Add past events page 2025-10-08 10:18:56 +02:00
e1130fa493 Prevent registrations after deadline 2025-10-07 21:38:12 +02:00
75 changed files with 838 additions and 495 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
config.php config.php
test.php test.php
melly-to-grist/.env melly-to-grist/.env
*/__pycache__ **/__pycache__
new-registration-app/database.db new-registration-app/database.db

View File

@@ -1,81 +0,0 @@
import typing
from datetime import datetime
from sqlmodel import Field, Relationship, SQLModel, String
WorkTypes = typing.Literal["cooking", "dishes", "tables"]
class Event(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
title: str = Field(nullable=False)
event_time: datetime = Field(nullable=False)
registration_deadline: datetime = Field(nullable=False)
description: str
recipe_link: str
# Min and max number of people needed for cooking, doing the dishes and preparing the tables
team_cooking_min: int = 3
team_cooking_max: int = 5
team_dishes_min: int = 3
team_dishes_max: int = 5
# Todo: Rename to "table"
team_prep_min: int = 1
team_prep_max: int = 1
registrations: list["Registration"] = Relationship()
team: list["TeamRegistration"] = Relationship()
def team_min_reached(self, work_type: WorkTypes):
threshold = {
"cooking": self.team_cooking_min,
"dishes": self.team_dishes_min,
"tables": self.team_prep_min,
}[work_type]
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
def team_max_reached(self, work_type: WorkTypes):
threshold = {
"cooking": self.team_cooking_max,
"dishes": self.team_dishes_max,
"tables": self.team_prep_max,
}[work_type]
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
def all_teams_min(self):
return all(
self.team_min_reached(work_type) for work_type in typing.get_args(WorkTypes)
)
def all_teams_max(self):
return all(
self.team_max_reached(work_type) for work_type in typing.get_args(WorkTypes)
)
class TeamRegistration(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
event_id: int | None = Field(default=None, foreign_key="event.id")
person_name: str = Field(nullable=False)
work_type: WorkTypes = Field(nullable=False, sa_type=String)
comment: str | None
class Household(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(nullable=False)
class Registration(SQLModel, table=True):
event_id: int | None = Field(default=None, foreign_key="event.id", primary_key=True)
household_id: int | None = Field(
default=None, foreign_key="household.id", primary_key=True
)
num_adult_meals: int
num_children_meals: int
num_small_children_meals: int
comment: str | None
household: Household = Relationship()

View File

@@ -1,16 +0,0 @@
[project]
name = "new-registration-app"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"fastapi[standard]>=0.116.0",
"sqlmodel>=0.0.24",
]
[dependency-groups]
dev = [
"black>=25.1.0",
"isort>=6.0.1",
]

42
pyproject.toml Normal file
View File

@@ -0,0 +1,42 @@
[project]
name = "meal-manager"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = "~=3.13.0"
dependencies = [
"alembic>=1.17.0",
"fastapi[standard]>=0.116.0",
"sqlalchemy>=2.0.44",
"uvicorn[standard]>=0.35.0",
]
[build-system]
requires = ["uv_build>=0.9.0,<0.10.0"]
build-backend = "uv_build"
[dependency-groups]
dev = [
"black>=25.1.0",
"isort>=6.0.1",
]
[tool.isort]
profile = "black"
[tool.alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = "%(here)s/alembic"
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
file_template = "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s"
# additional paths to be prepended to sys.path. defaults to the current working directory.
prepend_sys_path = [
"."
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,98 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Termin hinzufügen</title>
<link rel="stylesheet" href="style.css"> <!-- Link to the CSS file -->
</head>
<body>
<header>
<div class="container">
<div id="branding">
<img src="Logo.png" alt="Logo">
<h1>Termin hinzufügen</h1>
</div>
</div>
</header>
<div class="container">
<h2>Neuen Termin hinzufügen</h2>
<form action="add.php" method="POST">
<label for="title">Title:</label>
<input type="text" id="title" name="title" required>
<label for="melly">Melly Link:</label>
<input type="text" id="melly" name="melly" required>
<label for="date">Event Date:</label>
<input type="date" id="date" name="date" required>
<label for="signup_deadline">Anmeldung bis:</label>
<input type="datetime-local" id="signup_deadline" name="signup_deadline" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit" class="btn">Hinzfügen</button>
</form>
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config = require 'config.php';
// Database connection parameters
$host = $config['db']['host'];
$dbname = $config['db']['dbname'];
$username = $config['db']['username'];
$password = $config['db']['password'];
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Retrieve form data
$title = $_POST['title'];
$melly = $_POST['melly'];
$date = $_POST['date'];
$deadline = $_POST['signup_deadline'];
$webform_password = $_POST['password'];
if($webform_password != $config['webform_password']) {
echo 'Invalid Password!';
} else {
try {
// Create connection
$dsn = "pgsql:host=$host;dbname=$dbname";
$pdo = new PDO($dsn, $username, $password);
// Set error mode to exception for easier debugging
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL query to insert a new dinner option
$sql = "INSERT INTO meals (title, link, event_date, registration_closes) VALUES (:title, :melly, :date, :deadline)";
$stmt = $pdo->prepare($sql);
// Bind parameters
$stmt->bindParam(':title', $title);
$stmt->bindParam(':melly', $melly);
$stmt->bindParam(':date', $date);
$stmt->bindParam(':deadline', $deadline);
// Execute the statement
$stmt->execute();
echo '<p class="success">Dinner option added successfully!</p>';
} catch (PDOException $e) {
// Handle connection or query error
echo "Error: " . $e->getMessage();
}
}
}
?>
</div>
</body>
</html>

View File

@@ -1,13 +0,0 @@
CREATE TABLE meals (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
link TEXT NOT NULL,
event_date DATE NOT NULL,
registration_closes TIMESTAMP NOT NULL
);
INSERT INTO meals (title, link, event_date, registration_closes)
VALUES ('Kidneybohnen Burger mit veganem Coleslaw','https://melly.de/plan/2ZSNYWR37VB8','2025-03-05', '2025-03-02T17:30:30'),
('Gemüselasagne mit Salat','hhttps://melly.de/plan/M4XU9XMVM2HP','2025-02-28', '2025-02-23T17:30:30'),
RETURNING *;

View File

@@ -1,108 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Allmende-Essen</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div class="container">
<div id="branding">
<img src="Logo.png" alt="Logo"/>
<h1>Gemeinsames Essen in der Allmende</h1>
</div>
</div>
</header>
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config = require 'config.php';
// Database connection parameters
$host = $config['db']['host'];
$dbname = $config['db']['dbname'];
$username = $config['db']['username'];
$password = $config['db']['password'];
// Create connection
$dsn = "pgsql:host=$host;dbname=$dbname";
$pdo = new PDO($dsn, $username, $password);
// Query to fetch future dinner options
$sql = "SELECT id, title, link, event_date, registration_closes FROM meals WHERE event_date >= now()::date order by registration_closes < now(), event_date";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
$days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
$today = strtotime(date('Y-m-d'));
$now = strtotime(date("Y-m-d H:i:s"));
?>
<div class="container">
<?php // Display dinner options
for($i = 0; $i < sizeof($result); $i++) {
$row = $result[$i];
$event_date = strtotime($row["event_date"]);
$weekday = date("w", $event_date);
$date = new DateTimeImmutable($row["event_date"]);
$end_of_registration = strtotime($row["registration_closes"]);
echo '<div class="dinner-option">';
echo '<h2>' . $days[$weekday] . " " . $date->format('d.m.Y') .'</h2>';
echo '<p>' . htmlspecialchars($row["title"]) . '</p>';
if ($end_of_registration > $now) {
echo '<a href="' . $row["link"] . '" class="btn">Zur Anmeldung</a>';
} else {
echo '<a href="' . $row["link"] . '" class="btn btn-grey">Anmeldungen ansehen</a>';
}
echo '</div>';
}
// close container
echo '</div>';
// Query to fetch past dinner options
$sql = "SELECT id, title, link, event_date FROM meals WHERE event_date < now()::date order by event_date";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if(sizeof($result) > 0) {
?>
<div class="container">
<h2>Vergangene Essen</h2>
<?php
for($i = 0; $i < sizeof($result); $i++) {
$row = $result[$i];
$event_date = strtotime($row["event_date"]);
$weekday = date("w", $event_date);
$date = new DateTimeImmutable($row["event_date"]);
echo '<div class="dinner-option">';
echo '<h2>' . $days[$weekday] . " " . $date->format('d.m.Y') .'</h2>';
echo '<p>' . htmlspecialchars($row["title"]) . '</p>';
echo '<a href="' . $row["link"] . '" class="btn btn-grey">Anmeldungen ansehen</a>';
echo '</div>';
}
// close container
echo '</div>';
}
?>
</body>
</html>

View File

@@ -1,75 +0,0 @@
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
color: #333;
}
.container {
width: 80%;
margin: auto;
overflow: hidden;
}
header {
background: #333;
color: #fff;
padding-top: 30px;
min-height: 70px;
border-bottom: #77aaff 3px solid;
}
header a {
color: #fff;
text-decoration: none;
text-transform: uppercase;
font-size: 16px;
}
header ul {
padding: 0;
list-style: none;
}
header li {
float: left;
display: inline;
padding: 0 20px 0 20px;
}
header #branding {
float: left;
}
header #branding img {
height: 50px;
width: 40px;
margin-right: 10px;
}
header #branding h1 {
margin: 0;
}
header nav {
float: right;
margin-top: 10px;
}
.dinner-option {
background: #fff;
padding: 20px;
margin: 20px 0;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.dinner-option h2 {
margin-top: 0;
}
.btn {
background: #77aaff;
color: #fff;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
display: inline-block;
margin-top: 10px;
}
.btn-grey {
background: #ccc;
color: #333;
}
.btn:hover {
background: #5a99d0;
}

View File

View File

@@ -0,0 +1,43 @@
# A generic, single database configuration.
[alembic]
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = sqlite:///database.db
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@@ -0,0 +1 @@
pyproject configuration, based on the generic configuration.

View File

@@ -0,0 +1,76 @@
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from meal_manager.models import Base
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,110 @@
"""inital revision
Revision ID: 299a83240036
Revises:
Create Date: 2025-10-12 20:46:13.452705
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "299a83240036"
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"event",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("title", sa.String(), nullable=False),
sa.Column("event_time", sa.DateTime(), nullable=False),
sa.Column("registration_deadline", sa.DateTime(), nullable=False),
sa.Column("description", sa.String(), nullable=False),
sa.Column("recipe_link", sa.String(), nullable=False),
sa.Column("team_cooking_min", sa.Integer(), nullable=False),
sa.Column("team_cooking_max", sa.Integer(), nullable=False),
sa.Column("team_dishes_min", sa.Integer(), nullable=False),
sa.Column("team_dishes_max", sa.Integer(), nullable=False),
sa.Column("team_prep_min", sa.Integer(), nullable=False),
sa.Column("team_prep_max", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"household",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"registration",
sa.Column("event_id", sa.Integer(), nullable=False),
sa.Column("household_id", sa.Integer(), nullable=False),
sa.Column("num_adult_meals", sa.Integer(), nullable=False),
sa.Column("num_children_meals", sa.Integer(), nullable=False),
sa.Column("num_small_children_meals", sa.Integer(), nullable=False),
sa.Column("comment", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["event_id"],
["event.id"],
),
sa.ForeignKeyConstraint(
["household_id"],
["household.id"],
),
sa.PrimaryKeyConstraint("event_id", "household_id"),
)
op.create_table(
"subscription",
sa.Column("household_id", sa.Integer(), nullable=False),
sa.Column("num_adult_meals", sa.Integer(), nullable=False),
sa.Column("num_children_meals", sa.Integer(), nullable=False),
sa.Column("num_small_children_meals", sa.Integer(), nullable=False),
sa.Column("comment", sa.String(), nullable=True),
sa.Column("last_modified", sa.DateTime(), nullable=False),
sa.Column("monday", sa.Boolean(), nullable=False),
sa.Column("tuesday", sa.Boolean(), nullable=False),
sa.Column("wednesday", sa.Boolean(), nullable=False),
sa.Column("thursday", sa.Boolean(), nullable=False),
sa.Column("friday", sa.Boolean(), nullable=False),
sa.Column("saturday", sa.Boolean(), nullable=False),
sa.Column("sunday", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(
["household_id"],
["household.id"],
),
sa.PrimaryKeyConstraint("household_id"),
)
op.create_table(
"team_registration",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("event_id", sa.Integer(), nullable=False),
sa.Column("person_name", sa.String(), nullable=False),
sa.Column("work_type", sa.Text(), nullable=False),
sa.Column("comment", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["event_id"],
["event.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("team_registration")
op.drop_table("subscription")
op.drop_table("registration")
op.drop_table("household")
op.drop_table("event")
# ### end Alembic commands ###

View File

@@ -1,16 +1,24 @@
import locale import locale
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime from datetime import datetime, timedelta
from typing import Annotated, Union from typing import Annotated
import starlette.status as status import starlette.status as status
from fastapi import Depends, FastAPI, Request from fastapi import Depends, FastAPI, Request
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlmodel import Session, SQLModel, create_engine, select from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from models import Event, Household, Registration, TeamRegistration from meal_manager.models import (
Base,
Event,
Household,
Registration,
Subscription,
TeamRegistration,
)
sqlite_file_name = "database.db" sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}" sqlite_url = f"sqlite:///{sqlite_file_name}"
@@ -27,7 +35,7 @@ def get_session():
def create_db_and_tables(): def create_db_and_tables():
SQLModel.metadata.create_all(engine) Base.metadata.create_all(engine)
@asynccontextmanager @asynccontextmanager
@@ -37,24 +45,115 @@ async def on_startup(app_: FastAPI):
app = FastAPI(lifespan=on_startup) app = FastAPI(lifespan=on_startup)
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="src/meal_manager/static"), name="static")
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="src/meal_manager/templates")
SessionDep = Annotated[Session, Depends(get_session)] SessionDep = Annotated[Session, Depends(get_session)]
@app.get("/") @app.get("/")
async def read_root(request: Request, session: SessionDep): async def index(request: Request, session: SessionDep):
statement = select(Event).order_by(Event.event_time) """Displays coming events and a button to register new ones"""
events = session.exec(statement).all() now = datetime.now()
# TODO: Once we refactored to use SQLAlchemy directly, we can probably do a nicer filtering on the date alone
statement = (
select(Event)
.order_by(Event.event_time)
.where(Event.event_time >= now - timedelta(days=1))
)
events = session.scalars(statement)
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="index.html", name="index.html",
context={"events": events, "current_page": "home", "now": datetime.now()}, context={"events": events, "current_page": "home", "now": now},
) )
@app.get("/past_events")
async def past_events(request: Request, session: SessionDep):
now = datetime.now()
# TODO: Once we refactored to use SQLAlchemy directly, we can probably do a nicer filtering on the date alone
statement = (
select(Event)
.order_by(Event.event_time)
.where(Event.event_time < now - timedelta(days=1))
)
events = session.scalars(statement)
return templates.TemplateResponse(
request=request,
name="index.html",
context={"events": events, "current_page": "past", "now": now},
)
@app.get("/subscribe")
async def subscribe(request: Request, session: SessionDep):
statement = select(Household)
households = session.scalars(statement)
subscriptions = session.scalars(select(Subscription))
# filter out households with existing subscriptions
households = [
h for h in households if h.id not in [sub.household_id for sub in subscriptions]
]
return templates.TemplateResponse(
request=request,
name="subscribe.html",
context={"households": households, "subscriptions": subscriptions},
)
@app.post("/subscribe")
async def add_subscribe(request: Request, session: SessionDep):
form_data = await request.form()
# TODO: Make this return a nicer error message
try:
num_adult_meals = int(form_data["numAdults"]) if form_data["numAdults"] else 0
num_children_meals = int(form_data["numKids"]) if form_data["numKids"] else 0
num_small_children_meals = (
int(form_data["numSmallKids"]) if form_data["numSmallKids"] else 0
)
except ValueError:
raise ValueError("All number fields must be integers")
subscription = Subscription(
household_id=form_data["household"],
num_adult_meals=num_adult_meals,
num_children_meals=num_children_meals,
num_small_children_meals=num_small_children_meals,
)
selected_days = form_data.getlist("days")
if selected_days:
subscription.monday = "1" in selected_days
subscription.tuesday = "2" in selected_days
subscription.wednesday = "3" in selected_days
subscription.thursday = "4" in selected_days
subscription.friday = "5" in selected_days
subscription.saturday = "6" in selected_days
subscription.sunday = "7" in selected_days
session.add(subscription)
session.commit()
return RedirectResponse(url="/subscribe", status_code=status.HTTP_302_FOUND)
@app.get("/subscribe/{household_id}/delete")
async def delete_subscription(request: Request, session: SessionDep, household_id: int):
statement = select(Subscription).where(Subscription.household_id == household_id)
sub = session.scalars(statement).one()
session.delete(sub)
session.commit()
return RedirectResponse(url="/subscribe", status_code=status.HTTP_302_FOUND)
@app.get("/event/add") @app.get("/event/add")
async def add_event_form(request: Request, session: SessionDep): async def add_event_form(request: Request, session: SessionDep):
return templates.TemplateResponse(request=request, name="add_event.html") return templates.TemplateResponse(request=request, name="add_event.html")
@@ -92,10 +191,10 @@ async def add_event(request: Request, session: SessionDep):
@app.get("/event/{event_id}") @app.get("/event/{event_id}")
async def read_event(request: Request, event_id: int, session: SessionDep): async def read_event(request: Request, event_id: int, session: SessionDep):
statement = select(Event).where(Event.id == event_id) statement = select(Event).where(Event.id == event_id)
event = session.exec(statement).one() event = session.scalars(statement).one()
statement = select(Household) statement = select(Household)
households = session.exec(statement).all() households = session.scalars(statement)
# filter out households with existing registrations # filter out households with existing registrations
households = [ households = [
@@ -107,7 +206,7 @@ async def read_event(request: Request, event_id: int, session: SessionDep):
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="event.html", name="event.html",
context={"event": event, "households": households}, context={"event": event, "households": households, "now": datetime.now()},
) )
@@ -131,6 +230,7 @@ async def add_registration(request: Request, event_id: int, session: SessionDep)
num_adult_meals=num_adult_meals, num_adult_meals=num_adult_meals,
num_children_meals=num_children_meals, num_children_meals=num_children_meals,
num_small_children_meals=num_small_children_meals, num_small_children_meals=num_small_children_meals,
comment=form_data["comment"],
) )
session.add(registration) session.add(registration)
session.commit() session.commit()
@@ -149,7 +249,7 @@ async def delete_registration(
statement = select(Registration).where( statement = select(Registration).where(
Registration.household_id == household_id, Registration.event_id == event_id Registration.household_id == household_id, Registration.event_id == event_id
) )
session.delete(session.exec(statement).one()) session.delete(session.scalars(statement).one())
session.commit() session.commit()
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND) return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
@@ -165,13 +265,12 @@ async def add_team_registration(request: Request, event_id: int, session: Sessio
TeamRegistration.person_name == person, TeamRegistration.work_type == work_type TeamRegistration.person_name == person, TeamRegistration.work_type == work_type
) )
# if the person has already registered for the same work type, just ignore # if the person has already registered for the same work type, just ignore
if session.exec(statement).one_or_none() is None: if session.scalars(statement).one_or_none() is None:
registration = TeamRegistration( registration = TeamRegistration(
person_name=person, person_name=person,
event_id=event_id, event_id=event_id,
work_type=form_data["workType"], work_type=form_data["workType"],
) )
TeamRegistration.model_validate(registration)
session.add(registration) session.add(registration)
session.commit() session.commit()
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND) return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
@@ -185,6 +284,6 @@ async def delete_team_registration(
session: SessionDep, session: SessionDep,
): ):
statement = select(TeamRegistration).where(TeamRegistration.id == entry_id) statement = select(TeamRegistration).where(TeamRegistration.id == entry_id)
session.delete(session.exec(statement).one()) session.delete(session.scalars(statement).one())
session.commit() session.commit()
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND) return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)

143
src/meal_manager/models.py Normal file
View File

@@ -0,0 +1,143 @@
import typing
from datetime import datetime
from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.types import Text
class Base(DeclarativeBase):
pass
WorkTypes = typing.Literal["cooking", "dishes", "tables"]
class Event(Base):
__tablename__ = "event"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(nullable=False)
event_time: Mapped[datetime] = mapped_column(nullable=False)
registration_deadline: Mapped[datetime] = mapped_column(nullable=False)
description: Mapped[str] = mapped_column()
recipe_link: Mapped[str] = mapped_column()
# Min and max number of people needed for cooking, doing the dishes and preparing the tables
team_cooking_min: Mapped[int] = mapped_column(default=3, nullable=False)
team_cooking_max: Mapped[int] = mapped_column(default=5, nullable=False)
team_dishes_min: Mapped[int] = mapped_column(default=3, nullable=False)
team_dishes_max: Mapped[int] = mapped_column(default=5, nullable=False)
# Todo: Rename to "table"
team_prep_min: Mapped[int] = mapped_column(default=1, nullable=False)
team_prep_max: Mapped[int] = mapped_column(default=1, nullable=False)
registrations: Mapped[list["Registration"]] = relationship("Registration")
team: Mapped[list["TeamRegistration"]] = relationship("TeamRegistration")
def team_min_reached(self, work_type: WorkTypes):
threshold = {
"cooking": self.team_cooking_min,
"dishes": self.team_dishes_min,
"tables": self.team_prep_min,
}[work_type]
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
def team_max_reached(self, work_type: WorkTypes):
threshold = {
"cooking": self.team_cooking_max,
"dishes": self.team_dishes_max,
"tables": self.team_prep_max,
}[work_type]
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
def all_teams_min(self):
return all(
self.team_min_reached(work_type) for work_type in typing.get_args(WorkTypes)
)
def all_teams_max(self):
return all(
self.team_max_reached(work_type) for work_type in typing.get_args(WorkTypes)
)
class TeamRegistration(Base):
__tablename__ = "team_registration"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"))
person_name: Mapped[str] = mapped_column(nullable=False)
work_type: Mapped[WorkTypes] = mapped_column(Text, nullable=False)
comment: Mapped[str | None] = mapped_column()
class Household(Base):
__tablename__ = "household"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False)
class Registration(Base):
__tablename__ = "registration"
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"), primary_key=True)
household_id: Mapped[int] = mapped_column(
ForeignKey("household.id"), primary_key=True
)
num_adult_meals: Mapped[int] = mapped_column(nullable=False)
num_children_meals: Mapped[int] = mapped_column(nullable=False)
num_small_children_meals: Mapped[int] = mapped_column(nullable=False)
comment: Mapped[str | None] = mapped_column()
household: Mapped["Household"] = relationship()
class Subscription(Base):
__tablename__ = "subscription"
household_id: Mapped[int] = mapped_column(
ForeignKey("household.id"), primary_key=True
)
num_adult_meals: Mapped[int] = mapped_column(nullable=False)
num_children_meals: Mapped[int] = mapped_column(nullable=False)
num_small_children_meals: Mapped[int] = mapped_column(nullable=False)
comment: Mapped[str | None] = mapped_column()
last_modified: Mapped[datetime] = mapped_column(
default=datetime.now, nullable=False
)
monday: Mapped[bool] = mapped_column(default=True, nullable=False)
tuesday: Mapped[bool] = mapped_column(default=True, nullable=False)
wednesday: Mapped[bool] = mapped_column(default=True, nullable=False)
thursday: Mapped[bool] = mapped_column(default=True, nullable=False)
friday: Mapped[bool] = mapped_column(default=True, nullable=False)
saturday: Mapped[bool] = mapped_column(default=True, nullable=False)
sunday: Mapped[bool] = mapped_column(default=True, nullable=False)
household: Mapped["Household"] = relationship()
def day_string_de(self) -> str:
"""
Generates a string representation of selected days in German short form.
"""
result = []
if self.monday:
result.append("Mo")
if self.tuesday:
result.append("Di")
if self.wednesday:
result.append("Mi")
if self.thursday:
result.append("Do")
if self.friday:
result.append("Fr")
if self.saturday:
result.append("Sa")
if self.sunday:
result.append("So")
if len(result) < 7:
return ", ".join(result)
else:
return "Alle"

View File

@@ -0,0 +1,29 @@
/* theme.css */
/* Green Bootstrap Theme */
:root {
--bs-primary: #198754;
--bs-primary-rgb: 25, 135, 84;
--bs-success: #198754;
--bs-success-rgb: 25, 135, 84;
--bs-link-color: var(--bs-primary);
--bs-link-hover-color: #146c43;
}
/* Explicit fallback overrides for older versions (<=5.2) */
.btn-primary {
color: #fff;
background-color: #198754;
border-color: #198754;
}
.btn-primary:hover {
color: #fff;
background-color: #157347;
border-color: #146c43;
}
.btn-primary:focus,
.btn-primary:active {
color: #fff;
background-color: #146c43;
border-color: #125c39;
}

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -32,7 +32,6 @@
</div> </div>
<button type="submit" class="btn btn-primary">Event erstellen</button> <button type="submit" class="btn btn-primary">Event erstellen</button>
<a href="/new-registration-app/static" class="btn btn-secondary">Abbrechen</a>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@
<title>Allmende Essen</title> <title>Allmende Essen</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet"> <link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/icons/bootstrap-icons.min.css" rel="stylesheet"> <link href="/static/icons/bootstrap-icons.min.css" rel="stylesheet">
<link href="/static/css/allmende.css" rel="stylesheet">
</head> </head>
<body class="p-3 m-0 border-0 bd-example"> <body class="p-3 m-0 border-0 bd-example">
<nav class="navbar navbar-expand-lg bg-body-tertiary"> <nav class="navbar navbar-expand-lg bg-body-tertiary">
@@ -21,14 +22,14 @@
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if current_page == 'home' %}active{% endif %}" {% if current_page == 'home' %}aria-current="page"{% endif %} href="/">Home</a> <a class="nav-link {% if current_page == 'home' %}active{% endif %}" {% if current_page == 'home' %}aria-current="page"{% endif %} href="/">Kommende</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if current_page == 'vergangene' %}active{% endif %}" {% if current_page == 'vergangene' %}aria-current="page"{% endif %} href="/vergangene">Vergangene</a> <a class="nav-link {% if current_page == 'past' %}active{% endif %}" {% if current_page == 'past' %}aria-current="page"{% endif %} href="/past_events">Vergangene</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_page == 'preise' %}active{% endif %}" {% if current_page == 'preise' %}aria-current="page"{% endif %} href="/preise">Preise</a>
</li> </li>
<!-- <li class="nav-item">-->
<!-- <a class="nav-link {% if current_page == 'preise' %}active{% endif %}" {% if current_page == 'preise' %}aria-current="page"{% endif %} href="/preise">Preise</a>-->
<!-- </li>-->
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -14,17 +14,19 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<p class="h1">{{ event.title }}</p> <p class="h1">{{ event.title }}</p>
<p class="text-muted">{{ event.event_time.strftime('%A, %d.%m.%Y') }}</p>
<p>{{ event.description }}</p> <p>{{ event.description }}</p>
<hr class="hr"/> <hr class="hr"/>
<p class="h3">Anmeldungen</p>
<div class="container"> <div class="container">
<p class="h3">Anmeldungen</p>
<div class="row m-2"> <div class="row m-2">
<div class="col-md-4 d-flex justify-content-center align-items-center flex-column"> <div class="col-md-4 d-flex justify-content-center align-items-center flex-column">
<p class="text-muted w-100 mb-2">Anmeldung schließt {{ event.registration_deadline.strftime('%A, %d.%m.%Y, %H:%M Uhr') }}</p>
<!-- Button trigger modal --> <!-- Button trigger modal -->
<button type="button" class="btn btn-primary mb-2 w-100" data-bs-toggle="modal" data-bs-target="#registration"> <button type="button" class="btn btn-primary mb-2 w-100" {% if event.registration_deadline < now %}disabled{% endif%} data-bs-toggle="modal" data-bs-target="#registration">
Anmeldung hinzufügen Anmeldung hinzufügen
</button> </button>
<button type="button" class="btn btn-primary mb-2 w-100" {% if event.all_teams_max() %}disabled{% endif %} data-bs-toggle="modal" data-bs-target="#teamRegistration"> <button type="button" class="btn btn-primary mb-2 w-100" {% if event.all_teams_max() or event.event_time < now %}disabled{% endif %} data-bs-toggle="modal" data-bs-target="#teamRegistration">
Dienst übernehmen Dienst übernehmen
</button> </button>
{% if event.recipe_link %} {% if event.recipe_link %}
@@ -102,6 +104,7 @@
<th scope="col">Erwachsene</th> <th scope="col">Erwachsene</th>
<th scope="col">Kinder</th> <th scope="col">Kinder</th>
<th scope="col">Kleinkinder</th> <th scope="col">Kleinkinder</th>
<th scope="col">Kommentar</th>
<th scope="col">Löschen</th> <th scope="col">Löschen</th>
</tr> </tr>
</thead> </thead>
@@ -112,6 +115,7 @@
<td>{{ reg.num_adult_meals }}</td> <td>{{ reg.num_adult_meals }}</td>
<td>{{ reg.num_children_meals }}</td> <td>{{ reg.num_children_meals }}</td>
<td>{{ reg.num_small_children_meals }}</td> <td>{{ reg.num_small_children_meals }}</td>
<td>{{ reg.comment }}</td>
<td><a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete"><i class="bi bi-trash"></i></a></td> <td><a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete"><i class="bi bi-trash"></i></a></td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -131,18 +135,22 @@
</a> </a>
</div> </div>
<div class="row mt-3"> <div class="row mt-3">
<div class="col-4 text-center"> <div class="col-3 text-center">
<div class="text-muted small">Erwachsene</div> <div class="text-muted small">Erwachsene</div>
<div class="fw-bold">{{ reg.num_adult_meals }}</div> <div class="fw-bold">{{ reg.num_adult_meals }}</div>
</div> </div>
<div class="col-4 text-center"> <div class="col-3 text-center">
<div class="text-muted small">Kinder</div> <div class="text-muted small">Kinder</div>
<div class="fw-bold">{{ reg.num_children_meals }}</div> <div class="fw-bold">{{ reg.num_children_meals }}</div>
</div> </div>
<div class="col-4 text-center"> <div class="col-3 text-center">
<div class="text-muted small">Kleinkinder</div> <div class="text-muted small">Kleinkinder</div>
<div class="fw-bold">{{ reg.num_small_children_meals }}</div> <div class="fw-bold">{{ reg.num_small_children_meals }}</div>
</div> </div>
<div class="col-3 text-center">
<div class="text-muted small">Kommentar</div>
<div class="small">{{ reg.comment }}</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -162,15 +170,14 @@
<div class="modal-body"> <div class="modal-body">
<p>Wenn dein Haushalt nicht auswählbar ist, existiert schon eine Anmeldung. Wenn du die Anmeldung ändern willst, lösche die bestehende Anmeldung und lege eine neue an.</p> <p>Wenn dein Haushalt nicht auswählbar ist, existiert schon eine Anmeldung. Wenn du die Anmeldung ändern willst, lösche die bestehende Anmeldung und lege eine neue an.</p>
<div class="mb-3"> <div class="mb-3">
<select name="household" class="form-select" aria-label="Multiple select example"> <select name="household" class="form-select" aria-label="Multiple select example" required>
<option selected>Wer?</option> <option value="" disabled selected hidden>Wer?</option>
{% for household in households %} {% for household in households %}
<option value="{{household.id}}">{{household.name}}</option> <option value="{{household.id}}">{{household.name}}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<label for="InputAdults" class="form-label">Anzahl Erwachsene</label> <label for="InputAdults" class="form-label">Anzahl Erwachsene</label>
@@ -187,6 +194,11 @@
<input name="numSmallKids" id="InputSmallKids" type="number" class="form-control" <input name="numSmallKids" id="InputSmallKids" type="number" class="form-control"
aria-label="Anzahl Kinder <7" min="0" step="1" inputmode="numeric"> aria-label="Anzahl Kinder <7" min="0" step="1" inputmode="numeric">
</div> </div>
<div class="mb-3 mt-3">
<label for="InputComment" class="form-label">Kommentar</label>
<input name="comment" id="InputComment" class="form-control"
aria-label="Kommentar">
</div>
</div> </div>
</div> </div>
@@ -214,8 +226,15 @@
<div class="col-md-6"> <div class="col-md-6">
<label for="personName" class="form-label">Name</label> <label for="personName" class="form-label">Name</label>
<input name="personName" id="personName" type="text" class="form-control" <input name="personName" id="personName" type="text" class="form-control"
aria-label="Name"> aria-label="Name" list="people">
</div> </div>
<datalist id="people">
{% for household in households %}
{% for person in household.name.split(",") %}
<option value="{{ person.strip() }}">
{% endfor %}
{% endfor %}
</datalist>
<div class="col-md-6"> <div class="col-md-6">
<label for="workType" class="form-label">Dienst-Art</label> <label for="workType" class="form-label">Dienst-Art</label>
<select id="workType" name="workType" class="form-select" aria-label="Multiple select example"> <select id="workType" name="workType" class="form-select" aria-label="Multiple select example">

View File

@@ -2,13 +2,29 @@
{% block content %} {% block content %}
<div class="row mt-4 mb-3"> <div class="row mt-4 mb-3">
<div class="col d-flex justify-content-between align-items-center"> <div class="col d-flex justify-content-between align-items-center">
<h2>Kommende Events</h2> <h2>{% if current_page == "home" %}Kommende{% else %}Vergangene{% endif %} Kochabende</h2>
<a href="/event/add" class="btn btn-primary"> <a href="/event/add" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Neues Event erstellen <i class="bi bi-plus-circle"></i> Neues Event erstellen
</a> </a>
</div> </div>
</div> </div>
<div class="mb-4">
<div class="card bg-success-subtle text-success-emphasis border-0 shadow-sm text-center">
<div class="card-body py-4">
<i class="bi bi-calendar-heart fs-3 mb-2"></i>
<h5 class="card-title mb-2">Nie wieder die Anmeldung vergessen</h5>
<p class="card-text small mb-3">
Die Dauerhafte Anmeldung gilt für alle kommenden Kochabende.
</p>
<a href="/subscribe" class="btn btn-light btn-sm fw-semibold px-3">
Jetzt dauerhaft Anmelden
</a>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
{% for event in events %} {% for event in events %}
<div class="col"> <div class="col">
<div class="card h-100 shadow-sm"> <div class="card h-100 shadow-sm">

View File

@@ -0,0 +1,140 @@
{% extends "base.html" %}
{% block content %}
<div class="row mt-4">
<!-- Left column: subscription form -->
<div class="col-12 col-lg-6">
<div class="row justify-content-center">
<div class="col">
<div class="card shadow-sm mt-4">
<div class="card-header bg-primary text-white">
<h1 class="fs-5 mb-0">Dauerhafte Anmeldung zu allen Kochabenden</h1>
</div>
<form action="/subscribe" method="POST">
<div class="card-body">
<p>
Mit einer dauerhaften Anmeldung kannst du dich/euch für alle zukünftigen Kochabende
anmelden. Es ist möglich
diese Anmeldung auf bestimmte Wochentage zu beschränken.
</p>
<p>
Dauerhafte Anmeldungen werden eine Woche vor einem Kochabend als Anmeldungen für diesen
Abend eingetragen. Danach
können sie auch noch gelöscht bzw. bearbeitet werden.
</p>
<!-- Info box about 7-day limitation -->
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i>
<strong>Hinweis:</strong> Neu angelegte dauerhafte Anmeldungen werden erst nach einer Woche aktiv. Für Kochabende, die in weniger als 7 Tagen stattfinden, musst du dich noch separat anmelden.
</div>
<!-- Household selection -->
<div class="mb-3">
<select name="household" class="form-select" required>
<option value="" disabled selected hidden>Wer?</option>
{% for household in households %}
<option value="{{household.id}}">{{household.name}}</option>
{% endfor %}
</select>
</div>
<p class="text-muted">
Wenn dein Haushalt hier nicht auswählbar ist, besteht bereits eine dauerhafte Anmeldung.
Um Änderungen vorzunehmen, lösche die bestehende Anmeldung und lege eine neue an.
</p>
<!-- Person counts -->
<div class="row g-3 mb-3">
<div class="col">
<label for="InputAdults" class="form-label">Anzahl Erwachsene</label>
<input name="numAdults" id="InputAdults" type="number" class="form-control"
aria-label="Anzahl Erwachsene" min="0" step="1" inputmode="numeric">
</div>
<div class="col">
<label for="InputKids" class="form-label">Anzahl Kinder &gt;7</label>
<input name="numKids" id="InputKids" type="number" class="form-control"
aria-label="Anzahl Kinder &gt;7" min="0" step="1" inputmode="numeric">
</div>
<div class="col">
<label for="InputSmallKids" class="form-label">Anzahl Kinder &lt;7</label>
<input name="numSmallKids" id="InputSmallKids" type="number" class="form-control"
aria-label="Anzahl Kinder &lt;7" min="0" step="1" inputmode="numeric">
</div>
</div>
<!-- Days of the week -->
<div class="mb-3">
<label class="form-label">Wochentage auswählen (optional)</label>
<p class="text-muted small mb-2">
Wenn du nur an bestimmten Tagen teilnehmen möchtest, wähle sie hier aus.
</p>
<div class="d-flex flex-wrap gap-3">
{% set days = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag",
"Sonntag"] %}
{% for day in days %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="days" value="{{loop.index}}"
id="day-{{loop.index}}">
<label class="form-check-label" for="day-{{loop.index}}">
{{day}}
</label>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Footer -->
<div class="card-footer d-flex justify-content-end gap-2">
<button type="submit" class="btn btn-primary">Dauerhaft anmelden</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Right column: existing registrations -->
<div class="col-12 col-lg-6 mt-4 mt-lg-0">
<p class="h4 m-2">Bestehende dauerhafte Anmeldungen</p>
{% if subscriptions | length == 0 %}
<p class="m-2">Es gibt noch keine dauerhaften Anmeldungen</p>
{% else %}
{% for sub in subscriptions %}
<div class="card mb-2">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="mb-0">{{ sub.household.name }}</h6>
<a href="/subscribe/{{sub.household.id}}/delete" class="text-danger">
<i class="bi bi-trash"></i>
</a>
</div>
<div class="row g-2">
<div class="col-3 text-center">
<div class="text-muted" style="font-size: 0.7rem;">Erwachsene</div>
<div class="fw-bold small">{{ sub.num_adult_meals }}</div>
</div>
<div class="col-3 text-center">
<div class="text-muted" style="font-size: 0.7rem;">Kinder</div>
<div class="fw-bold small">{{ sub.num_children_meals }}</div>
</div>
<div class="col-3 text-center">
<div class="text-muted" style="font-size: 0.7rem;">Kleinkinder</div>
<div class="fw-bold small">{{ sub.num_small_children_meals }}</div>
</div>
<div class="col-3 text-center">
<div class="text-muted" style="font-size: 0.7rem;">Tage</div>
<div class="fw-bold small">{{ sub.day_string_de() }}</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,20 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = "==3.13.*"
[[package]]
name = "alembic"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mako" },
{ name = "sqlalchemy" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6b/45/6f4555f2039f364c3ce31399529dcf48dd60726ff3715ad67f547d87dfd2/alembic-1.17.0.tar.gz", hash = "sha256:4652a0b3e19616b57d652b82bfa5e38bf5dbea0813eed971612671cb9e90c0fe", size = 1975526, upload-time = "2025-10-11T18:40:13.585Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/1f/38e29b06bfed7818ebba1f84904afdc8153ef7b6c7e0d8f3bc6643f5989c/alembic-1.17.0-py3-none-any.whl", hash = "sha256:80523bc437d41b35c5db7e525ad9d908f79de65c27d6a5a5eab6df348a352d99", size = 247449, upload-time = "2025-10-11T18:40:16.288Z" },
]
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@@ -173,13 +187,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
{ url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" },
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
] ]
[[package]] [[package]]
@@ -264,6 +271,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
] ]
[[package]]
name = "mako"
version = "1.3.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "4.0.0" version = "4.0.0"
@@ -314,21 +333,14 @@ wheels = [
] ]
[[package]] [[package]]
name = "mypy-extensions" name = "meal-manager"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "new-registration-app"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "alembic" },
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "sqlmodel" }, { name = "sqlalchemy" },
{ name = "uvicorn", extra = ["standard"] },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -339,8 +351,10 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "alembic", specifier = ">=1.17.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.116.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.116.0" },
{ name = "sqlmodel", specifier = ">=0.0.24" }, { name = "sqlalchemy", specifier = ">=2.0.44" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -349,6 +363,15 @@ dev = [
{ name = "isort", specifier = ">=6.0.1" }, { name = "isort", specifier = ">=6.0.1" },
] ]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@@ -523,8 +546,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/3a/7e7ea6f0d31d3f5beb0f2cf2c4c362672f5f7f125714458673fc579e2bed/rignore-0.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:91dc94b1cc5af8d6d25ce6edd29e7351830f19b0a03b75cb3adf1f76d00f3007", size = 1134598, upload-time = "2025-07-19T19:24:15.039Z" }, { url = "https://files.pythonhosted.org/packages/3b/3a/7e7ea6f0d31d3f5beb0f2cf2c4c362672f5f7f125714458673fc579e2bed/rignore-0.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:91dc94b1cc5af8d6d25ce6edd29e7351830f19b0a03b75cb3adf1f76d00f3007", size = 1134598, upload-time = "2025-07-19T19:24:15.039Z" },
{ url = "https://files.pythonhosted.org/packages/7e/06/1b3307f6437d29bede5a95738aa89e6d910ba68d4054175c9f60d8e2c6b1/rignore-0.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4d1918221a249e5342b60fd5fa513bf3d6bf272a8738e66023799f0c82ecd788", size = 1108862, upload-time = "2025-07-19T19:24:26.765Z" }, { url = "https://files.pythonhosted.org/packages/7e/06/1b3307f6437d29bede5a95738aa89e6d910ba68d4054175c9f60d8e2c6b1/rignore-0.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4d1918221a249e5342b60fd5fa513bf3d6bf272a8738e66023799f0c82ecd788", size = 1108862, upload-time = "2025-07-19T19:24:26.765Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d5/b37c82519f335f2c472a63fc6215c6f4c51063ecf3166e3acf508011afbd/rignore-0.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:240777332b859dc89dcba59ab6e3f1e062bc8e862ffa3e5f456e93f7fd5cb415", size = 1120002, upload-time = "2025-07-19T19:24:38.952Z" }, { url = "https://files.pythonhosted.org/packages/b0/d5/b37c82519f335f2c472a63fc6215c6f4c51063ecf3166e3acf508011afbd/rignore-0.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:240777332b859dc89dcba59ab6e3f1e062bc8e862ffa3e5f456e93f7fd5cb415", size = 1120002, upload-time = "2025-07-19T19:24:38.952Z" },
{ url = "https://files.pythonhosted.org/packages/ac/72/2f05559ed5e69bdfdb56ea3982b48e6c0017c59f7241f7e1c5cae992b347/rignore-0.6.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b0e548753e55cc648f1e7b02d9f74285fe48bb49cec93643d31e563773ab3f", size = 949454, upload-time = "2025-07-19T19:23:38.664Z" },
{ url = "https://files.pythonhosted.org/packages/0b/92/186693c8f838d670510ac1dfb35afbe964320fbffb343ba18f3d24441941/rignore-0.6.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6971ac9fdd5a0bd299a181096f091c4f3fd286643adceba98eccc03c688a6637", size = 974663, upload-time = "2025-07-19T19:23:28.24Z" },
] ]
[[package]] [[package]]
@@ -560,36 +581,23 @@ wheels = [
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.43" version = "2.0.44"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" },
{ url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" },
{ url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" },
{ url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" },
{ url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" },
{ url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" },
{ url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" },
{ url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" },
]
[[package]]
name = "sqlmodel"
version = "0.0.24"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload-time = "2025-03-07T05:43:32.887Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload-time = "2025-03-07T05:43:30.37Z" },
] ]
[[package]] [[package]]
@@ -719,26 +727,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" }, { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" },
{ url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" }, { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" },
{ url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" }, { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" },
{ url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" },
{ url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" },
{ url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" },
{ url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" },
{ url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" },
{ url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" },
{ url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" },
{ url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" },
{ url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" },
{ url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" },
{ url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" },
{ url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" },
{ url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" },
{ url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" },
{ url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" },
{ url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" },
{ url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" },
{ url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" },
{ url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" },
] ]
[[package]] [[package]]