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.",
|
"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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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.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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user