Compare commits

..

19 Commits

Author SHA1 Message Date
Renovate b1a038fb24 Update dependency django-permissions-policy to v4.19.0 2024-04-01 14:01:15 +01:00
Jake Howard fe43b9c683
install inflection for OpenAPI support
Required by DRF
2024-04-01 12:15:56 +01:00
Jake Howard 6cbac34f2d
Add continue reading button to feed items 2024-03-31 23:59:56 +01:00
Jake Howard e5de558958
Correct argument name when proxy view fails 2024-03-10 15:25:48 +00:00
Jake Howard 1a9d981c7d
Add location information 2024-03-01 17:52:14 +00:00
Jake Howard b0f1191d8f
Only show listing images if there are some
This saves a bit more space for page listings which don't have images
2024-03-01 17:34:26 +00:00
Jake Howard bd4c1a193a
Add page type for talks
Content coming soon, probably
2024-03-01 17:09:56 +00:00
Jake Howard 1934b36ec1
Use `urljoin` to safely join activitypub URL 2024-02-17 21:21:37 +00:00
Jake Howard 23ce49ca8f
Improve readability of recent posts 2024-02-17 20:44:43 +00:00
Jake Howard 926e62518c
Improve sizing of recent posts 2024-02-17 20:43:48 +00:00
Jake Howard ec609ae562
Add redirect view for old pages listing 2024-02-17 19:44:45 +00:00
Jake Howard a19964199f
Put similar content above comments 2024-02-10 15:06:01 +00:00
Jake Howard c69d8d8329
Add recent post cards to homepage 2024-02-10 14:56:26 +00:00
Jake Howard 0424c2dba2
Fix issues with public tags in templates 2024-02-06 18:35:40 +00:00
Jake Howard 4c600651b6
Actually allow customizing the page size 2024-02-05 19:40:39 +00:00
Jake Howard 7d3605f5e1
Add API for latest posts 2024-01-28 18:06:17 +00:00
Jake Howard ae4ea780b7
Only show published tags 2024-01-27 19:59:59 +00:00
Renovate 552639ec40 Update dependency sass to v1.70.0 2024-01-21 22:24:24 +00:00
Renovate 316ab7b628 Update dependency lxml to v5.1.0 2024-01-21 22:19:00 +00:00
35 changed files with 813 additions and 72 deletions

View File

@ -1,3 +1,6 @@
{
"extends": ["stylelint-config-standard-scss", "stylelint-config-prettier-scss"]
"extends": ["stylelint-config-standard-scss", "stylelint-config-prettier-scss"],
"rules": {
"scss/at-extend-no-missing-placeholder": null
}
}

14
package-lock.json generated
View File

@ -21,7 +21,7 @@
"lodash.debounce": "4.0.8",
"lodash.throttle": "4.1.1",
"npm-run-all": "4.1.5",
"sass": "1.67.0",
"sass": "1.70.0",
"shareon": "2.4.0"
},
"devDependencies": {
@ -3303,9 +3303,9 @@
}
},
"node_modules/sass": {
"version": "1.67.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz",
"integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==",
"version": "1.70.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
"integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@ -6262,9 +6262,9 @@
}
},
"sass": {
"version": "1.67.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz",
"integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==",
"version": "1.70.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
"integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",

View File

@ -41,7 +41,7 @@
"lodash.debounce": "4.0.8",
"lodash.throttle": "4.1.1",
"npm-run-all": "4.1.5",
"sass": "1.67.0",
"sass": "1.70.0",
"shareon": "2.4.0"
}
}

View File

@ -4,7 +4,7 @@ django-environ==0.11.2
whitenoise[brotli]==6.6.0
Pygments==2.17.2
beautifulsoup4
lxml==5.0.0
lxml==5.1.0
requests
wagtail-generic-chooser==0.6
django-rq==2.10.1
@ -32,6 +32,7 @@ wagtail-lite-youtube-embed==0.1.0
# DRF OpenAPI dependencies
uritemplate
PyYAML
inflection
# Use custom `wagtail-favicon` with performance improvements
git+https://github.com/RealOrangeOne/wagtail-favicon@b892165e047b35c46d7244109b9ad9226d32a213

View File

