So far we’ve been using streamfields, which are great for dynamic content generation where the order of the content doesn’t matter.
An Orderable is a way to add custom models that can be ordered within another model. Think of it like a list of items that can be re-ordered by dragging and dropping them into a new position. This is useful for things like team members, portfolio items, or other content where the order matters.
We’re going to create an Orderable where somebody could upload 1-5 images for a carousel on our homepage.
Let’s go to models.py in our home app and create a new class for our Orderable!
from wagtail.models import Page, Orderable
from modelcluster.fields import ParentalKey
class HomePageCarouselImages(Orderable):
"""Between 1 and 5 images for the home page carousel."""
page = ParentalKey("home.HomePage", related_name="carousel_images")
carousel_image = models.ForeignKey(
"wagtailimages.Image",
null=True, # we don't know what the default will be
blank=False, # should not be deleted
on_delete=models.SET_NULL, # we don't want anything else to delete
related_name="+"
)
panels = [
FieldPanel("carousel_images")
]
ParentalKey is saying “What is this inline model related to?” And we are telling it to belong to the Home app, and the HomePage class.
With this new class set up, you need to make migrations, and then add it to the HomePage’s content_panels!
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import FieldPanel, PageChooserPanel, MultipleChooserPanel, InlinePanel
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page, Orderable
from wagtail.images.edit_handlers import ImageFieldComparison
from streams import blocks
class HomePageCarouselImages(Orderable):
"""Between 1 and 5 images for the home page carousel."""
page = ParentalKey("home.HomePage", related_name="carousel_images")
carousel_image = models.ForeignKey(
"wagtailimages.Image",
null=True, # we don't know what the default will be
blank=False, # should not be deleted
on_delete=models.SET_NULL, # we don't want anything else to delete
related_name="+"
)
panels = [
FieldPanel("carousel_images")
]
class HomePage(Page):
"""Home Page Model"""
templates = "home/home_page.html"
max_count = 1
banner_title = models.CharField(max_length=100, blank=False, null=True)
banner_subtitle = RichTextField(
features=["bold", "italic"]
)
banner_image = models.ForeignKey(
"wagtailimages.Image",
null=True, # we don't know what the default will be
blank=False, # should not be deleted
on_delete=models.SET_NULL, # we don't want anything else to delete
related_name="+"
)
banner_cta = models.ForeignKey(
"wagtailcore.Page",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+"
)
content = StreamField(
[
("cta", blocks.CTABlock()),
],
null=True,
blank=True,
use_json_field=True
)
content_panels = Page.content_panels + [
FieldPanel("banner_title"),
FieldPanel("banner_subtitle"),
FieldPanel('banner_image'),
PageChooserPanel("banner_cta"),
FieldPanel("content"),
InlinePanel("carousel_images"),
]
class Meta:
verbose_name = "Home Page"
verbose_name_plural = "Home Pages"
You can now add images to this new Class inside the admin page, but there’s nothing labeling or separating it from the rest of the page! We must edit the content_panels again.
content_panels = Page.content_panels + [
MultiFieldPanel([
FieldPanel("banner_title"),
FieldPanel("banner_subtitle"),
FieldPanel('banner_image'),
PageChooserPanel("banner_cta"),
], heading="Banner Options"),
MultiFieldPanel([
InlinePanel("carousel_images", max_num=5, min_num=1, label="Image"),
], heading="Carousel Images"),
FieldPanel("content"),
]
Multifield panels allow us to group similar items together for displaying on the admin page.
So when we add 3 images, nothing will happen yet because we need to set up the template to tell it how to handle these images.
I got a component of a Tailwind Carousel online and heavily editted it:
{% load wagtailcore_tags wagtailimages_tags %}
<div
id="carouselExampleCaptions"
class="relative border-2 border-slate-800 dark:border-gray-300"
data-te-carousel-init
data-te-carousel-slide>
<!--Carousel indicators-->
<div
class="carousel-indicators"
data-te-carousel-indicators>
{% for loop_cycle in self.carousel_images.all %}
<button
type="button"
data-te-target="#carouselExampleCaptions"
data-te-slide-to="{{ forloop.counter0 }}"
data-te-carousel-active
class="mx-[3px] box-content h-[3px] w-[30px] flex-initial cursor-pointer border-0 border-y-[10px] border-solid border-transparent
bg-black dark:bg-white
bg-clip-padding p-0 -indent-[999px] opacity-50 transition-opacity duration-[600ms] ease-[cubic-bezier(0.25,0.1,0.25,1.0)] motion-reduce:transition-none"
aria-current="true"
aria-label="Slide {{ forloop.counter }}"></button>
{% endfor %}
</div>
<!--Carousel items-->
<div
class="relative w-full overflow-hidden after:clear-both after:block after:content-['']">
{% for loop_cycle in self.carousel_images.all %}
{% image loop_cycle.carousel_image fill-900x400 as img %}
<!--Carousel item-->
<div
class="relative float-left -mr-[100%] w-full transition-transform duration-[1000ms] ease-in-out motion-reduce:transition-none
{% if forloop.counter != 1%}
hidden
{% endif %}"
{% if forloop.counter == 1 %}
data-te-carousel-active
{% endif %}
data-te-carousel-item
style="backface-visibility: hidden">
<img
src="{{ img.url }}"
class="block w-full"
alt="{{ img.alt }}" />
<a href="
{% if loop_cycle.button_page %}
{{ card.button_page.url }}
{% endif %}
">
<div
class="absolute inset-x-[15%] bottom-5 py-5 text-center text-black dark:text-white bg-white dark:bg-black rounded-lg hover:bg-slate-300 dark:hover:bg-slate-700
transition-all duration-200 ease-linear my-6 mx-10
" style="--tw-bg-opacity:0.3;">
<h5 class="text-xl -mt-5">
{{loop_cycle.image_title}}
</h5>
<p class="text-xs">
{{loop_cycle.image_subtitle|safe}}
</p>
<div class="-mt-5"></div>
</div>
</a>
</div>
{% endfor %}
</div>
<!--Carousel controls - prev item-->
<button
class="carousel-controls left-0 rounded-r-xl"
type="button"
data-te-target="#carouselExampleCaptions"
data-te-slide="prev">
<span class="inline-block h-8 w-8">
<svg
xmlns="<http://www.w3.org/2000/svg>"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</span>
<span
class="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]"
>Previous</span
>
</button>
<!--Carousel controls - next item-->
<button
class="carousel-controls right-0 rounded-l-xl"
type="button"
data-te-target="#carouselExampleCaptions"
data-te-slide="next">
<span class="inline-block h-8 w-8">
<svg
xmlns="<http://www.w3.org/2000/svg>"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</span>
<span
class="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]"
>Next</span
>
</button>
</div>
<script>
// Initialization for ES Users
import {
Carousel,
initTE,
} from "tw-elements";
initTE({ Carousel });
</script>