initial commit

This commit is contained in:
mohamad
2026-06-21 17:54:19 +03:30
commit 20d6df3b27
191 changed files with 14362 additions and 0 deletions
+479
View File
@@ -0,0 +1,479 @@
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. '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)'}"