Files
com2care_website/apps/pages/models.py
T
2026-06-08 17:08:43 +03:30

442 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
from apps.core.video import VideoBlockMixin
from apps.pages.contact_uploads import (
contact_attachment_storage,
contact_attachment_upload_to,
)
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(VideoBlockMixin, 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_VIDEO = "video"
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"),
(TYPE_VIDEO, "Video"),
]
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 clean(self):
super().clean()
if self.section_type == self.TYPE_VIDEO:
self.clean_video_fields(require=True)
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(VideoBlockMixin, models.Model):
TYPE_HERO = "hero"
TYPE_INTRO = "intro"
TYPE_GRID = "grid"
TYPE_HISTORY = "history"
TYPE_CUSTOM = "custom"
TYPE_SUPPORTERS = "supporters"
TYPE_VIDEO = "video"
TYPE_CHOICES = [
(TYPE_HERO, "Hero"),
(TYPE_INTRO, "Intro Card"),
(TYPE_GRID, "Grid Cards"),
(TYPE_HISTORY, "History Block"),
(TYPE_CUSTOM, "Custom Content"),
(TYPE_SUPPORTERS, "Supporters"),
(TYPE_VIDEO, "Video"),
]
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 clean(self):
super().clean()
if self.section_type == self.TYPE_VIDEO:
self.clean_video_fields(require=True)
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 ContactSubmissionAttachment(models.Model):
submission = models.ForeignKey(
ContactSubmission,
on_delete=models.CASCADE,
related_name="attachments",
)
file = models.FileField(
upload_to=contact_attachment_upload_to,
storage=contact_attachment_storage,
)
original_filename = models.CharField(max_length=255)
uploaded_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["uploaded_at"]
verbose_name = "Contact Attachment"
verbose_name_plural = "Contact Attachments"
def __str__(self):
return self.original_filename
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(VideoBlockMixin, 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_VIDEO = "video"
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"),
(TYPE_VIDEO, "Video"),
]
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 clean(self):
super().clean()
if self.section_type == self.TYPE_VIDEO:
self.clean_video_fields(require=True)
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 PageVideo(VideoBlockMixin, models.Model):
PAGE_CONTACT = "contact"
PAGE_FAQ = "faq"
PAGE_CHOICES = [
(PAGE_CONTACT, "Contact"),
(PAGE_FAQ, "FAQ"),
]
page = models.CharField(max_length=20, choices=PAGE_CHOICES)
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
description = models.TextField(blank=True)
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["page", "order"]
verbose_name = "Page Video"
verbose_name_plural = "Page Videos"
def clean(self):
super().clean()
self.clean_video_fields(require=True)
def __str__(self):
label = self.title or self.badge or self.get_page_display()
return f"{self.get_page_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)'}"