Refactor slides to use StreamField in EventPage and HomePage

Replaced legacy slide fields with a flexible StreamField approach for EventPage and HomePage models. Includes migrations for data transfer, template updates to support the new StreamField, and deprecation of legacy fields for backward compatibility.
main
Arne Schauf 2 weeks ago
parent c3fa6104f5
commit ae29bd6b19
  1. 24
      README.md
  2. 141
      core/migrations/0016_refactor_eventpage_slides.py
  3. 79
      core/migrations/0017_homepage_slides.py
  4. 109
      core/models.py
  5. 94
      core/templates/core/event_page.html
  6. 40
      core/templates/core/home_page.html
  7. 15
      feo_homepage/settings/prod.py

@ -0,0 +1,24 @@
# HomePage Slides Refactoring
This change refactors the slides on the HomePage to use a StreamField instead of individual fields for each slide. This makes the slides more flexible and allows for an arbitrary number of slides.
## Changes Made
1. Added a new `slides` StreamField to the HomePage model
2. Updated the HomePage content panels to include the new StreamField
3. Updated the home_page.html template to render the new StreamField slides
4. Created a migration for the changes to the HomePage model
## Data Migration
The existing slide fields are still available for backward compatibility, but they are marked as deprecated. The template will first check for the new StreamField slides, and fall back to the legacy fields if the StreamField is empty.
A data migration has been integrated into the Django migration system to automatically transfer existing data from the legacy slide fields to the new StreamField. When you run the migrations, the system will:
1. Add the new `slides` StreamField to the HomePage model
2. Automatically migrate any existing data from the legacy slide fields to the new StreamField
3. Preserve the legacy fields for backward compatibility
This means you don't need to manually migrate the data - it's handled automatically by the migration system.
After confirming that all data has been successfully migrated and that the new StreamField is working correctly in production, you can remove the legacy fields from the HomePage model in a future update.

@ -0,0 +1,141 @@
# Generated by Django 5.2 on 2025-05-01 12:00
import json
from django.db import migrations, models
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
def migrate_slides_to_streamfield(apps, schema_editor):
EventPage = apps.get_model('core', 'EventPage')
for page in EventPage.objects.all():
slides = []
# Check if slide1_img exists and add it to the slides list
if page.slide1_img_id:
slides.append({
'type': 'slide',
'value': {
'image': page.slide1_img_id,
'headline': page.slide1_headline,
'subline': page.slide1_subline,
'link_url': page.slide1_link_url,
'link_text': page.slide1_link_text,
},
'id': '1'
})
# Check if slide2_img exists and add it to the slides list
if page.slide2_img_id:
slides.append({
'type': 'slide',
'value': {
'image': page.slide2_img_id,
'headline': page.slide2_headline,
'subline': page.slide2_subline,
'link_url': page.slide2_link_url,
'link_text': page.slide2_link_text,
},
'id': '2'
})
# Check if slide3_img exists and add it to the slides list
if page.slide3_img_id:
slides.append({
'type': 'slide',
'value': {
'image': page.slide3_img_id,
'headline': page.slide3_headline,
'subline': page.slide3_subline,
'link_url': page.slide3_link_url,
'link_text': page.slide3_link_text,
},
'id': '3'
})
# Save the slides to the new StreamField
if slides:
page.slides = json.dumps(slides)
page.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0015_remove_eventpage_external_registration_code_and_more'),
]
operations = [
# Add the new slides StreamField
migrations.AddField(
model_name='eventpage',
name='slides',
field=wagtail.fields.StreamField([('slide', wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('headline', wagtail.blocks.CharBlock(max_length=512, required=False)), ('subline', wagtail.blocks.CharBlock(max_length=512, required=False)), ('link_url', wagtail.blocks.URLBlock(required=False)), ('link_text', wagtail.blocks.CharBlock(max_length=64, required=False))]))], blank=True, null=True, use_json_field=True),
),
# Run the data migration
migrations.RunPython(migrate_slides_to_streamfield),
# Remove the old slide fields
migrations.RemoveField(
model_name='eventpage',
name='slide1_img',
),
migrations.RemoveField(
model_name='eventpage',
name='slide1_headline',
),
migrations.RemoveField(
model_name='eventpage',
name='slide1_subline',
),
migrations.RemoveField(
model_name='eventpage',
name='slide1_link_url',
),
migrations.RemoveField(
model_name='eventpage',
name='slide1_link_text',
),
migrations.RemoveField(
model_name='eventpage',
name='slide2_img',
),
migrations.RemoveField(
model_name='eventpage',
name='slide2_headline',
),
migrations.RemoveField(
model_name='eventpage',
name='slide2_subline',
),
migrations.RemoveField(
model_name='eventpage',
name='slide2_link_url',
),
migrations.RemoveField(
model_name='eventpage',
name='slide2_link_text',
),
migrations.RemoveField(
model_name='eventpage',
name='slide3_img',
),
migrations.RemoveField(
model_name='eventpage',
name='slide3_headline',
),
migrations.RemoveField(
model_name='eventpage',
name='slide3_subline',
),
migrations.RemoveField(
model_name='eventpage',
name='slide3_link_url',
),
migrations.RemoveField(
model_name='eventpage',
name='slide3_link_text',
),
]