@ -1,17 +1,27 @@
body.page-homepage {
height: 100vh;
min-height: 100vh;
main {
background-repeat: no-repeat;
background-size: cover;
background-position: center;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: $white;
margin-bottom: 0;
padding-bottom: 2rem;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.top-section {
min-width: 90%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 2rem;
margin-bottom: 1rem;
}
h1 {
@ -23,7 +33,7 @@ body.page-homepage {
min-width: 45%;
}
.latest {
.box {
padding: 0.25rem 0.5rem;
margin-top: 2rem;
background-color: color.adjust($dark, $alpha: -0.2);
@ -59,4 +69,60 @@ body.page-homepage {
#to-top {
display: none;
}
.content-list {
width: 90%;
.card-image {
overflow: hidden;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
transition: filter 0.25s;
filter: brightness(0.85);
}
p {
position: absolute;
bottom: 0;
color: $white;
padding: 0.5rem;
transition: background-color 0.25s;
width: 100%;
background-color: rgb(0 0 0 / 55%);
}
figure {
margin: 0;
text-align: initial;
}
a:hover {
img {
filter: unset;
}
p {
background-color: rgb(0 0 0 / 75%);
}
}
}
.recent-posts {
display: flex;
align-items: center;
flex-direction: column;
width: 100%;
flex-grow: unset;
margin-top: 2rem;
.box {
margin: 0 auto;
display: inline-block;
}
}
}

View File

@ -48,7 +48,7 @@
}
}
.page-blogpostlistpage {
.container.listing {
.date-header {
font-size: $size-2;
font-weight: $weight-bold;

View File

@ -2,5 +2,6 @@ from rest_framework.pagination import PageNumberPagination
class CustomPageNumberPagination(PageNumberPagination):
page_size = 15
max_page_size = 40
page_size = 10
page_size_query_param = "page_size"
max_page_size = 25

View File

@ -36,3 +36,14 @@ class LMOTFYSerializer(serializers.ModelSerializer):
return self.context["request"].build_absolute_uri(image_url)
return image_url
class LatestPostSerializer(serializers.ModelSerializer):
full_url = serializers.SerializerMethodField()
class Meta:
model = BlogPostPage
fields = read_only_fields = ["full_url", "title", "date"]
def get_full_url(self, page: Page) -> str:
return page.get_full_url(request=self.context["request"])

View File

@ -86,3 +86,20 @@ class SchemaTestCase(APISimpleTestCase):
def test_schema(self) -> None:
response = self.client.get(reverse("api:schema"))
self.assertEqual(response.status_code, 200)
class LatestPostsAPIViewTestCase(APITestCase):
url = reverse("api:latest-posts")
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
for i in range(4):
BlogPostPageFactory(parent=cls.home_page, title=f"Post {i}")
def test_accessible(self) -> None:
with self.assertNumQueries(5):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], 4)

View File

@ -10,6 +10,7 @@ api_urlpatterns = [
path("ping/", views.PingAPIView.as_view(), name="ping"),
path("page-links/", views.PageLinksAPIView.as_view(), name="page-links"),
path("lmotfy/", views.LMOTFYAPIView.as_view(), name="lmotfy"),
path("latest-posts/", views.LatestPostsAPIView.as_view(), name="latest-posts"),
]
schema_view = get_schema_view(

View File

@ -60,3 +60,20 @@ class SwaggerRedirectView(RedirectView):
return HttpResponseRedirect(
self.SWAGGER_EDITOR_URL + request.build_absolute_uri(reverse("api:schema"))
)
class LatestPostsAPIView(ListAPIView):
"""
List my latest blog posts
"""
serializer_class = serializers.LatestPostSerializer
pagination_class = CustomPageNumberPagination
def get_queryset(self) -> PageQuerySet:
return (
BlogPostPage.objects.live()
.public()
.only("id", "url_path", "title", "date")
.order_by("-date")
)

View File

@ -7,6 +7,7 @@ from django.utils import timezone
from django.utils.functional import cached_property
from modelcluster.fields import ParentalManyToManyField
from wagtail.admin.panels import FieldPanel
from wagtail.models import PageQuerySet
from wagtail.search import index
from wagtailautocomplete.edit_handlers import AutocompletePanel
@ -61,6 +62,19 @@ class BlogPostPage(BaseContentPage):
def tag_list_page_url(self) -> Optional[str]:
return SingletonPageCache.get_url(BlogPostTagListPage)
@cached_property
def tags_list(self) -> models.QuerySet:
"""
Use this to get a page's tags.
"""
tags = self.tags.order_by("slug")
# In drafts, `django-modelcluster` doesn't support these filters
if isinstance(tags, PageQuerySet):
return tags.public().live()
return tags
@cached_property
def blog_post_list_page_url(self) -> Optional[str]:
return SingletonPageCache.get_url(BlogPostListPage)
@ -79,7 +93,7 @@ class BlogPostPage(BaseContentPage):
else models.Value(1),
)
page_tags = list(self.tags.values_list("id", flat=True))
page_tags = list(self.tags.public().live().values_list("id", flat=True))
similar_posts = similar_posts.alias(
# If this page has no tags, ignore it as part of similarity
# NB: Cast to a float, because `COUNT` returns a `bigint`.

View File

@ -8,7 +8,7 @@
{% endblock %}
{% block post_content %}
<section class="container">
<section class="container listing">
{% for page in listing_pages %}
{% ifchanged %}
<h2 id="date-{{ page.date|date:'Y-m' }}" class="date-header">

View File

@ -1,11 +1,11 @@
{% extends "common/content_page.html" %}
{% load wagtail_cache %}
{% load wagtail_cache navbar_tags %}
{% block post_content %}
{{ block.super }}
{% if not request.is_preview %}
{% include "common/shareon.html" %}
{% wagtailpagecache FRAGMENT_CACHE_TTL "similar-content" %}
<section class="container similar-content" id="similar-content">
<h2 class="subtitle is-size-2">Similar content</h2>
@ -22,5 +22,11 @@
</section>
{% endwagtailpagecache %}
{% include "common/comments.html" %}
{% if not request.user.is_authenticated %}
{% support_pill %}
{% endif %}
{% endif %}
{% endblock %}

View File

@ -18,7 +18,7 @@ class BlogPostPageTestCase(TestCase):
self.assertEqual(response.status_code, 200)
def test_queries(self) -> None:
with self.assertNumQueries(41):
with self.assertNumQueries(43):
self.client.get(self.page.url)
@ -76,7 +76,7 @@ class BlogPostListPageTestCase(TestCase):
self.assertEqual(len(response.context["listing_pages"]), 2)
def test_queries(self) -> None:
with self.assertNumQueries(37):
with self.assertNumQueries(39):
self.client.get(self.page.url)
def test_feed_accessible(self) -> None:

View File

@ -263,7 +263,12 @@ class BaseListingPage(RoutablePageMixin, BaseContentPage):
def get_context(self, request: HttpRequest) -> dict:
context = super().get_context(request)
context["listing_pages"] = self.get_paginator_page()
listing_pages = self.get_paginator_page()
context["listing_pages"] = listing_pages
# Show listing images if at least 1 page has an image
context["show_listing_images"] = any(p.list_image_url for p in listing_pages)
return context
@cached_property

View File

@ -20,17 +20,50 @@
</div>
{% endif %}
{% if page.tags.all %}
{% if page.tags_list %}
<div class="icon-text is-family-code">
<span class="icon">
<a href="{{ page.tag_list_page_url }}" title="View all tags">
<i class="fas fa-lg fa-tags"></i>
</a>
</span>
{% for tag in page.tags.all|dictsort:"slug" %}
{% for tag in page.tags_list %}
<span><a title="{{ tag.name }}" href="{% pageurl tag %}">#{{ tag.slug }}</a></span>
{% endfor %}
</div>
{% endif %}
{% if page.slides_url %}
<span class="icon-text">
<a href="{{ page.slides_url }}">
<span class="icon">
<i class="fas fa-lg fa-images"></i>
</span>
<span>Slides</span>
</a>
</span>
{% endif %}
{% if page.video_url %}
<span class="icon-text">
<a href="{{ page.video_url }}">
<span class="icon">
<i class="fas fa-lg fa-film"></i>
</span>
<span>Video</span>
</a>
</span>
{% endif %}
{% if page.location_name and page.location_url %}
<span class="icon-text">
<a href="{{ page.location_url }}">
<span class="icon">
<i class="fas fa-lg fa-map-marker-alt"></i>
</span>
<span>{{ page.location_name }}</span>
</a>
</span>
{% endif %}
</div>
{% endwagtailpagecache %}

View File

@ -1,9 +1,9 @@
{% load wagtailcore_tags wagtail_cache util_tags %}
{% wagtailpagecache FRAGMENT_CACHE_TTL "listing-item" breadcrumbs %}
{% wagtailpagecache FRAGMENT_CACHE_TTL "listing-item" breadcrumbs show_listing_images %}
<article class="media listing-item">
<div class="columns">
<figure class="media-left column is-3 image-column">
<figure class="media-left column is-{{ show_listing_images|yesno:'3,1' }} image-column">
{% if page.list_image_url %}
<a href="{% pageurl page %}" class="image" title="{{ page.title }}">
<img src="{{ page.list_image_url }}" alt="{{ page.hero_image_alt }}" loading="lazy" decoding="async" referrerpolicy="no-referrer" />

View File

@ -0,0 +1,9 @@
{% load wagtailcore_tags %}
{% spaceless %}
{{ obj.content_html | truncatewords_html:75 | safe }}
<p>
<a href="{% fullpageurl obj %}">Continue Reading&hellip;</a>
</p>
{% endspaceless %}

View File

@ -15,6 +15,7 @@ from wagtail.query import PageQuerySet
from wagtail_favicon.models import FaviconSettings
from wagtail_favicon.utils import get_rendition_url
from website.blog.models import BlogPostPage
from website.common.utils import get_site_title
from website.contrib.singleton_page.utils import SingletonPageCache
from website.home.models import HomePage
@ -63,6 +64,7 @@ class KeybaseView(TemplateView):
class AllPagesFeed(Feed):
feed_type = CustomFeed
link = "/"
description_template = "feed-description.html"
def __init__(self) -> None:
self.style_tag = f'<?xml-stylesheet href="{static("contrib/pretty-feed-v3.xsl")}" type="text/xsl"?>'.encode()
@ -122,12 +124,9 @@ class AllPagesFeed(Feed):
def item_updateddate(self, item: BasePage) -> datetime:
return item.last_published_at
def item_description(self, item: BasePage) -> str:
return getattr(item, "summary", None) or item.title
def item_categories(self, item: BasePage) -> Optional[list[str]]:
if tags := getattr(item, "tags", None):
return tags.order_by("slug").values_list("slug", flat=True)
if isinstance(item, BlogPostPage):
return item.tags_list.values_list("slug", flat=True)
return None
def item_enclosure_url(self, item: BasePage) -> Optional[str]:

View File

@ -1,8 +1,5 @@
from typing import Optional, Tuple
from django.db import models
from django.http.request import HttpRequest
from django_cache_decorator import django_cache_decorator
from wagtail.admin.panels import FieldPanel
from wagtail.images import get_image_model_string
from wagtail.images.models import Image
@ -12,20 +9,6 @@ from website.common.models import BasePage
from website.contrib.singleton_page.utils import SingletonPageCache
@django_cache_decorator(time=600)
def get_latest_blog_post() -> Optional[Tuple[str, str]]:
from website.blog.models import BlogPostPage
try:
latest_blog_post = (
BlogPostPage.objects.live().public().defer_streamfields().latest("date")
)
except BlogPostPage.DoesNotExist:
return None
return latest_blog_post.title, latest_blog_post.get_url()
class HomePage(BasePage, WagtailImageMetadataMixin):
max_count = 1
@ -55,9 +38,22 @@ class HomePage(BasePage, WagtailImageMetadataMixin):
return self.html_title
def get_context(self, request: HttpRequest) -> dict:
from website.blog.models import BlogPostListPage, BlogPostPage
from website.search.models import SearchPage
context = super().get_context(request)
context["latest_blog_post"] = get_latest_blog_post()
context["recent_posts"] = list(
BlogPostPage.objects.live()
.public()
.defer_streamfields()
.order_by("-date")[:7]
)
context["latest_blog_post"] = (
context["recent_posts"].pop(0) if context["recent_posts"] else None
)
context["search_page_url"] = SingletonPageCache.get_url(SearchPage, request)
context["blog_post_list_url"] = SingletonPageCache.get_url(
BlogPostListPage, request
)
return context

View File

@ -4,21 +4,38 @@
{% block main %}
<main {% if page.image %}style="background-image: url({% image_url page.image 'width-1200' %})"{% endif %}>
<div class="heading-wrapper">
<h1>{{ page.heading }}</h1>
{% if search_page_url %}
<form action="{{ search_page_url }}">
<input id="search-input" class="input" type="text" placeholder="Search" name="q" />
</form>
<div class="top-section">
<div class="heading-wrapper">
<h1>{{ page.heading }}</h1>
{% if search_page_url %}
<form action="{{ search_page_url }}">
<input id="search-input" class="input" type="text" placeholder="Search" name="q" />
</form>
{% endif %}
</div>
{% if latest_blog_post %}
<div class="box latest is-size-5">
<strong>Latest Post</strong>:
<a href="{% pageurl latest_blog_post %}">{{ latest_blog_post.title }}</a>
&rarr;
</div>
{% endif %}
</div>
{% if latest_blog_post %}
<div class="box latest">
<strong>Latest Post</strong>:
<a href="{{ latest_blog_post.1 }}">{{ latest_blog_post.0 }}</a>
&rarr;
<section class="container content recent-posts">
<h2 class="has-text-centered has-text-white is-size-3">Recent Posts</h2>
<div class="columns content-list is-multiline">
{% for page in recent_posts %}
{% include "home/home_page_card.html" %}
{% endfor %}
</div>
{% endif %}
{% if blog_post_list_url %}
<div class="box">
<a href="{{ blog_post_list_url }}">View more &rarr;</a>
</div>
{% endif %}
</section>
</main>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% load wagtail_cache wagtailcore_tags %}
{% wagtailpagecache FRAGMENT_CACHE_TTL "homepage-card" %}
<div class="column is-one-third-widescreen is-half">
<div class="card">
<div class="card-image">
<a href="{% pageurl page %}">
<figure class="image is-16by9">
<img src="{{ page.list_image_url }}" alt="{{ page.hero_image_alt }}" loading="lazy" decoding="async" referrerpolicy="no-referrer" />
<p>{{ page.title }}</p>
</figure>
</a>
</div>
</div>
</div>
{% endwagtailpagecache %}

View File

@ -10,4 +10,5 @@ urlpatterns = [
path("tags/<slug:slug>/", views.TagView.as_view()),
path("tags/", views.TagsView.as_view()),
path("categories/", views.TagsView.as_view()),
path("index.json", views.PageLinksView.as_view()),
]

View File

@ -12,12 +12,18 @@ class AllPagesFeedView(RedirectView):
permanent = True
@method_decorator(cache_control(max_age=60 * 60), name="dispatch")
class PageLinksView(RedirectView):
pattern_name = "api:page-links"
permanent = True
@method_decorator(cache_control(max_age=60 * 60), name="dispatch")
class TagView(RedirectView):
permanent = True
def get_redirect_url(self, slug: str) -> str:
tag = get_object_or_404(BlogPostTagPage, slug=slug)
tag = get_object_or_404(BlogPostTagPage.objects.public().live(), slug=slug)
return tag.get_url(request=self.request)

View File

@ -42,6 +42,7 @@ INSTALLED_APPS = [
"website.utils",
"website.well_known",
"website.legacy",
"website.talks",
"website.contrib.code_block",
"website.contrib.mermaid_block",
"website.contrib.unsplash",

View File

View File

@ -0,0 +1,17 @@
from datetime import timedelta
from website.common.factories import BaseContentFactory, BaseListingFactory
from . import models
class TalksListPageFactory(BaseListingFactory):
class Meta:
model = models.TalksListPage
class TalkPageFactory(BaseContentFactory):
duration = timedelta(minutes=30)
class Meta:
model = models.TalkPage

View File

@ -0,0 +1,354 @@
# Generated by Django 5.0.1 on 2024-03-01 17:44
import django.db.models.deletion
import django.utils.timezone
import wagtail.blocks
import wagtail.contrib.routable_page.models
import wagtail.contrib.typed_table_block.blocks
import wagtail.embeds.blocks
import wagtail.fields
import wagtail.images.blocks
import wagtailmetadata.models
from django.db import migrations, models
import website.contrib.code_block.blocks
class Migration(migrations.Migration):
initial = True
dependencies = [
("images", "0002_alter_customimage_file_alter_customrendition_file"),
("unsplash", "0001_initial"),
("wagtailcore", "0089_log_entry_data_json_null_to_object"),
]
operations = [
migrations.CreateModel(
name="TalkPage",
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",
),
),
("subtitle", wagtail.fields.RichTextField(blank=True)),
(
"body",
wagtail.fields.StreamField(
[
("embed", wagtail.embeds.blocks.EmbedBlock()),
("rich_text", wagtail.blocks.RichTextBlock()),
(
"lorem",
wagtail.blocks.StructBlock(
[
(
"paragraphs",
wagtail.blocks.IntegerBlock(min_value=1),
)
]
),
),
("html", wagtail.blocks.RawHTMLBlock()),
(
"image",
wagtail.blocks.StructBlock(
[
(
"image",
wagtail.images.blocks.ImageChooserBlock(),
),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
(
"code",
wagtail.blocks.StructBlock(
[
(
"filename",
wagtail.blocks.CharBlock(
max_length=128, required=False
),
),
(
"language",
wagtail.blocks.ChoiceBlock(
choices=website.contrib.code_block.blocks.get_language_choices
),
),
("source", wagtail.blocks.TextBlock()),
]
),
),
(
"tangent",
wagtail.blocks.StructBlock(
[
(
"name",
wagtail.blocks.CharBlock(max_length=64),
),
(
"content",
wagtail.blocks.RichTextBlock(
editor="simple"
),
),
]
),
),
(
"mermaid",
wagtail.blocks.StructBlock(
[
("source", wagtail.blocks.TextBlock()),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
(
"table",
wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
[
(
"rich_text",
wagtail.blocks.RichTextBlock(
editor="plain"
),
),
("numeric", wagtail.blocks.FloatBlock()),
("text", wagtail.blocks.CharBlock()),
]
),
),
(
"iframe",
wagtail.blocks.StructBlock(
[
("url", wagtail.blocks.URLBlock()),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
],
blank=True,
use_json_field=True,
),
),
("date", models.DateField(default=django.utils.timezone.now)),
("duration", models.DurationField()),
("slides_url", models.URLField(blank=True)),
("video_url", models.URLField(blank=True)),
("location_name", models.CharField(blank=True, max_length=64)),
("location_url", models.URLField(blank=True)),
(
"hero_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="images.customimage",
),
),
(
"hero_unsplash_photo",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="unsplash.unsplashphoto",
),
),
],
options={
"abstract": False,
},
bases=("wagtailcore.page", wagtailmetadata.models.MetadataMixin),
),
migrations.CreateModel(
name="TalksListPage",
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",
),
),
(
"body",
wagtail.fields.StreamField(
[
("embed", wagtail.embeds.blocks.EmbedBlock()),
("rich_text", wagtail.blocks.RichTextBlock()),
(
"lorem",
wagtail.blocks.StructBlock(
[
(
"paragraphs",
wagtail.blocks.IntegerBlock(min_value=1),
)
]
),
),
("html", wagtail.blocks.RawHTMLBlock()),
(
"image",
wagtail.blocks.StructBlock(
[
(
"image",
wagtail.images.blocks.ImageChooserBlock(),
),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
(
"code",
wagtail.blocks.StructBlock(
[
(
"filename",
wagtail.blocks.CharBlock(
max_length=128, required=False
),
),
(
"language",
wagtail.blocks.ChoiceBlock(
choices=website.contrib.code_block.blocks.get_language_choices
),
),
("source", wagtail.blocks.TextBlock()),
]
),
),
(
"tangent",
wagtail.blocks.StructBlock(
[
(
"name",
wagtail.blocks.CharBlock(max_length=64),
),
(
"content",
wagtail.blocks.RichTextBlock(
editor="simple"
),
),
]
),
),
(
"mermaid",
wagtail.blocks.StructBlock(
[
("source", wagtail.blocks.TextBlock()),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
(
"table",
wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
[
(
"rich_text",
wagtail.blocks.RichTextBlock(
editor="plain"
),
),
("numeric", wagtail.blocks.FloatBlock()),
("text", wagtail.blocks.CharBlock()),
]
),
),
(
"iframe",
wagtail.blocks.StructBlock(
[
("url", wagtail.blocks.URLBlock()),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
],
blank=True,
use_json_field=True,
),
),
(
"hero_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="images.customimage",
),
),
(
"hero_unsplash_photo",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="unsplash.unsplashphoto",
),
),
],
options={
"abstract": False,
},
bases=(
wagtail.contrib.routable_page.models.RoutablePageMixin,
"wagtailcore.page",
wagtailmetadata.models.MetadataMixin,
),
),
]

View File

62
website/talks/models.py Normal file
View File

@ -0,0 +1,62 @@
from datetime import timedelta
from typing import Any
from django.db import models
from django.utils import timezone
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from website.common.models import BaseContentPage, BaseListingPage
class TalksListPage(BaseListingPage):
max_count = 1
subpage_types = ["talks.TalkPage"]
class TalkPage(BaseContentPage):
subpage_types: list[Any] = []
parent_page_types = [TalksListPage]
date = models.DateField(default=timezone.now)
duration = models.DurationField()
slides_url = models.URLField(blank=True)
video_url = models.URLField(blank=True)
location_name = models.CharField(max_length=64, blank=True)
location_url = models.URLField(blank=True)
content_panels = BaseContentPage.content_panels + [
MultiFieldPanel(
[
FieldPanel("slides_url"),
FieldPanel("video_url"),
],
heading="Media",
),
MultiFieldPanel(
[
FieldPanel("location_name"),
FieldPanel("location_url"),
],
heading="Location",
),
FieldPanel("duration"),
]
promote_panels = BaseContentPage.promote_panels + [
FieldPanel("date"),
]
@property
def show_table_of_contents(self) -> bool:
return False
@property
def reading_time(self) -> timedelta:
return self.duration
@property
def word_count(self) -> int:
return 0

View File

@ -0,0 +1,11 @@
{% extends "common/content_page.html" %}
{% load wagtailembeds_tags %}
{% block pre_content %}
{% if page.video_url %}
<section class="container mb-5 content">
<div class="block-embed">{% embed page.video_url %}</div>
</section>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "common/listing_page.html" %}
{% load wagtailroutablepage_tags %}
{% block post_content %}
<section class="container listing">
{% for page in listing_pages %}
{% ifchanged %}
<h2 id="date-{{ page.date.year }}" class="date-header">
<time datetime="{{ page.date.year }}" title="{{ page.date.year }}">
{{ page.date.year }}
</time>
</h2>
{% endifchanged %}
{% include "common/listing-item.html" %}
{% endfor %}
</section>
{% if listing_pages.has_other_pages %}
<section class="container">
<hr class="my-5" />
{% include "common/pagination.html" with page=listing_pages %}
</section>
{% endif %}
{% endblock %}

47
website/talks/tests.py Normal file
View File

@ -0,0 +1,47 @@
from django.test import TestCase
from django.urls import reverse
from website.home.models import HomePage
from .factories import TalkPageFactory, TalksListPageFactory
class TalkPageTestCase(TestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
cls.list_page = TalksListPageFactory(parent=cls.home_page)
cls.page = TalkPageFactory(parent=cls.list_page)
def test_accessible(self) -> None:
response = self.client.get(self.page.url)
self.assertEqual(response.status_code, 200)
def test_queries(self) -> None:
with self.assertNumQueries(34):
self.client.get(self.page.url)
class TalksListPageTestCase(TestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
cls.page = TalksListPageFactory(parent=cls.home_page)
TalkPageFactory(parent=cls.page)
TalkPageFactory(parent=cls.page)
def test_accessible(self) -> None:
response = self.client.get(self.page.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["listing_pages"]), 2)
def test_queries(self) -> None:
with self.assertNumQueries(35):
self.client.get(self.page.url)
def test_feed_accessible(self) -> None:
response = self.client.get(self.page.url + self.page.reverse_subpage("feed"))
self.assertRedirects(
response, reverse("feed"), status_code=301, fetch_redirect_response=True
)

View File

@ -1,4 +1,5 @@
from datetime import timedelta
from urllib.parse import urljoin
from django.conf import settings
from django.http.request import HttpRequest
@ -56,10 +57,13 @@ def activitypub_proxy(request: HttpRequest) -> HttpResponse:
if not settings.ACTIVITYPUB_HOST:
raise Http404
activitypub_url = urljoin(
"https://" + settings.ACTIVITYPUB_HOST,
request.path,
allow_fragments=True,
)
try:
return proxy_view(
request,
f"https://{settings.ACTIVITYPUB_HOST}{request.path}",
)
return proxy_view(request, activitypub_url)
except RequestException:
return HttpResponse(status_code=502)
return HttpResponse(status=502)