feat: add people sections and opt-in read more
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user