@ -0,0 +1,79 @@
# Generated by Django 4.2.1 on 2023-11-15 12:00
from django.db import migrations
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
def migrate_legacy_slides_to_streamfield(apps, schema_editor):
"""
Migrate data from legacy slide fields to the new StreamField.
"""
HomePage = apps.get_model('core', 'HomePage')
# Get all HomePage instances
homepages = HomePage.objects.all()
# For each HomePage, migrate the legacy slides to the new StreamField
for homepage in homepages:
slides = []
# Check if slide1 exists
if homepage.slide1_img:
slides.append({
'type': 'slide',
'value': {
'image': homepage.slide1_img.id,
'headline': homepage.slide1_headline,
'subline': homepage.slide1_subline,
'link_url': homepage.slide1_link_url,
'link_text': homepage.slide1_link_text,
}
})
# Check if slide2 exists
if homepage.slide2_img:
slides.append({
'type': 'slide',
'value': {
'image': homepage.slide2_img.id,
'headline': homepage.slide2_headline,
'subline': homepage.slide2_subline,
'link_url': homepage.slide2_link_url,
'link_text': homepage.slide2_link_text,
}
})
# Check if slide3 exists
if homepage.slide3_img:
slides.append({
'type': 'slide',
'value': {
'image': homepage.slide3_img.id,
'headline': homepage.slide3_headline,
'subline': homepage.slide3_subline,
'link_url': homepage.slide3_link_url,
'link_text': homepage.slide3_link_text,
}
})
# Set the new slides StreamField
homepage.slides = slides
homepage.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0016_refactor_eventpage_slides'),
]
operations = [
migrations.AddField(
model_name='homepage',
name='slides',
field=wagtail.fields.StreamField([('slide', wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('headline', wagtail.blocks.CharBlock(max_length=512, required=False)), ('subline', wagtail.blocks.CharBlock(max_length=512, required=False)), ('link_url', wagtail.blocks.URLBlock(required=False)), ('link_text', wagtail.blocks.CharBlock(max_length=64, required=False))]))], blank=True, null=True, use_json_field=True),
),
migrations.RunPython(migrate_legacy_slides_to_streamfield, migrations.RunPython.noop),
]

