Add testimonials (#2069)

This commit is contained in:
Greg Kaleka
2026-02-10 15:19:42 -05:00
committed by GitHub
parent 077b6c56de
commit f31e05e8e7
21 changed files with 514 additions and 17 deletions

View File

@@ -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:

View File

@@ -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']

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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,

View 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;}

View File

@@ -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&nbsp;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">

View 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 %}

View 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 &rarr;
</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
View File

1
testimonials/admin.py Normal file
View File

@@ -0,0 +1 @@
# Register your models here.

5
testimonials/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class TestimonialsConfig(AppConfig):
name = "testimonials"

View File

View 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!")
)

View 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",),
),
]

View File

80
testimonials/models.py Normal file
View 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
View File

@@ -0,0 +1 @@
# Create your tests here.

12
testimonials/urls.py Normal file
View 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
View 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