change release section put video and more
This commit is contained in:
+43
-2
@@ -3,6 +3,9 @@ from django.http import FileResponse, Http404, HttpResponseRedirect
|
||||
from django.urls import path, reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
from apps.core.admin_video import video_admin_preview
|
||||
from apps.core.video import VIDEO_ADMIN_FIELDSET
|
||||
|
||||
from .models import (
|
||||
AboutSection,
|
||||
AboutSectionItem,
|
||||
@@ -16,6 +19,7 @@ from .models import (
|
||||
HeroSection,
|
||||
HomepageSection,
|
||||
HomepageSectionItem,
|
||||
PageVideo,
|
||||
)
|
||||
|
||||
|
||||
@@ -58,12 +62,18 @@ class HomepageSectionAdmin(admin.ModelAdmin):
|
||||
list_filter = ("section_type", "is_active")
|
||||
list_editable = ("order", "is_active")
|
||||
inlines = [HomepageSectionItemInline]
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("section_type", "badge", "title", "description")}),
|
||||
(None, {"fields": ("section_type", "badge", "title", "description_format", "description")}),
|
||||
("CTA Link (About Strip)", {"fields": ("link_text", "link_url"), "classes": ("collapse",)}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
|
||||
@admin.display(description="Preview")
|
||||
def video_preview(self, obj):
|
||||
return video_admin_preview(obj)
|
||||
|
||||
|
||||
class AboutSectionItemInline(admin.TabularInline):
|
||||
model = AboutSectionItem
|
||||
@@ -78,12 +88,18 @@ class AboutSectionAdmin(admin.ModelAdmin):
|
||||
list_filter = ("section_type", "is_active")
|
||||
list_editable = ("order", "is_active")
|
||||
inlines = [AboutSectionItemInline]
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("section_type", "badge", "title", "subtitle")}),
|
||||
("Content", {"fields": ("content_format", "content")}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
|
||||
@admin.display(description="Preview")
|
||||
def video_preview(self, obj):
|
||||
return video_admin_preview(obj)
|
||||
|
||||
|
||||
class ContactSubmissionAttachmentInline(admin.TabularInline):
|
||||
model = ContactSubmissionAttachment
|
||||
@@ -177,6 +193,7 @@ class CustomPageSectionInline(admin.StackedInline):
|
||||
"badge",
|
||||
"title",
|
||||
"subtitle",
|
||||
"description_format",
|
||||
"description",
|
||||
"content_format",
|
||||
"content",
|
||||
@@ -221,13 +238,19 @@ class CustomPageSectionAdmin(admin.ModelAdmin):
|
||||
list_filter = ("section_type", "is_active", "page")
|
||||
list_editable = ("order", "is_active")
|
||||
inlines = [CustomPageSectionItemInline]
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("page", "section_type", "badge", "title", "subtitle")}),
|
||||
("Text", {"fields": ("description", "content_format", "content")}),
|
||||
("Text", {"fields": ("description_format", "description", "content_format", "content")}),
|
||||
("CTA Link", {"fields": ("link_text", "link_url"), "classes": ("collapse",)}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
|
||||
@admin.display(description="Preview")
|
||||
def video_preview(self, obj):
|
||||
return video_admin_preview(obj)
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
instances = formset.save(commit=False)
|
||||
for instance in instances:
|
||||
@@ -267,6 +290,24 @@ class CustomPageAdmin(admin.ModelAdmin):
|
||||
admin.site.register(CustomPageSection, CustomPageSectionAdmin)
|
||||
|
||||
|
||||
@admin.register(PageVideo)
|
||||
class PageVideoAdmin(admin.ModelAdmin):
|
||||
list_display = ("page", "title", "video_source", "video_size", "order", "is_active")
|
||||
list_filter = ("page", "video_source", "is_active")
|
||||
list_editable = ("order", "is_active")
|
||||
search_fields = ("title", "description", "video_url")
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("page", "badge", "title", "description_format", "description")}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
|
||||
@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")
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# Generated by Django 5.2.13 on 2026-06-08 12:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0014_alter_aboutsection_section_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PageVideo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('video_source', models.CharField(blank=True, choices=[('youtube', 'YouTube / external link'), ('upload', 'Uploaded file')], default='youtube', max_length=20)),
|
||||
('video_url', models.CharField(blank=True, help_text='YouTube watch, embed, or youtu.be link.', max_length=500)),
|
||||
('video_file', models.FileField(blank=True, help_text='MP4, WebM, OGG, or MOV file.', upload_to='videos/')),
|
||||
('video_poster', models.ImageField(blank=True, help_text='Optional thumbnail shown before an uploaded video plays.', null=True, upload_to='videos/posters/')),
|
||||
('video_size', models.CharField(choices=[('sm', 'Small (480px)'), ('md', 'Medium (720px)'), ('lg', 'Large (960px)'), ('full', 'Full width')], default='md', max_length=10)),
|
||||
('video_aspect_ratio', models.CharField(choices=[('16/9', '16:9 (widescreen)'), ('4/3', '4:3 (standard)'), ('1/1', '1:1 (square)')], default='16/9', max_length=10)),
|
||||
('page', models.CharField(choices=[('contact', 'Contact'), ('faq', 'FAQ')], max_length=20)),
|
||||
('badge', models.CharField(blank=True, max_length=100)),
|
||||
('title', models.CharField(blank=True, max_length=300)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('order', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Page Video',
|
||||
'verbose_name_plural': 'Page Videos',
|
||||
'ordering': ['page', 'order'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='video_aspect_ratio',
|
||||
field=models.CharField(choices=[('16/9', '16:9 (widescreen)'), ('4/3', '4:3 (standard)'), ('1/1', '1:1 (square)')], default='16/9', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='video_file',
|
||||
field=models.FileField(blank=True, help_text='MP4, WebM, OGG, or MOV file.', upload_to='videos/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='video_poster',
|
||||
field=models.ImageField(blank=True, help_text='Optional thumbnail shown before an uploaded video plays.', null=True, upload_to='videos/posters/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='video_size',
|
||||
field=models.CharField(choices=[('sm', 'Small (480px)'), ('md', 'Medium (720px)'), ('lg', 'Large (960px)'), ('full', 'Full width')], default='md', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='video_source',
|
||||
field=models.CharField(blank=True, choices=[('youtube', 'YouTube / external link'), ('upload', 'Uploaded file')], default='youtube', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='video_url',
|
||||
field=models.CharField(blank=True, help_text='YouTube watch, embed, or youtu.be link.', max_length=500),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='video_aspect_ratio',
|
||||
field=models.CharField(choices=[('16/9', '16:9 (widescreen)'), ('4/3', '4:3 (standard)'), ('1/1', '1:1 (square)')], default='16/9', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='video_file',
|
||||
field=models.FileField(blank=True, help_text='MP4, WebM, OGG, or MOV file.', upload_to='videos/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='video_poster',
|
||||
field=models.ImageField(blank=True, help_text='Optional thumbnail shown before an uploaded video plays.', null=True, upload_to='videos/posters/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='video_size',
|
||||
field=models.CharField(choices=[('sm', 'Small (480px)'), ('md', 'Medium (720px)'), ('lg', 'Large (960px)'), ('full', 'Full width')], default='md', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='video_source',
|
||||
field=models.CharField(blank=True, choices=[('youtube', 'YouTube / external link'), ('upload', 'Uploaded file')], default='youtube', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='video_url',
|
||||
field=models.CharField(blank=True, help_text='YouTube watch, embed, or youtu.be link.', max_length=500),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='video_aspect_ratio',
|
||||
field=models.CharField(choices=[('16/9', '16:9 (widescreen)'), ('4/3', '4:3 (standard)'), ('1/1', '1:1 (square)')], default='16/9', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='video_file',
|
||||
field=models.FileField(blank=True, help_text='MP4, WebM, OGG, or MOV file.', upload_to='videos/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='video_poster',
|
||||
field=models.ImageField(blank=True, help_text='Optional thumbnail shown before an uploaded video plays.', null=True, upload_to='videos/posters/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='video_size',
|
||||
field=models.CharField(choices=[('sm', 'Small (480px)'), ('md', 'Medium (720px)'), ('lg', 'Large (960px)'), ('full', 'Full width')], default='md', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='video_source',
|
||||
field=models.CharField(blank=True, choices=[('youtube', 'YouTube / external link'), ('upload', 'Uploaded file')], default='youtube', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='video_url',
|
||||
field=models.CharField(blank=True, help_text='YouTube watch, embed, or youtu.be link.', max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aboutsection',
|
||||
name='section_type',
|
||||
field=models.CharField(choices=[('hero', 'Hero'), ('intro', 'Intro Card'), ('grid', 'Grid Cards'), ('history', 'History Block'), ('custom', 'Custom Content'), ('supporters', 'Supporters'), ('video', 'Video')], default='custom', max_length=30),
|
||||
),
|
||||
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', 'Products Grid'), ('products_catalog', 'Products with Sub-products'), ('problems', 'Problems / Value Proposition'), ('supporters', 'Supporters'), ('about_strip', 'About Strip'), ('faq', 'FAQ Accordion'), ('video', 'Video')], default='custom', max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='homepagesection',
|
||||
name='section_type',
|
||||
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('products_catalog', 'Products with Sub-products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters'), ('video', 'Video')], max_length=30),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.13 on 2026-06-08 13:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0015_video_support'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='video_styled_background',
|
||||
field=models.BooleanField(default=False, help_text='Glass panel with ambient glow (similar to the Downloads section).'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='video_styled_background',
|
||||
field=models.BooleanField(default=False, help_text='Glass panel with ambient glow (similar to the Downloads section).'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='video_styled_background',
|
||||
field=models.BooleanField(default=False, help_text='Glass panel with ambient glow (similar to the Downloads section).'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pagevideo',
|
||||
name='video_styled_background',
|
||||
field=models.BooleanField(default=False, help_text='Glass panel with ambient glow (similar to the Downloads section).'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.13 on 2026-06-08 13:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0016_video_styled_background'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='description_format',
|
||||
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='description_format',
|
||||
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='description_format',
|
||||
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pagevideo',
|
||||
name='description_format',
|
||||
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
|
||||
),
|
||||
]
|
||||
+55
-3
@@ -3,6 +3,7 @@ from django.db import models
|
||||
from django.utils.text import slugify
|
||||
|
||||
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_MARKDOWN, FORMAT_PLAIN, render_content
|
||||
from apps.core.video import VideoBlockMixin
|
||||
from apps.pages.contact_uploads import (
|
||||
contact_attachment_storage,
|
||||
contact_attachment_upload_to,
|
||||
@@ -41,7 +42,7 @@ class HeroSection(models.Model):
|
||||
return "Hero Section"
|
||||
|
||||
|
||||
class HomepageSection(models.Model):
|
||||
class HomepageSection(VideoBlockMixin, models.Model):
|
||||
TYPE_FEATURES = "features"
|
||||
TYPE_SCREENSHOTS = "screenshots"
|
||||
TYPE_PRODUCTS = "products"
|
||||
@@ -49,6 +50,7 @@ class HomepageSection(models.Model):
|
||||
TYPE_PROBLEMS = "problems"
|
||||
TYPE_ABOUT_STRIP = "about_strip"
|
||||
TYPE_SUPPORTERS = "supporters"
|
||||
TYPE_VIDEO = "video"
|
||||
|
||||
TYPE_CHOICES = [
|
||||
(TYPE_FEATURES, "Features"),
|
||||
@@ -58,6 +60,7 @@ class HomepageSection(models.Model):
|
||||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||||
(TYPE_ABOUT_STRIP, "About Strip"),
|
||||
(TYPE_SUPPORTERS, "Supporters"),
|
||||
(TYPE_VIDEO, "Video"),
|
||||
]
|
||||
|
||||
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES)
|
||||
@@ -74,6 +77,11 @@ class HomepageSection(models.Model):
|
||||
verbose_name = "Homepage Section"
|
||||
verbose_name_plural = "Homepage Sections"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.section_type == self.TYPE_VIDEO:
|
||||
self.clean_video_fields(require=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.get_section_type_display()}] {self.title or self.badge}"
|
||||
|
||||
@@ -96,13 +104,14 @@ class HomepageSectionItem(models.Model):
|
||||
return f"{self.section} › {self.title or self.icon or '(item)'}"
|
||||
|
||||
|
||||
class AboutSection(models.Model):
|
||||
class AboutSection(VideoBlockMixin, models.Model):
|
||||
TYPE_HERO = "hero"
|
||||
TYPE_INTRO = "intro"
|
||||
TYPE_GRID = "grid"
|
||||
TYPE_HISTORY = "history"
|
||||
TYPE_CUSTOM = "custom"
|
||||
TYPE_SUPPORTERS = "supporters"
|
||||
TYPE_VIDEO = "video"
|
||||
|
||||
TYPE_CHOICES = [
|
||||
(TYPE_HERO, "Hero"),
|
||||
@@ -111,6 +120,7 @@ class AboutSection(models.Model):
|
||||
(TYPE_HISTORY, "History Block"),
|
||||
(TYPE_CUSTOM, "Custom Content"),
|
||||
(TYPE_SUPPORTERS, "Supporters"),
|
||||
(TYPE_VIDEO, "Video"),
|
||||
]
|
||||
|
||||
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, default=TYPE_CUSTOM)
|
||||
@@ -133,6 +143,11 @@ class AboutSection(models.Model):
|
||||
def rendered_content(self):
|
||||
return render_content(self.content, self.content_format)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.section_type == self.TYPE_VIDEO:
|
||||
self.clean_video_fields(require=True)
|
||||
|
||||
def __str__(self):
|
||||
label = self.title or self.badge or self.get_section_type_display()
|
||||
return f"[{self.get_section_type_display()}] {label}"
|
||||
@@ -295,7 +310,7 @@ class CustomPage(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class CustomPageSection(models.Model):
|
||||
class CustomPageSection(VideoBlockMixin, models.Model):
|
||||
TYPE_HERO = "hero"
|
||||
TYPE_INTRO = "intro"
|
||||
TYPE_GRID = "grid"
|
||||
@@ -309,6 +324,7 @@ class CustomPageSection(models.Model):
|
||||
TYPE_SUPPORTERS = "supporters"
|
||||
TYPE_ABOUT_STRIP = "about_strip"
|
||||
TYPE_FAQ = "faq"
|
||||
TYPE_VIDEO = "video"
|
||||
|
||||
TYPE_CHOICES = [
|
||||
(TYPE_HERO, "Hero"),
|
||||
@@ -324,6 +340,7 @@ class CustomPageSection(models.Model):
|
||||
(TYPE_SUPPORTERS, "Supporters"),
|
||||
(TYPE_ABOUT_STRIP, "About Strip"),
|
||||
(TYPE_FAQ, "FAQ Accordion"),
|
||||
(TYPE_VIDEO, "Video"),
|
||||
]
|
||||
|
||||
page = models.ForeignKey(CustomPage, on_delete=models.CASCADE, related_name="sections")
|
||||
@@ -348,11 +365,46 @@ class CustomPageSection(models.Model):
|
||||
def rendered_content(self):
|
||||
return render_content(self.content, self.content_format)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.section_type == self.TYPE_VIDEO:
|
||||
self.clean_video_fields(require=True)
|
||||
|
||||
def __str__(self):
|
||||
label = self.title or self.badge or self.get_section_type_display()
|
||||
return f"{self.page} › [{self.get_section_type_display()}] {label}"
|
||||
|
||||
|
||||
class PageVideo(VideoBlockMixin, models.Model):
|
||||
PAGE_CONTACT = "contact"
|
||||
PAGE_FAQ = "faq"
|
||||
|
||||
PAGE_CHOICES = [
|
||||
(PAGE_CONTACT, "Contact"),
|
||||
(PAGE_FAQ, "FAQ"),
|
||||
]
|
||||
|
||||
page = models.CharField(max_length=20, choices=PAGE_CHOICES)
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["page", "order"]
|
||||
verbose_name = "Page Video"
|
||||
verbose_name_plural = "Page Videos"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
self.clean_video_fields(require=True)
|
||||
|
||||
def __str__(self):
|
||||
label = self.title or self.badge or self.get_page_display()
|
||||
return f"{self.get_page_display()} › {label}"
|
||||
|
||||
|
||||
class CustomPageSectionItem(models.Model):
|
||||
page = models.ForeignKey(
|
||||
CustomPage,
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.core.video import parse_youtube_video_id, youtube_embed_url
|
||||
from apps.pages.models import (
|
||||
AboutSection,
|
||||
CustomPage,
|
||||
CustomPageSection,
|
||||
HomepageSection,
|
||||
PageVideo,
|
||||
)
|
||||
from apps.products.models import MainProduct, ProductVideo
|
||||
|
||||
|
||||
class YouTubeParsingTest(TestCase):
|
||||
def test_watch_url(self):
|
||||
self.assertEqual(
|
||||
parse_youtube_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
|
||||
"dQw4w9WgXcQ",
|
||||
)
|
||||
|
||||
def test_short_url(self):
|
||||
self.assertEqual(
|
||||
parse_youtube_video_id("https://youtu.be/dQw4w9WgXcQ"),
|
||||
"dQw4w9WgXcQ",
|
||||
)
|
||||
|
||||
def test_embed_url(self):
|
||||
url = youtube_embed_url("https://youtu.be/dQw4w9WgXcQ")
|
||||
self.assertEqual(url, "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ")
|
||||
|
||||
|
||||
class HomepageVideoSectionTest(TestCase):
|
||||
def test_video_section_renders_youtube_embed(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_VIDEO,
|
||||
title="Demo",
|
||||
video_source="youtube",
|
||||
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertContains(response, "youtube-nocookie.com/embed/dQw4w9WgXcQ")
|
||||
self.assertContains(response, "video-block--md")
|
||||
|
||||
|
||||
class AboutVideoSectionTest(TestCase):
|
||||
def test_video_section_renders_on_about_page(self):
|
||||
AboutSection.objects.create(
|
||||
section_type=AboutSection.TYPE_VIDEO,
|
||||
title="Overview",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
video_size="lg",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:about"))
|
||||
self.assertContains(response, "video-block--lg")
|
||||
self.assertContains(response, "Overview")
|
||||
|
||||
|
||||
class CustomPageVideoSectionTest(TestCase):
|
||||
def test_video_section_renders_on_custom_page(self):
|
||||
page = CustomPage.objects.create(
|
||||
title="Media",
|
||||
slug="media-page",
|
||||
is_published=True,
|
||||
)
|
||||
CustomPageSection.objects.create(
|
||||
page=page,
|
||||
section_type=CustomPageSection.TYPE_VIDEO,
|
||||
title="Walkthrough",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "media-page"}))
|
||||
self.assertContains(response, "Walkthrough")
|
||||
self.assertContains(response, "iframe")
|
||||
|
||||
|
||||
class PageVideoTest(TestCase):
|
||||
def test_contact_page_video(self):
|
||||
PageVideo.objects.create(
|
||||
page=PageVideo.PAGE_CONTACT,
|
||||
title="Intro",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:contact"))
|
||||
self.assertContains(response, "Intro")
|
||||
self.assertContains(response, "youtube-nocookie.com/embed/dQw4w9WgXcQ")
|
||||
|
||||
def test_faq_page_video(self):
|
||||
PageVideo.objects.create(
|
||||
page=PageVideo.PAGE_FAQ,
|
||||
title="Tutorial",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
self.assertContains(response, "Tutorial")
|
||||
|
||||
|
||||
class ProductVideoTest(TestCase):
|
||||
def setUp(self):
|
||||
self.main_product = MainProduct.objects.create(
|
||||
name="Radiuma",
|
||||
slug="radiuma",
|
||||
short_description="Short",
|
||||
description="Long",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
def test_product_video_renders_on_detail_page(self):
|
||||
ProductVideo.objects.create(
|
||||
main_product=self.main_product,
|
||||
title="Product Demo",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
video_size="full",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"})
|
||||
)
|
||||
self.assertContains(response, "Product Demo")
|
||||
self.assertContains(response, "video-block--full")
|
||||
|
||||
|
||||
class VideoStyledBackgroundTest(TestCase):
|
||||
def test_styled_background_renders_panel(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_VIDEO,
|
||||
title="Styled Demo",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
video_styled_background=True,
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertContains(response, "video-panel--styled")
|
||||
self.assertContains(response, "video-panel-blob")
|
||||
|
||||
def test_plain_background_without_panel(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_VIDEO,
|
||||
title="Plain Demo",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
video_styled_background=False,
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertNotContains(response, "video-panel--styled")
|
||||
self.assertContains(response, "Plain Demo")
|
||||
|
||||
|
||||
class VideoDescriptionFormatTest(TestCase):
|
||||
def test_plain_description_preserves_line_breaks(self):
|
||||
HomepageSection.objects.create(
|
||||
section_type=HomepageSection.TYPE_VIDEO,
|
||||
title="Demo",
|
||||
description="First line\nSecond line",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:home"))
|
||||
self.assertContains(response, "First line")
|
||||
self.assertContains(response, "Second line")
|
||||
self.assertContains(response, "<br>")
|
||||
|
||||
def test_markdown_description_renders(self):
|
||||
PageVideo.objects.create(
|
||||
page=PageVideo.PAGE_FAQ,
|
||||
title="Guide",
|
||||
description="**Bold** intro",
|
||||
description_format="markdown",
|
||||
video_source="youtube",
|
||||
video_url="https://youtu.be/dQw4w9WgXcQ",
|
||||
is_active=True,
|
||||
)
|
||||
response = self.client.get(reverse("pages:faq"))
|
||||
self.assertContains(response, "<strong>Bold</strong>")
|
||||
+17
-1
@@ -17,6 +17,7 @@ from .models import (
|
||||
FAQEntry,
|
||||
HeroSection,
|
||||
HomepageSection,
|
||||
PageVideo,
|
||||
)
|
||||
|
||||
|
||||
@@ -71,6 +72,14 @@ class FAQView(ListView):
|
||||
context_object_name = "faq_entries"
|
||||
queryset = FAQEntry.objects.filter(is_active=True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx["faq_videos"] = PageVideo.objects.filter(
|
||||
page=PageVideo.PAGE_FAQ,
|
||||
is_active=True,
|
||||
).order_by("order")
|
||||
return ctx
|
||||
|
||||
|
||||
class ContactView(View):
|
||||
template_name = "pages/contact.html"
|
||||
@@ -84,7 +93,14 @@ class ContactView(View):
|
||||
return render(
|
||||
request,
|
||||
self.template_name,
|
||||
{"form": ContactForm(), "captcha_question": self._new_captcha(request)},
|
||||
{
|
||||
"form": ContactForm(),
|
||||
"captcha_question": self._new_captcha(request),
|
||||
"contact_videos": PageVideo.objects.filter(
|
||||
page=PageVideo.PAGE_CONTACT,
|
||||
is_active=True,
|
||||
).order_by("order"),
|
||||
},
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user