feat: add people sections and opt-in read more

This commit is contained in:
mohamad
2026-10-14 18:33:11 +03:30
parent 4bcb5f196d
commit 473637f477
33 changed files with 722 additions and 68 deletions
+88
View File
@@ -30,7 +30,9 @@ class HeroSection(models.Model):
title = models.CharField(max_length=300, blank=True, help_text="Main title line (e.g. 'Communication to Care,').")
title_highlight = models.CharField(max_length=300, blank=True, help_text="Second title line shown in gradient colour (e.g. 'A Powerful Workflow Generator').")
subtitle = models.CharField(max_length=500, blank=True, help_text="Short line below the title (e.g. 'for Standardized Radiomics Analysis…').")
read_more_subtitle = models.BooleanField(default=False, help_text="Collapse the subtitle with Read more when it is long.")
description = models.TextField(blank=True, help_text="Longer paragraph below the subtitle.")
read_more_description = models.BooleanField(default=False, help_text="Collapse this description with Read more when it is long.")
primary_cta_text = models.CharField(max_length=100, blank=True, help_text="Primary button label.")
primary_cta_url = models.CharField(max_length=300, blank=True, help_text="Primary button URL (relative or absolute).")
secondary_cta_text = models.CharField(max_length=100, blank=True, help_text="Secondary (ghost) button label.")
@@ -71,6 +73,7 @@ class HomepageSection(VideoBlockMixin, models.Model):
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
description = models.TextField(blank=True)
read_more_description = models.BooleanField(default=False, help_text="Collapse this description with a Read more button.")
link_text = models.CharField(max_length=100, blank=True, help_text="CTA button label (About Strip).")
link_url = models.CharField(max_length=300, blank=True, help_text="CTA button URL (About Strip).")
order = models.PositiveIntegerField(default=0)
@@ -95,6 +98,7 @@ class HomepageSectionItem(models.Model):
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
title = models.CharField(max_length=300, blank=True)
content = models.TextField(blank=True)
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
image = models.ImageField(upload_to="homepage/items/", blank=True, null=True, help_text="Logo or image (used for Supporters cards).")
order = models.PositiveIntegerField(default=0)
@@ -115,6 +119,7 @@ class AboutSection(VideoBlockMixin, models.Model):
TYPE_HISTORY = "history"
TYPE_CUSTOM = "custom"
TYPE_SUPPORTERS = "supporters"
TYPE_PEOPLE = "people"
TYPE_VIDEO = "video"
TYPE_CHOICES = [
@@ -124,6 +129,7 @@ class AboutSection(VideoBlockMixin, models.Model):
(TYPE_HISTORY, "History Block"),
(TYPE_CUSTOM, "Custom Content"),
(TYPE_SUPPORTERS, "Supporters"),
(TYPE_PEOPLE, "People / Organizers & Collaborators"),
(TYPE_VIDEO, "Video"),
]
@@ -131,7 +137,9 @@ class AboutSection(VideoBlockMixin, models.Model):
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
subtitle = models.CharField(max_length=500, blank=True, help_text="Used as subtitle in Hero and year in History.")
read_more_subtitle = models.BooleanField(default=False, help_text="Collapse a long hero subtitle with Read more.")
content = models.TextField(blank=True)
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_MARKDOWN)
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
@@ -147,6 +155,15 @@ class AboutSection(VideoBlockMixin, models.Model):
def rendered_content(self):
return render_content(self.content, self.content_format)
@property
def ordered_people_items(self):
def sort_key(item):
role = item.role.strip().casefold()
priority = 0 if role == "organizer" else 1 if role == "collaborator" else 2
return priority, item.order, item.pk or 0
return sorted(self.items.all(), key=sort_key)
def clean(self):
super().clean()
if self.section_type == self.TYPE_VIDEO:
@@ -162,7 +179,14 @@ class AboutSectionItem(models.Model):
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
role = models.CharField(
"Role / group",
max_length=100,
blank=True,
help_text="For People sections: Organizer, Collaborator, or any custom role name.",
)
content = models.TextField(blank=True, help_text="Description text or link label for History links.")
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
image = models.ImageField(
upload_to="about/items/",
@@ -182,6 +206,16 @@ class AboutSectionItem(models.Model):
def __str__(self):
return f"{self.section} › {self.title or self.icon or self.badge or '(item)'}"
@property
def name(self):
return self.title
@property
def role_label(self):
if self.role.casefold() in {"organizer", "collaborator"}:
return self.role.title()
return self.role
class ContactSubmission(models.Model):
name = models.CharField(max_length=200)
@@ -329,7 +363,9 @@ class BlogPost(models.Model):
blank=True,
help_text="Short summary shown on the blog listing and in search previews.",
)
read_more_excerpt = models.BooleanField(default=False, help_text="Collapse a long excerpt with Read more on the listing and article page.")
content = models.TextField()
read_more_content = models.BooleanField(default=False, help_text="Collapse article text with a Read more button.")
content_format = models.CharField(
max_length=20,
choices=CONTENT_FORMAT_CHOICES,
@@ -370,6 +406,53 @@ class BlogPost(models.Model):
return reverse("pages:blog_detail", kwargs={"slug": self.slug})
class BlogPostPartner(models.Model):
ORGANIZER = "organizer"
COLLABORATOR = "collaborator"
post = models.ForeignKey(BlogPost, on_delete=models.CASCADE, related_name="partners")
role = models.CharField(
"Role / group",
max_length=100,
default="Collaborator",
help_text="Type Organizer, Collaborator, or any other role name you want to display.",
)
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
read_more_description = models.BooleanField(default=False, help_text="Collapse this person's description with Read more when it is long.")
image = models.ImageField(upload_to="blog/partners/", blank=True, null=True)
image_alt = models.CharField(max_length=240, blank=True)
url = models.URLField(blank=True)
order = models.PositiveIntegerField(
"Display order",
default=0,
help_text="Smaller numbers appear first. Organizers appear before collaborators, then custom roles.",
)
class Meta:
ordering = [
models.Case(
models.When(role__iexact="organizer", then=models.Value(0)),
models.When(role__iexact="collaborator", then=models.Value(1)),
default=models.Value(2),
output_field=models.IntegerField(),
),
"order",
"pk",
]
verbose_name = "Article person"
verbose_name_plural = "Article people"
def __str__(self):
return f"{self.name} ({self.role_label})"
@property
def role_label(self):
if self.role.casefold() in {self.ORGANIZER, self.COLLABORATOR}:
return self.role.title()
return self.role
class CustomPageSection(VideoBlockMixin, models.Model):
TYPE_HERO = "hero"
TYPE_INTRO = "intro"
@@ -406,8 +489,11 @@ class CustomPageSection(VideoBlockMixin, models.Model):
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
subtitle = models.CharField(max_length=500, blank=True)
read_more_subtitle = models.BooleanField(default=False, help_text="Collapse a long hero subtitle with Read more.")
description = models.TextField(blank=True, help_text="Short intro text (homepage-style sections).")
read_more_description = models.BooleanField(default=False, help_text="Collapse this description with a Read more button.")
content = models.TextField(blank=True)
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_MARKDOWN)
link_text = models.CharField(max_length=100, blank=True)
link_url = models.CharField(max_length=300, blank=True)
@@ -444,6 +530,7 @@ class PageVideo(VideoBlockMixin, models.Model):
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
description = models.TextField(blank=True)
read_more_description = models.BooleanField(default=False, help_text="Collapse this video description with Read more.")
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
@@ -472,6 +559,7 @@ class CustomPageSectionItem(models.Model):
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
content = models.TextField(blank=True)
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN)
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
image = models.ImageField(upload_to="pages/custom/", blank=True, null=True)