feat: add people sections and opt-in read more
This commit is contained in:
@@ -36,6 +36,13 @@ class SiteBrandingAdmin(admin.ModelAdmin):
|
||||
"description": "Names and text used throughout the public website.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"Blog article people section",
|
||||
{
|
||||
"fields": ("article_people_eyebrow", "article_people_title"),
|
||||
"description": "These headings are shared by every blog article that has people cards.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"Brand assets",
|
||||
{
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 11:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_sitebranding_accent_color_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='article_people_eyebrow',
|
||||
field=models.CharField(blank=True, default='The people behind this article', help_text='Small heading above the people cards on every blog article.', max_length=160),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='article_people_title',
|
||||
field=models.CharField(default='Organizers & collaborators', help_text='Heading above the people cards on every blog article.', max_length=200),
|
||||
),
|
||||
]
|
||||
@@ -30,6 +30,17 @@ class SiteBranding(models.Model):
|
||||
default="Communication to Care — Advanced Medical Imaging and Radiomics Solutions",
|
||||
blank=True,
|
||||
)
|
||||
article_people_eyebrow = models.CharField(
|
||||
max_length=160,
|
||||
blank=True,
|
||||
default="The people behind this article",
|
||||
help_text="Small heading above the people cards on every blog article.",
|
||||
)
|
||||
article_people_title = models.CharField(
|
||||
max_length=200,
|
||||
default="Organizers & collaborators",
|
||||
help_text="Heading above the people cards on every blog article.",
|
||||
)
|
||||
|
||||
icon = models.ImageField(
|
||||
upload_to="branding/",
|
||||
|
||||
+24
-11
@@ -10,6 +10,7 @@ from .models import (
|
||||
AboutSection,
|
||||
AboutSectionItem,
|
||||
BlogPost,
|
||||
BlogPostPartner,
|
||||
ContactSubmission,
|
||||
ContactSubmissionAttachment,
|
||||
CustomPage,
|
||||
@@ -27,7 +28,7 @@ class HeroSectionAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
("Badge", {"fields": ("badge",)}),
|
||||
("Title", {"fields": ("title", "title_highlight")}),
|
||||
("Text", {"fields": ("subtitle", "description")}),
|
||||
("Text", {"fields": ("subtitle", "read_more_subtitle", "description", "read_more_description")}),
|
||||
("Primary Button", {"fields": ("primary_cta_text", "primary_cta_url")}),
|
||||
("Secondary Button", {"fields": ("secondary_cta_text", "secondary_cta_url")}),
|
||||
("Image", {"fields": ("image", "image_alt")}),
|
||||
@@ -51,7 +52,7 @@ class HeroSectionAdmin(admin.ModelAdmin):
|
||||
class HomepageSectionItemInline(admin.TabularInline):
|
||||
model = HomepageSectionItem
|
||||
extra = 1
|
||||
fields = ("icon", "title", "content", "url", "image", "order")
|
||||
fields = ("icon", "title", "content", "read_more_content", "url", "image", "order")
|
||||
ordering = ("order",)
|
||||
|
||||
|
||||
@@ -63,7 +64,7 @@ class HomepageSectionAdmin(admin.ModelAdmin):
|
||||
inlines = [HomepageSectionItemInline]
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("section_type", "badge", "title", "description_format", "description")}),
|
||||
(None, {"fields": ("section_type", "badge", "title", "description_format", "description", "read_more_description")}),
|
||||
("CTA Link (About Strip)", {"fields": ("link_text", "link_url"), "classes": ("collapse",)}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
@@ -76,7 +77,7 @@ class HomepageSectionAdmin(admin.ModelAdmin):
|
||||
class AboutSectionItemInline(admin.TabularInline):
|
||||
model = AboutSectionItem
|
||||
extra = 1
|
||||
fields = ("icon", "title", "content", "url", "image", "image_alt", "badge", "is_featured", "order")
|
||||
fields = ("icon", "title", "role", "content", "read_more_content", "url", "image", "image_alt", "badge", "is_featured", "order")
|
||||
ordering = ("order",)
|
||||
|
||||
|
||||
@@ -88,8 +89,8 @@ class AboutSectionAdmin(admin.ModelAdmin):
|
||||
inlines = [AboutSectionItemInline]
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("section_type", "badge", "title", "subtitle")}),
|
||||
("Content", {"fields": ("content_format", "content")}),
|
||||
(None, {"fields": ("section_type", "badge", "title", "subtitle", "read_more_subtitle")}),
|
||||
("Content", {"fields": ("content_format", "content", "read_more_content")}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
@@ -161,6 +162,7 @@ class CustomPageSectionItemInline(admin.TabularInline):
|
||||
"title",
|
||||
"content_format",
|
||||
"content",
|
||||
"read_more_content",
|
||||
"url",
|
||||
"image",
|
||||
"image_alt",
|
||||
@@ -178,10 +180,13 @@ class CustomPageSectionInline(admin.StackedInline):
|
||||
"badge",
|
||||
"title",
|
||||
"subtitle",
|
||||
"read_more_subtitle",
|
||||
"description_format",
|
||||
"description",
|
||||
"read_more_description",
|
||||
"content_format",
|
||||
"content",
|
||||
"read_more_content",
|
||||
"link_text",
|
||||
"link_url",
|
||||
"order",
|
||||
@@ -204,6 +209,7 @@ class CustomPageSectionItemOnPageInline(admin.TabularInline):
|
||||
"title",
|
||||
"content_format",
|
||||
"content",
|
||||
"read_more_content",
|
||||
"url",
|
||||
"image",
|
||||
"order",
|
||||
@@ -225,8 +231,8 @@ class CustomPageSectionAdmin(admin.ModelAdmin):
|
||||
inlines = [CustomPageSectionItemInline]
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("page", "section_type", "badge", "title", "subtitle")}),
|
||||
("Text", {"fields": ("description_format", "description", "content_format", "content")}),
|
||||
(None, {"fields": ("page", "section_type", "badge", "title", "subtitle", "read_more_subtitle")}),
|
||||
("Text", {"fields": ("description_format", "description", "read_more_description", "content_format", "content", "read_more_content")}),
|
||||
("CTA Link", {"fields": ("link_text", "link_url"), "classes": ("collapse",)}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
@@ -275,6 +281,12 @@ class CustomPageAdmin(admin.ModelAdmin):
|
||||
admin.site.register(CustomPageSection, CustomPageSectionAdmin)
|
||||
|
||||
|
||||
class BlogPostPartnerInline(admin.TabularInline):
|
||||
model = BlogPostPartner
|
||||
extra = 1
|
||||
fields = ("role", "name", "description", "read_more_description", "image", "image_alt", "url", "order")
|
||||
|
||||
|
||||
@admin.register(BlogPost)
|
||||
class BlogPostAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "status", "published_at", "updated_at")
|
||||
@@ -282,9 +294,10 @@ class BlogPostAdmin(admin.ModelAdmin):
|
||||
search_fields = ("title", "excerpt", "content", "author_name")
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
date_hierarchy = "published_at"
|
||||
inlines = [BlogPostPartnerInline]
|
||||
fieldsets = (
|
||||
(None, {"fields": ("title", "slug", "excerpt")}),
|
||||
("Article", {"fields": ("content_format", "content")}),
|
||||
(None, {"fields": ("title", "slug", "excerpt", "read_more_excerpt")}),
|
||||
("Article", {"fields": ("content_format", "content", "read_more_content")}),
|
||||
("Cover image", {"fields": ("cover_image", "cover_image_alt")}),
|
||||
("Publishing", {"fields": ("status", "published_at", "author_name")}),
|
||||
)
|
||||
@@ -298,7 +311,7 @@ class PageVideoAdmin(admin.ModelAdmin):
|
||||
search_fields = ("title", "description", "video_url")
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("page", "badge", "title", "description_format", "description")}),
|
||||
(None, {"fields": ("page", "badge", "title", "description_format", "description", "read_more_description")}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 04:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0019_blogpost_alter_aboutsection_video_styled_background_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogPostPartner',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('organizer', 'Organizer'), ('collaborator', 'Collaborator')], default='collaborator', max_length=20)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='blog/partners/')),
|
||||
('image_alt', models.CharField(blank=True, max_length=240)),
|
||||
('url', models.URLField(blank=True)),
|
||||
('order', models.PositiveIntegerField(default=0)),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='partners', to='pages.blogpost')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Article organizer or collaborator',
|
||||
'verbose_name_plural': 'Article organizers and collaborators',
|
||||
'ordering': ['role', 'order', 'pk'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 04:42
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0020_blogpostpartner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blogpostpartner',
|
||||
options={'ordering': ['order', 'pk'], 'verbose_name': 'Article organizer or collaborator', 'verbose_name_plural': 'Article organizers and collaborators'},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 04:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0021_alter_blogpostpartner_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='read_more_content',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this text with a Read more button.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aboutsectionitem',
|
||||
name='read_more_content',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this text with a Read more button.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='blogpost',
|
||||
name='read_more_content',
|
||||
field=models.BooleanField(default=False, help_text='Collapse article text with a Read more button.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='read_more_content',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this text with a Read more button.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this description with a Read more button.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesectionitem',
|
||||
name='read_more_content',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this text with a Read more button.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesection',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this description with a Read more button.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepagesectionitem',
|
||||
name='read_more_content',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this text with a Read more button.'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 11:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0022_aboutsection_read_more_content_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blogpostpartner',
|
||||
options={'ordering': ['-role', 'order', 'pk'], 'verbose_name': 'Article organizer or collaborator', 'verbose_name_plural': 'Article organizers and collaborators'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpostpartner',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, help_text='Smaller numbers appear first within each role. Organizers always appear before collaborators.', verbose_name='Display order'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 11:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0023_alter_blogpostpartner_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aboutsection',
|
||||
name='read_more_subtitle',
|
||||
field=models.BooleanField(default=False, help_text='Collapse a long hero subtitle with Read more.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='blogpost',
|
||||
name='partners_eyebrow',
|
||||
field=models.CharField(blank=True, default='The people behind this article', max_length=160),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='blogpost',
|
||||
name='partners_title',
|
||||
field=models.CharField(default='Organizers & collaborators', max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='blogpost',
|
||||
name='read_more_excerpt',
|
||||
field=models.BooleanField(default=False, help_text='Collapse a long excerpt with Read more on the listing and article page.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='blogpostpartner',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text="Collapse this person's description with Read more when it is long."),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='custompagesection',
|
||||
name='read_more_subtitle',
|
||||
field=models.BooleanField(default=False, help_text='Collapse a long hero subtitle with Read more.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='herosection',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this description with Read more when it is long.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='herosection',
|
||||
name='read_more_subtitle',
|
||||
field=models.BooleanField(default=False, help_text='Collapse the subtitle with Read more when it is long.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pagevideo',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this video description with Read more.'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 11:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0024_aboutsection_read_more_subtitle_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blogpostpartner',
|
||||
options={'ordering': [models.Case(models.When(role__iexact='organizer', then=models.Value(0)), models.When(role__iexact='collaborator', then=models.Value(1)), default=models.Value(2), output_field=models.IntegerField()), 'order', 'pk'], 'verbose_name': 'Article organizer or collaborator', 'verbose_name_plural': 'Article organizers and collaborators'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='blogpost',
|
||||
name='partners_eyebrow',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='blogpost',
|
||||
name='partners_title',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpostpartner',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, help_text='Smaller numbers appear first. Organizers appear before collaborators, then custom roles.', verbose_name='Display order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpostpartner',
|
||||
name='role',
|
||||
field=models.CharField(default='Collaborator', help_text='Type Organizer, Collaborator, or any other role name you want to display.', max_length=100, verbose_name='Role / group'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 11:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0025_alter_blogpostpartner_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blogpostpartner',
|
||||
options={'ordering': [models.Case(models.When(role__iexact='organizer', then=models.Value(0)), models.When(role__iexact='collaborator', then=models.Value(1)), default=models.Value(2), output_field=models.IntegerField()), 'order', 'pk'], 'verbose_name': 'Article person', 'verbose_name_plural': 'Article people'},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-14 15:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0026_alter_blogpostpartner_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aboutsectionitem',
|
||||
name='role',
|
||||
field=models.CharField(blank=True, help_text='For People sections: Organizer, Collaborator, or any custom role name.', max_length=100, verbose_name='Role / group'),
|
||||
),
|
||||
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'), ('people', 'People / Organizers & Collaborators'), ('video', 'Video')], default='custom', max_length=30),
|
||||
),
|
||||
]
|
||||
@@ -30,7 +30,9 @@ class HeroSection(models.Model):
|
||||
title = models.CharField(max_length=300, blank=True, help_text="Main title line (e.g. 'Communication to Care,').")
|
||||
title_highlight = models.CharField(max_length=300, blank=True, help_text="Second title line shown in gradient colour (e.g. 'A Powerful Workflow Generator').")
|
||||
subtitle = models.CharField(max_length=500, blank=True, help_text="Short line below the title (e.g. 'for Standardized Radiomics Analysis…').")
|
||||
read_more_subtitle = models.BooleanField(default=False, help_text="Collapse the subtitle with Read more when it is long.")
|
||||
description = models.TextField(blank=True, help_text="Longer paragraph below the subtitle.")
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse this description with Read more when it is long.")
|
||||
primary_cta_text = models.CharField(max_length=100, blank=True, help_text="Primary button label.")
|
||||
primary_cta_url = models.CharField(max_length=300, blank=True, help_text="Primary button URL (relative or absolute).")
|
||||
secondary_cta_text = models.CharField(max_length=100, blank=True, help_text="Secondary (ghost) button label.")
|
||||
@@ -71,6 +73,7 @@ class HomepageSection(VideoBlockMixin, models.Model):
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse this description with a Read more button.")
|
||||
link_text = models.CharField(max_length=100, blank=True, help_text="CTA button label (About Strip).")
|
||||
link_url = models.CharField(max_length=300, blank=True, help_text="CTA button URL (About Strip).")
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
@@ -95,6 +98,7 @@ class HomepageSectionItem(models.Model):
|
||||
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
content = models.TextField(blank=True)
|
||||
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
|
||||
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||||
image = models.ImageField(upload_to="homepage/items/", blank=True, null=True, help_text="Logo or image (used for Supporters cards).")
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
@@ -115,6 +119,7 @@ class AboutSection(VideoBlockMixin, models.Model):
|
||||
TYPE_HISTORY = "history"
|
||||
TYPE_CUSTOM = "custom"
|
||||
TYPE_SUPPORTERS = "supporters"
|
||||
TYPE_PEOPLE = "people"
|
||||
TYPE_VIDEO = "video"
|
||||
|
||||
TYPE_CHOICES = [
|
||||
@@ -124,6 +129,7 @@ class AboutSection(VideoBlockMixin, models.Model):
|
||||
(TYPE_HISTORY, "History Block"),
|
||||
(TYPE_CUSTOM, "Custom Content"),
|
||||
(TYPE_SUPPORTERS, "Supporters"),
|
||||
(TYPE_PEOPLE, "People / Organizers & Collaborators"),
|
||||
(TYPE_VIDEO, "Video"),
|
||||
]
|
||||
|
||||
@@ -131,7 +137,9 @@ class AboutSection(VideoBlockMixin, models.Model):
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
subtitle = models.CharField(max_length=500, blank=True, help_text="Used as subtitle in Hero and year in History.")
|
||||
read_more_subtitle = models.BooleanField(default=False, help_text="Collapse a long hero subtitle with Read more.")
|
||||
content = models.TextField(blank=True)
|
||||
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
|
||||
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_MARKDOWN)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
@@ -147,6 +155,15 @@ class AboutSection(VideoBlockMixin, models.Model):
|
||||
def rendered_content(self):
|
||||
return render_content(self.content, self.content_format)
|
||||
|
||||
@property
|
||||
def ordered_people_items(self):
|
||||
def sort_key(item):
|
||||
role = item.role.strip().casefold()
|
||||
priority = 0 if role == "organizer" else 1 if role == "collaborator" else 2
|
||||
return priority, item.order, item.pk or 0
|
||||
|
||||
return sorted(self.items.all(), key=sort_key)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.section_type == self.TYPE_VIDEO:
|
||||
@@ -162,7 +179,14 @@ class AboutSectionItem(models.Model):
|
||||
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
role = models.CharField(
|
||||
"Role / group",
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text="For People sections: Organizer, Collaborator, or any custom role name.",
|
||||
)
|
||||
content = models.TextField(blank=True, help_text="Description text or link label for History links.")
|
||||
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
|
||||
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||||
image = models.ImageField(
|
||||
upload_to="about/items/",
|
||||
@@ -182,6 +206,16 @@ class AboutSectionItem(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.section} › {self.title or self.icon or self.badge or '(item)'}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def role_label(self):
|
||||
if self.role.casefold() in {"organizer", "collaborator"}:
|
||||
return self.role.title()
|
||||
return self.role
|
||||
|
||||
|
||||
class ContactSubmission(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
@@ -329,7 +363,9 @@ class BlogPost(models.Model):
|
||||
blank=True,
|
||||
help_text="Short summary shown on the blog listing and in search previews.",
|
||||
)
|
||||
read_more_excerpt = models.BooleanField(default=False, help_text="Collapse a long excerpt with Read more on the listing and article page.")
|
||||
content = models.TextField()
|
||||
read_more_content = models.BooleanField(default=False, help_text="Collapse article text with a Read more button.")
|
||||
content_format = models.CharField(
|
||||
max_length=20,
|
||||
choices=CONTENT_FORMAT_CHOICES,
|
||||
@@ -370,6 +406,53 @@ class BlogPost(models.Model):
|
||||
return reverse("pages:blog_detail", kwargs={"slug": self.slug})
|
||||
|
||||
|
||||
class BlogPostPartner(models.Model):
|
||||
ORGANIZER = "organizer"
|
||||
COLLABORATOR = "collaborator"
|
||||
|
||||
post = models.ForeignKey(BlogPost, on_delete=models.CASCADE, related_name="partners")
|
||||
role = models.CharField(
|
||||
"Role / group",
|
||||
max_length=100,
|
||||
default="Collaborator",
|
||||
help_text="Type Organizer, Collaborator, or any other role name you want to display.",
|
||||
)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True)
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse this person's description with Read more when it is long.")
|
||||
image = models.ImageField(upload_to="blog/partners/", blank=True, null=True)
|
||||
image_alt = models.CharField(max_length=240, blank=True)
|
||||
url = models.URLField(blank=True)
|
||||
order = models.PositiveIntegerField(
|
||||
"Display order",
|
||||
default=0,
|
||||
help_text="Smaller numbers appear first. Organizers appear before collaborators, then custom roles.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = [
|
||||
models.Case(
|
||||
models.When(role__iexact="organizer", then=models.Value(0)),
|
||||
models.When(role__iexact="collaborator", then=models.Value(1)),
|
||||
default=models.Value(2),
|
||||
output_field=models.IntegerField(),
|
||||
),
|
||||
"order",
|
||||
"pk",
|
||||
]
|
||||
verbose_name = "Article person"
|
||||
verbose_name_plural = "Article people"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.role_label})"
|
||||
|
||||
@property
|
||||
def role_label(self):
|
||||
if self.role.casefold() in {self.ORGANIZER, self.COLLABORATOR}:
|
||||
return self.role.title()
|
||||
return self.role
|
||||
|
||||
|
||||
class CustomPageSection(VideoBlockMixin, models.Model):
|
||||
TYPE_HERO = "hero"
|
||||
TYPE_INTRO = "intro"
|
||||
@@ -406,8 +489,11 @@ class CustomPageSection(VideoBlockMixin, models.Model):
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
subtitle = models.CharField(max_length=500, blank=True)
|
||||
read_more_subtitle = models.BooleanField(default=False, help_text="Collapse a long hero subtitle with Read more.")
|
||||
description = models.TextField(blank=True, help_text="Short intro text (homepage-style sections).")
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse this description with a Read more button.")
|
||||
content = models.TextField(blank=True)
|
||||
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
|
||||
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_MARKDOWN)
|
||||
link_text = models.CharField(max_length=100, blank=True)
|
||||
link_url = models.CharField(max_length=300, blank=True)
|
||||
@@ -444,6 +530,7 @@ class PageVideo(VideoBlockMixin, models.Model):
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse this video description with Read more.")
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
@@ -472,6 +559,7 @@ class CustomPageSectionItem(models.Model):
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
content = models.TextField(blank=True)
|
||||
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
|
||||
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN)
|
||||
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||||
image = models.ImageField(upload_to="pages/custom/", blank=True, null=True)
|
||||
|
||||
@@ -2,7 +2,8 @@ from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.pages.models import BlogPost, HomepageSection
|
||||
from apps.core.models import SiteBranding
|
||||
from apps.pages.models import AboutSection, AboutSectionItem, BlogPost, BlogPostPartner, HomepageSection
|
||||
|
||||
|
||||
class HomeViewTest(TestCase):
|
||||
@@ -34,6 +35,28 @@ class AboutViewTest(TestCase):
|
||||
response = self.client.get(reverse("pages:about"))
|
||||
self.assertTemplateUsed(response, "pages/about.html")
|
||||
|
||||
def test_people_section_orders_roles_and_controls_read_more_per_text(self):
|
||||
section = AboutSection.objects.create(
|
||||
section_type=AboutSection.TYPE_PEOPLE,
|
||||
badge="Our team", title="People behind the work",
|
||||
content="Section introduction", read_more_content=True,
|
||||
)
|
||||
AboutSectionItem.objects.create(section=section, title="Advisor", role="Scientific advisor", content="Advisor profile", order=1)
|
||||
AboutSectionItem.objects.create(section=section, title="Collaborator", role="Collaborator", content="Collaboration profile", read_more_content=True, order=3)
|
||||
AboutSectionItem.objects.create(section=section, title="Organizer", role="Organizer", content="Organizer profile", order=9)
|
||||
html = self.client.get(reverse("pages:about")).content.decode()
|
||||
self.assertIn("People behind the work", html)
|
||||
self.assertIn("Scientific advisor", html)
|
||||
self.assertLess(html.index(">Organizer</h3>"), html.index(">Collaborator</h3>"))
|
||||
self.assertLess(html.index(">Collaborator</h3>"), html.index(">Advisor</h3>"))
|
||||
self.assertIn('data-readmore="200"', html)
|
||||
self.assertEqual(html.count('data-readmore="88"'), 1)
|
||||
|
||||
def test_empty_people_section_is_hidden(self):
|
||||
AboutSection.objects.create(section_type=AboutSection.TYPE_PEOPLE, title="Nobody yet")
|
||||
response = self.client.get(reverse("pages:about"))
|
||||
self.assertNotContains(response, "Nobody yet")
|
||||
|
||||
|
||||
class BlogViewTest(TestCase):
|
||||
def setUp(self):
|
||||
@@ -53,6 +76,8 @@ class BlogViewTest(TestCase):
|
||||
self.assertContains(listing, self.post.title)
|
||||
self.assertContains(detail, "<h2>Results</h2>")
|
||||
self.assertContains(detail, "<strong>Promising</strong>")
|
||||
self.assertNotContains(detail, "blog-partners")
|
||||
self.assertNotContains(detail, "blog-detail-layout--with-partners")
|
||||
|
||||
def test_draft_post_is_not_public(self):
|
||||
self.post.status = BlogPost.STATUS_DRAFT
|
||||
@@ -60,6 +85,76 @@ class BlogViewTest(TestCase):
|
||||
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)
|
||||
|
||||
def test_article_partners_render_with_roles_and_optional_details(self):
|
||||
BlogPostPartner.objects.create(
|
||||
post=self.post, role=BlogPostPartner.ORGANIZER, name="Research Group",
|
||||
description="Project lead", image="blog/partners/group.png",
|
||||
image_alt="Research Group logo", url="https://example.org",
|
||||
)
|
||||
BlogPostPartner.objects.create(
|
||||
post=self.post, role=BlogPostPartner.COLLABORATOR, name="Care Team"
|
||||
)
|
||||
detail = self.client.get(self.post.get_absolute_url())
|
||||
self.assertContains(detail, "Research Group")
|
||||
self.assertContains(detail, "Collaborator")
|
||||
self.assertContains(detail, "Research Group logo")
|
||||
self.assertContains(detail, "https://example.org")
|
||||
self.assertContains(detail, "Care Team")
|
||||
|
||||
def test_blog_read_more_is_opt_in(self):
|
||||
detail = self.client.get(self.post.get_absolute_url())
|
||||
self.assertNotContains(detail, 'data-readmore="600"')
|
||||
self.post.read_more_content = True
|
||||
self.post.save(update_fields=["read_more_content"])
|
||||
detail = self.client.get(self.post.get_absolute_url())
|
||||
self.assertContains(detail, 'data-readmore="600"')
|
||||
|
||||
def test_partners_follow_article_and_sort_by_role_then_number(self):
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.COLLABORATOR, name="Second collaborator", order=2)
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.COLLABORATOR, name="First collaborator", order=1)
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.ORGANIZER, name="Organizer", order=9)
|
||||
html = self.client.get(self.post.get_absolute_url()).content.decode()
|
||||
self.assertLess(html.index('class="blog-article rich-content'), html.index('class="blog-partners"'))
|
||||
self.assertLess(html.index('>Organizer</h3>'), html.index('>First collaborator</h3>'))
|
||||
self.assertLess(html.index('>First collaborator</h3>'), html.index('>Second collaborator</h3>'))
|
||||
|
||||
def test_partner_headings_are_shared_and_description_read_more_is_independent(self):
|
||||
branding = SiteBranding.load()
|
||||
branding.article_people_eyebrow = "Our team"
|
||||
branding.article_people_title = "Project contributors"
|
||||
branding.save(update_fields=["article_people_eyebrow", "article_people_title"])
|
||||
partner = BlogPostPartner.objects.create(
|
||||
post=self.post, role=BlogPostPartner.ORGANIZER,
|
||||
name="Coordinator", description="A detailed profile", read_more_description=True,
|
||||
)
|
||||
html = self.client.get(self.post.get_absolute_url()).content.decode()
|
||||
self.assertIn("Our team", html)
|
||||
self.assertIn("Project contributors", html)
|
||||
self.assertNotIn("The people behind this article", html)
|
||||
self.assertIn('data-readmore="88"', html)
|
||||
partner.read_more_description = False
|
||||
partner.save(update_fields=["read_more_description"])
|
||||
html = self.client.get(self.post.get_absolute_url()).content.decode()
|
||||
self.assertNotIn('data-readmore="88"', html)
|
||||
|
||||
other_post = BlogPost.objects.create(
|
||||
title="Another update", slug="another-update", content="Another article",
|
||||
status=BlogPost.STATUS_PUBLISHED, published_at=timezone.now(),
|
||||
)
|
||||
BlogPostPartner.objects.create(post=other_post, name="Contributor")
|
||||
other_html = self.client.get(other_post.get_absolute_url()).content.decode()
|
||||
self.assertIn("Our team", other_html)
|
||||
self.assertIn("Project contributors", other_html)
|
||||
|
||||
def test_custom_role_can_be_named_by_admin(self):
|
||||
BlogPostPartner.objects.create(post=self.post, role="Scientific advisor", name="Advisor", order=0)
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.COLLABORATOR, name="Partner", order=5)
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.ORGANIZER, name="Lead", order=9)
|
||||
html = self.client.get(self.post.get_absolute_url()).content.decode()
|
||||
self.assertIn("Scientific advisor", html)
|
||||
self.assertLess(html.index(">Lead</h3>"), html.index(">Partner</h3>"))
|
||||
self.assertLess(html.index(">Partner</h3>"), html.index(">Advisor</h3>"))
|
||||
|
||||
|
||||
class ContactViewTest(TestCase):
|
||||
def test_contact_returns_200(self):
|
||||
|
||||
+2
-2
@@ -184,7 +184,7 @@ class BlogPostListView(ListView):
|
||||
return BlogPost.objects.filter(
|
||||
status=BlogPost.STATUS_PUBLISHED,
|
||||
published_at__lte=timezone.now(),
|
||||
)
|
||||
).prefetch_related("partners")
|
||||
|
||||
|
||||
class BlogPostDetailView(DetailView):
|
||||
@@ -196,7 +196,7 @@ class BlogPostDetailView(DetailView):
|
||||
return BlogPost.objects.filter(
|
||||
status=BlogPost.STATUS_PUBLISHED,
|
||||
published_at__lte=timezone.now(),
|
||||
)
|
||||
).prefetch_related("partners")
|
||||
|
||||
|
||||
class PageVideoArchiveView(ListView):
|
||||
|
||||
+14
-10
@@ -9,14 +9,14 @@ from .models import Article, ArticleCitation, ArticleSection, MainProduct, Produ
|
||||
class ArticleCitationInline(admin.TabularInline):
|
||||
model = ArticleCitation
|
||||
extra = 1
|
||||
fields = ("text", "url", "order")
|
||||
fields = ("text", "read_more_text", "url", "order")
|
||||
ordering = ("order",)
|
||||
|
||||
|
||||
class ArticleSectionInline(admin.TabularInline):
|
||||
model = ArticleSection
|
||||
extra = 1
|
||||
fields = ("title", "value_format", "value", "order")
|
||||
fields = ("title", "value_format", "value", "read_more_value", "order")
|
||||
ordering = ("order",)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class SubProductArticleInline(admin.StackedInline):
|
||||
model = Article
|
||||
fk_name = "sub_product"
|
||||
extra = 0
|
||||
fields = ("badge", "title", "description", "order")
|
||||
fields = ("badge", "title", "description", "read_more_description", "order")
|
||||
ordering = ("order",)
|
||||
show_change_link = True
|
||||
|
||||
@@ -33,7 +33,7 @@ class MainProductArticleInline(admin.StackedInline):
|
||||
model = Article
|
||||
fk_name = "main_product"
|
||||
extra = 0
|
||||
fields = ("badge", "title", "description", "order")
|
||||
fields = ("badge", "title", "description", "read_more_description", "order")
|
||||
ordering = ("order",)
|
||||
show_change_link = True
|
||||
|
||||
@@ -47,6 +47,7 @@ class ProductVideoInline(admin.StackedInline):
|
||||
"title",
|
||||
"description_format",
|
||||
"description",
|
||||
"read_more_description",
|
||||
"video_source",
|
||||
"video_url",
|
||||
"video_file",
|
||||
@@ -63,7 +64,7 @@ class ProductVideoInline(admin.StackedInline):
|
||||
class SubProductInline(admin.StackedInline):
|
||||
model = SubProduct
|
||||
extra = 0
|
||||
fields = ("name", "slug", "short_description", "image", "logo", "show_on_homepage", "homepage_order", "order", "is_active")
|
||||
fields = ("name", "slug", "short_description", "read_more_short_description", "image", "logo", "show_on_homepage", "homepage_order", "order", "is_active")
|
||||
ordering = ("order",)
|
||||
show_change_link = True
|
||||
prepopulated_fields = {"slug": ("name",)}
|
||||
@@ -85,10 +86,11 @@ class MainProductAdmin(admin.ModelAdmin):
|
||||
"name",
|
||||
"slug",
|
||||
"short_description",
|
||||
"read_more_short_description",
|
||||
)
|
||||
},
|
||||
),
|
||||
("Description", {"fields": ("description_format", "description")}),
|
||||
("Description", {"fields": ("description_format", "description", "read_more_description")}),
|
||||
("Media", {"fields": ("image",)}),
|
||||
("Homepage", {"fields": ("show_on_homepage", "homepage_order")}),
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
@@ -104,6 +106,7 @@ class SubProductVideoInline(admin.StackedInline):
|
||||
"title",
|
||||
"description_format",
|
||||
"description",
|
||||
"read_more_description",
|
||||
"video_source",
|
||||
"video_url",
|
||||
"video_file",
|
||||
@@ -143,10 +146,11 @@ class SubProductAdmin(admin.ModelAdmin):
|
||||
"name",
|
||||
"slug",
|
||||
"short_description",
|
||||
"read_more_short_description",
|
||||
)
|
||||
},
|
||||
),
|
||||
("Description", {"fields": ("description_format", "description")}),
|
||||
("Description", {"fields": ("description_format", "description", "read_more_description")}),
|
||||
("Media", {"fields": ("image", "logo")}),
|
||||
("Homepage", {"fields": ("show_on_homepage", "homepage_order")}),
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
@@ -163,7 +167,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||
inlines = [ArticleSectionInline, ArticleCitationInline]
|
||||
fieldsets = (
|
||||
(None, {"fields": ("main_product", "sub_product", "badge", "title")}),
|
||||
("Description", {"fields": ("description_format", "description")}),
|
||||
("Description", {"fields": ("description_format", "description", "read_more_description")}),
|
||||
("Citation Badge", {"fields": ("citation_count_display", "citation_count_label"), "description": "Number and optional label for the dashed citation circle badge. Both are optional — a label without a number shows an empty circle with the label; neither hides the badge entirely."}),
|
||||
("Settings", {"fields": ("order",)}),
|
||||
)
|
||||
@@ -186,7 +190,7 @@ class ProductVideoAdmin(admin.ModelAdmin):
|
||||
raw_id_fields = ("main_product", "sub_product")
|
||||
readonly_fields = ("video_preview",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("main_product", "sub_product", "badge", "title", "description_format", "description")}),
|
||||
(None, {"fields": ("main_product", "sub_product", "badge", "title", "description_format", "description", "read_more_description")}),
|
||||
VIDEO_ADMIN_FIELDSET,
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
@@ -212,6 +216,6 @@ class ArticleSectionAdmin(admin.ModelAdmin):
|
||||
raw_id_fields = ("article",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("article", "title")}),
|
||||
("Content", {"fields": ("value_format", "value")}),
|
||||
("Content", {"fields": ("value_format", "value", "read_more_value")}),
|
||||
("Settings", {"fields": ("order",)}),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 04:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0022_alter_mainproduct_options_alter_productvideo_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='article',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this article text with Read more.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='articlesection',
|
||||
name='read_more_value',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this section text with Read more.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mainproduct',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse the full description with Read more.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mainproduct',
|
||||
name='read_more_short_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse the short description with Read more.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subproduct',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse the full description with Read more.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subproduct',
|
||||
name='read_more_short_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse the short description with Read more.'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-12 11:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0023_article_read_more_description_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='articlecitation',
|
||||
name='read_more_text',
|
||||
field=models.BooleanField(default=False, help_text='Collapse a long citation with Read more.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='productvideo',
|
||||
name='read_more_description',
|
||||
field=models.BooleanField(default=False, help_text='Collapse this video description with Read more.'),
|
||||
),
|
||||
]
|
||||
@@ -63,7 +63,9 @@ class MainProduct(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
short_description = models.CharField(max_length=300)
|
||||
read_more_short_description = models.BooleanField(default=False, help_text="Collapse the short description with Read more.")
|
||||
description = models.TextField()
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse the full description with Read more.")
|
||||
description_format = models.CharField(
|
||||
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||||
)
|
||||
@@ -104,7 +106,9 @@ class SubProduct(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(blank=True)
|
||||
short_description = models.CharField(max_length=300)
|
||||
read_more_short_description = models.BooleanField(default=False, help_text="Collapse the short description with Read more.")
|
||||
description = models.TextField()
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse the full description with Read more.")
|
||||
description_format = models.CharField(
|
||||
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||||
)
|
||||
@@ -166,6 +170,7 @@ class Article(models.Model):
|
||||
)
|
||||
title = models.CharField(max_length=300)
|
||||
description = models.TextField()
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse this article text with Read more.")
|
||||
description_format = models.CharField(
|
||||
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||||
)
|
||||
@@ -433,6 +438,7 @@ class ArticleSection(models.Model):
|
||||
)
|
||||
title = models.CharField(max_length=200)
|
||||
value = models.TextField()
|
||||
read_more_value = models.BooleanField(default=False, help_text="Collapse this section text with Read more.")
|
||||
value_format = models.CharField(
|
||||
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||||
)
|
||||
@@ -458,6 +464,7 @@ class ArticleCitation(models.Model):
|
||||
related_name="citations",
|
||||
)
|
||||
text = models.TextField(help_text="Full citation text.")
|
||||
read_more_text = models.BooleanField(default=False, help_text="Collapse a long citation with Read more.")
|
||||
url = models.URLField(blank=True, help_text="Optional link to the cited source.")
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
|
||||
@@ -488,6 +495,7 @@ class ProductVideo(VideoBlockMixin, models.Model):
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
read_more_description = models.BooleanField(default=False, help_text="Collapse this video description with Read more.")
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
|
||||
@@ -56,6 +56,21 @@ class ProductOverviewViewTest(ProductViewsSetup):
|
||||
|
||||
|
||||
class MainProductDetailViewTest(ProductViewsSetup):
|
||||
def test_read_more_can_be_set_per_article_and_section(self):
|
||||
url = self.main_product.get_absolute_url()
|
||||
article = Article.objects.create(main_product=self.main_product, title="Notes", description="Long notes")
|
||||
section = ArticleSection.objects.create(article=article, title="Detail", value="Long detail")
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, 'data-readmore="260"')
|
||||
self.assertNotContains(response, 'data-readmore="160"')
|
||||
article.read_more_description = True
|
||||
article.save(update_fields=["read_more_description"])
|
||||
section.read_more_value = True
|
||||
section.save(update_fields=["read_more_value"])
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, 'data-readmore="260"')
|
||||
self.assertContains(response, 'data-readmore="160"')
|
||||
|
||||
def test_detail_returns_200(self):
|
||||
url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
|
||||
response = self.client.get(url)
|
||||
|
||||
+22
-3
@@ -112,9 +112,27 @@ body::before {
|
||||
.blog-card-title a { color: var(--text-primary); }
|
||||
.blog-card-excerpt { color: var(--text-secondary); margin-bottom: var(--spacing-sm); }
|
||||
.blog-detail-header { max-width: 900px; }
|
||||
.blog-article-wrap { max-width: 900px; }
|
||||
.blog-cover { width: 100%; max-height: 520px; object-fit: cover; border-radius: var(--radius-lg); margin-bottom: var(--spacing-md); }
|
||||
.blog-article { padding: clamp(1.5rem, 4vw, 3rem); }
|
||||
.blog-article-wrap { max-width: 1200px; }
|
||||
.blog-cover { width: 100%; max-width: 900px; max-height: 520px; object-fit: cover; border-radius: var(--radius-lg); margin: 0 auto var(--spacing-md); }
|
||||
.blog-article { max-width: 900px; margin-inline: auto; padding: clamp(1.5rem, 4vw, 3rem); }
|
||||
.blog-partners { min-width: 0; margin-top: clamp(2.5rem, 5vw, 4rem); }
|
||||
.blog-partners--about { margin-top: 0; }
|
||||
.blog-partners-heading { padding: .3rem .15rem 1.25rem; }
|
||||
.blog-partners-eyebrow { display: block; color: var(--accent-blue-light); font-size: .65rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.blog-partners-heading h2 { margin-top: .45rem; font-size: clamp(1.2rem, 2vw, 1.5rem); line-height: 1.35; }
|
||||
.blog-partners-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 1rem; }
|
||||
.blog-partner-card { display: flex; flex-direction: column; gap: .85rem; padding: .8rem; min-width: 0; }
|
||||
.blog-partner-image { width: 100%; aspect-ratio: 1 / 1; border-radius: 12px; background: rgba(255,255,255,.08); overflow: hidden; display: grid; place-items: center; }
|
||||
.blog-partner-image img { width: 100%; height: 100%; object-fit: cover; object-position: center 25%; }
|
||||
.blog-partner-info { min-width: 0; }
|
||||
.blog-partner-role { display: block; color: var(--accent-blue-light); font-size: .62rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; margin-bottom: .35rem; }
|
||||
.blog-partner-info h3 { font-size: .9rem; line-height: 1.35; margin: 0 0 .4rem; }
|
||||
.blog-partner-info h3 a { color: inherit; }
|
||||
.blog-partner-info h3 a:hover { color: var(--accent-blue-light); }
|
||||
.blog-partner-info p { color: var(--text-secondary); font-size: .76rem; line-height: 1.55; margin: 0; }
|
||||
@media (max-width: 950px) { .blog-partners-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media (max-width: 620px) { .blog-partners-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 380px) { .blog-partners-grid { grid-template-columns: 1fr; } }
|
||||
.pagination { display: flex; align-items: center; justify-content: center; gap: var(--spacing-sm); margin-top: var(--spacing-xl); color: var(--text-secondary); }
|
||||
|
||||
#main-content {
|
||||
@@ -1438,6 +1456,7 @@ a.citation-count-badge:hover {
|
||||
.citation-text {
|
||||
flex: 1;
|
||||
}
|
||||
.citation-text.readmore-wrap { display: block; min-width: 0; }
|
||||
|
||||
.citation-actions {
|
||||
flex-shrink: 0;
|
||||
|
||||
+3
-1
@@ -237,7 +237,9 @@ function initReadMore() {
|
||||
btn.innerHTML = 'Read more ' + chevronSVG;
|
||||
el.insertAdjacentElement('afterend', btn);
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
btn.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var collapsed = el.classList.toggle('is-clamped');
|
||||
if (collapsed) {
|
||||
el.style.maxHeight = threshold + 'px';
|
||||
|
||||
@@ -19,13 +19,28 @@
|
||||
</nav>
|
||||
<p class="blog-meta">{{ post.published_at|date:"F j, Y" }}{% if post.author_name %} · {{ post.author_name }}{% endif %}</p>
|
||||
<h1 class="page-hero-title">{{ post.title }}</h1>
|
||||
{% if post.excerpt %}<p class="page-hero-subtitle">{{ post.excerpt }}</p>{% endif %}
|
||||
{% if post.excerpt %}<p class="page-hero-subtitle" {% if post.read_more_excerpt %}data-readmore="110"{% endif %}>{{ post.excerpt }}</p>{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
<div class="section">
|
||||
<div class="container blog-article-wrap">
|
||||
{% if post.cover_image %}<img class="blog-cover" src="{{ post.cover_image.url }}" alt="{{ post.cover_image_alt|default:post.title }}" />{% endif %}
|
||||
<div class="blog-article rich-content glass-card">{{ post.rendered_content }}</div>
|
||||
<div class="blog-article rich-content glass-card" {% if post.read_more_content %}data-readmore="600"{% endif %}>{{ post.rendered_content }}</div>
|
||||
{% with partners=post.partners.all %}
|
||||
{% if partners %}
|
||||
<section class="blog-partners" aria-labelledby="blog-partners-title">
|
||||
<div class="blog-partners-heading">
|
||||
{% if site_branding.article_people_eyebrow %}<span class="blog-partners-eyebrow">{{ site_branding.article_people_eyebrow }}</span>{% endif %}
|
||||
<h2 id="blog-partners-title">{{ site_branding.article_people_title }}</h2>
|
||||
</div>
|
||||
<div class="blog-partners-grid">
|
||||
{% for partner in partners %}
|
||||
{% include "partials/_person_card.html" with person=partner body=partner.description read_more=partner.read_more_description %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="blog-card-body">
|
||||
<p class="blog-meta">{{ post.published_at|date:"F j, Y" }}{% if post.author_name %} · {{ post.author_name }}{% endif %}</p>
|
||||
<h2 class="blog-card-title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
|
||||
{% if post.excerpt %}<p class="blog-card-excerpt">{{ post.excerpt }}</p>{% endif %}
|
||||
{% if post.excerpt %}<p class="blog-card-excerpt" {% if post.read_more_excerpt %}data-readmore="110"{% endif %}>{{ post.excerpt }}</p>{% endif %}
|
||||
<a href="{{ post.get_absolute_url }}" class="link-arrow">Read article</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
{% if hero.title_highlight %}<span class="gradient-text">{{ hero.title_highlight }}</span>{% endif %}
|
||||
</h1>
|
||||
{% endif %}
|
||||
{% if hero.subtitle %}<p class="hero-subtitle">{{ hero.subtitle }}</p>{% endif %}
|
||||
{% if hero.description %}<p class="hero-description">{{ hero.description }}</p>{% endif %}
|
||||
{% if hero.subtitle %}<p class="hero-subtitle" {% if hero.read_more_subtitle %}data-readmore="120"{% endif %}>{{ hero.subtitle }}</p>{% endif %}
|
||||
{% if hero.description %}<p class="hero-description" {% if hero.read_more_description %}data-readmore="160"{% endif %}>{{ hero.description }}</p>{% endif %}
|
||||
{% if hero.primary_cta_text or hero.secondary_cta_text %}
|
||||
<div class="hero-actions">
|
||||
{% if hero.primary_cta_text %}<a href="{{ hero.primary_cta_url }}" class="btn-primary">{{ hero.primary_cta_text }}</a>{% endif %}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
<div class="page-hero-content">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h1 class="page-hero-title" id="page-hero-heading-{{ section.pk }}">{{ section.title }}</h1>{% endif %}
|
||||
{% if section.subtitle %}<p class="page-hero-subtitle">{{ section.subtitle }}</p>{% endif %}
|
||||
{% if section.content %}<div class="rich-content page-hero-body" data-readmore="160">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.subtitle %}<p class="page-hero-subtitle" {% if section.read_more_subtitle %}data-readmore="110"{% endif %}>{{ section.subtitle }}</p>{% endif %}
|
||||
{% if section.content %}<div class="rich-content page-hero-body" {% if section.read_more_content %}data-readmore="160"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
</div>
|
||||
{% with items=section.items.all %}
|
||||
{% if items %}
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="about-intro-text">
|
||||
{% if section.title %}<h2 id="intro-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.content %}
|
||||
<div class="rich-content" data-readmore="200">{{ section.rendered_content }}</div>
|
||||
<div class="rich-content" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@
|
||||
{% if section.title %}<h2 class="section-title" id="grid-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if section.content %}<div class="rich-content" data-readmore="200">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.content %}<div class="rich-content" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
{% with items=section.items.all %}
|
||||
{% if items %}
|
||||
<div class="standards-grid">
|
||||
@@ -98,7 +98,7 @@
|
||||
<div class="history-content">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h2 id="history-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.content %}<div class="rich-content" data-readmore="200">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.content %}<div class="rich-content" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if links %}
|
||||
<div class="history-links">
|
||||
{% for link in links %}
|
||||
@@ -138,7 +138,7 @@
|
||||
{% endif %}
|
||||
{% if section.content %}
|
||||
<div class="glass-card fade-in about-custom-card">
|
||||
<div class="rich-content" data-readmore="200">{{ section.rendered_content }}</div>
|
||||
<div class="rich-content" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% with items=section.items.all %}
|
||||
@@ -163,14 +163,14 @@
|
||||
<div class="section-header">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h2 class="section-title" id="features-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc">{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc" {% if section.read_more_description %}data-readmore="200"{% endif %}>{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
{% for item in section.items.all %}
|
||||
{% include "partials/_linked_card_open.html" with url=item.url card_class="feature-card" %}
|
||||
{% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
||||
<h3 class="feature-title">{{ item.title }}</h3>
|
||||
<p class="feature-desc" data-readmore="88">{{ item.content }}</p>
|
||||
<p class="feature-desc" {% if item.read_more_content %}data-readmore="88"{% endif %}>{{ item.content }}</p>
|
||||
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -183,7 +183,7 @@
|
||||
<div class="section-header">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h2 class="section-title" id="screenshots-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc">{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc" {% if section.read_more_description %}data-readmore="200"{% endif %}>{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="screenshots-grid">
|
||||
{% for item in section.items.all %}
|
||||
@@ -217,7 +217,7 @@
|
||||
<div class="section-header">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h2 class="section-title" id="products-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc">{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc" {% if section.read_more_description %}data-readmore="200"{% endif %}>{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="products-grid">
|
||||
{% for product in homepage_products %}
|
||||
@@ -229,7 +229,7 @@
|
||||
{% endif %}
|
||||
<div class="product-card-body">
|
||||
<h3 class="product-card-title">{{ product.name }}</h3>
|
||||
<p class="product-card-desc" data-readmore="88">{{ product.short_description }}</p>
|
||||
<p class="product-card-desc" {% if product.read_more_short_description %}data-readmore="88"{% endif %}>{{ product.short_description }}</p>
|
||||
<span class="product-card-link">
|
||||
Explore
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
@@ -251,7 +251,7 @@
|
||||
<div class="section-header">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h2 class="section-title" id="products-catalog-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc">{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc" {% if section.read_more_description %}data-readmore="200"{% endif %}>{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="products-overview-grid">
|
||||
{% for product in homepage_products_catalog %}
|
||||
@@ -263,8 +263,8 @@
|
||||
{% endif %}
|
||||
<div class="product-overview-body">
|
||||
<h3 class="product-overview-title">{{ product.name }}</h3>
|
||||
<p class="product-overview-short">{{ product.short_description }}</p>
|
||||
<div class="product-overview-desc rich-content" data-readmore="110">{{ product.rendered_description }}</div>
|
||||
<p class="product-overview-short" {% if product.read_more_short_description %}data-readmore="88"{% endif %}>{{ product.short_description }}</p>
|
||||
<div class="product-overview-desc rich-content" {% if product.read_more_description %}data-readmore="110"{% endif %}>{{ product.rendered_description }}</div>
|
||||
<a href="{{ product.get_absolute_url }}" class="btn-primary">Explore {{ product.name }}</a>
|
||||
</div>
|
||||
{% if product.sub_products.all %}
|
||||
@@ -297,27 +297,49 @@
|
||||
<div class="section-header">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h2 class="section-title" id="problems-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc">{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc" {% if section.read_more_description %}data-readmore="200"{% endif %}>{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="problems-grid">
|
||||
{% for item in section.items.all %}
|
||||
{% include "partials/_linked_card_open.html" with url=item.url card_class="problem-item" %}
|
||||
{% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
||||
<h3 class="problem-title">{{ item.title }}</h3>
|
||||
<p class="problem-desc" data-readmore="88">{{ item.content }}</p>
|
||||
<p class="problem-desc" {% if item.read_more_content %}data-readmore="88"{% endif %}>{{ item.content }}</p>
|
||||
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% elif section.section_type == "people" %}
|
||||
{% with people=section.ordered_people_items %}
|
||||
{% if people %}
|
||||
<section class="section" aria-labelledby="people-heading-{{ section.pk }}">
|
||||
<div class="container blog-article-wrap">
|
||||
<div class="blog-partners blog-partners--about">
|
||||
<div class="blog-partners-heading">
|
||||
{% if section.badge %}<span class="blog-partners-eyebrow">{{ section.badge }}</span>{% endif %}
|
||||
<h2 id="people-heading-{{ section.pk }}">{{ section.title|default:"People" }}</h2>
|
||||
{% if section.content %}<div class="rich-content section-desc" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="blog-partners-grid">
|
||||
{% for person in people %}
|
||||
{% include "partials/_person_card.html" with person=person body=person.content read_more=person.read_more_content %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% elif section.section_type == "supporters" %}
|
||||
<section class="section supporters-section" aria-labelledby="supporters-heading-{{ section.pk }}">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h2 class="section-title" id="supporters-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc">{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.description %}<div class="rich-content section-desc" {% if section.read_more_description %}data-readmore="200"{% endif %}>{{ section.rendered_description }}</div>{% elif section.content %}<div class="rich-content section-desc" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="supporters-grid">
|
||||
{% for item in section.items.all %}
|
||||
@@ -329,7 +351,7 @@
|
||||
{% endif %}
|
||||
<div class="supporter-body">
|
||||
<h3 class="supporter-name">{{ item.title }}</h3>
|
||||
{% if item.content %}<p class="supporter-desc">{{ item.content }}</p>{% endif %}
|
||||
{% if item.content %}<p class="supporter-desc" {% if item.read_more_content %}data-readmore="88"{% endif %}>{{ item.content }}</p>{% endif %}
|
||||
</div>
|
||||
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||
{% endfor %}
|
||||
@@ -344,7 +366,7 @@
|
||||
<div class="about-strip-content">
|
||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||
{% if section.title %}<h2 class="section-title" id="about-strip-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
|
||||
{% if section.description %}<div class="about-strip-text rich-content">{{ section.rendered_description }}</div>{% elif section.content %}<div class="about-strip-text rich-content">{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.description %}<div class="about-strip-text rich-content" {% if section.read_more_description %}data-readmore="200"{% endif %}>{{ section.rendered_description }}</div>{% elif section.content %}<div class="about-strip-text rich-content" {% if section.read_more_content %}data-readmore="200"{% endif %}>{{ section.rendered_content }}</div>{% endif %}
|
||||
{% if section.link_text and section.link_url %}
|
||||
<a href="{{ section.link_url }}" class="btn-primary">{{ section.link_text }}</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="blog-partner-card glass-card">
|
||||
{% if person.image %}<div class="blog-partner-image"><img src="{{ person.image.url }}" alt="{{ person.image_alt|default:person.name }}" loading="lazy"></div>{% endif %}
|
||||
<div class="blog-partner-info">
|
||||
{% if person.role_label %}<span class="blog-partner-role">{{ person.role_label }}</span>{% endif %}
|
||||
<h3>{% if person.url %}<a href="{{ person.url }}" target="_blank" rel="noopener noreferrer">{{ person.name }}</a>{% else %}{{ person.name }}{% endif %}</h3>
|
||||
{% if body %}<p {% if read_more %}data-readmore="88"{% endif %}>{{ body }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@
|
||||
{% if item.icon %}<div class="feature-icon feature-icon--inline" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
||||
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
|
||||
{% if item.title %}<h3 class="supporter-name">{{ item.title }}</h3>{% endif %}
|
||||
{% if item.content %}<p class="supporter-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
|
||||
{% if item.content %}<p class="supporter-desc" {% if item.read_more_content %}data-readmore="88"{% endif %}>{{ item.content }}</p>{% endif %}
|
||||
</div>
|
||||
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||
{% else %}
|
||||
@@ -15,6 +15,6 @@
|
||||
{% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
||||
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
|
||||
{% if item.title %}<h3 class="feature-title">{{ item.title }}</h3>{% endif %}
|
||||
{% if item.content %}<p class="feature-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
|
||||
{% if item.content %}<p class="feature-desc" {% if item.read_more_content %}data-readmore="88"{% endif %}>{{ item.content }}</p>{% endif %}
|
||||
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||
{% endif %}
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
<h2 class="sr-only" id="video-heading-{{ video.pk }}">Video</h2>
|
||||
{% endif %}
|
||||
{% if video.description %}
|
||||
<div class="rich-content section-desc video-panel-desc">{{ video.rendered_description }}</div>
|
||||
<div class="rich-content section-desc video-panel-desc" {% if video.read_more_description %}data-readmore="200"{% endif %}>{{ video.rendered_description }}</div>
|
||||
{% elif video.content %}
|
||||
<div class="rich-content section-desc video-panel-desc">{{ video.rendered_content }}</div>
|
||||
<div class="rich-content section-desc video-panel-desc" {% if video.read_more_content %}data-readmore="200"{% endif %}>{{ video.rendered_content }}</div>
|
||||
{% endif %}
|
||||
{% include "partials/_video_player.html" with video=video %}
|
||||
</div>
|
||||
@@ -46,9 +46,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if video.description %}
|
||||
<div class="rich-content section-desc">{{ video.rendered_description }}</div>
|
||||
<div class="rich-content section-desc" {% if video.read_more_description %}data-readmore="200"{% endif %}>{{ video.rendered_description }}</div>
|
||||
{% elif video.content %}
|
||||
<div class="rich-content section-desc">{{ video.rendered_content }}</div>
|
||||
<div class="rich-content section-desc" {% if video.read_more_content %}data-readmore="200"{% endif %}>{{ video.rendered_content }}</div>
|
||||
{% endif %}
|
||||
{% include "partials/_video_player.html" with video=video %}
|
||||
{% endif %}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
<article class="article-card glass-card fade-in" aria-labelledby="article-{{ article.pk }}-title">
|
||||
{% if article.badge %}<div class="article-card-header"><div class="section-badge article-badge">{{ article.badge }}</div></div>{% endif %}
|
||||
<h3 class="article-title" id="article-{{ article.pk }}-title">{{ article.title }}</h3>
|
||||
<div class="article-description rich-content" data-readmore="260">{{ article.rendered_description }}</div>
|
||||
<div class="article-description rich-content" {% if article.read_more_description %}data-readmore="260"{% endif %}>{{ article.rendered_description }}</div>
|
||||
{% if article.sections.all %}
|
||||
<div class="article-sections">
|
||||
<dl class="article-sections-list">
|
||||
{% for section in article.sections.all %}
|
||||
<div class="article-section-item">
|
||||
<dt class="section-key">{{ section.title }}</dt>
|
||||
<dd class="section-value rich-content" data-readmore="160">{{ section.rendered_value }}</dd>
|
||||
<dd class="section-value rich-content" {% if section.read_more_value %}data-readmore="160"{% endif %}>{{ section.rendered_value }}</dd>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
@@ -24,7 +24,7 @@
|
||||
<ol class="article-citations-list">
|
||||
{% for citation in article.citations.all %}
|
||||
<li class="article-citation-item">
|
||||
<span class="citation-text">{{ citation.text }}</span>
|
||||
<span class="citation-text" {% if citation.read_more_text %}data-readmore="120"{% endif %}>{{ citation.text }}</span>
|
||||
{% if citation.url %}
|
||||
<span class="citation-actions">
|
||||
<a href="{{ citation.url }}" class="citation-link" target="_blank" rel="noopener noreferrer" aria-label="Open citation source">
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="page-hero-content">
|
||||
<div class="section-badge">Project</div>
|
||||
<h1 class="page-hero-title" id="main-product-heading">{{ main_product.name }}</h1>
|
||||
<p class="page-hero-subtitle">{{ main_product.short_description }}</p>
|
||||
<p class="page-hero-subtitle" {% if main_product.read_more_short_description %}data-readmore="110"{% endif %}>{{ main_product.short_description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
<div class="product-detail-text">
|
||||
<h2 id="product-desc-heading">About {{ main_product.name }}</h2>
|
||||
<div class="product-detail-desc rich-content" data-readmore="220">{{ main_product.rendered_description }}</div>
|
||||
<div class="product-detail-desc rich-content" {% if main_product.read_more_description %}data-readmore="220"{% endif %}>{{ main_product.rendered_description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
{% endif %}
|
||||
<div class="subproduct-card-body">
|
||||
<h3 class="subproduct-card-title">{{ sub.name }}</h3>
|
||||
<p class="subproduct-card-desc" data-readmore="88">{{ sub.short_description }}</p>
|
||||
<p class="subproduct-card-desc" {% if sub.read_more_short_description %}data-readmore="88"{% endif %}>{{ sub.short_description }}</p>
|
||||
<span class="subproduct-card-link">
|
||||
Learn More
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
{% endif %}
|
||||
<div class="product-overview-body">
|
||||
<h2 class="product-overview-title">{{ product.name }}</h2>
|
||||
<p class="product-overview-short">{{ product.short_description }}</p>
|
||||
<div class="product-overview-desc rich-content" data-readmore="110">{{ product.rendered_description }}</div>
|
||||
<p class="product-overview-short" {% if product.read_more_short_description %}data-readmore="88"{% endif %}>{{ product.short_description }}</p>
|
||||
<div class="product-overview-desc rich-content" {% if product.read_more_description %}data-readmore="110"{% endif %}>{{ product.rendered_description }}</div>
|
||||
<a href="{{ product.get_absolute_url }}" class="btn-primary">Explore {{ product.name }}</a>
|
||||
</div>
|
||||
{% with subs=product.sub_products.all %}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="page-hero-content">
|
||||
<div class="section-badge">{{ main_product.name }}</div>
|
||||
<h1 class="page-hero-title" id="sub-product-heading">{{ sub_product.name }}</h1>
|
||||
<p class="page-hero-subtitle">{{ sub_product.short_description }}</p>
|
||||
<p class="page-hero-subtitle" {% if sub_product.read_more_short_description %}data-readmore="110"{% endif %}>{{ sub_product.short_description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<div class="sub-product-desc glass-card fade-in">
|
||||
<h2>About {{ sub_product.name }}</h2>
|
||||
<div class="rich-content" data-readmore="220">{{ sub_product.rendered_description }}</div>
|
||||
<div class="rich-content" {% if sub_product.read_more_description %}data-readmore="220"{% endif %}>{{ sub_product.rendered_description }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user