feat: build headless theatre laboratory website

This commit is contained in:
bisco
2026-06-22 19:42:43 +02:00
parent cae9180bc6
commit cff9a17c3c
65 changed files with 8994 additions and 128 deletions
+1
View File
@@ -0,0 +1 @@
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class HomeConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "home"
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1,212 @@
from io import BytesIO
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand
from PIL import Image as PillowImage
from wagtail.images import get_image_model
from wagtail.models import Page, Site
from home.models import GalleryItem, HomePage, LessonInfo, Show, SiteSettings, TeacherProfile
class Command(BaseCommand):
help = "Create or refresh the Italian demo content without creating duplicates."
def placeholder(self, title: str, filename: str, color: str):
image_model = get_image_model()
existing = image_model.objects.filter(title=title).first()
if existing:
return existing
output = BytesIO()
PillowImage.new("RGB", (1200, 800), color=color).save(
output,
format="JPEG",
quality=85,
)
return image_model.objects.create(
title=title,
file=ContentFile(output.getvalue(), name=filename),
)
def handle(self, *args, **options):
hero_image = self.placeholder("Prova in sala", "demo-hero.jpg", "#D8C7AE")
laboratory_image = self.placeholder(
"Esercizio di gruppo", "demo-laboratory.jpg", "#A9A497"
)
teacher_image = self.placeholder("Il maestro", "demo-teacher.jpg", "#B8674C")
show_image = self.placeholder("Locandina Tracce", "demo-show.jpg", "#515D53")
second_show_image = self.placeholder(
"Locandina Sottovoce", "demo-show-sottovoce.jpg", "#B8674C"
)
gallery_images = [
self.placeholder("Lezione", "demo-gallery-lesson.jpg", "#D8C7AE"),
self.placeholder("Backstage", "demo-gallery-backstage.jpg", "#A4513B"),
self.placeholder("In scena", "demo-gallery-show.jpg", "#515D53"),
self.placeholder("Il gruppo", "demo-gallery-group.jpg", "#4A433A"),
]
homepage = HomePage.objects.first()
if homepage is None:
homepage = HomePage(
title="Home",
slug="laboratorio",
hero_title="Il teatro diventa presenza.",
hero_subtitle=(
"Un laboratorio dove corpo, voce, ascolto e relazione "
"diventano scena."
),
)
Page.get_first_root_node().add_child(instance=homepage)
homepage.hero_title = "Il teatro diventa presenza."
homepage.hero_subtitle = (
"Un laboratorio dove corpo, voce, ascolto e relazione diventano scena."
)
homepage.hero_image = hero_image
homepage.intro_title = "Uno spazio per esserci davvero"
homepage.intro_body = (
"<p>Ogni incontro è uno spazio di pratica: si prova, si sbaglia, "
"si ascolta, si ricomincia. Il gruppo diventa materia viva, la scena "
"diventa occasione di scoperta.</p>"
)
homepage.cta_primary_label = "Vieni a conoscerci"
homepage.cta_primary_url = "#contatti"
homepage.cta_secondary_label = "Guarda la galleria"
homepage.cta_secondary_url = "#galleria"
homepage.laboratory_title = "Il laboratorio"
homepage.laboratory_body = (
"<p>Corpo, voce e improvvisazione sono strumenti per allenare "
"l'ascolto, la relazione e una presenza scenica autentica.</p>"
)
homepage.laboratory_image = laboratory_image
homepage.save_revision().publish()
homepage.feature_cards.all().delete()
cards = [
(
"Per chi inizia",
"Non serve esperienza: servono curiosità e voglia di mettersi in gioco.",
),
(
"Per chi vuole crescere",
"Una pratica costante per conoscere meglio corpo, voce e presenza.",
),
(
"Per chi ama il gruppo",
"La scena nasce dall'ascolto e dalla fiducia costruita insieme.",
),
]
for order, (title, text) in enumerate(cards):
homepage.feature_cards.create(title=title, text=text, sort_order=order)
homepage.save_revision().publish()
site = Site.objects.filter(is_default_site=True).first()
if site is None:
site = Site.objects.create(
hostname="localhost",
port=8000,
site_name="Laboratorio Teatrale",
root_page=homepage,
is_default_site=True,
)
elif site.root_page_id != homepage.id:
site.root_page = homepage
site.site_name = "Laboratorio Teatrale"
site.save(update_fields=["root_page", "site_name"])
settings, _ = SiteSettings.objects.get_or_create(site=site)
settings.laboratory_name = "Laboratorio Teatrale"
settings.short_claim = "Il teatro diventa presenza."
settings.email = "ciao@laboratorioteatrale.it"
settings.phone = "+39 333 123 4567"
settings.whatsapp = "+39 333 123 4567"
settings.instagram = "https://www.instagram.com/"
settings.facebook = "https://www.facebook.com/"
settings.address = "Via del Teatro 12, Roma"
settings.footer_text = "Uno spazio di ricerca, ascolto e presenza."
settings.save()
TeacherProfile.objects.update_or_create(
name="Andrea Rossi",
defaults={
"photo": teacher_image,
"short_bio": (
"Attore, regista e formatore. Accompagna gruppi e persone "
"nella ricerca di una presenza scenica sincera."
),
"full_bio": (
"<p>Da oltre quindici anni lavora tra formazione, creazione "
"collettiva e spettacolo dal vivo.</p>"
),
"quote": "Il teatro non è fingere: è imparare a essere presenti.",
},
)
LessonInfo.objects.update_or_create(
day_time="Ogni mercoledì, 19:3022:00",
defaults={
"location": "Spazio Teatro, Via del Teatro 12, Roma",
"audience": "Adulti, con o senza esperienza precedente.",
"description": (
"<p>Training fisico e vocale, improvvisazione, ascolto, "
"composizione e lavoro sulla scena.</p>"
),
"trial_lesson": "La prima lezione di prova è gratuita su prenotazione.",
"notes": "Abbigliamento comodo consigliato.",
},
)
Show.objects.update_or_create(
title="Tracce",
year=2025,
defaults={
"short_description": (
"Un lavoro corale sulle storie che lasciamo e che ci attraversano."
),
"description": (
"<p>Esito scenico nato dalle improvvisazioni e dalle scritture "
"del gruppo.</p>"
),
"poster": show_image,
"location": "Teatro di Quartiere",
"cast": "Allieve e allievi del laboratorio",
"order": 0,
"visible_on_home": True,
},
)
Show.objects.update_or_create(
title="Sottovoce",
year=2024,
defaults={
"short_description": (
"Un attraversamento delicato delle parole che restano inascoltate."
),
"description": (
"<p>Un progetto costruito attraverso voce, silenzio e movimento.</p>"
),
"poster": second_show_image,
"location": "Spazio Teatro",
"cast": "Laboratorio annuale",
"order": 1,
"visible_on_home": True,
},
)
gallery_entries = [
(GalleryItem.Category.LESSONS, "Ascolto e movimento"),
(GalleryItem.Category.BACKSTAGE, "Prima dello spettacolo"),
(GalleryItem.Category.SHOWS, "Una scena di Tracce"),
(GalleryItem.Category.GROUP, "Il laboratorio, insieme"),
]
for order, (image, (category, caption)) in enumerate(
zip(gallery_images, gallery_entries, strict=True)
):
GalleryItem.objects.update_or_create(
image=image,
defaults={
"category": category,
"caption": caption,
"order": order,
"visible_on_home": True,
},
)
self.stdout.write(self.style.SUCCESS("Demo content is ready."))
+116
View File
@@ -0,0 +1,116 @@
import django.db.models.deletion
import modelcluster.fields
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("wagtailcore", "0001_squashed_0016_change_page_url_path_to_text_field"),
("wagtailimages", "0001_squashed_0021"),
]
operations = [
migrations.CreateModel(
name="GalleryItem",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("category", models.CharField(choices=[("lessons", "Lessons"), ("backstage", "Backstage"), ("shows", "Shows"), ("group", "Group")], max_length=20)),
("caption", models.CharField(blank=True, max_length=255)),
("order", models.PositiveIntegerField(default=0)),
("visible_on_home", models.BooleanField(default=True)),
("image", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="+", to="wagtailimages.image")),
],
options={"ordering": ["order", "id"]},
),
migrations.CreateModel(
name="HomePage",
fields=[
("page_ptr", models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to="wagtailcore.page")),
("hero_title", models.CharField(max_length=180)),
("hero_subtitle", models.TextField()),
("intro_title", models.CharField(blank=True, max_length=180)),
("intro_body", wagtail.fields.RichTextField(blank=True)),
("cta_primary_label", models.CharField(blank=True, max_length=80)),
("cta_primary_url", models.CharField(blank=True, max_length=255)),
("cta_secondary_label", models.CharField(blank=True, max_length=80)),
("cta_secondary_url", models.CharField(blank=True, max_length=255)),
("laboratory_title", models.CharField(blank=True, max_length=180)),
("laboratory_body", wagtail.fields.RichTextField(blank=True)),
("hero_image", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="wagtailimages.image")),
("laboratory_image", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="wagtailimages.image")),
],
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="LessonInfo",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("day_time", models.CharField(max_length=180)),
("location", models.CharField(max_length=255)),
("audience", models.TextField()),
("description", wagtail.fields.RichTextField()),
("trial_lesson", models.CharField(blank=True, max_length=255)),
("notes", models.TextField(blank=True)),
],
options={"verbose_name": "lesson information", "verbose_name_plural": "lesson information"},
),
migrations.CreateModel(
name="Show",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(max_length=180)),
("year", models.PositiveSmallIntegerField()),
("short_description", models.TextField()),
("description", wagtail.fields.RichTextField(blank=True)),
("location", models.CharField(blank=True, max_length=255)),
("cast", models.TextField(blank=True)),
("order", models.PositiveIntegerField(default=0)),
("visible_on_home", models.BooleanField(default=True)),
("poster", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="wagtailimages.image")),
],
options={"ordering": ["order", "-year", "title"]},
),
migrations.CreateModel(
name="SiteSettings",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("laboratory_name", models.CharField(default="Laboratorio Teatrale", max_length=120)),
("short_claim", models.CharField(blank=True, max_length=180)),
("email", models.EmailField(blank=True, max_length=254)),
("phone", models.CharField(blank=True, max_length=40)),
("whatsapp", models.CharField(blank=True, max_length=40)),
("instagram", models.URLField(blank=True)),
("facebook", models.URLField(blank=True)),
("address", models.CharField(blank=True, max_length=255)),
("footer_text", models.CharField(blank=True, max_length=255)),
("site", models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to="wagtailcore.site")),
],
options={"verbose_name": "site settings", "abstract": False},
),
migrations.CreateModel(
name="TeacherProfile",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=120)),
("short_bio", models.TextField()),
("full_bio", wagtail.fields.RichTextField(blank=True)),
("quote", models.TextField(blank=True)),
("photo", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="wagtailimages.image")),
],
options={"verbose_name": "teacher profile"},
),
migrations.CreateModel(
name="FeatureCard",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("sort_order", models.IntegerField(blank=True, editable=False, null=True)),
("title", models.CharField(max_length=120)),
("text", models.TextField()),
("page", modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name="feature_cards", to="home.homepage")),
],
options={"ordering": ["sort_order"], "abstract": False},
),
]
+1
View File
@@ -0,0 +1 @@
+245
View File
@@ -0,0 +1,245 @@
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
from wagtail.fields import RichTextField
from wagtail.models import Orderable, Page
from wagtail.snippets.models import register_snippet
@register_setting(icon="cog")
class SiteSettings(BaseSiteSetting):
laboratory_name = models.CharField(max_length=120, default="Laboratorio Teatrale")
short_claim = models.CharField(max_length=180, blank=True)
email = models.EmailField(blank=True)
phone = models.CharField(max_length=40, blank=True)
whatsapp = models.CharField(max_length=40, blank=True)
instagram = models.URLField(blank=True)
facebook = models.URLField(blank=True)
address = models.CharField(max_length=255, blank=True)
footer_text = models.CharField(max_length=255, blank=True)
panels = [
MultiFieldPanel(
[FieldPanel("laboratory_name"), FieldPanel("short_claim")],
heading="Identity",
),
MultiFieldPanel(
[
FieldPanel("email"),
FieldPanel("phone"),
FieldPanel("whatsapp"),
FieldPanel("instagram"),
FieldPanel("facebook"),
FieldPanel("address"),
],
heading="Contacts",
),
FieldPanel("footer_text"),
]
class Meta:
verbose_name = "site settings"
class HomePage(Page):
hero_title = models.CharField(max_length=180)
hero_subtitle = models.TextField()
hero_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
intro_title = models.CharField(max_length=180, blank=True)
intro_body = RichTextField(blank=True)
cta_primary_label = models.CharField(max_length=80, blank=True)
cta_primary_url = models.CharField(max_length=255, blank=True)
cta_secondary_label = models.CharField(max_length=80, blank=True)
cta_secondary_url = models.CharField(max_length=255, blank=True)
laboratory_title = models.CharField(max_length=180, blank=True)
laboratory_body = RichTextField(blank=True)
laboratory_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
max_count = 1
subpage_types: list[str] = []
content_panels = Page.content_panels + [
MultiFieldPanel(
[
FieldPanel("hero_title"),
FieldPanel("hero_subtitle"),
FieldPanel("hero_image"),
],
heading="Hero",
),
MultiFieldPanel(
[FieldPanel("intro_title"), FieldPanel("intro_body")],
heading="Manifesto",
),
MultiFieldPanel(
[
FieldPanel("cta_primary_label"),
FieldPanel("cta_primary_url"),
FieldPanel("cta_secondary_label"),
FieldPanel("cta_secondary_url"),
],
heading="Calls to action",
),
InlinePanel("feature_cards", label="Feature card", max_num=3),
MultiFieldPanel(
[
FieldPanel("laboratory_title"),
FieldPanel("laboratory_body"),
FieldPanel("laboratory_image"),
],
heading="Laboratory",
),
]
class FeatureCard(Orderable):
page = ParentalKey(
HomePage,
on_delete=models.CASCADE,
related_name="feature_cards",
)
title = models.CharField(max_length=120)
text = models.TextField()
panels = [FieldPanel("title"), FieldPanel("text")]
def __str__(self) -> str:
return self.title
@register_snippet
class TeacherProfile(models.Model):
name = models.CharField(max_length=120)
photo = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
short_bio = models.TextField()
full_bio = RichTextField(blank=True)
quote = models.TextField(blank=True)
panels = [
FieldPanel("name"),
FieldPanel("photo"),
FieldPanel("short_bio"),
FieldPanel("full_bio"),
FieldPanel("quote"),
]
class Meta:
verbose_name = "teacher profile"
def __str__(self) -> str:
return self.name
@register_snippet
class LessonInfo(models.Model):
day_time = models.CharField(max_length=180)
location = models.CharField(max_length=255)
audience = models.TextField()
description = RichTextField()
trial_lesson = models.CharField(max_length=255, blank=True)
notes = models.TextField(blank=True)
panels = [
FieldPanel("day_time"),
FieldPanel("location"),
FieldPanel("audience"),
FieldPanel("description"),
FieldPanel("trial_lesson"),
FieldPanel("notes"),
]
class Meta:
verbose_name = "lesson information"
verbose_name_plural = "lesson information"
def __str__(self) -> str:
return self.day_time
@register_snippet
class Show(models.Model):
title = models.CharField(max_length=180)
year = models.PositiveSmallIntegerField()
short_description = models.TextField()
description = RichTextField(blank=True)
poster = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
location = models.CharField(max_length=255, blank=True)
cast = models.TextField(blank=True)
order = models.PositiveIntegerField(default=0)
visible_on_home = models.BooleanField(default=True)
panels = [
FieldPanel("title"),
FieldPanel("year"),
FieldPanel("short_description"),
FieldPanel("description"),
FieldPanel("poster"),
FieldPanel("location"),
FieldPanel("cast"),
FieldPanel("order"),
FieldPanel("visible_on_home"),
]
class Meta:
ordering = ["order", "-year", "title"]
def __str__(self) -> str:
return f"{self.title} ({self.year})"
@register_snippet
class GalleryItem(models.Model):
class Category(models.TextChoices):
LESSONS = "lessons", "Lessons"
BACKSTAGE = "backstage", "Backstage"
SHOWS = "shows", "Shows"
GROUP = "group", "Group"
image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.CASCADE,
related_name="+",
)
category = models.CharField(max_length=20, choices=Category.choices)
caption = models.CharField(max_length=255, blank=True)
order = models.PositiveIntegerField(default=0)
visible_on_home = models.BooleanField(default=True)
panels = [
FieldPanel("image"),
FieldPanel("category"),
FieldPanel("caption"),
FieldPanel("order"),
FieldPanel("visible_on_home"),
]
class Meta:
ordering = ["order", "id"]
def __str__(self) -> str:
return self.caption or self.image.title
+1
View File
@@ -0,0 +1 @@
+70
View File
@@ -0,0 +1,70 @@
import tempfile
from django.core.management import call_command
from django.test import TestCase, override_settings
from home.models import GalleryItem, HomePage, Show
@override_settings(WAGTAILADMIN_BASE_URL="https://cms.example.test")
class HomeApiTests(TestCase):
def setUp(self):
self.media_directory = tempfile.TemporaryDirectory()
self.settings_override = override_settings(MEDIA_ROOT=self.media_directory.name)
self.settings_override.enable()
call_command("seed_demo", verbosity=0)
def tearDown(self):
self.settings_override.disable()
self.media_directory.cleanup()
def test_home_endpoint_returns_aggregated_content(self):
response = self.client.get("/api/site/home/", HTTP_HOST="localhost")
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(
set(payload),
{
"settings",
"homepage",
"feature_cards",
"teacher",
"lesson_info",
"shows",
"gallery_items",
},
)
self.assertEqual(payload["homepage"]["hero_title"], "Il teatro diventa presenza.")
self.assertEqual(len(payload["feature_cards"]), 3)
self.assertTrue(
payload["homepage"]["hero_image"]["url"].startswith(
"https://cms.example.test/media/"
)
)
self.assertEqual(payload["homepage"]["hero_image"]["alt"], "Prova in sala")
def test_home_endpoint_only_includes_visible_items(self):
Show.objects.update(visible_on_home=False)
GalleryItem.objects.update(visible_on_home=False)
payload = self.client.get("/api/site/home/", HTTP_HOST="localhost").json()
self.assertEqual(payload["shows"], [])
self.assertEqual(payload["gallery_items"], [])
def test_seed_command_is_idempotent(self):
call_command("seed_demo", verbosity=0)
self.assertEqual(HomePage.objects.count(), 1)
self.assertEqual(HomePage.objects.first().feature_cards.count(), 3)
self.assertEqual(Show.objects.count(), 2)
self.assertEqual(GalleryItem.objects.count(), 4)
class HealthTests(TestCase):
def test_health_endpoint_checks_database(self):
response = self.client.get("/health/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"status": "ok"})
+139
View File
@@ -0,0 +1,139 @@
from urllib.parse import urljoin
from django.conf import settings as django_settings
from django.db import DatabaseError, connection
from django.http import JsonResponse
from django.views.decorators.http import require_GET
from wagtail.models import Site
from .models import GalleryItem, HomePage, LessonInfo, Show, SiteSettings, TeacherProfile
def serialize_image(request, image):
if image is None:
return None
public_base_url = django_settings.WAGTAILADMIN_BASE_URL or request.build_absolute_uri("/")
return {
"url": urljoin(f"{public_base_url.rstrip('/')}/", image.file.url),
"alt": image.title,
}
def find_site(request):
return Site.find_for_request(request) or Site.objects.filter(is_default_site=True).first()
@require_GET
def health(request):
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
except DatabaseError:
return JsonResponse({"status": "unavailable"}, status=503)
return JsonResponse({"status": "ok"})
@require_GET
def home_api(request):
site = find_site(request)
settings = SiteSettings.for_site(site) if site else None
homepage = (
HomePage.objects.live().filter(id=site.root_page_id).first()
if site
else HomePage.objects.live().first()
)
teacher = TeacherProfile.objects.order_by("id").first()
lesson = LessonInfo.objects.order_by("id").first()
settings_data = None
if settings:
settings_data = {
"laboratory_name": settings.laboratory_name,
"short_claim": settings.short_claim,
"email": settings.email,
"phone": settings.phone,
"whatsapp": settings.whatsapp,
"instagram": settings.instagram,
"facebook": settings.facebook,
"address": settings.address,
"footer_text": settings.footer_text,
}
homepage_data = None
feature_cards = []
if homepage:
homepage_data = {
"hero_title": homepage.hero_title,
"hero_subtitle": homepage.hero_subtitle,
"hero_image": serialize_image(request, homepage.hero_image),
"intro_title": homepage.intro_title,
"intro_body": str(homepage.intro_body),
"cta_primary_label": homepage.cta_primary_label,
"cta_primary_url": homepage.cta_primary_url,
"cta_secondary_label": homepage.cta_secondary_label,
"cta_secondary_url": homepage.cta_secondary_url,
"laboratory_title": homepage.laboratory_title,
"laboratory_body": str(homepage.laboratory_body),
"laboratory_image": serialize_image(request, homepage.laboratory_image),
}
feature_cards = [
{"title": card.title, "text": card.text, "order": card.sort_order}
for card in homepage.feature_cards.all()
]
teacher_data = None
if teacher:
teacher_data = {
"name": teacher.name,
"photo": serialize_image(request, teacher.photo),
"short_bio": teacher.short_bio,
"full_bio": str(teacher.full_bio),
"quote": teacher.quote,
}
lesson_data = None
if lesson:
lesson_data = {
"day_time": lesson.day_time,
"location": lesson.location,
"audience": lesson.audience,
"description": str(lesson.description),
"trial_lesson": lesson.trial_lesson,
"notes": lesson.notes,
}
shows = [
{
"title": show.title,
"year": show.year,
"short_description": show.short_description,
"description": str(show.description),
"poster": serialize_image(request, show.poster),
"location": show.location,
"cast": show.cast,
"order": show.order,
}
for show in Show.objects.filter(visible_on_home=True).select_related("poster")
]
gallery_items = [
{
"image": serialize_image(request, item.image),
"category": item.category,
"caption": item.caption,
"order": item.order,
}
for item in GalleryItem.objects.filter(visible_on_home=True).select_related("image")
]
return JsonResponse(
{
"settings": settings_data,
"homepage": homepage_data,
"feature_cards": feature_cards,
"teacher": teacher_data,
"lesson_info": lesson_data,
"shows": shows,
"gallery_items": gallery_items,
}
)