feat: rebuild site content management
Replace legacy FAQ, downloads, and release flows with project and blog publishing. Add configurable branding and theme fields plus clean-start Compose seeding support.
This commit is contained in:
+16
-28
@@ -9,13 +9,12 @@ from apps.core.video import VIDEO_ADMIN_FIELDSET
|
||||
from .models import (
|
||||
AboutSection,
|
||||
AboutSectionItem,
|
||||
BlogPost,
|
||||
ContactSubmission,
|
||||
ContactSubmissionAttachment,
|
||||
CustomPage,
|
||||
CustomPageSection,
|
||||
CustomPageSectionItem,
|
||||
DownloadItem,
|
||||
FAQEntry,
|
||||
HeroSection,
|
||||
HomepageSection,
|
||||
HomepageSectionItem,
|
||||
@@ -74,7 +73,6 @@ class HomepageSectionAdmin(admin.ModelAdmin):
|
||||
def video_preview(self, obj):
|
||||
return video_admin_preview(obj)
|
||||
|
||||
|
||||
class AboutSectionItemInline(admin.TabularInline):
|
||||
model = AboutSectionItem
|
||||
extra = 1
|
||||
@@ -154,19 +152,6 @@ class ContactSubmissionAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
|
||||
@admin.register(FAQEntry)
|
||||
class FAQEntryAdmin(admin.ModelAdmin):
|
||||
list_display = ("question", "order", "is_active", "created_at")
|
||||
list_filter = ("is_active",)
|
||||
search_fields = ("question", "answer")
|
||||
list_editable = ("order", "is_active")
|
||||
fieldsets = (
|
||||
(None, {"fields": ("question",)}),
|
||||
("Answer", {"fields": ("answer_format", "answer")}),
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
|
||||
|
||||
class CustomPageSectionItemInline(admin.TabularInline):
|
||||
model = CustomPageSectionItem
|
||||
extra = 1
|
||||
@@ -290,6 +275,21 @@ class CustomPageAdmin(admin.ModelAdmin):
|
||||
admin.site.register(CustomPageSection, CustomPageSectionAdmin)
|
||||
|
||||
|
||||
@admin.register(BlogPost)
|
||||
class BlogPostAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "status", "published_at", "updated_at")
|
||||
list_filter = ("status", "published_at")
|
||||
search_fields = ("title", "excerpt", "content", "author_name")
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
date_hierarchy = "published_at"
|
||||
fieldsets = (
|
||||
(None, {"fields": ("title", "slug", "excerpt")}),
|
||||
("Article", {"fields": ("content_format", "content")}),
|
||||
("Cover image", {"fields": ("cover_image", "cover_image_alt")}),
|
||||
("Publishing", {"fields": ("status", "published_at", "author_name")}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(PageVideo)
|
||||
class PageVideoAdmin(admin.ModelAdmin):
|
||||
list_display = ("page", "title", "video_source", "video_size", "order", "is_active")
|
||||
@@ -306,15 +306,3 @@ class PageVideoAdmin(admin.ModelAdmin):
|
||||
@admin.display(description="Preview")
|
||||
def video_preview(self, obj):
|
||||
return video_admin_preview(obj)
|
||||
|
||||
|
||||
@admin.register(DownloadItem)
|
||||
class DownloadItemAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "platform", "version", "is_active", "order")
|
||||
list_filter = ("platform", "is_active")
|
||||
search_fields = ("name", "description")
|
||||
list_editable = ("order", "is_active")
|
||||
fieldsets = (
|
||||
(None, {"fields": ("name", "platform", "version", "download_url", "description")}),
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Generated by Django 5.2.16 on 2026-09-23 22:42
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0018_alter_herosection_title'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=240)),
|
||||
('slug', models.SlugField(blank=True, max_length=240, unique=True)),
|
||||
('excerpt', models.CharField(blank=True, help_text='Short summary shown on the blog listing and in search previews.', max_length=400)),
|
||||
('content', models.TextField()),
|
||||
('content_format', models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='markdown', max_length=20)),
|
||||
('cover_image', models.ImageField(blank=True, null=True, upload_to='blog/')),
|
||||
('cover_image_alt', models.CharField(blank=True, max_length=240)),
|
||||
('author_name', models.CharField(blank=True, max_length=120)),
|
||||
('status', models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=20)),
|
||||
('published_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog post',
|
||||
'verbose_name_plural': 'Blog posts',
|
||||
'ordering': ['-published_at', '-pk'],
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aboutsection',
|
||||
name='video_styled_background',
|
||||
field=models.BooleanField(default=False, help_text='Show the video in a glass panel with an ambient glow.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='custompagesection',
|
||||
name='section_type',
|
||||
field=models.CharField(choices=[('hero', 'Hero'), ('intro', 'Intro Card'), ('grid', 'Grid Cards'), ('history', 'History Block'), ('custom', 'Custom Content'), ('features', 'Features Grid'), ('screenshots', 'Screenshots Gallery'), ('products', 'Projects Grid'), ('products_catalog', 'Projects with sub-projects'), ('problems', 'Problems / Value Proposition'), ('supporters', 'Supporters'), ('about_strip', 'About Strip'), ('video', 'Video')], default='custom', max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='custompagesection',
|
||||
name='video_styled_background',
|
||||
field=models.BooleanField(default=False, help_text='Show the video in a glass panel with an ambient glow.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='homepagesection',
|
||||
name='section_type',
|
||||
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Projects'), ('products_catalog', 'Projects with sub-projects'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters'), ('video', 'Video')], max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='homepagesection',
|
||||
name='video_styled_background',
|
||||
field=models.BooleanField(default=False, help_text='Show the video in a glass panel with an ambient glow.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pagevideo',
|
||||
name='page',
|
||||
field=models.CharField(choices=[('contact', 'Become a Member')], max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pagevideo',
|
||||
name='video_styled_background',
|
||||
field=models.BooleanField(default=False, help_text='Show the video in a glass panel with an ambient glow.'),
|
||||
),
|
||||
]
|
||||
+65
-9
@@ -1,5 +1,7 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_MARKDOWN, FORMAT_PLAIN, render_content
|
||||
@@ -13,8 +15,10 @@ RESERVED_PAGE_SLUGS = frozenset({
|
||||
"admin",
|
||||
"about",
|
||||
"contact",
|
||||
"blog",
|
||||
"faq",
|
||||
"home",
|
||||
"projects",
|
||||
"products",
|
||||
"static",
|
||||
"media",
|
||||
@@ -55,8 +59,8 @@ class HomepageSection(VideoBlockMixin, models.Model):
|
||||
TYPE_CHOICES = [
|
||||
(TYPE_FEATURES, "Features"),
|
||||
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
||||
(TYPE_PRODUCTS, "Products"),
|
||||
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
|
||||
(TYPE_PRODUCTS, "Projects"),
|
||||
(TYPE_PRODUCTS_CATALOG, "Projects with sub-projects"),
|
||||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||||
(TYPE_ABOUT_STRIP, "About Strip"),
|
||||
(TYPE_SUPPORTERS, "Supporters"),
|
||||
@@ -310,6 +314,62 @@ class CustomPage(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class BlogPost(models.Model):
|
||||
STATUS_DRAFT = "draft"
|
||||
STATUS_PUBLISHED = "published"
|
||||
STATUS_CHOICES = [
|
||||
(STATUS_DRAFT, "Draft"),
|
||||
(STATUS_PUBLISHED, "Published"),
|
||||
]
|
||||
|
||||
title = models.CharField(max_length=240)
|
||||
slug = models.SlugField(max_length=240, unique=True, blank=True)
|
||||
excerpt = models.CharField(
|
||||
max_length=400,
|
||||
blank=True,
|
||||
help_text="Short summary shown on the blog listing and in search previews.",
|
||||
)
|
||||
content = models.TextField()
|
||||
content_format = models.CharField(
|
||||
max_length=20,
|
||||
choices=CONTENT_FORMAT_CHOICES,
|
||||
default=FORMAT_MARKDOWN,
|
||||
)
|
||||
cover_image = models.ImageField(upload_to="blog/", blank=True, null=True)
|
||||
cover_image_alt = models.CharField(max_length=240, blank=True)
|
||||
author_name = models.CharField(max_length=120, blank=True)
|
||||
status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default=STATUS_DRAFT
|
||||
)
|
||||
published_at = models.DateTimeField(default=timezone.now)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-published_at", "-pk"]
|
||||
verbose_name = "Blog post"
|
||||
verbose_name_plural = "Blog posts"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def rendered_content(self):
|
||||
return render_content(self.content, self.content_format)
|
||||
|
||||
@property
|
||||
def is_public(self):
|
||||
return self.status == self.STATUS_PUBLISHED and self.published_at <= timezone.now()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.title)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("pages:blog_detail", kwargs={"slug": self.slug})
|
||||
|
||||
|
||||
class CustomPageSection(VideoBlockMixin, models.Model):
|
||||
TYPE_HERO = "hero"
|
||||
TYPE_INTRO = "intro"
|
||||
@@ -323,7 +383,6 @@ class CustomPageSection(VideoBlockMixin, models.Model):
|
||||
TYPE_PROBLEMS = "problems"
|
||||
TYPE_SUPPORTERS = "supporters"
|
||||
TYPE_ABOUT_STRIP = "about_strip"
|
||||
TYPE_FAQ = "faq"
|
||||
TYPE_VIDEO = "video"
|
||||
|
||||
TYPE_CHOICES = [
|
||||
@@ -334,12 +393,11 @@ class CustomPageSection(VideoBlockMixin, models.Model):
|
||||
(TYPE_CUSTOM, "Custom Content"),
|
||||
(TYPE_FEATURES, "Features Grid"),
|
||||
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
||||
(TYPE_PRODUCTS, "Products Grid"),
|
||||
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
|
||||
(TYPE_PRODUCTS, "Projects Grid"),
|
||||
(TYPE_PRODUCTS_CATALOG, "Projects with sub-projects"),
|
||||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||||
(TYPE_SUPPORTERS, "Supporters"),
|
||||
(TYPE_ABOUT_STRIP, "About Strip"),
|
||||
(TYPE_FAQ, "FAQ Accordion"),
|
||||
(TYPE_VIDEO, "Video"),
|
||||
]
|
||||
|
||||
@@ -377,11 +435,9 @@ class CustomPageSection(VideoBlockMixin, models.Model):
|
||||
|
||||
class PageVideo(VideoBlockMixin, models.Model):
|
||||
PAGE_CONTACT = "contact"
|
||||
PAGE_FAQ = "faq"
|
||||
|
||||
PAGE_CHOICES = [
|
||||
(PAGE_CONTACT, "Contact"),
|
||||
(PAGE_FAQ, "FAQ"),
|
||||
(PAGE_CONTACT, "Become a Member"),
|
||||
]
|
||||
|
||||
page = models.CharField(max_length=20, choices=PAGE_CHOICES)
|
||||
|
||||
@@ -103,37 +103,37 @@ class PageVideoTest(TestCase):
|
||||
self.assertContains(response, "Intro")
|
||||
self.assertContains(response, "youtube-nocookie.com/embed/dQw4w9WgXcQ")
|
||||
|
||||
def test_faq_page_video(self):
|
||||
def test_membership_page_video(self):
|
||||
PageVideo.objects.create(
|
||||
page=PageVideo.PAGE_FAQ,
|
||||
page=PageVideo.PAGE_CONTACT,
|
||||
title="Tutorial",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
response = self.client.get(reverse("pages:contact"))
|
||||
self.assertContains(response, "Tutorial")
|
||||
|
||||
def test_page_shows_three_video_preview_and_archive_link(self):
|
||||
self._create_page_videos(PageVideo.PAGE_FAQ, 8)
|
||||
self._create_page_videos(PageVideo.PAGE_CONTACT, 8)
|
||||
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
response = self.client.get(reverse("pages:contact"))
|
||||
|
||||
self.assertEqual(response.content.count(b"youtube-nocookie.com/embed"), 3)
|
||||
self.assertContains(
|
||||
response,
|
||||
reverse(
|
||||
"pages:video_archive",
|
||||
kwargs={"library": PageVideo.PAGE_FAQ},
|
||||
kwargs={"library": PageVideo.PAGE_CONTACT},
|
||||
),
|
||||
)
|
||||
self.assertContains(response, "More videos")
|
||||
|
||||
def test_page_video_archive_is_paginated(self):
|
||||
self._create_page_videos(PageVideo.PAGE_FAQ, 8)
|
||||
self._create_page_videos(PageVideo.PAGE_CONTACT, 8)
|
||||
archive_url = reverse(
|
||||
"pages:video_archive",
|
||||
kwargs={"library": PageVideo.PAGE_FAQ},
|
||||
kwargs={"library": PageVideo.PAGE_CONTACT},
|
||||
)
|
||||
|
||||
first_page = self.client.get(archive_url)
|
||||
@@ -248,7 +248,7 @@ class VideoDescriptionFormatTest(TestCase):
|
||||
|
||||
def test_markdown_description_renders(self):
|
||||
PageVideo.objects.create(
|
||||
page=PageVideo.PAGE_FAQ,
|
||||
page=PageVideo.PAGE_CONTACT,
|
||||
title="Guide",
|
||||
description="**Bold** intro",
|
||||
description_format="markdown",
|
||||
@@ -256,5 +256,5 @@ class VideoDescriptionFormatTest(TestCase):
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
response = self.client.get(reverse("pages:contact"))
|
||||
self.assertContains(response, "<strong>Bold</strong>")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.pages.models import FAQEntry
|
||||
from apps.pages.models import BlogPost, HomepageSection
|
||||
|
||||
|
||||
class HomeViewTest(TestCase):
|
||||
@@ -13,6 +14,16 @@ class HomeViewTest(TestCase):
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertTemplateUsed(response, "pages/home.html")
|
||||
|
||||
def test_section_description_renders_markdown_and_html(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_FEATURES,
|
||||
title="Rich section",
|
||||
description="**Formatted** content",
|
||||
description_format="markdown",
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertContains(response, "<strong>Formatted</strong>")
|
||||
|
||||
|
||||
class AboutViewTest(TestCase):
|
||||
def test_about_returns_200(self):
|
||||
@@ -24,34 +35,30 @@ class AboutViewTest(TestCase):
|
||||
self.assertTemplateUsed(response, "pages/about.html")
|
||||
|
||||
|
||||
class FAQViewTest(TestCase):
|
||||
class BlogViewTest(TestCase):
|
||||
def setUp(self):
|
||||
FAQEntry.objects.create(
|
||||
question="What is the license?",
|
||||
answer="It is CC BY-NC-SA.",
|
||||
order=1,
|
||||
is_active=True,
|
||||
)
|
||||
FAQEntry.objects.create(
|
||||
question="Hidden question",
|
||||
answer="Hidden answer",
|
||||
order=2,
|
||||
is_active=False,
|
||||
self.post = BlogPost.objects.create(
|
||||
title="Research update",
|
||||
slug="research-update",
|
||||
excerpt="A short summary.",
|
||||
content="## Results\n\n**Promising** findings.",
|
||||
content_format="markdown",
|
||||
status=BlogPost.STATUS_PUBLISHED,
|
||||
published_at=timezone.now(),
|
||||
)
|
||||
|
||||
def test_faq_returns_200(self):
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
def test_blog_list_and_detail_render_published_post(self):
|
||||
listing = self.client.get(reverse("pages:blog_list"))
|
||||
detail = self.client.get(self.post.get_absolute_url())
|
||||
self.assertContains(listing, self.post.title)
|
||||
self.assertContains(detail, "<h2>Results</h2>")
|
||||
self.assertContains(detail, "<strong>Promising</strong>")
|
||||
|
||||
def test_faq_uses_correct_template(self):
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
self.assertTemplateUsed(response, "pages/faq.html")
|
||||
|
||||
def test_faq_only_shows_active_entries(self):
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
entries = response.context["faq_entries"]
|
||||
self.assertEqual(entries.count(), 1)
|
||||
self.assertEqual(entries.first().question, "What is the license?")
|
||||
def test_draft_post_is_not_public(self):
|
||||
self.post.status = BlogPost.STATUS_DRAFT
|
||||
self.post.save(update_fields=["status"])
|
||||
self.assertNotContains(self.client.get(reverse("pages:blog_list")), self.post.title)
|
||||
self.assertEqual(self.client.get(self.post.get_absolute_url()).status_code, 404)
|
||||
|
||||
|
||||
class ContactViewTest(TestCase):
|
||||
@@ -69,7 +76,7 @@ class NavigationContextTest(TestCase):
|
||||
urls = [
|
||||
reverse("pages:home"),
|
||||
reverse("pages:about"),
|
||||
reverse("pages:faq"),
|
||||
reverse("pages:blog_list"),
|
||||
reverse("pages:contact"),
|
||||
reverse("products:overview"),
|
||||
]
|
||||
@@ -85,7 +92,7 @@ class NavigationContextTest(TestCase):
|
||||
urls = [
|
||||
reverse("pages:home"),
|
||||
reverse("pages:about"),
|
||||
reverse("pages:faq"),
|
||||
reverse("pages:blog_list"),
|
||||
reverse("pages:contact"),
|
||||
]
|
||||
for url in urls:
|
||||
|
||||
+2
-1
@@ -7,8 +7,9 @@ app_name = "pages"
|
||||
urlpatterns = [
|
||||
path("", views.HomeView.as_view(), name="home"),
|
||||
path("about/", views.AboutView.as_view(), name="about"),
|
||||
path("faq/", views.FAQView.as_view(), name="faq"),
|
||||
path("contact/", views.ContactView.as_view(), name="contact"),
|
||||
path("blog/", views.BlogPostListView.as_view(), name="blog_list"),
|
||||
path("blog/<slug:slug>/", views.BlogPostDetailView.as_view(), name="blog_detail"),
|
||||
path(
|
||||
"videos/<slug:library>/",
|
||||
views.PageVideoArchiveView.as_view(),
|
||||
|
||||
+27
-31
@@ -4,6 +4,7 @@ from django.db.models import Prefetch
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, ListView, TemplateView
|
||||
|
||||
@@ -12,10 +13,10 @@ from apps.products.models import MainProduct, SubProduct
|
||||
from .forms import ContactForm
|
||||
from .models import (
|
||||
AboutSection,
|
||||
BlogPost,
|
||||
ContactSubmission,
|
||||
ContactSubmissionAttachment,
|
||||
CustomPage,
|
||||
FAQEntry,
|
||||
HeroSection,
|
||||
HomepageSection,
|
||||
PageVideo,
|
||||
@@ -79,31 +80,6 @@ class AboutView(TemplateView):
|
||||
return ctx
|
||||
|
||||
|
||||
class FAQView(ListView):
|
||||
model = FAQEntry
|
||||
template_name = "pages/faq.html"
|
||||
context_object_name = "faq_entries"
|
||||
queryset = FAQEntry.objects.filter(is_active=True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
videos = PageVideo.objects.filter(
|
||||
page=PageVideo.PAGE_FAQ,
|
||||
is_active=True,
|
||||
).order_by("order", "pk")
|
||||
preview = _video_preview_context(
|
||||
videos,
|
||||
reverse(
|
||||
"pages:video_archive",
|
||||
kwargs={"library": PageVideo.PAGE_FAQ},
|
||||
),
|
||||
)
|
||||
ctx["faq_videos"] = preview["videos"]
|
||||
ctx["videos_total_count"] = preview["videos_total_count"]
|
||||
ctx["videos_archive_url"] = preview["videos_archive_url"]
|
||||
return ctx
|
||||
|
||||
|
||||
class ContactView(View):
|
||||
template_name = "pages/contact.html"
|
||||
|
||||
@@ -198,6 +174,31 @@ class CustomPageView(DetailView):
|
||||
return ctx
|
||||
|
||||
|
||||
class BlogPostListView(ListView):
|
||||
model = BlogPost
|
||||
template_name = "pages/blog_list.html"
|
||||
context_object_name = "posts"
|
||||
paginate_by = 9
|
||||
|
||||
def get_queryset(self):
|
||||
return BlogPost.objects.filter(
|
||||
status=BlogPost.STATUS_PUBLISHED,
|
||||
published_at__lte=timezone.now(),
|
||||
)
|
||||
|
||||
|
||||
class BlogPostDetailView(DetailView):
|
||||
model = BlogPost
|
||||
template_name = "pages/blog_detail.html"
|
||||
context_object_name = "post"
|
||||
|
||||
def get_queryset(self):
|
||||
return BlogPost.objects.filter(
|
||||
status=BlogPost.STATUS_PUBLISHED,
|
||||
published_at__lte=timezone.now(),
|
||||
)
|
||||
|
||||
|
||||
class PageVideoArchiveView(ListView):
|
||||
model = PageVideo
|
||||
template_name = "videos/archive.html"
|
||||
@@ -210,11 +211,6 @@ class PageVideoArchiveView(ListView):
|
||||
"Guides and updates from the Communication to Care team.",
|
||||
"pages:contact",
|
||||
),
|
||||
PageVideo.PAGE_FAQ: (
|
||||
"FAQ videos",
|
||||
"Video answers to common questions.",
|
||||
"pages:faq",
|
||||
),
|
||||
}
|
||||
|
||||
def get_page_config(self):
|
||||
|
||||
Reference in New Issue
Block a user