From bd4c1a193a378409242ca10a6c4cf3f2e24e0fa4 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Fri, 1 Mar 2024 17:09:56 +0000 Subject: [PATCH] Add page type for talks Content coming soon, probably --- .stylelintrc | 5 +- static/src/scss/_listing.scss | 2 +- .../templates/blog/blog_post_list_page.html | 2 +- .../templates/common/content-details.html | 22 ++ website/settings.py | 1 + website/talks/__init__.py | 0 website/talks/factories.py | 17 + website/talks/migrations/0001_initial.py | 352 ++++++++++++++++++ website/talks/migrations/__init__.py | 0 website/talks/models.py | 52 +++ website/talks/templates/talks/talk_page.html | 11 + .../templates/talks/talks_list_page.html | 26 ++ website/talks/tests.py | 47 +++ 13 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 website/talks/__init__.py create mode 100644 website/talks/factories.py create mode 100644 website/talks/migrations/0001_initial.py create mode 100644 website/talks/migrations/__init__.py create mode 100644 website/talks/models.py create mode 100644 website/talks/templates/talks/talk_page.html create mode 100644 website/talks/templates/talks/talks_list_page.html create mode 100644 website/talks/tests.py diff --git a/.stylelintrc b/.stylelintrc index fac097b..96d55eb 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -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 + } } diff --git a/static/src/scss/_listing.scss b/static/src/scss/_listing.scss index e6b095c..0afc4db 100644 --- a/static/src/scss/_listing.scss +++ b/static/src/scss/_listing.scss @@ -48,7 +48,7 @@ } } -.page-blogpostlistpage { +.container.listing { .date-header { font-size: $size-2; font-weight: $weight-bold; diff --git a/website/blog/templates/blog/blog_post_list_page.html b/website/blog/templates/blog/blog_post_list_page.html index 6327720..9a8b717 100644 --- a/website/blog/templates/blog/blog_post_list_page.html +++ b/website/blog/templates/blog/blog_post_list_page.html @@ -8,7 +8,7 @@ {% endblock %} {% block post_content %} -
+
{% for page in listing_pages %} {% ifchanged %}

diff --git a/website/common/templates/common/content-details.html b/website/common/templates/common/content-details.html index f57a57b..956a7da 100644 --- a/website/common/templates/common/content-details.html +++ b/website/common/templates/common/content-details.html @@ -32,5 +32,27 @@ {% endfor %} {% endif %} + + {% if page.slides_url %} + + + + + + Slides + + + {% endif %} + + {% if page.video_url %} + + + + + + Video + + + {% endif %} {% endwagtailpagecache %} diff --git a/website/settings.py b/website/settings.py index 5b944ea..405f1ee 100644 --- a/website/settings.py +++ b/website/settings.py @@ -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", diff --git a/website/talks/__init__.py b/website/talks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/website/talks/factories.py b/website/talks/factories.py new file mode 100644 index 0000000..8817efe --- /dev/null +++ b/website/talks/factories.py @@ -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 diff --git a/website/talks/migrations/0001_initial.py b/website/talks/migrations/0001_initial.py new file mode 100644 index 0000000..a3c5487 --- /dev/null +++ b/website/talks/migrations/0001_initial.py @@ -0,0 +1,352 @@ +# Generated by Django 5.0.1 on 2024-03-01 16:55 + +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)), + ( + "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, + ), + ), + ] diff --git a/website/talks/migrations/__init__.py b/website/talks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/website/talks/models.py b/website/talks/models.py new file mode 100644 index 0000000..076eebf --- /dev/null +++ b/website/talks/models.py @@ -0,0 +1,52 @@ +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) + + content_panels = BaseContentPage.content_panels + [ + MultiFieldPanel( + [ + FieldPanel("slides_url"), + FieldPanel("video_url"), + ], + heading="Media", + ), + 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 diff --git a/website/talks/templates/talks/talk_page.html b/website/talks/templates/talks/talk_page.html new file mode 100644 index 0000000..897a151 --- /dev/null +++ b/website/talks/templates/talks/talk_page.html @@ -0,0 +1,11 @@ +{% extends "common/content_page.html" %} + +{% load wagtailembeds_tags %} + +{% block pre_content %} + {% if page.video_url %} +
+
{% embed page.video_url %}
+
+ {% endif %} +{% endblock %} diff --git a/website/talks/templates/talks/talks_list_page.html b/website/talks/templates/talks/talks_list_page.html new file mode 100644 index 0000000..88b1839 --- /dev/null +++ b/website/talks/templates/talks/talks_list_page.html @@ -0,0 +1,26 @@ +{% extends "common/listing_page.html" %} + +{% load wagtailroutablepage_tags %} + +{% block post_content %} +
+ {% for page in listing_pages %} + {% ifchanged %} +

+ +

+ {% endifchanged %} + + {% include "common/listing-item.html" %} + {% endfor %} +
+ + {% if listing_pages.has_other_pages %} +
+
+ {% include "common/pagination.html" with page=listing_pages %} +
+ {% endif %} +{% endblock %} diff --git a/website/talks/tests.py b/website/talks/tests.py new file mode 100644 index 0000000..b13aeed --- /dev/null +++ b/website/talks/tests.py @@ -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 + )