feat: add people sections and opt-in read more

This commit is contained in:
mohamad
2026-10-14 18:33:11 +03:30
parent 4bcb5f196d
commit 473637f477
33 changed files with 722 additions and 68 deletions
+7
View File
@@ -36,6 +36,13 @@ class SiteBrandingAdmin(admin.ModelAdmin):
"description": "Names and text used throughout the public website.", "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", "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),
),
]
+11
View File
@@ -30,6 +30,17 @@ class SiteBranding(models.Model):
default="Communication to Care — Advanced Medical Imaging and Radiomics Solutions", default="Communication to Care — Advanced Medical Imaging and Radiomics Solutions",
blank=True, 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( icon = models.ImageField(
upload_to="branding/", upload_to="branding/",
+24 -11
View File
@@ -10,6 +10,7 @@ from .models import (
AboutSection, AboutSection,
AboutSectionItem, AboutSectionItem,
BlogPost, BlogPost,
BlogPostPartner,
ContactSubmission, ContactSubmission,
ContactSubmissionAttachment, ContactSubmissionAttachment,
CustomPage, CustomPage,
@@ -27,7 +28,7 @@ class HeroSectionAdmin(admin.ModelAdmin):
fieldsets = ( fieldsets = (
("Badge", {"fields": ("badge",)}), ("Badge", {"fields": ("badge",)}),
("Title", {"fields": ("title", "title_highlight")}), ("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")}), ("Primary Button", {"fields": ("primary_cta_text", "primary_cta_url")}),
("Secondary Button", {"fields": ("secondary_cta_text", "secondary_cta_url")}), ("Secondary Button", {"fields": ("secondary_cta_text", "secondary_cta_url")}),
("Image", {"fields": ("image", "image_alt")}), ("Image", {"fields": ("image", "image_alt")}),
@@ -51,7 +52,7 @@ class HeroSectionAdmin(admin.ModelAdmin):
class HomepageSectionItemInline(admin.TabularInline): class HomepageSectionItemInline(admin.TabularInline):
model = HomepageSectionItem model = HomepageSectionItem
extra = 1 extra = 1
fields = ("icon", "title", "content", "url", "image", "order") fields = ("icon", "title", "content", "read_more_content", "url", "image", "order")
ordering = ("order",) ordering = ("order",)
@@ -63,7 +64,7 @@ class HomepageSectionAdmin(admin.ModelAdmin):
inlines = [HomepageSectionItemInline] inlines = [HomepageSectionItemInline]
readonly_fields = ("video_preview",) readonly_fields = ("video_preview",)
fieldsets = ( 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",)}), ("CTA Link (About Strip)", {"fields": ("link_text", "link_url"), "classes": ("collapse",)}),
VIDEO_ADMIN_FIELDSET, VIDEO_ADMIN_FIELDSET,
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
@@ -76,7 +77,7 @@ class HomepageSectionAdmin(admin.ModelAdmin):
class AboutSectionItemInline(admin.TabularInline): class AboutSectionItemInline(admin.TabularInline):
model = AboutSectionItem model = AboutSectionItem
extra = 1 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",) ordering = ("order",)
@@ -88,8 +89,8 @@ class AboutSectionAdmin(admin.ModelAdmin):
inlines = [AboutSectionItemInline] inlines = [AboutSectionItemInline]
readonly_fields = ("video_preview",) readonly_fields = ("video_preview",)
fieldsets = ( fieldsets = (
(None, {"fields": ("section_type", "badge", "title", "subtitle")}), (None, {"fields": ("section_type", "badge", "title", "subtitle", "read_more_subtitle")}),
("Content", {"fields": ("content_format", "content")}), ("Content", {"fields": ("content_format", "content", "read_more_content")}),
VIDEO_ADMIN_FIELDSET, VIDEO_ADMIN_FIELDSET,
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
) )
@@ -161,6 +162,7 @@ class CustomPageSectionItemInline(admin.TabularInline):
"title", "title",
"content_format", "content_format",
"content", "content",
"read_more_content",
"url", "url",
"image", "image",
"image_alt", "image_alt",
@@ -178,10 +180,13 @@ class CustomPageSectionInline(admin.StackedInline):
"badge", "badge",
"title", "title",
"subtitle", "subtitle",
"read_more_subtitle",
"description_format", "description_format",
"description", "description",
"read_more_description",
"content_format", "content_format",
"content", "content",
"read_more_content",
"link_text", "link_text",
"link_url", "link_url",
"order", "order",
@@ -204,6 +209,7 @@ class CustomPageSectionItemOnPageInline(admin.TabularInline):
"title", "title",
"content_format", "content_format",
"content", "content",
"read_more_content",
"url", "url",
"image", "image",
"order", "order",
@@ -225,8 +231,8 @@ class CustomPageSectionAdmin(admin.ModelAdmin):
inlines = [CustomPageSectionItemInline] inlines = [CustomPageSectionItemInline]
readonly_fields = ("video_preview",) readonly_fields = ("video_preview",)
fieldsets = ( fieldsets = (
(None, {"fields": ("page", "section_type", "badge", "title", "subtitle")}), (None, {"fields": ("page", "section_type", "badge", "title", "subtitle", "read_more_subtitle")}),
("Text", {"fields": ("description_format", "description", "content_format", "content")}), ("Text", {"fields": ("description_format", "description", "read_more_description", "content_format", "content", "read_more_content")}),
("CTA Link", {"fields": ("link_text", "link_url"), "classes": ("collapse",)}), ("CTA Link", {"fields": ("link_text", "link_url"), "classes": ("collapse",)}),
VIDEO_ADMIN_FIELDSET, VIDEO_ADMIN_FIELDSET,
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
@@ -275,6 +281,12 @@ class CustomPageAdmin(admin.ModelAdmin):
admin.site.register(CustomPageSection, CustomPageSectionAdmin) 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) @admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin): class BlogPostAdmin(admin.ModelAdmin):
list_display = ("title", "status", "published_at", "updated_at") list_display = ("title", "status", "published_at", "updated_at")
@@ -282,9 +294,10 @@ class BlogPostAdmin(admin.ModelAdmin):
search_fields = ("title", "excerpt", "content", "author_name") search_fields = ("title", "excerpt", "content", "author_name")
prepopulated_fields = {"slug": ("title",)} prepopulated_fields = {"slug": ("title",)}
date_hierarchy = "published_at" date_hierarchy = "published_at"
inlines = [BlogPostPartnerInline]
fieldsets = ( fieldsets = (
(None, {"fields": ("title", "slug", "excerpt")}), (None, {"fields": ("title", "slug", "excerpt", "read_more_excerpt")}),
("Article", {"fields": ("content_format", "content")}), ("Article", {"fields": ("content_format", "content", "read_more_content")}),
("Cover image", {"fields": ("cover_image", "cover_image_alt")}), ("Cover image", {"fields": ("cover_image", "cover_image_alt")}),
("Publishing", {"fields": ("status", "published_at", "author_name")}), ("Publishing", {"fields": ("status", "published_at", "author_name")}),
) )
@@ -298,7 +311,7 @@ class PageVideoAdmin(admin.ModelAdmin):
search_fields = ("title", "description", "video_url") search_fields = ("title", "description", "video_url")
readonly_fields = ("video_preview",) readonly_fields = ("video_preview",)
fieldsets = ( fieldsets = (
(None, {"fields": ("page", "badge", "title", "description_format", "description")}), (None, {"fields": ("page", "badge", "title", "description_format", "description", "read_more_description")}),
VIDEO_ADMIN_FIELDSET, VIDEO_ADMIN_FIELDSET,
("Settings", {"fields": ("order", "is_active")}), ("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),
),
]
+88
View File
@@ -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 = 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').") 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…').") 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.") 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_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).") 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.") 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) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
description = models.TextField(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_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).") link_url = models.CharField(max_length=300, blank=True, help_text="CTA button URL (About Strip).")
order = models.PositiveIntegerField(default=0) 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. ⚗️).") icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
content = models.TextField(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.") 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).") 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) order = models.PositiveIntegerField(default=0)
@@ -115,6 +119,7 @@ class AboutSection(VideoBlockMixin, models.Model):
TYPE_HISTORY = "history" TYPE_HISTORY = "history"
TYPE_CUSTOM = "custom" TYPE_CUSTOM = "custom"
TYPE_SUPPORTERS = "supporters" TYPE_SUPPORTERS = "supporters"
TYPE_PEOPLE = "people"
TYPE_VIDEO = "video" TYPE_VIDEO = "video"
TYPE_CHOICES = [ TYPE_CHOICES = [
@@ -124,6 +129,7 @@ class AboutSection(VideoBlockMixin, models.Model):
(TYPE_HISTORY, "History Block"), (TYPE_HISTORY, "History Block"),
(TYPE_CUSTOM, "Custom Content"), (TYPE_CUSTOM, "Custom Content"),
(TYPE_SUPPORTERS, "Supporters"), (TYPE_SUPPORTERS, "Supporters"),
(TYPE_PEOPLE, "People / Organizers & Collaborators"),
(TYPE_VIDEO, "Video"), (TYPE_VIDEO, "Video"),
] ]
@@ -131,7 +137,9 @@ class AboutSection(VideoBlockMixin, models.Model):
badge = models.CharField(max_length=100, blank=True) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, 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.") 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) 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) content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_MARKDOWN)
order = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
@@ -147,6 +155,15 @@ class AboutSection(VideoBlockMixin, models.Model):
def rendered_content(self): def rendered_content(self):
return render_content(self.content, self.content_format) 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): def clean(self):
super().clean() super().clean()
if self.section_type == self.TYPE_VIDEO: 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. ⚗️).") icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
badge = models.CharField(max_length=100, blank=True) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, 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.") 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.") url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
image = models.ImageField( image = models.ImageField(
upload_to="about/items/", upload_to="about/items/",
@@ -182,6 +206,16 @@ class AboutSectionItem(models.Model):
def __str__(self): def __str__(self):
return f"{self.section} › {self.title or self.icon or self.badge or '(item)'}" 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): class ContactSubmission(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
@@ -329,7 +363,9 @@ class BlogPost(models.Model):
blank=True, blank=True,
help_text="Short summary shown on the blog listing and in search previews.", 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() content = models.TextField()
read_more_content = models.BooleanField(default=False, help_text="Collapse article text with a Read more button.")
content_format = models.CharField( content_format = models.CharField(
max_length=20, max_length=20,
choices=CONTENT_FORMAT_CHOICES, choices=CONTENT_FORMAT_CHOICES,
@@ -370,6 +406,53 @@ class BlogPost(models.Model):
return reverse("pages:blog_detail", kwargs={"slug": self.slug}) 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): class CustomPageSection(VideoBlockMixin, models.Model):
TYPE_HERO = "hero" TYPE_HERO = "hero"
TYPE_INTRO = "intro" TYPE_INTRO = "intro"
@@ -406,8 +489,11 @@ class CustomPageSection(VideoBlockMixin, models.Model):
badge = models.CharField(max_length=100, blank=True) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
subtitle = models.CharField(max_length=500, 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).") 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) 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) content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_MARKDOWN)
link_text = models.CharField(max_length=100, blank=True) link_text = models.CharField(max_length=100, blank=True)
link_url = models.CharField(max_length=300, 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) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
description = models.TextField(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) order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
@@ -472,6 +559,7 @@ class CustomPageSectionItem(models.Model):
badge = models.CharField(max_length=100, blank=True) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
content = models.TextField(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) 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.") 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) image = models.ImageField(upload_to="pages/custom/", blank=True, null=True)
+96 -1
View File
@@ -2,7 +2,8 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone 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): class HomeViewTest(TestCase):
@@ -34,6 +35,28 @@ class AboutViewTest(TestCase):
response = self.client.get(reverse("pages:about")) response = self.client.get(reverse("pages:about"))
self.assertTemplateUsed(response, "pages/about.html") 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): class BlogViewTest(TestCase):
def setUp(self): def setUp(self):
@@ -53,6 +76,8 @@ class BlogViewTest(TestCase):
self.assertContains(listing, self.post.title) self.assertContains(listing, self.post.title)
self.assertContains(detail, "<h2>Results</h2>") self.assertContains(detail, "<h2>Results</h2>")
self.assertContains(detail, "<strong>Promising</strong>") 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): def test_draft_post_is_not_public(self):
self.post.status = BlogPost.STATUS_DRAFT 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.assertNotContains(self.client.get(reverse("pages:blog_list")), self.post.title)
self.assertEqual(self.client.get(self.post.get_absolute_url()).status_code, 404) 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): class ContactViewTest(TestCase):
def test_contact_returns_200(self): def test_contact_returns_200(self):
+2 -2
View File
@@ -184,7 +184,7 @@ class BlogPostListView(ListView):
return BlogPost.objects.filter( return BlogPost.objects.filter(
status=BlogPost.STATUS_PUBLISHED, status=BlogPost.STATUS_PUBLISHED,
published_at__lte=timezone.now(), published_at__lte=timezone.now(),
) ).prefetch_related("partners")
class BlogPostDetailView(DetailView): class BlogPostDetailView(DetailView):
@@ -196,7 +196,7 @@ class BlogPostDetailView(DetailView):
return BlogPost.objects.filter( return BlogPost.objects.filter(
status=BlogPost.STATUS_PUBLISHED, status=BlogPost.STATUS_PUBLISHED,
published_at__lte=timezone.now(), published_at__lte=timezone.now(),
) ).prefetch_related("partners")
class PageVideoArchiveView(ListView): class PageVideoArchiveView(ListView):
+14 -10
View File
@@ -9,14 +9,14 @@ from .models import Article, ArticleCitation, ArticleSection, MainProduct, Produ
class ArticleCitationInline(admin.TabularInline): class ArticleCitationInline(admin.TabularInline):
model = ArticleCitation model = ArticleCitation
extra = 1 extra = 1
fields = ("text", "url", "order") fields = ("text", "read_more_text", "url", "order")
ordering = ("order",) ordering = ("order",)
class ArticleSectionInline(admin.TabularInline): class ArticleSectionInline(admin.TabularInline):
model = ArticleSection model = ArticleSection
extra = 1 extra = 1
fields = ("title", "value_format", "value", "order") fields = ("title", "value_format", "value", "read_more_value", "order")
ordering = ("order",) ordering = ("order",)
@@ -24,7 +24,7 @@ class SubProductArticleInline(admin.StackedInline):
model = Article model = Article
fk_name = "sub_product" fk_name = "sub_product"
extra = 0 extra = 0
fields = ("badge", "title", "description", "order") fields = ("badge", "title", "description", "read_more_description", "order")
ordering = ("order",) ordering = ("order",)
show_change_link = True show_change_link = True
@@ -33,7 +33,7 @@ class MainProductArticleInline(admin.StackedInline):
model = Article model = Article
fk_name = "main_product" fk_name = "main_product"
extra = 0 extra = 0
fields = ("badge", "title", "description", "order") fields = ("badge", "title", "description", "read_more_description", "order")
ordering = ("order",) ordering = ("order",)
show_change_link = True show_change_link = True
@@ -47,6 +47,7 @@ class ProductVideoInline(admin.StackedInline):
"title", "title",
"description_format", "description_format",
"description", "description",
"read_more_description",
"video_source", "video_source",
"video_url", "video_url",
"video_file", "video_file",
@@ -63,7 +64,7 @@ class ProductVideoInline(admin.StackedInline):
class SubProductInline(admin.StackedInline): class SubProductInline(admin.StackedInline):
model = SubProduct model = SubProduct
extra = 0 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",) ordering = ("order",)
show_change_link = True show_change_link = True
prepopulated_fields = {"slug": ("name",)} prepopulated_fields = {"slug": ("name",)}
@@ -85,10 +86,11 @@ class MainProductAdmin(admin.ModelAdmin):
"name", "name",
"slug", "slug",
"short_description", "short_description",
"read_more_short_description",
) )
}, },
), ),
("Description", {"fields": ("description_format", "description")}), ("Description", {"fields": ("description_format", "description", "read_more_description")}),
("Media", {"fields": ("image",)}), ("Media", {"fields": ("image",)}),
("Homepage", {"fields": ("show_on_homepage", "homepage_order")}), ("Homepage", {"fields": ("show_on_homepage", "homepage_order")}),
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
@@ -104,6 +106,7 @@ class SubProductVideoInline(admin.StackedInline):
"title", "title",
"description_format", "description_format",
"description", "description",
"read_more_description",
"video_source", "video_source",
"video_url", "video_url",
"video_file", "video_file",
@@ -143,10 +146,11 @@ class SubProductAdmin(admin.ModelAdmin):
"name", "name",
"slug", "slug",
"short_description", "short_description",
"read_more_short_description",
) )
}, },
), ),
("Description", {"fields": ("description_format", "description")}), ("Description", {"fields": ("description_format", "description", "read_more_description")}),
("Media", {"fields": ("image", "logo")}), ("Media", {"fields": ("image", "logo")}),
("Homepage", {"fields": ("show_on_homepage", "homepage_order")}), ("Homepage", {"fields": ("show_on_homepage", "homepage_order")}),
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
@@ -163,7 +167,7 @@ class ArticleAdmin(admin.ModelAdmin):
inlines = [ArticleSectionInline, ArticleCitationInline] inlines = [ArticleSectionInline, ArticleCitationInline]
fieldsets = ( fieldsets = (
(None, {"fields": ("main_product", "sub_product", "badge", "title")}), (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."}), ("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",)}), ("Settings", {"fields": ("order",)}),
) )
@@ -186,7 +190,7 @@ class ProductVideoAdmin(admin.ModelAdmin):
raw_id_fields = ("main_product", "sub_product") raw_id_fields = ("main_product", "sub_product")
readonly_fields = ("video_preview",) readonly_fields = ("video_preview",)
fieldsets = ( 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, VIDEO_ADMIN_FIELDSET,
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
) )
@@ -212,6 +216,6 @@ class ArticleSectionAdmin(admin.ModelAdmin):
raw_id_fields = ("article",) raw_id_fields = ("article",)
fieldsets = ( fieldsets = (
(None, {"fields": ("article", "title")}), (None, {"fields": ("article", "title")}),
("Content", {"fields": ("value_format", "value")}), ("Content", {"fields": ("value_format", "value", "read_more_value")}),
("Settings", {"fields": ("order",)}), ("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.'),
),
]
+8
View File
@@ -63,7 +63,9 @@ class MainProduct(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True) slug = models.SlugField(unique=True, blank=True)
short_description = models.CharField(max_length=300) 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() description = models.TextField()
read_more_description = models.BooleanField(default=False, help_text="Collapse the full description with Read more.")
description_format = models.CharField( description_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
) )
@@ -104,7 +106,9 @@ class SubProduct(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
slug = models.SlugField(blank=True) slug = models.SlugField(blank=True)
short_description = models.CharField(max_length=300) 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() description = models.TextField()
read_more_description = models.BooleanField(default=False, help_text="Collapse the full description with Read more.")
description_format = models.CharField( description_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
) )
@@ -166,6 +170,7 @@ class Article(models.Model):
) )
title = models.CharField(max_length=300) title = models.CharField(max_length=300)
description = models.TextField() description = models.TextField()
read_more_description = models.BooleanField(default=False, help_text="Collapse this article text with Read more.")
description_format = models.CharField( description_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
) )
@@ -433,6 +438,7 @@ class ArticleSection(models.Model):
) )
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
value = models.TextField() value = models.TextField()
read_more_value = models.BooleanField(default=False, help_text="Collapse this section text with Read more.")
value_format = models.CharField( value_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
) )
@@ -458,6 +464,7 @@ class ArticleCitation(models.Model):
related_name="citations", related_name="citations",
) )
text = models.TextField(help_text="Full citation text.") 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.") url = models.URLField(blank=True, help_text="Optional link to the cited source.")
order = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0)
@@ -488,6 +495,7 @@ class ProductVideo(VideoBlockMixin, models.Model):
badge = models.CharField(max_length=100, blank=True) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
description = models.TextField(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) order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
+15
View File
@@ -56,6 +56,21 @@ class ProductOverviewViewTest(ProductViewsSetup):
class MainProductDetailViewTest(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): def test_detail_returns_200(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
+22 -3
View File
@@ -112,9 +112,27 @@ body::before {
.blog-card-title a { color: var(--text-primary); } .blog-card-title a { color: var(--text-primary); }
.blog-card-excerpt { color: var(--text-secondary); margin-bottom: var(--spacing-sm); } .blog-card-excerpt { color: var(--text-secondary); margin-bottom: var(--spacing-sm); }
.blog-detail-header { max-width: 900px; } .blog-detail-header { max-width: 900px; }
.blog-article-wrap { max-width: 900px; } .blog-article-wrap { max-width: 1200px; }
.blog-cover { width: 100%; max-height: 520px; object-fit: cover; border-radius: var(--radius-lg); margin-bottom: var(--spacing-md); } .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 { padding: clamp(1.5rem, 4vw, 3rem); } .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); } .pagination { display: flex; align-items: center; justify-content: center; gap: var(--spacing-sm); margin-top: var(--spacing-xl); color: var(--text-secondary); }
#main-content { #main-content {
@@ -1438,6 +1456,7 @@ a.citation-count-badge:hover {
.citation-text { .citation-text {
flex: 1; flex: 1;
} }
.citation-text.readmore-wrap { display: block; min-width: 0; }
.citation-actions { .citation-actions {
flex-shrink: 0; flex-shrink: 0;
+3 -1
View File
@@ -237,7 +237,9 @@ function initReadMore() {
btn.innerHTML = 'Read more ' + chevronSVG; btn.innerHTML = 'Read more ' + chevronSVG;
el.insertAdjacentElement('afterend', btn); el.insertAdjacentElement('afterend', btn);
btn.addEventListener('click', function () { btn.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
var collapsed = el.classList.toggle('is-clamped'); var collapsed = el.classList.toggle('is-clamped');
if (collapsed) { if (collapsed) {
el.style.maxHeight = threshold + 'px'; el.style.maxHeight = threshold + 'px';
+17 -2
View File
@@ -19,13 +19,28 @@
</nav> </nav>
<p class="blog-meta">{{ post.published_at|date:"F j, Y" }}{% if post.author_name %} · {{ post.author_name }}{% endif %}</p> <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> <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> </div>
</header> </header>
<div class="section"> <div class="section">
<div class="container blog-article-wrap"> <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 %} {% 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>
</div> </div>
</article> </article>
+1 -1
View File
@@ -32,7 +32,7 @@
<div class="blog-card-body"> <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> <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> <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> <a href="{{ post.get_absolute_url }}" class="link-arrow">Read article</a>
</div> </div>
</article> </article>
+2 -2
View File
@@ -25,8 +25,8 @@
{% if hero.title_highlight %}<span class="gradient-text">{{ hero.title_highlight }}</span>{% endif %} {% if hero.title_highlight %}<span class="gradient-text">{{ hero.title_highlight }}</span>{% endif %}
</h1> </h1>
{% endif %} {% endif %}
{% if hero.subtitle %}<p class="hero-subtitle">{{ hero.subtitle }}</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">{{ hero.description }}</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 %} {% if hero.primary_cta_text or hero.secondary_cta_text %}
<div class="hero-actions"> <div class="hero-actions">
{% if hero.primary_cta_text %}<a href="{{ hero.primary_cta_url }}" class="btn-primary">{{ hero.primary_cta_text }}</a>{% endif %} {% if hero.primary_cta_text %}<a href="{{ hero.primary_cta_url }}" class="btn-primary">{{ hero.primary_cta_text }}</a>{% endif %}
+41 -19
View File
@@ -14,8 +14,8 @@
<div class="page-hero-content"> <div class="page-hero-content">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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.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" data-readmore="160">{{ section.rendered_content }}</div>{% 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> </div>
{% with items=section.items.all %} {% with items=section.items.all %}
{% if items %} {% if items %}
@@ -44,7 +44,7 @@
<div class="about-intro-text"> <div class="about-intro-text">
{% if section.title %}<h2 id="intro-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %} {% if section.title %}<h2 id="intro-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
{% if section.content %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
@@ -73,7 +73,7 @@
{% if section.title %}<h2 class="section-title" id="grid-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %} {% if section.title %}<h2 class="section-title" id="grid-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
</div> </div>
{% endif %} {% 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 %} {% with items=section.items.all %}
{% if items %} {% if items %}
<div class="standards-grid"> <div class="standards-grid">
@@ -98,7 +98,7 @@
<div class="history-content"> <div class="history-content">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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 %} {% if links %}
<div class="history-links"> <div class="history-links">
{% for link in links %} {% for link in links %}
@@ -138,7 +138,7 @@
{% endif %} {% endif %}
{% if section.content %} {% if section.content %}
<div class="glass-card fade-in about-custom-card"> <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> </div>
{% endif %} {% endif %}
{% with items=section.items.all %} {% with items=section.items.all %}
@@ -163,14 +163,14 @@
<div class="section-header"> <div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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>
<div class="features-grid"> <div class="features-grid">
{% for item in section.items.all %} {% for item in section.items.all %}
{% include "partials/_linked_card_open.html" with url=item.url card_class="feature-card" %} {% 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 %} {% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
<h3 class="feature-title">{{ item.title }}</h3> <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 %} {% include "partials/_linked_card_close.html" with url=item.url %}
{% endfor %} {% endfor %}
</div> </div>
@@ -183,7 +183,7 @@
<div class="section-header"> <div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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>
<div class="screenshots-grid"> <div class="screenshots-grid">
{% for item in section.items.all %} {% for item in section.items.all %}
@@ -217,7 +217,7 @@
<div class="section-header"> <div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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>
<div class="products-grid"> <div class="products-grid">
{% for product in homepage_products %} {% for product in homepage_products %}
@@ -229,7 +229,7 @@
{% endif %} {% endif %}
<div class="product-card-body"> <div class="product-card-body">
<h3 class="product-card-title">{{ product.name }}</h3> <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"> <span class="product-card-link">
Explore Explore
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
@@ -251,7 +251,7 @@
<div class="section-header"> <div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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>
<div class="products-overview-grid"> <div class="products-overview-grid">
{% for product in homepage_products_catalog %} {% for product in homepage_products_catalog %}
@@ -263,8 +263,8 @@
{% endif %} {% endif %}
<div class="product-overview-body"> <div class="product-overview-body">
<h3 class="product-overview-title">{{ product.name }}</h3> <h3 class="product-overview-title">{{ product.name }}</h3>
<p class="product-overview-short">{{ product.short_description }}</p> <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" data-readmore="110">{{ product.rendered_description }}</div> <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> <a href="{{ product.get_absolute_url }}" class="btn-primary">Explore {{ product.name }}</a>
</div> </div>
{% if product.sub_products.all %} {% if product.sub_products.all %}
@@ -297,27 +297,49 @@
<div class="section-header"> <div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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>
<div class="problems-grid"> <div class="problems-grid">
{% for item in section.items.all %} {% for item in section.items.all %}
{% include "partials/_linked_card_open.html" with url=item.url card_class="problem-item" %} {% 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 %} {% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %}
<h3 class="problem-title">{{ item.title }}</h3> <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 %} {% include "partials/_linked_card_close.html" with url=item.url %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</section> </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" %} {% elif section.section_type == "supporters" %}
<section class="section supporters-section" aria-labelledby="supporters-heading-{{ section.pk }}"> <section class="section supporters-section" aria-labelledby="supporters-heading-{{ section.pk }}">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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>
<div class="supporters-grid"> <div class="supporters-grid">
{% for item in section.items.all %} {% for item in section.items.all %}
@@ -329,7 +351,7 @@
{% endif %} {% endif %}
<div class="supporter-body"> <div class="supporter-body">
<h3 class="supporter-name">{{ item.title }}</h3> <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> </div>
{% include "partials/_linked_card_close.html" with url=item.url %} {% include "partials/_linked_card_close.html" with url=item.url %}
{% endfor %} {% endfor %}
@@ -344,7 +366,7 @@
<div class="about-strip-content"> <div class="about-strip-content">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %} {% 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.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 %} {% if section.link_text and section.link_url %}
<a href="{{ section.link_url }}" class="btn-primary">{{ section.link_text }}</a> <a href="{{ section.link_url }}" class="btn-primary">{{ section.link_text }}</a>
{% endif %} {% endif %}
+8
View File
@@ -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>
+2 -2
View File
@@ -7,7 +7,7 @@
{% if item.icon %}<div class="feature-icon feature-icon--inline" aria-hidden="true">{{ item.icon }}</div>{% endif %} {% 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.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
{% if item.title %}<h3 class="supporter-name">{{ item.title }}</h3>{% 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> </div>
{% include "partials/_linked_card_close.html" with url=item.url %} {% include "partials/_linked_card_close.html" with url=item.url %}
{% else %} {% else %}
@@ -15,6 +15,6 @@
{% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %} {% 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.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
{% if item.title %}<h3 class="feature-title">{{ item.title }}</h3>{% 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 %} {% include "partials/_linked_card_close.html" with url=item.url %}
{% endif %} {% endif %}
+4 -4
View File
@@ -27,9 +27,9 @@
<h2 class="sr-only" id="video-heading-{{ video.pk }}">Video</h2> <h2 class="sr-only" id="video-heading-{{ video.pk }}">Video</h2>
{% endif %} {% endif %}
{% if video.description %} {% 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 %} {% 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 %} {% endif %}
{% include "partials/_video_player.html" with video=video %} {% include "partials/_video_player.html" with video=video %}
</div> </div>
@@ -46,9 +46,9 @@
</div> </div>
{% endif %} {% endif %}
{% if video.description %} {% 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 %} {% 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 %} {% endif %}
{% include "partials/_video_player.html" with video=video %} {% include "partials/_video_player.html" with video=video %}
{% endif %} {% endif %}
+3 -3
View File
@@ -5,14 +5,14 @@
<article class="article-card glass-card fade-in" aria-labelledby="article-{{ article.pk }}-title"> <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 %} {% 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> <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 %} {% if article.sections.all %}
<div class="article-sections"> <div class="article-sections">
<dl class="article-sections-list"> <dl class="article-sections-list">
{% for section in article.sections.all %} {% for section in article.sections.all %}
<div class="article-section-item"> <div class="article-section-item">
<dt class="section-key">{{ section.title }}</dt> <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> </div>
{% endfor %} {% endfor %}
</dl> </dl>
@@ -24,7 +24,7 @@
<ol class="article-citations-list"> <ol class="article-citations-list">
{% for citation in article.citations.all %} {% for citation in article.citations.all %}
<li class="article-citation-item"> <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 %} {% if citation.url %}
<span class="citation-actions"> <span class="citation-actions">
<a href="{{ citation.url }}" class="citation-link" target="_blank" rel="noopener noreferrer" aria-label="Open citation source"> <a href="{{ citation.url }}" class="citation-link" target="_blank" rel="noopener noreferrer" aria-label="Open citation source">
+3 -3
View File
@@ -26,7 +26,7 @@
<div class="page-hero-content"> <div class="page-hero-content">
<div class="section-badge">Project</div> <div class="section-badge">Project</div>
<h1 class="page-hero-title" id="main-product-heading">{{ main_product.name }}</h1> <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>
</div> </div>
</section> </section>
@@ -44,7 +44,7 @@
</div> </div>
<div class="product-detail-text"> <div class="product-detail-text">
<h2 id="product-desc-heading">About {{ main_product.name }}</h2> <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>
</div> </div>
@@ -84,7 +84,7 @@
{% endif %} {% endif %}
<div class="subproduct-card-body"> <div class="subproduct-card-body">
<h3 class="subproduct-card-title">{{ sub.name }}</h3> <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"> <span class="subproduct-card-link">
Learn More Learn More
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
+2 -2
View File
@@ -34,8 +34,8 @@
{% endif %} {% endif %}
<div class="product-overview-body"> <div class="product-overview-body">
<h2 class="product-overview-title">{{ product.name }}</h2> <h2 class="product-overview-title">{{ product.name }}</h2>
<p class="product-overview-short">{{ product.short_description }}</p> <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" data-readmore="110">{{ product.rendered_description }}</div> <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> <a href="{{ product.get_absolute_url }}" class="btn-primary">Explore {{ product.name }}</a>
</div> </div>
{% with subs=product.sub_products.all %} {% with subs=product.sub_products.all %}
+2 -2
View File
@@ -26,7 +26,7 @@
<div class="page-hero-content"> <div class="page-hero-content">
<div class="section-badge">{{ main_product.name }}</div> <div class="section-badge">{{ main_product.name }}</div>
<h1 class="page-hero-title" id="sub-product-heading">{{ sub_product.name }}</h1> <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>
</div> </div>
</section> </section>
@@ -44,7 +44,7 @@
<div class="sub-product-desc glass-card fade-in"> <div class="sub-product-desc glass-card fade-in">
<h2>About {{ sub_product.name }}</h2> <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> </div>