This repository has been archived on 2026-02-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
events/bp/calendar/routes.py
giles 154f968296 feat: decouple events from shared_lib, add app-owned models
Phase 1-3 of decoupling:
- path_setup.py adds project root to sys.path
- Events-owned models in events/models/ (calendars with all related models)
- All imports updated: shared.infrastructure, shared.db, shared.browser, etc.
- Calendar uses container_type/container_id instead of post_id FK
- CalendarEntryPost uses content_type/content_id (generic content refs)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 12:46:36 +00:00

252 lines
7.6 KiB
Python

from __future__ import annotations
from datetime import datetime, timezone
from quart import (
request, render_template, make_response, Blueprint, g, abort, session as qsession
)
from sqlalchemy import select
from models.calendars import Calendar
from sqlalchemy.orm import selectinload, with_loader_criteria
from shared.browser.app.authz import require_admin
from .admin.routes import register as register_admin
from .services import get_visible_entries_for_period
from .services.calendar_view import (
parse_int_arg,
add_months,
build_calendar_weeks,
get_calendar_by_post_and_slug,
get_calendar_by_slug,
update_calendar_description,
)
from shared.browser.app.utils.htmx import is_htmx_request
from ..slots.routes import register as register_slots
from models.calendars import CalendarSlot
from bp.calendars.services.calendars import soft_delete
from bp.day.routes import register as register_day
from shared.browser.app.redis_cacher import cache_page, clear_cache
from sqlalchemy import select
import calendar as pycalendar
def register():
bp = Blueprint("calendar", __name__, url_prefix='/<calendar_slug>')
bp.register_blueprint(
register_admin(),
)
bp.register_blueprint(
register_slots(),
)
bp.register_blueprint(
register_day()
)
@bp.url_value_preprocessor
def pull(endpoint, values):
g.calendar_slug = values.get("calendar_slug")
@bp.before_request
async def hydrate_calendar_data():
calendar_slug = getattr(g, "calendar_slug", None)
# Standalone mode (events app): no post context
post_data = getattr(g, "post_data", None)
if post_data:
post_id = (post_data.get("post") or {}).get("id")
cal = await get_calendar_by_post_and_slug(g.s, post_id, calendar_slug)
else:
cal = await get_calendar_by_slug(g.s, calendar_slug)
if not cal:
abort(404)
return
g.calendar = cal
@bp.context_processor
async def inject_root():
return {
"calendar": getattr(g, "calendar", None),
}
# ---------- Pages ----------
# ---------- Pages ----------
@bp.get("/")
@cache_page(tag="calendars")
async def get(calendar_slug: str, **kwargs):
"""
Show a month-view calendar for this calendar.
- One month at a time
- Outer arrows: +/- 1 year
- Inner arrows: +/- 1 month
"""
# --- Determine year & month from query params ---
today = datetime.now(timezone.utc).date()
month = parse_int_arg("month")
year = parse_int_arg("year")
if year is None:
year = today.year
if month is None or not (1 <= month <= 12):
month = today.month
# --- Helpers to move between months ---
prev_month_year, prev_month = add_months(year, month, -1)
next_month_year, next_month = add_months(year, month, +1)
prev_year = year - 1
next_year = year + 1
# --- Build weeks grid (list of weeks, each week = 7 days) ---
weeks = build_calendar_weeks(year, month)
month_name = pycalendar.month_name[month]
weekday_names = [pycalendar.day_abbr[i] for i in range(7)]
# --- Period boundaries for this calendar view ---
period_start = datetime(year, month, 1, tzinfo=timezone.utc)
next_y, next_m = add_months(year, month, +1)
period_end = datetime(next_y, next_m, 1, tzinfo=timezone.utc)
# --- Identity & admin flag ---
user = getattr(g, "user", None)
session_id = qsession.get("calendar_sid")
visible = await get_visible_entries_for_period(
sess=g.s,
calendar_id=g.calendar.id,
period_start=period_start,
period_end=period_end,
user=user,
session_id=session_id,
)
month_entries = visible.merged_entries
user_entries = visible.user_entries
confirmed_entries = visible.confirmed_entries
if not is_htmx_request():
# Normal browser request: full page with layout
html = await render_template(
"_types/calendar/index.html",
qsession=qsession,
year=year,
month=month,
month_name=month_name,
weekday_names=weekday_names,
weeks=weeks,
prev_month=prev_month,
prev_month_year=prev_month_year,
next_month=next_month,
next_month_year=next_month_year,
prev_year=prev_year,
next_year=next_year,
user_entries=user_entries,
confirmed_entries=confirmed_entries,
month_entries=month_entries,
)
else:
html = await render_template(
"_types/calendar/_oob_elements.html",
qsession=qsession,
year=year,
month=month,
month_name=month_name,
weekday_names=weekday_names,
weeks=weeks,
prev_month=prev_month,
prev_month_year=prev_month_year,
next_month=next_month,
next_month_year=next_month_year,
prev_year=prev_year,
next_year=next_year,
user_entries=user_entries,
confirmed_entries=confirmed_entries,
month_entries=month_entries,
)
return await make_response(html)
@bp.put("/")
@require_admin
@clear_cache(tag="calendars", tag_scope="all")
async def put(calendar_slug: str, **kwargs):
"""
Idempotent update for calendar configuration.
Accepts HTMX form (POST/PUT) and optional JSON.
"""
# Try JSON first
data = await request.get_json(silent=True)
description = None
if data and isinstance(data, dict):
description = (data.get("description") or "").strip()
else:
form = await request.form
description = (form.get("description") or "").strip()
await update_calendar_description(g.calendar, description)
html = await render_template("_types/calendar/admin/index.html")
return await make_response(html, 200)
@bp.delete("/")
@require_admin
@clear_cache(tag="calendars", tag_scope="all")
async def delete(calendar_slug: str, **kwargs):
from shared.browser.app.utils.htmx import is_htmx_request
cal = g.calendar
cal.deleted_at = datetime.now(timezone.utc)
await g.s.flush()
# If we have post context (blog-embedded mode), update nav
post_data = getattr(g, "post_data", None)
html = await render_template("_types/calendars/index.html")
if post_data:
from ..post.services.entry_associations import get_associated_entries
post_id = (post_data.get("post") or {}).get("id")
cals = (
await g.s.execute(
select(Calendar)
.where(Calendar.container_type == "page", Calendar.container_id == post_id, Calendar.deleted_at.is_(None))
.order_by(Calendar.name.asc())
)
).scalars().all()
associated_entries = await get_associated_entries(g.s, post_id)
nav_oob = await render_template(
"_types/post/admin/_nav_entries_oob.html",
associated_entries=associated_entries,
calendars=cals,
post=post_data["post"],
)
html = html + nav_oob
return await make_response(html, 200)
return bp