mirror of
https://github.com/boostorg/website-v2.git
synced 2026-02-22 03:52:12 +00:00
Add testimonials (#2069)
This commit is contained in:
@@ -11,6 +11,7 @@ from core.calendar import extract_calendar_events, events_by_month, get_calendar
|
||||
from libraries.constants import LATEST_RELEASE_URL_PATH_STR
|
||||
from libraries.mixins import ContributorMixin
|
||||
from news.models import Entry
|
||||
from testimonials.models import Testimonial
|
||||
|
||||
|
||||
logger = structlog.get_logger()
|
||||
@@ -27,6 +28,9 @@ class HomepageView(ContributorMixin, TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["entries"] = Entry.objects.published().order_by("-publish_at")[:3]
|
||||
context["events"] = self.get_events()
|
||||
context["testimonial"] = (
|
||||
Testimonial.objects.live().filter(pull_quote__gt="").last()
|
||||
)
|
||||
if context["events"]:
|
||||
context["num_months"] = len(context["events"])
|
||||
else:
|
||||
|
||||
@@ -120,6 +120,7 @@ INSTALLED_APPS += [
|
||||
"reports",
|
||||
"core",
|
||||
"slack",
|
||||
"testimonials",
|
||||
"asciidoctor_sandbox",
|
||||
]
|
||||
|
||||
@@ -644,6 +645,18 @@ WAGTAILDOCS_EXTENSIONS = [
|
||||
"xlsx",
|
||||
"zip",
|
||||
]
|
||||
RICH_TEXT_FEATURES = [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"bold",
|
||||
"italic",
|
||||
"link",
|
||||
"ol",
|
||||
"ul",
|
||||
"code",
|
||||
"blockquote",
|
||||
]
|
||||
WAGTAILMARKDOWN = {
|
||||
"autodownload_fontawesome": True,
|
||||
"allowed_tags": [], # optional. a list of HTML tags. e.g. ['div', 'p', 'a']
|
||||
|
||||
@@ -409,7 +409,8 @@ urlpatterns = (
|
||||
# Wagtail stuff
|
||||
path("cms/", include(wagtailadmin_urls)),
|
||||
path("documents/", include(wagtaildocs_urls)),
|
||||
path("outreach/", include(wagtail_urls)),
|
||||
# Custom Django views (must come before Wagtail catch-all)
|
||||
path("testimonials/", include("testimonials.urls")),
|
||||
]
|
||||
+ [
|
||||
re_path(
|
||||
@@ -480,14 +481,18 @@ urlpatterns = (
|
||||
ImageView.as_view(),
|
||||
name="images-page",
|
||||
),
|
||||
# Static content
|
||||
# Static content (exclude Wagtail paths)
|
||||
re_path(
|
||||
r"^(?!__debug__)(?P<content_path>.+)/?",
|
||||
r"^(?!__debug__|outreach/|testimonials/)(?P<content_path>.+)/?",
|
||||
StaticContentTemplateView.as_view(),
|
||||
name="static-content-page",
|
||||
),
|
||||
]
|
||||
+ djdt_urls
|
||||
+ [
|
||||
# Wagtail catch-all (must be last!)
|
||||
path("", include(wagtail_urls)),
|
||||
]
|
||||
)
|
||||
|
||||
handler404 = "ak.views.custom_404_view"
|
||||
|
||||
@@ -173,3 +173,11 @@ html:has(#docsiframe) .version_alert {
|
||||
.version_alert a {
|
||||
@apply font-semibold underline dark:text-white text-charcoal;
|
||||
}
|
||||
|
||||
.testimonials blockquote {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.testimonials a {
|
||||
@apply text-sky-600 dark:text-sky-300 hover:text-orange dark:hover:text-orange;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db import models
|
||||
from django.http import Http404
|
||||
@@ -10,19 +11,6 @@ from wagtail.models import Page
|
||||
from wagtail.url_routing import RouteResult
|
||||
from wagtailmarkdown.blocks import MarkdownBlock
|
||||
|
||||
RICH_TEXT_FEATURES = [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"bold",
|
||||
"italic",
|
||||
"link",
|
||||
"ol",
|
||||
"ul",
|
||||
"code",
|
||||
"blockquote",
|
||||
]
|
||||
|
||||
|
||||
class CapturedEmail(models.Model):
|
||||
email = models.EmailField()
|
||||
@@ -76,7 +64,10 @@ class EmailCapturePage(Page):
|
||||
)
|
||||
body = StreamField(
|
||||
[
|
||||
("rich", RichTextBlock(features=RICH_TEXT_FEATURES, label="Rich text")),
|
||||
(
|
||||
"rich",
|
||||
RichTextBlock(features=settings.RICH_TEXT_FEATURES, label="Rich text"),
|
||||
),
|
||||
("md", MarkdownBlock(label="Markdown")),
|
||||
],
|
||||
use_json_field=True,
|
||||
|
||||
16
static/css/testimonial-style.css
Normal file
16
static/css/testimonial-style.css
Normal file
@@ -0,0 +1,16 @@
|
||||
hr {margin:2rem auto; width: 150px;}
|
||||
p {padding-top: 0.5rem; padding-bottom: 0.5rem;}
|
||||
a {color:#0284c7;}
|
||||
a:hover, a:active {color:darkblue}
|
||||
a:visited {color:#0284c7;}
|
||||
h1 {font-size: 1.44rem; font-weight:700;}
|
||||
h2 {font-size: 1rem; font-weight:600; text-transform: uppercase;}
|
||||
h3 {font-size: 1.1rem; font-weight:700;}
|
||||
h4 {font-size: 0.95rem; font-weight:700;}
|
||||
h5 {font-size: 0.69rem; font-weight:700;}
|
||||
ul, ol {margin: 1rem 0; padding-left: 2rem;}
|
||||
ul {list-style-type: disc;}
|
||||
ol {list-style-type: decimal;}
|
||||
li {padding: 0.25rem 0;}
|
||||
ul ul, ol ul {list-style-type: circle;}
|
||||
ol ol, ul ol {list-style-type: lower-alpha;}
|
||||
@@ -59,6 +59,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if testimonial %}
|
||||
<div class="testimonials my-12 mb-3 md:mb-6 space-y-4 lg:mt-16 lg:mb-4 lg:space-y-0 lg:space-x-4 md:shadow-lg">
|
||||
<div class="testimonial p-6 dark:text-white text-slate bg-white md:rounded-lg dark:bg-charcoal dark:bg-neutral-700 md:shadow-lg">
|
||||
<h5 class="text-3xl leading-tight mb-2">Testimonials</h5>
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="flex-grow">
|
||||
{{ testimonial.pull_quote }}
|
||||
</div>
|
||||
<a href="{% url 'testimonial-detail' testimonial.author_slug %}" class="flex-shrink-0 mb-4">
|
||||
<button class="py-2 px-3 text-sm text-white rounded bg-orange">Read More</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if events %}
|
||||
<div class="my-12 mb-3 md:mb-6 space-y-4 lg:flex lg:mt-16 lg:mb-4 lg:space-y-0 lg:space-x-4 md:shadow-lg">
|
||||
<div class="p-6 relative bg-white md:rounded-lg md:p-11 w-full dark:bg-charcoal">
|
||||
|
||||
32
templates/testimonials/testimonial.html
Normal file
32
templates/testimonials/testimonial.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static wagtailcore_tags %}
|
||||
|
||||
{% block title %}
|
||||
{{ page.author }} - Testimonial
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static 'css/testimonial-style.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">
|
||||
<div class="py-8 md:mx-auto md:w-3/4">
|
||||
<h1 class="text-3xl mb-4">{{ page.title }}</h1>
|
||||
<h3 class="text-xl mb-6 text-gray-600 dark:text-gray-400">
|
||||
By
|
||||
{% if page.author_url %}
|
||||
<a href="{{ page.author_url }}" class="text-orange hover:underline">{{ page.author }}</a>
|
||||
{% else %}
|
||||
{{ page.author }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
<div class="bg-white dark:bg-charcoal rounded-lg p-6">
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
{{ page.body }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
38
templates/testimonials/testimonials_index_page.html
Normal file
38
templates/testimonials/testimonials_index_page.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
Testimonials
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">
|
||||
<div class="py-8 md:mx-auto md:w-3/4">
|
||||
<h1 class="text-3xl mb-6">Testimonials</h1>
|
||||
|
||||
<div class="space-y-6">
|
||||
{% for testimonial in testimonials %}
|
||||
<div class="bg-white dark:bg-charcoal rounded-lg p-6">
|
||||
<h2 class="text-2xl mb-3">
|
||||
<a href="{{ testimonial.url }}" class="text-orange hover:text-orange/75">
|
||||
{{ testimonial.author }}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
{% if testimonial.pull_quote %}
|
||||
<blockquote class="text-lg italic text-gray-700 dark:text-gray-300 border-l-4 border-orange pl-4 mb-3">
|
||||
"{{ testimonial.pull_quote }}"
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ testimonial.url }}" class="text-sky-600 dark:text-sky-300 hover:text-orange dark:hover:text-orange">
|
||||
Read more →
|
||||
</a>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-gray-600 dark:text-gray-400">No testimonials yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
0
testimonials/__init__.py
Normal file
0
testimonials/__init__.py
Normal file
1
testimonials/admin.py
Normal file
1
testimonials/admin.py
Normal file
@@ -0,0 +1 @@
|
||||
# Register your models here.
|
||||
5
testimonials/apps.py
Normal file
5
testimonials/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TestimonialsConfig(AppConfig):
|
||||
name = "testimonials"
|
||||
0
testimonials/management/__init__.py
Normal file
0
testimonials/management/__init__.py
Normal file
0
testimonials/management/commands/__init__.py
Normal file
0
testimonials/management/commands/__init__.py
Normal file
129
testimonials/management/commands/load_initial_testimonials.py
Normal file
129
testimonials/management/commands/load_initial_testimonials.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from wagtail.models import Page, Site
|
||||
|
||||
from testimonials.models import Testimonial, TestimonialsIndexPage
|
||||
|
||||
# Initial testimonial data
|
||||
INITIAL_TESTIMONIAL = {
|
||||
"author": "Oleg Trott, PHD",
|
||||
"author_slug": "oleg_trott",
|
||||
"author_url": "https://www.olegtrott.com",
|
||||
"pull_quote": [
|
||||
{
|
||||
"id": "4c6ac70f-fd45-4ab5-85d7-69a790c14515",
|
||||
"type": "md",
|
||||
"value": """> What I really liked about Boost was that the libraries are peer-reviewed, raising expectations about quality and security. And I don't think I encountered a single bug in any of the Boost libraries I used. My thanks to the developers!
|
||||
|
||||
\\- [Oleg Trott](https://www.olegtrott.com), PHD
|
||||
<br>
|
||||
Creator, [AutoDoc Vina](https://vina.scripps.edu/)""",
|
||||
}
|
||||
],
|
||||
"title": "The use of Boost C++ libraries in drug discovery",
|
||||
"body": [
|
||||
{
|
||||
"id": "92af493a-144c-406c-acce-ac10ab865911",
|
||||
"type": "md",
|
||||
"value": """[AutoDock Vina](https://vina.scripps.edu/) is the most popular molecular docking program, with [40,000 citations](https://scholar.google.com/citations?user=4BD7MkgAAAAJ), as of this writing. It is used widely to look for treatments for various diseases from cardiovascular and infectious ones to cancer. I created AutoDock Vina back when I was a postdoc at The Scripps Research Institute. And Boost C++ libraries were of great help.
|
||||
|
||||
The mechanisms of action of various drugs are different in each case, but what they have in common is that the drug (typically a small molecule consisting of dozens of atoms) binds a huge molecule, like a protein (consisting of thousands of atoms). This binding is normally non-covalent (think "physics", not "chemistry"). It is also quite specific – the shape and other properties of the drug have to be complementary to the protein, not unlike a lock and key. This binding interferes with the normal operation of the protein in question, and this may have some desired biological effect.
|
||||
|
||||
Modeling this binding process computationally is challenging, but, if done well, it can predict which small molecules would be promising as drugs.
|
||||
|
||||
When I got hired by The Scripps Research Institute almost 20 years ago, they had already been developing a molecular docking program, which they called "AutoDock", for many years. AutoDock was being used widely, including in huge efforts like the IBM World Community Grid, where volunteers contributed their personal compute to do docking calculations. In one such project, AutoDock was being used to look for new anti-HIV drugs. I estimated that in that single project, millions of dollars were being spent just on electricity (a cost borne by the volunteers). So performance was important.
|
||||
|
||||
Initially, my plan was to contribute to AutoDock, but after a few weeks on the job, I realized that the best path forward would be to write a new docking program instead. I thought I could re-implement the same or equivalent algorithm in a fraction of the lines of code, using modern (at the time) C++, employing STL and Boost.
|
||||
|
||||
While I didn't get fired right away, I'll say this: If you set out to do something ambitious in academia, the clock starts ticking for you, because while you are busy working on your new high-effort and high-risk project, you are probably not publishing some low-effort and low-risk work that is encouraged in academia. And what if your project fails? Rather perilous for your career.
|
||||
|
||||
To make matters worse, during this rewrite, my ambitions grew much further. I was no longer content with just a rewrite and started experimenting with alternative algorithms and scoring functions. (The scoring function tells us which binding is better.) Long story short, after 1.5 years, I released a new docking program and called it "VINA" (short for "VINA Is Not AutoDock"). It was superior to AutoDock:
|
||||
|
||||
* It was roughly 60 times faster, when using a single thread (potentially saving many millions in electricity and compute)
|
||||
* Additionally, it supported parallelism across multiple CPU cores seamlessly
|
||||
* It was significantly more accurate in its binding pose predictions, on average
|
||||
* It supported all major platforms directly (AutoDock required a Unix-like environment)
|
||||
* The code was a few times smaller
|
||||
|
||||
Later, I was asked to change the name to "AutoDock Vina". "AutoDock" became a brand, rather than the name of a particular program. Sadly, this is causing confusion to this day. Many people think that "Vina" was a new version of old software, but it was brand-new and simpler code implementing a more complex algorithm.
|
||||
|
||||
Boost C++ libraries were quite useful to me in cutting down on the development time, which as I mentioned was important. In particular, I used
|
||||
|
||||
* Boost.Thread – it enabled parallelism in a platform-independent way
|
||||
* Boost.Serialization – for object persistence
|
||||
* Boost.Math – for quaternions, which are used to represent 3D rotations conveniently
|
||||
(Boost.QVM would have been more appropriate, but I don't think it was part of Boost back then)
|
||||
* Boost.ProgramOptions – for parsing command line options and configuration files, as well as to
|
||||
display the help message
|
||||
* Boost.Filesystem – for handling files in a platform-independent way
|
||||
* Boost.PointerContainer – for containers of pointers to objects
|
||||
* Boost.Array – for "vectors" of statically known length
|
||||
* Boost.Optional – for objects that may or may not be there
|
||||
* Boost.LexicalCast – for parsing numbers, mostly
|
||||
* Boost.Random – for thread-safe random number generation
|
||||
* Boost.Timer – to show the users a progress bar, while they are waiting for the results
|
||||
|
||||
Since then, some of these libraries made it into the C++ standard, I believe.
|
||||
|
||||
What I really liked about Boost was that the libraries are peer-reviewed, raising expectations about quality and security. And I don't think I encountered a single bug in any of the Boost libraries I used. My thanks to the developers!""",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Load initial testimonials data"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Check if testimonials index page already exists
|
||||
if TestimonialsIndexPage.objects.exists():
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"Testimonials index page already exists. Skipping data load."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Get the site root page (typically the homepage)
|
||||
try:
|
||||
site = Site.objects.get(is_default_site=True)
|
||||
root_page = site.root_page
|
||||
except Site.DoesNotExist:
|
||||
# Fallback to the root page if no site exists
|
||||
root_page = Page.objects.get(depth=1)
|
||||
|
||||
# Create the testimonials index page
|
||||
self.stdout.write("Creating testimonials index page...")
|
||||
index_page = TestimonialsIndexPage(
|
||||
title="Testimonials",
|
||||
slug="testimonials",
|
||||
show_in_menus=False,
|
||||
)
|
||||
root_page.add_child(instance=index_page)
|
||||
index_page.save_revision().publish()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Created testimonials index page (ID: {index_page.id})")
|
||||
)
|
||||
|
||||
# Create the initial testimonial
|
||||
self.stdout.write(
|
||||
f"Creating testimonial for {INITIAL_TESTIMONIAL['author']}..."
|
||||
)
|
||||
testimonial = Testimonial(
|
||||
title=INITIAL_TESTIMONIAL["title"],
|
||||
slug=INITIAL_TESTIMONIAL["author_slug"],
|
||||
author=INITIAL_TESTIMONIAL["author"],
|
||||
author_slug=INITIAL_TESTIMONIAL["author_slug"],
|
||||
author_url=INITIAL_TESTIMONIAL["author_url"],
|
||||
pull_quote=INITIAL_TESTIMONIAL["pull_quote"],
|
||||
body=INITIAL_TESTIMONIAL["body"],
|
||||
show_in_menus=False,
|
||||
)
|
||||
index_page.add_child(instance=testimonial)
|
||||
testimonial.save_revision().publish()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Created testimonial (ID: {testimonial.id})")
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("\nSuccessfully loaded initial testimonials data!")
|
||||
)
|
||||
122
testimonials/migrations/0001_initial.py
Normal file
122
testimonials/migrations/0001_initial.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# Generated by Django 6.0.2 on 2026-02-10 20:02
|
||||
|
||||
import django.db.models.deletion
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("wagtailcore", "0096_referenceindex_referenceindex_source_object_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Testimonial",
|
||||
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",
|
||||
),
|
||||
),
|
||||
("author", models.CharField(max_length=255)),
|
||||
(
|
||||
"author_slug",
|
||||
models.SlugField(
|
||||
help_text="Slug used for author's URL - must be unique",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"author_url",
|
||||
models.URLField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="Optional URL to link the author's name to",
|
||||
),
|
||||
),
|
||||
(
|
||||
"pull_quote",
|
||||
wagtail.fields.StreamField(
|
||||
[("md", 0)],
|
||||
blank=True,
|
||||
block_lookup={
|
||||
0: (
|
||||
"wagtailmarkdown.blocks.MarkdownBlock",
|
||||
(),
|
||||
{"label": "Markdown"},
|
||||
)
|
||||
},
|
||||
help_text="Optional pull quote to highlight on the homepage",
|
||||
),
|
||||
),
|
||||
(
|
||||
"body",
|
||||
wagtail.fields.StreamField(
|
||||
[("rich", 0), ("md", 1)],
|
||||
blank=True,
|
||||
block_lookup={
|
||||
0: (
|
||||
"wagtail.blocks.RichTextBlock",
|
||||
(),
|
||||
{
|
||||
"features": [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"bold",
|
||||
"italic",
|
||||
"link",
|
||||
"ol",
|
||||
"ul",
|
||||
"code",
|
||||
"blockquote",
|
||||
],
|
||||
"label": "Rich text",
|
||||
},
|
||||
),
|
||||
1: (
|
||||
"wagtailmarkdown.blocks.MarkdownBlock",
|
||||
(),
|
||||
{"label": "Markdown"},
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Testimonial",
|
||||
"verbose_name_plural": "Testimonials",
|
||||
},
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="TestimonialsIndexPage",
|
||||
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",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
]
|
||||
0
testimonials/migrations/__init__.py
Normal file
0
testimonials/migrations/__init__.py
Normal file
80
testimonials/models.py
Normal file
80
testimonials/models.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.blocks import RichTextBlock
|
||||
from wagtail.fields import StreamField
|
||||
from wagtail.models import Page
|
||||
from wagtailmarkdown.blocks import MarkdownBlock
|
||||
|
||||
|
||||
class TestimonialsIndexPage(Page):
|
||||
"""Container page for all testimonials."""
|
||||
|
||||
max_count = 1 # Only allow one testimonials index page
|
||||
parent_page_types = ["wagtailcore.Page"]
|
||||
subpage_types = ["testimonials.Testimonial"]
|
||||
|
||||
def get_url(self, request=None, current_site=None):
|
||||
"""Override to return the correct URL for this page."""
|
||||
return reverse("testimonials-index")
|
||||
|
||||
def get_context(self, request, *args, **kwargs):
|
||||
context = super().get_context(request, *args, **kwargs)
|
||||
# Get all live testimonials that are children of this page
|
||||
context["testimonials"] = (
|
||||
Testimonial.objects.live().child_of(self).order_by("-first_published_at")
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class Testimonial(Page):
|
||||
author = models.CharField(max_length=255)
|
||||
author_slug = models.SlugField(
|
||||
help_text="Slug used for author's URL - must be unique", unique=True
|
||||
)
|
||||
author_url = models.URLField(
|
||||
help_text="Optional URL to link the author's name to", blank=True, default=""
|
||||
)
|
||||
pull_quote = StreamField(
|
||||
[
|
||||
("md", MarkdownBlock(label="Markdown")),
|
||||
],
|
||||
use_json_field=True,
|
||||
blank=True,
|
||||
help_text="Optional pull quote to highlight on the homepage",
|
||||
)
|
||||
body = StreamField(
|
||||
[
|
||||
(
|
||||
"rich",
|
||||
RichTextBlock(features=settings.RICH_TEXT_FEATURES, label="Rich text"),
|
||||
),
|
||||
("md", MarkdownBlock(label="Markdown")),
|
||||
],
|
||||
use_json_field=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# Configure Wagtail admin panels
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("title"),
|
||||
FieldPanel("author"),
|
||||
FieldPanel("author_slug"),
|
||||
FieldPanel("author_url"),
|
||||
FieldPanel("pull_quote"),
|
||||
FieldPanel("body"),
|
||||
]
|
||||
|
||||
# Define where this page type can be created
|
||||
parent_page_types = ["testimonials.TestimonialsIndexPage"]
|
||||
subpage_types = [] # Testimonials can't have child pages
|
||||
|
||||
def get_url(self, request=None, current_site=None):
|
||||
"""Override to return the correct URL for this page."""
|
||||
# Use the page's slug (set in Wagtail admin) for the URL
|
||||
return reverse("testimonial-detail", kwargs={"author_slug": self.slug})
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Testimonial"
|
||||
verbose_name_plural = "Testimonials"
|
||||
1
testimonials/tests.py
Normal file
1
testimonials/tests.py
Normal file
@@ -0,0 +1 @@
|
||||
# Create your tests here.
|
||||
12
testimonials/urls.py
Normal file
12
testimonials/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import TestimonialsIndexView, TestimonialDetailView
|
||||
|
||||
urlpatterns = [
|
||||
path("", TestimonialsIndexView.as_view(), name="testimonials-index"),
|
||||
path(
|
||||
"<slug:author_slug>/",
|
||||
TestimonialDetailView.as_view(),
|
||||
name="testimonial-detail",
|
||||
),
|
||||
]
|
||||
24
testimonials/views.py
Normal file
24
testimonials/views.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
from .models import Testimonial
|
||||
|
||||
|
||||
class TestimonialsIndexView(ListView):
|
||||
"""Optional non-Wagtail view for listing testimonials."""
|
||||
|
||||
model = Testimonial
|
||||
template_name = "testimonials/testimonials_index_page.html"
|
||||
context_object_name = "testimonials"
|
||||
|
||||
def get_queryset(self):
|
||||
return Testimonial.objects.live().order_by("-first_published_at")
|
||||
|
||||
|
||||
class TestimonialDetailView(DetailView):
|
||||
"""Optional non-Wagtail view for testimonial detail."""
|
||||
|
||||
model = Testimonial
|
||||
template_name = "testimonials/testimonial.html"
|
||||
slug_field = "author_slug"
|
||||
slug_url_kwarg = "author_slug"
|
||||
context_object_name = "page" # Changed to 'page' to match Wagtail convention
|
||||
Reference in New Issue
Block a user