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", "projects", "accounts", "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. 'Advanced Solutions').") 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_PROJECTS = "projects" TYPE_EXPERIENCE = "experience" 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_PROJECTS, "Projects Showcase"), (TYPE_EXPERIENCE, "Experience / Benefits"), (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) title_highlight = models.CharField( max_length=300, blank=True, help_text="Second title line shown in accent colour (e.g. 'Outstanding Projects').", ) 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): STATUS_NEW = "new" STATUS_ONGOING = "ongoing" STATUS_DONE = "done" PROJECT_STATUS_CHOICES = [ ("", "—"), (STATUS_NEW, "New"), (STATUS_ONGOING, "Ongoing"), (STATUS_DONE, "Done"), ] 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="Card image or icon.") image_alt = models.CharField(max_length=200, blank=True) tags = models.CharField( max_length=300, blank=True, help_text="Comma-separated tags shown on project cards (e.g. Web Development, Publication).", ) project_status = models.CharField( max_length=10, choices=PROJECT_STATUS_CHOICES, blank=True, help_text="Project filter category (Projects Showcase section).", ) 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)'}" @property def tag_list(self): if not self.tags.strip(): return [] return [tag.strip() for tag in self.tags.split(",") if tag.strip()] 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)'}"