364 lines
14 KiB
Python
364 lines
14 KiB
Python
from django.core.exceptions import ValidationError
|
||
from django.db import models
|
||
from django.utils.text import slugify
|
||
|
||
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_MARKDOWN, FORMAT_PLAIN, render_content
|
||
|
||
RESERVED_PAGE_SLUGS = frozenset({
|
||
"admin",
|
||
"about",
|
||
"contact",
|
||
"faq",
|
||
"home",
|
||
"products",
|
||
"static",
|
||
"media",
|
||
})
|
||
|
||
|
||
class HeroSection(models.Model):
|
||
badge = models.CharField(max_length=200, blank=True, help_text="Small label above the title (e.g. 'Developing since 2021').")
|
||
title = models.CharField(max_length=300, blank=True, help_text="Main title line (e.g. 'Radiuma,').")
|
||
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…').")
|
||
description = models.TextField(blank=True, help_text="Longer paragraph below the subtitle.")
|
||
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.")
|
||
secondary_cta_url = models.CharField(max_length=300, blank=True, help_text="Secondary (ghost) button URL.")
|
||
image = models.ImageField(upload_to="hero/", blank=True, null=True, help_text="App preview screenshot shown on the right.")
|
||
image_alt = models.CharField(max_length=300, blank=True, help_text="Alt text for the preview image.")
|
||
|
||
class Meta:
|
||
verbose_name = "Hero Section"
|
||
verbose_name_plural = "Hero Section"
|
||
|
||
def __str__(self):
|
||
return "Hero Section"
|
||
|
||
|
||
class HomepageSection(models.Model):
|
||
TYPE_FEATURES = "features"
|
||
TYPE_SCREENSHOTS = "screenshots"
|
||
TYPE_PRODUCTS = "products"
|
||
TYPE_PRODUCTS_CATALOG = "products_catalog"
|
||
TYPE_PROBLEMS = "problems"
|
||
TYPE_ABOUT_STRIP = "about_strip"
|
||
TYPE_SUPPORTERS = "supporters"
|
||
|
||
TYPE_CHOICES = [
|
||
(TYPE_FEATURES, "Features"),
|
||
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
||
(TYPE_PRODUCTS, "Products"),
|
||
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
|
||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||
(TYPE_ABOUT_STRIP, "About Strip"),
|
||
(TYPE_SUPPORTERS, "Supporters"),
|
||
]
|
||
|
||
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES)
|
||
badge = models.CharField(max_length=100, blank=True)
|
||
title = models.CharField(max_length=300, blank=True)
|
||
description = models.TextField(blank=True)
|
||
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)
|
||
is_active = models.BooleanField(default=True)
|
||
|
||
class Meta:
|
||
ordering = ["order"]
|
||
verbose_name = "Homepage Section"
|
||
verbose_name_plural = "Homepage Sections"
|
||
|
||
def __str__(self):
|
||
return f"[{self.get_section_type_display()}] {self.title or self.badge}"
|
||
|
||
|
||
class HomepageSectionItem(models.Model):
|
||
section = models.ForeignKey(HomepageSection, on_delete=models.CASCADE, related_name="items")
|
||
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)
|
||
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)
|
||
|
||
class Meta:
|
||
ordering = ["order"]
|
||
verbose_name = "Homepage Section Item"
|
||
verbose_name_plural = "Homepage Section Items"
|
||
|
||
def __str__(self):
|
||
return f"{self.section} › {self.title or self.icon or '(item)'}"
|
||
|
||
|
||
class AboutSection(models.Model):
|
||
TYPE_HERO = "hero"
|
||
TYPE_INTRO = "intro"
|
||
TYPE_GRID = "grid"
|
||
TYPE_HISTORY = "history"
|
||
TYPE_CUSTOM = "custom"
|
||
TYPE_SUPPORTERS = "supporters"
|
||
|
||
TYPE_CHOICES = [
|
||
(TYPE_HERO, "Hero"),
|
||
(TYPE_INTRO, "Intro Card"),
|
||
(TYPE_GRID, "Grid Cards"),
|
||
(TYPE_HISTORY, "History Block"),
|
||
(TYPE_CUSTOM, "Custom Content"),
|
||
(TYPE_SUPPORTERS, "Supporters"),
|
||
]
|
||
|
||
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, default=TYPE_CUSTOM)
|
||
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.")
|
||
content = models.TextField(blank=True)
|
||
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)
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
class Meta:
|
||
ordering = ["order"]
|
||
verbose_name = "About Section"
|
||
verbose_name_plural = "About Sections"
|
||
|
||
@property
|
||
def rendered_content(self):
|
||
return render_content(self.content, self.content_format)
|
||
|
||
def __str__(self):
|
||
label = self.title or self.badge or self.get_section_type_display()
|
||
return f"[{self.get_section_type_display()}] {label}"
|
||
|
||
|
||
class AboutSectionItem(models.Model):
|
||
section = models.ForeignKey(AboutSection, on_delete=models.CASCADE, related_name="items")
|
||
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)
|
||
content = models.TextField(blank=True, help_text="Description text or link label for History links.")
|
||
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||
image = models.ImageField(
|
||
upload_to="about/items/",
|
||
blank=True,
|
||
null=True,
|
||
help_text="Logo or image (used for Supporters cards).",
|
||
)
|
||
image_alt = models.CharField(max_length=200, blank=True)
|
||
is_featured = models.BooleanField(default=False, help_text="Mark as featured item (e.g. large screenshot).")
|
||
order = models.PositiveIntegerField(default=0)
|
||
|
||
class Meta:
|
||
ordering = ["order"]
|
||
verbose_name = "About Section Item"
|
||
verbose_name_plural = "About Section Items"
|
||
|
||
def __str__(self):
|
||
return f"{self.section} › {self.title or self.icon or self.badge or '(item)'}"
|
||
|
||
|
||
class ContactSubmission(models.Model):
|
||
name = models.CharField(max_length=200)
|
||
title = models.CharField(max_length=300)
|
||
description = models.TextField()
|
||
email = models.EmailField(blank=True)
|
||
submitted_at = models.DateTimeField(auto_now_add=True)
|
||
is_read = models.BooleanField(default=False)
|
||
|
||
class Meta:
|
||
ordering = ["-submitted_at"]
|
||
verbose_name = "Contact Submission"
|
||
verbose_name_plural = "Contact Submissions"
|
||
|
||
def __str__(self):
|
||
return f"{self.name} — {self.title}"
|
||
|
||
|
||
class FAQEntry(models.Model):
|
||
question = models.CharField(max_length=500)
|
||
answer = models.TextField()
|
||
answer_format = models.CharField(
|
||
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||
)
|
||
order = models.PositiveIntegerField(default=0)
|
||
is_active = models.BooleanField(default=True)
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
class Meta:
|
||
ordering = ["order"]
|
||
verbose_name = "FAQ Entry"
|
||
verbose_name_plural = "FAQ Entries"
|
||
|
||
def __str__(self):
|
||
return self.question
|
||
|
||
@property
|
||
def rendered_answer(self):
|
||
return render_content(self.answer, self.answer_format)
|
||
|
||
|
||
class DownloadItem(models.Model):
|
||
PLATFORM_WINDOWS = "windows"
|
||
PLATFORM_MACOS = "macos"
|
||
PLATFORM_LINUX = "linux"
|
||
|
||
PLATFORM_CHOICES = [
|
||
(PLATFORM_WINDOWS, "Windows"),
|
||
(PLATFORM_MACOS, "macOS"),
|
||
(PLATFORM_LINUX, "Linux"),
|
||
]
|
||
|
||
name = models.CharField(max_length=200)
|
||
platform = models.CharField(max_length=20, choices=PLATFORM_CHOICES)
|
||
version = models.CharField(max_length=50)
|
||
download_url = models.URLField()
|
||
description = models.TextField(blank=True)
|
||
is_active = models.BooleanField(default=True)
|
||
order = models.PositiveIntegerField(default=0)
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
|
||
class Meta:
|
||
ordering = ["order", "platform"]
|
||
verbose_name = "Download Item"
|
||
verbose_name_plural = "Download Items"
|
||
|
||
def __str__(self):
|
||
return f"{self.name} ({self.get_platform_display()})"
|
||
|
||
|
||
class CustomPage(models.Model):
|
||
title = models.CharField(max_length=200)
|
||
slug = models.SlugField(max_length=200, unique=True)
|
||
menu_label = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
help_text="Nav label when shown in menu. Defaults to title.",
|
||
)
|
||
meta_description = models.CharField(max_length=300, blank=True)
|
||
show_in_nav = models.BooleanField(default=True)
|
||
menu_order = models.PositiveIntegerField(default=0)
|
||
is_published = models.BooleanField(default=True)
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
class Meta:
|
||
ordering = ["menu_order", "title"]
|
||
verbose_name = "Custom Page"
|
||
verbose_name_plural = "Custom Pages"
|
||
|
||
def __str__(self):
|
||
return self.title
|
||
|
||
@property
|
||
def nav_label(self):
|
||
return self.menu_label or self.title
|
||
|
||
def clean(self):
|
||
super().clean()
|
||
if self.slug in RESERVED_PAGE_SLUGS:
|
||
raise ValidationError({"slug": f'"{self.slug}" is reserved and cannot be used.'})
|
||
|
||
def save(self, *args, **kwargs):
|
||
if not self.slug:
|
||
self.slug = slugify(self.title)
|
||
self.full_clean()
|
||
super().save(*args, **kwargs)
|
||
|
||
|
||
class CustomPageSection(models.Model):
|
||
TYPE_HERO = "hero"
|
||
TYPE_INTRO = "intro"
|
||
TYPE_GRID = "grid"
|
||
TYPE_HISTORY = "history"
|
||
TYPE_CUSTOM = "custom"
|
||
TYPE_FEATURES = "features"
|
||
TYPE_SCREENSHOTS = "screenshots"
|
||
TYPE_PRODUCTS = "products"
|
||
TYPE_PRODUCTS_CATALOG = "products_catalog"
|
||
TYPE_PROBLEMS = "problems"
|
||
TYPE_SUPPORTERS = "supporters"
|
||
TYPE_ABOUT_STRIP = "about_strip"
|
||
TYPE_FAQ = "faq"
|
||
|
||
TYPE_CHOICES = [
|
||
(TYPE_HERO, "Hero"),
|
||
(TYPE_INTRO, "Intro Card"),
|
||
(TYPE_GRID, "Grid Cards"),
|
||
(TYPE_HISTORY, "History Block"),
|
||
(TYPE_CUSTOM, "Custom Content"),
|
||
(TYPE_FEATURES, "Features Grid"),
|
||
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
||
(TYPE_PRODUCTS, "Products Grid"),
|
||
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
|
||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||
(TYPE_SUPPORTERS, "Supporters"),
|
||
(TYPE_ABOUT_STRIP, "About Strip"),
|
||
(TYPE_FAQ, "FAQ Accordion"),
|
||
]
|
||
|
||
page = models.ForeignKey(CustomPage, on_delete=models.CASCADE, related_name="sections")
|
||
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, default=TYPE_CUSTOM)
|
||
badge = models.CharField(max_length=100, blank=True)
|
||
title = models.CharField(max_length=300, blank=True)
|
||
subtitle = models.CharField(max_length=500, blank=True)
|
||
description = models.TextField(blank=True, help_text="Short intro text (homepage-style sections).")
|
||
content = models.TextField(blank=True)
|
||
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)
|
||
order = models.PositiveIntegerField(default=0)
|
||
is_active = models.BooleanField(default=True)
|
||
|
||
class Meta:
|
||
ordering = ["order"]
|
||
verbose_name = "Custom Page Section"
|
||
verbose_name_plural = "Custom Page Sections"
|
||
|
||
@property
|
||
def rendered_content(self):
|
||
return render_content(self.content, self.content_format)
|
||
|
||
def __str__(self):
|
||
label = self.title or self.badge or self.get_section_type_display()
|
||
return f"{self.page} › [{self.get_section_type_display()}] {label}"
|
||
|
||
|
||
class CustomPageSectionItem(models.Model):
|
||
page = models.ForeignKey(
|
||
CustomPage,
|
||
on_delete=models.CASCADE,
|
||
related_name="section_items",
|
||
)
|
||
section = models.ForeignKey(CustomPageSection, on_delete=models.CASCADE, related_name="items")
|
||
icon = models.CharField(max_length=20, blank=True)
|
||
badge = models.CharField(max_length=100, blank=True)
|
||
title = models.CharField(max_length=300, blank=True)
|
||
content = models.TextField(blank=True)
|
||
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)
|
||
image_alt = models.CharField(max_length=200, blank=True)
|
||
is_featured = models.BooleanField(default=False)
|
||
order = models.PositiveIntegerField(default=0)
|
||
|
||
class Meta:
|
||
ordering = ["order"]
|
||
verbose_name = "Custom Page Section Item"
|
||
verbose_name_plural = "Custom Page Section Items"
|
||
|
||
@property
|
||
def rendered_content(self):
|
||
return render_content(self.content, self.content_format)
|
||
|
||
def save(self, *args, **kwargs):
|
||
if self.section_id:
|
||
self.page_id = self.section.page_id
|
||
super().save(*args, **kwargs)
|
||
|
||
def __str__(self):
|
||
return f"{self.section} › {self.title or self.icon or '(item)'}"
|