generated from bisco/codex-bootstrap
feat: build headless theatre laboratory website
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class HomeConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "home"
|
||||
@@ -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:30–22: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."))
|
||||
@@ -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},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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"})
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user