Simplify templates to pure Jinja2 + CodeMirror editor + variable reference
Some checks failed
Validate / Hassfest (push) Has been cancelled

Major template system overhaul:
- TemplateConfig simplified from 21 fields to 9: removed all sub-templates
  (asset_image, asset_video, assets_format, people_format, etc.)
  Users write full Jinja2 with {% for %}, {% if %} inline.
- Default EN/RU templates seeded on first startup (user_id=0, system-owned)
  with proper Jinja2 loops over added_assets, people, albums.
- build_full_context() simplified: passes raw data directly to Jinja2
  instead of pre-rendering sub-templates.
- CodeMirror editor for template slots (HTML syntax highlighting,
  line wrapping, dark theme support via oneDark).
- Variable reference API: GET /api/template-configs/variables returns
  per-slot variable descriptions + asset_fields for loop contexts.
- Variable reference modal in UI: click "{{ }} Variables" next to any
  slot to see available variables with Jinja2 syntax examples.
- Route ordering fix: /variables registered before /{config_id}.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 18:57:51 +03:00
parent bc8fda5984
commit 0bb4d8a949
9 changed files with 791 additions and 181 deletions

View File

@@ -39,101 +39,21 @@ def build_full_context(
event_data: dict[str, Any],
template_config: TemplateConfig | None = None,
) -> dict[str, Any]:
"""Build the full template context with all variables from the blueprint.
"""Build template context by passing raw data directly to Jinja2.
This assembles the ~40 variables the blueprint supports:
- Direct event fields (album_name, added_count, etc.)
- Computed fields (common_date, common_location, people, assets, video_warning)
- Formatted sub-templates (asset items, people format, etc.)
The templates use {% for %}, {% if %} etc. to handle formatting,
so no pre-rendering of sub-templates is needed.
"""
tc = template_config
ctx = dict(event_data)
# People formatting
people_list = ctx.get("people", [])
if isinstance(people_list, list) and people_list and tc:
people_str = ", ".join(str(p) for p in people_list)
ctx["people"] = _render(tc.message_people_format, {"people": people_str})
elif isinstance(people_list, list):
ctx["people"] = ", ".join(str(p) for p in people_list) if people_list else ""
else:
ctx["people"] = str(people_list) if people_list else ""
# Asset list formatting
added_assets = ctx.get("added_assets", [])
if added_assets and tc:
date_fmt = tc.date_format or "%d.%m.%Y, %H:%M UTC"
# Detect common date/location
dates = set()
locations = set()
for a in added_assets:
if a.get("created_at"):
try:
dt = datetime.fromisoformat(str(a["created_at"]).replace("Z", "+00:00"))
dates.add(dt.strftime(date_fmt))
except (ValueError, TypeError):
pass
loc_parts = [a.get("city"), a.get("country")]
loc = ", ".join(p for p in loc_parts if p)
if loc:
locations.add(loc)
common_date = ""
if len(dates) == 1:
common_date = _render(tc.common_date_template, {"date": next(iter(dates))})
ctx["common_date"] = common_date
common_location = ""
if len(locations) == 1:
common_location = _render(tc.common_location_template, {"location": next(iter(locations))})
ctx["common_location"] = common_location
# Format individual assets
asset_lines = []
for a in added_assets:
asset_type = a.get("type", "IMAGE")
tmpl = tc.message_asset_image if asset_type == "IMAGE" else tc.message_asset_video
# Per-asset date (only if dates differ)
created_if_unique = ""
if len(dates) > 1 and a.get("created_at"):
try:
dt = datetime.fromisoformat(str(a["created_at"]).replace("Z", "+00:00"))
created_if_unique = _render(tc.date_if_unique_template, {"date": dt.strftime(date_fmt)})
except (ValueError, TypeError):
pass
# Per-asset location
location_if_unique = ""
loc_parts = [a.get("city"), a.get("country")]
loc = ", ".join(p for p in loc_parts if p)
if loc and len(locations) > 1:
location_if_unique = _render(tc.location_if_unique_template, {"location": loc})
# Favorite indicator
fav = tc.favorite_indicator if a.get("is_favorite") else ""
asset_ctx = {
**a,
"created_if_unique": created_if_unique,
"location_if_unique": location_if_unique,
"location": loc,
"is_favorite": fav,
}
asset_lines.append(_render(tmpl, asset_ctx))
# Assemble assets list
assets_str = "".join(asset_lines)
ctx["assets"] = _render(tc.message_assets_format, {"assets": assets_str})
else:
ctx.setdefault("assets", "")
ctx.setdefault("common_date", "")
ctx.setdefault("common_location", "")
# Ensure lists are actual lists (not strings)
if isinstance(ctx.get("people"), str):
ctx["people"] = [ctx["people"]] if ctx["people"] else []
# Video warning
added_assets = ctx.get("added_assets", [])
has_videos = any(a.get("type") == "VIDEO" for a in added_assets) if added_assets else False
ctx["video_warning"] = (tc.video_warning if tc and has_videos else "")
ctx["video_warning"] = (template_config.video_warning if template_config and has_videos else "")
return ctx