@ -63,7 +63,18 @@ class MediaBlock(AbstractMediaChooserBlock):
class HomePage(Page): class HomePage(Page):
block1 = RichTextField(blank=True) block1 = RichTextField(blank=True)
# revolution slider # New StreamField for slides
slides = StreamField([
('slide', blocks.StructBlock([
('image', ImageChooserBlock()),
('headline', blocks.CharBlock(max_length=512, required=False)),
('subline', blocks.CharBlock(max_length=512, required=False)),
('link_url', blocks.URLBlock(required=False)),
('link_text', blocks.CharBlock(max_length=64, required=False)),
])),
], null=True, blank=True, use_json_field=True)
# Legacy fields for backward compatibility - deprecated
slide1_img = models.ForeignKey( slide1_img = models.ForeignKey(
get_image_model(), get_image_model(),
null=True, null=True,
@ -153,31 +164,6 @@ class HomePage(Page):
thumbnail3_text = RichTextField(blank=True) thumbnail3_text = RichTextField(blank=True)
REVOLUTION_SLIDER_FIELDS = [
MultiFieldPanel([
FieldPanel('slide1_img'),
FieldPanel('slide1_headline'),
FieldPanel('slide1_subline'),
FieldPanel('slide1_link_url'),
FieldPanel('slide1_link_text'),
], heading='Slide 1', classname="collapsible"),
MultiFieldPanel([
FieldPanel('slide2_img'),
FieldPanel('slide2_headline'),
FieldPanel('slide2_subline'),
FieldPanel('slide2_link_url'),
FieldPanel('slide2_link_text'),
], heading='Slide 2', classname="collapsible"),
MultiFieldPanel([
FieldPanel('slide3_img'),
FieldPanel('slide3_headline'),
FieldPanel('slide3_subline'),
FieldPanel('slide3_link_url'),
FieldPanel('slide3_link_text'),
], heading='Slide 3'),
]
HOME_THUMBNAIL_FIELDS = [ HOME_THUMBNAIL_FIELDS = [
MultiFieldPanel([ MultiFieldPanel([
@ -205,9 +191,32 @@ HOME_THUMBNAIL_FIELDS = [
HomePage.content_panels = [ HomePage.content_panels = [
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
MultiFieldPanel(REVOLUTION_SLIDER_FIELDS, heading='Slider elements', classname="collapsible collapsed"), FieldPanel('slides', heading='Slider elements'),
FieldPanel('block1'), FieldPanel('block1'),
MultiFieldPanel(HOME_THUMBNAIL_FIELDS, heading='Thumbnail elements', classname="collapsible collapsed"), MultiFieldPanel(HOME_THUMBNAIL_FIELDS, heading='Thumbnail elements', classname="collapsible collapsed"),
MultiFieldPanel([
MultiFieldPanel([
FieldPanel('slide1_img'),
FieldPanel('slide1_headline'),
FieldPanel('slide1_subline'),
FieldPanel('slide1_link_url'),
FieldPanel('slide1_link_text'),
], heading='Slide 1', classname="collapsible"),
MultiFieldPanel([
FieldPanel('slide2_img'),
FieldPanel('slide2_headline'),
FieldPanel('slide2_subline'),
FieldPanel('slide2_link_url'),
FieldPanel('slide2_link_text'),
], heading='Slide 2', classname="collapsible"),
MultiFieldPanel([
FieldPanel('slide3_img'),
FieldPanel('slide3_headline'),
FieldPanel('slide3_subline'),
FieldPanel('slide3_link_url'),
FieldPanel('slide3_link_text'),
], heading='Slide 3', classname="collapsible"),
], heading='Legacy Slider elements (deprecated)', classname="collapsible collapsed"),
] ]
@ -269,40 +278,16 @@ class EventPage(Page):
subtitle = models.CharField(max_length=512, null=True, blank=True) subtitle = models.CharField(max_length=512, null=True, blank=True)
description = RichTextField(blank=True) description = RichTextField(blank=True)
# revolution slider # revolution slider as StreamField
slide1_img = models.ForeignKey( slides = StreamField([
get_image_model(), ('slide', blocks.StructBlock([
null=True, ('image', ImageChooserBlock()),
blank=True, ('headline', blocks.CharBlock(max_length=512, required=False)),
on_delete=models.SET_NULL, ('subline', blocks.CharBlock(max_length=512, required=False)),
related_name='+' ('link_url', blocks.URLBlock(required=False)),
) ('link_text', blocks.CharBlock(max_length=64, required=False)),
slide1_headline = models.CharField(max_length=512, blank=True) ])),
slide1_subline = models.CharField(max_length=512, blank=True) ], null=True, blank=True, use_json_field=True)
slide1_link_url = models.URLField(blank=True)
slide1_link_text = models.CharField(max_length=64, blank=True)
slide2_img = models.ForeignKey(
get_image_model(),
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
slide2_headline = models.CharField(max_length=512, blank=True)
slide2_subline = models.CharField(max_length=512, blank=True)
slide2_link_url = models.URLField(blank=True)
slide2_link_text = models.CharField(max_length=64, blank=True)
slide3_img = models.ForeignKey(
get_image_model(),
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
slide3_headline = models.CharField(max_length=512, blank=True)
slide3_subline = models.CharField(max_length=512, blank=True)
slide3_link_url = models.URLField(blank=True)
slide3_link_text = models.CharField(max_length=64, blank=True)
img1_img = models.ForeignKey( img1_img = models.ForeignKey(
get_image_model(), get_image_model(),
@ -424,10 +409,10 @@ class EventRegistrationField(AbstractFormField):
EventPage.content_panels = [ EventPage.content_panels = [
MultiFieldPanel(REVOLUTION_SLIDER_FIELDS, heading='Slider elements', classname="collapsible collapsed"),
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
FieldPanel('subtitle'), FieldPanel('subtitle'),
FieldPanel('description'), FieldPanel('description'),
FieldPanel('slides', heading='Slider elements', classname="collapsible collapsed"),
FieldPanel('img1_img'), FieldPanel('img1_img'),
FieldPanel('img1_caption'), FieldPanel('img1_caption'),
FieldPanel('img2_img'), FieldPanel('img2_img'),

@ -23,77 +23,39 @@
{% block content %} {% block content %}
<!-- Hero Section with Carousel --> <!-- Hero Section with Carousel -->
{% if self.slide1_img %} {% if self.slides %}
<div class="event-hero"> <div class="event-hero">
<div id="eventCarousel" class="carousel slide event-hero-carousel" data-bs-ride="carousel"> <div id="eventCarousel" class="carousel slide event-hero-carousel" data-bs-ride="carousel">
<div class="carousel-indicators"> <div class="carousel-indicators">
<button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button> {% for slide_block in self.slides %}
{% if self.slide2_img %} {% with slide=slide_block.value %}
<button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button> <button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="{{ forloop.counter0 }}" {% if forloop.first %}class="active" aria-current="true"{% endif %} aria-label="Slide {{ forloop.counter }}"></button>
{% endif %} {% endwith %}
{% if self.slide3_img %} {% endfor %}
<button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button>
{% endif %}
</div> </div>
<div class="carousel-inner"> <div class="carousel-inner">
<div class="carousel-item active"> {% for slide_block in self.slides %}
{% image self.slide1_img fill-2000x500 class="d-block w-100" alt=self.slide1_headline %} {% with slide=slide_block.value %}
{% if self.slide1_headline or self.slide1_subline %} <div class="carousel-item {% if forloop.first %}active{% endif %}">
<div class="carousel-caption"> {% image slide.image fill-2000x500 class="d-block w-100" alt=slide.headline %}
{% if self.slide1_headline %} {% if slide.headline or slide.subline %}
<h2 class="display-4 fw-light">{{ self.slide1_headline }}</h2> <div class="carousel-caption">
{% endif %} {% if slide.headline %}
{% if self.slide1_subline %} <h2 class="display-4 fw-light">{{ slide.headline }}</h2>
<p class="lead">{{ self.slide1_subline }}</p> {% endif %}
{% endif %} {% if slide.subline %}
{% if self.slide1_link_url and self.slide1_link_text %} <p class="lead">{{ slide.subline }}</p>
<a href="{{ self.slide1_link_url }}" class="btn btn-outline-light btn-lg mt-3"> {% endif %}
<i class="bi bi-arrow-right-circle me-2"></i>{{ self.slide1_link_text }} {% if slide.link_url and slide.link_text %}
</a> <a href="{{ slide.link_url }}" class="btn btn-outline-light btn-lg mt-3">
{% endif %} <i class="bi bi-arrow-right-circle me-2"></i>{{ slide.link_text }}
</div> </a>
{% endif %} {% endif %}
</div> </div>
{% if self.slide2_img %} {% endif %}
<div class="carousel-item"> </div>
{% image self.slide2_img fill-2000x500 class="d-block w-100" alt=self.slide2_headline %} {% endwith %}
{% if self.slide2_headline or self.slide2_subline %} {% endfor %}
<div class="carousel-caption">
{% if self.slide2_headline %}
<h2 class="display-4 fw-light">{{ self.slide2_headline }}</h2>
{% endif %}
{% if self.slide2_subline %}
<p class="lead">{{ self.slide2_subline }}</p>
{% endif %}
{% if self.slide2_link_url and self.slide2_link_text %}
<a href="{{ self.slide2_link_url }}" class="btn btn-outline-light btn-lg mt-3">
<i class="bi bi-arrow-right-circle me-2"></i>{{ self.slide2_link_text }}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% if self.slide3_img %}
<div class="carousel-item">
{% image self.slide3_img fill-2000x500 class="d-block w-100" alt=self.slide3_headline %}
{% if self.slide3_headline or self.slide3_subline %}
<div class="carousel-caption">
{% if self.slide3_headline %}
<h2 class="display-4 fw-light">{{ self.slide3_headline }}</h2>
{% endif %}
{% if self.slide3_subline %}
<p class="lead">{{ self.slide3_subline }}</p>
{% endif %}
{% if self.slide3_link_url and self.slide3_link_text %}
<a href="{{ self.slide3_link_url }}" class="btn btn-outline-light btn-lg mt-3">
<i class="bi bi-arrow-right-circle me-2"></i>{{ self.slide3_link_text }}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
</div> </div>
<button class="carousel-control-prev" type="button" data-bs-target="#eventCarousel" data-bs-slide="prev"> <button class="carousel-control-prev" type="button" data-bs-target="#eventCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="carousel-control-prev-icon" aria-hidden="true"></span>

@ -233,7 +233,45 @@
{% endblock %} {% endblock %}
{% block fullwidth_header %} {% block fullwidth_header %}
{% if self.slide1_img %} {% if self.slides %}
<div id="heroCarousel" class="carousel slide hero-carousel" data-bs-ride="carousel">
<div class="carousel-indicators">
{% for slide in self.slides %}
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="{{ forloop.counter0 }}" {% if forloop.first %}class="active" aria-current="true"{% endif %} aria-label="Slide {{ forloop.counter }}"></button>
{% endfor %}
</div>
<div class="carousel-inner">
{% for slide in self.slides %}
<div class="carousel-item {% if forloop.first %}active{% endif %}">
{% image slide.value.image fill-2000x1200 as slide_img %}
<div class="carousel-item-bg" style="background-image: url('{{ slide_img.url }}'); height: 80vh; min-height: 500px; background-size: cover; background-position: center;"></div>
{% if slide.value.headline or slide.value.subline %}
<div class="carousel-caption d-md-block">
{% if slide.value.headline %}
<h2>{{ slide.value.headline }}</h2>
{% endif %}
{% if slide.value.subline %}
<p>{{ slide.value.subline }}</p>
{% endif %}
{% if slide.value.link_url and slide.value.link_text %}
<a href="{{ slide.value.link_url }}" class="btn btn-outline-light mt-3">{{ slide.value.link_text }} <i class="bi bi-arrow-right"></i></a>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#heroCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#heroCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
{% elif self.slide1_img %}
<!-- Legacy slide implementation - deprecated -->
<div id="heroCarousel" class="carousel slide hero-carousel" data-bs-ride="carousel"> <div id="heroCarousel" class="carousel slide hero-carousel" data-bs-ride="carousel">
<div class="carousel-indicators"> <div class="carousel-indicators">
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button> <button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>

@ -4,12 +4,25 @@ from .base import *
DEBUG = False DEBUG = False
TEMPLATE_DEBUG = True TEMPLATE_DEBUG = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
ALLOWED_HOSTS = ['feoneu.asw.io'] ALLOWED_HOSTS = ['feoneu.asw.io']
USE_X_FORWARDED_HOST = True USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
CSRF_TRUSTED_ORIGINS = ["https://feoneu.asw.io",] CSRF_TRUSTED_ORIGINS = ["https://feoneu.asw.io",]
ENV = 'prod'
DEFAULT_FROM_EMAIL = "info@feo.gmbh"
SERVER_EMAIL = "server@feo.gmbh"
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'mail.asw.io'
EMAIL_HOST_PASSWORD = 'e6166f289851034304ba7d2e2269bdd0'
EMAIL_HOST_USER = 'service@feo.gmbh'
EMAIL_PORT = 993
EMAIL_SUBJECT_PREFIX = '[FEO GmbH] '
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
try: try:
from .local import * from .local import *
except ImportError: except ImportError:

Loading…
Cancel
Save