feat: initialize events app with calendars, slots, tickets, and internal API
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Extract events/calendar functionality into standalone microservice: - app.py and events_api.py from apps/events/ - Calendar blueprints (calendars, calendar, calendar_entries, calendar_entry, day, slots, slot, ticket_types, ticket_type) - Templates for all calendar/event views including admin - Dockerfile (APP_MODULE=app:app, IMAGE=events) - entrypoint.sh (no Alembic - migrations managed by blog app) - Gitea CI workflow for build and deploy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
132
bp/ticket_types/routes.py
Normal file
132
bp/ticket_types/routes.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import (
|
||||
request, render_template, make_response, Blueprint, g, jsonify
|
||||
)
|
||||
|
||||
from suma_browser.app.authz import require_admin
|
||||
from suma_browser.app.redis_cacher import clear_cache
|
||||
|
||||
from .services.tickets import (
|
||||
list_ticket_types as svc_list_ticket_types,
|
||||
create_ticket_type as svc_create_ticket_type,
|
||||
)
|
||||
|
||||
from ..ticket_type.routes import register as register_ticket_type
|
||||
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
|
||||
|
||||
def register():
|
||||
bp = Blueprint("ticket_types", __name__, url_prefix='/ticket-types')
|
||||
|
||||
# Register individual ticket routes
|
||||
bp.register_blueprint(
|
||||
register_ticket_type()
|
||||
)
|
||||
|
||||
@bp.context_processor
|
||||
async def get_ticket_types():
|
||||
"""Make ticket types available to all templates in this blueprint."""
|
||||
entry = getattr(g, "entry", None)
|
||||
if entry:
|
||||
return {
|
||||
"ticket_types": await svc_list_ticket_types(g.s, entry.id)
|
||||
}
|
||||
return {"ticket_types": []}
|
||||
|
||||
@bp.get("/")
|
||||
async def get(**kwargs):
|
||||
"""List all ticket types for the current entry."""
|
||||
if not is_htmx_request():
|
||||
# Normal browser request: full page with layout
|
||||
html = await render_template(
|
||||
"_types/ticket_types/index.html",
|
||||
)
|
||||
else:
|
||||
|
||||
html = await render_template(
|
||||
"_types/ticket_types/_oob_elements.html",
|
||||
)
|
||||
|
||||
return await make_response(html)
|
||||
|
||||
@bp.post("/")
|
||||
@require_admin
|
||||
@clear_cache(tag="calendars", tag_scope="all")
|
||||
async def post(**kwargs):
|
||||
"""Create a new ticket type."""
|
||||
form = await request.form
|
||||
|
||||
name = (form.get("name") or "").strip()
|
||||
cost_str = (form.get("cost") or "").strip()
|
||||
count_str = (form.get("count") or "").strip()
|
||||
|
||||
field_errors: dict[str, list[str]] = {}
|
||||
|
||||
# Validate name
|
||||
if not name:
|
||||
field_errors.setdefault("name", []).append("Please enter a ticket type name.")
|
||||
|
||||
# Validate cost
|
||||
cost = None
|
||||
if not cost_str:
|
||||
field_errors.setdefault("cost", []).append("Please enter a cost.")
|
||||
else:
|
||||
try:
|
||||
cost = float(cost_str)
|
||||
if cost < 0:
|
||||
field_errors.setdefault("cost", []).append("Cost must be positive.")
|
||||
except ValueError:
|
||||
field_errors.setdefault("cost", []).append("Please enter a valid number.")
|
||||
|
||||
# Validate count
|
||||
count = None
|
||||
if not count_str:
|
||||
field_errors.setdefault("count", []).append("Please enter a ticket count.")
|
||||
else:
|
||||
try:
|
||||
count = int(count_str)
|
||||
if count < 0:
|
||||
field_errors.setdefault("count", []).append("Count must be positive.")
|
||||
except ValueError:
|
||||
field_errors.setdefault("count", []).append("Please enter a valid whole number.")
|
||||
|
||||
if field_errors:
|
||||
return jsonify({
|
||||
"message": "Please fix the highlighted fields.",
|
||||
"errors": field_errors,
|
||||
}), 422
|
||||
|
||||
# Create ticket type
|
||||
await svc_create_ticket_type(
|
||||
g.s,
|
||||
g.entry.id,
|
||||
name=name,
|
||||
cost=cost,
|
||||
count=count,
|
||||
)
|
||||
|
||||
# Success → re-render the ticket types table
|
||||
html = await render_template("_types/ticket_types/_main_panel.html")
|
||||
return await make_response(html)
|
||||
|
||||
@bp.get("/add")
|
||||
@require_admin
|
||||
async def add_form(**kwargs):
|
||||
"""Show the add ticket type form."""
|
||||
html = await render_template(
|
||||
"_types/ticket_types/_add.html",
|
||||
)
|
||||
return await make_response(html)
|
||||
|
||||
@bp.get("/add-button")
|
||||
@require_admin
|
||||
async def add_button(**kwargs):
|
||||
"""Show the add ticket type button."""
|
||||
html = await render_template(
|
||||
"_types/ticket_types/_add_button.html",
|
||||
)
|
||||
return await make_response(html)
|
||||
|
||||
return bp
|
||||
47
bp/ticket_types/services/tickets.py
Normal file
47
bp/ticket_types/services/tickets.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from models.calendars import TicketType
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
async def list_ticket_types(session: AsyncSession, entry_id: int) -> list[TicketType]:
|
||||
"""Get all active ticket types for a calendar entry."""
|
||||
result = await session.execute(
|
||||
select(TicketType)
|
||||
.where(
|
||||
TicketType.entry_id == entry_id,
|
||||
TicketType.deleted_at.is_(None)
|
||||
)
|
||||
.order_by(TicketType.name)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def create_ticket_type(
|
||||
session: AsyncSession,
|
||||
entry_id: int,
|
||||
*,
|
||||
name: str,
|
||||
cost: float,
|
||||
count: int,
|
||||
) -> TicketType:
|
||||
"""Create a new ticket type for a calendar entry."""
|
||||
ticket_type = TicketType(
|
||||
entry_id=entry_id,
|
||||
name=name,
|
||||
cost=cost,
|
||||
count=count,
|
||||
created_at=utcnow(),
|
||||
updated_at=utcnow(),
|
||||
)
|
||||
session.add(ticket_type)
|
||||
await session.flush()
|
||||
return ticket_type
|
||||
Reference in New Issue
Block a user