302 lines
10 KiB
Python
302 lines
10 KiB
Python
from django.core.validators import FileExtensionValidator, RegexValidator
|
|
from django.db import models
|
|
|
|
|
|
class SiteBranding(models.Model):
|
|
hex_color_validator = RegexValidator(
|
|
regex=r"^#[0-9A-Fa-f]{6}$",
|
|
message="Enter a six-digit hex colour such as #4f8ef7.",
|
|
)
|
|
OBJECT_FIT_COVER = "cover"
|
|
OBJECT_FIT_CONTAIN = "contain"
|
|
OBJECT_FIT_CHOICES = [
|
|
(OBJECT_FIT_COVER, "Cover (fill square, may crop)"),
|
|
(OBJECT_FIT_CONTAIN, "Contain (fit inside square)"),
|
|
]
|
|
|
|
site_name = models.CharField(max_length=120, default="Communication to Care")
|
|
short_name = models.CharField(
|
|
max_length=60,
|
|
default="com2care",
|
|
help_text="Compact name used in the navigation bar.",
|
|
)
|
|
tagline = models.CharField(
|
|
max_length=300,
|
|
default="Turning complex medical-imaging evidence into clearer, reproducible research conversations.",
|
|
blank=True,
|
|
)
|
|
meta_description = models.CharField(
|
|
max_length=300,
|
|
default="Communication to Care — Advanced Medical Imaging and Radiomics Solutions",
|
|
blank=True,
|
|
)
|
|
article_people_eyebrow = models.CharField(
|
|
max_length=160,
|
|
blank=True,
|
|
default="The people behind this article",
|
|
help_text="Small heading above the people cards on blog and project articles.",
|
|
)
|
|
article_people_title = models.CharField(
|
|
max_length=200,
|
|
default="Organizers & collaborators",
|
|
help_text="Heading above the people cards on blog and project articles.",
|
|
)
|
|
project_people_eyebrow = models.CharField(
|
|
max_length=160, blank=True, default="The people behind this project",
|
|
help_text="Small heading above the people cards on main projects and sub-projects.",
|
|
)
|
|
project_people_title = models.CharField(
|
|
max_length=200, default="Organizers & collaborators",
|
|
help_text="Heading above the people cards on main projects and sub-projects.",
|
|
)
|
|
lead_group_label = models.CharField(
|
|
max_length=100, blank=True, default="Lead / Leader",
|
|
help_text="Heading for Lead and Leader people. Leave empty to hide the heading while keeping their cards.",
|
|
)
|
|
organizer_group_label = models.CharField(
|
|
max_length=100, blank=True, default="Organizers",
|
|
help_text="Heading for Organizers. Leave empty to hide the heading while keeping their cards.",
|
|
)
|
|
collaborator_group_label = models.CharField(
|
|
max_length=100, blank=True, default="Collaborators",
|
|
help_text="Heading for Collaborators. Leave empty to hide the heading while keeping their cards.",
|
|
)
|
|
|
|
icon = models.ImageField(
|
|
upload_to="branding/",
|
|
blank=True,
|
|
null=True,
|
|
verbose_name="Header and footer logo",
|
|
help_text="Brand logo shown in the site navigation and footer. Leave empty to use the default logo.",
|
|
)
|
|
icon_alt = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
help_text="Alt text for the brand icon (decorative icons can stay empty).",
|
|
)
|
|
website_icon = models.FileField(
|
|
upload_to="branding/icons/",
|
|
blank=True,
|
|
null=True,
|
|
validators=[
|
|
FileExtensionValidator(
|
|
allowed_extensions=("ico", "png", "svg", "jpg", "jpeg", "webp")
|
|
)
|
|
],
|
|
help_text=(
|
|
"Icon shown in browser tabs and bookmarks. Use a square ICO, PNG, SVG, "
|
|
"JPG, or WebP file. Leave empty to use the default Communication to Care icons."
|
|
),
|
|
)
|
|
hero_logo = models.ImageField(
|
|
upload_to="branding/hero/",
|
|
blank=True,
|
|
null=True,
|
|
help_text=(
|
|
"Large brand artwork shown in the homepage hero. Leave empty to use "
|
|
"the existing hero image."
|
|
),
|
|
)
|
|
hero_logo_alt = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
default="Communication to Care",
|
|
help_text="Accessible description for the homepage hero logo.",
|
|
)
|
|
navbar_icon_size = models.PositiveSmallIntegerField(
|
|
default=26,
|
|
help_text="Maximum height in pixels for the header logo. Width scales automatically.",
|
|
)
|
|
footer_icon_size = models.PositiveSmallIntegerField(
|
|
default=34,
|
|
help_text="Maximum height in pixels for the footer logo. Width scales automatically.",
|
|
)
|
|
show_border = models.BooleanField(
|
|
default=False,
|
|
help_text="Draw a border around the brand icon.",
|
|
)
|
|
border_color = models.CharField(
|
|
max_length=7,
|
|
default="#c4b5fd",
|
|
help_text="Border color as hex (e.g. #c4b5fd).",
|
|
)
|
|
border_width = models.PositiveSmallIntegerField(
|
|
default=1,
|
|
help_text="Border width in pixels.",
|
|
)
|
|
object_fit = models.CharField(
|
|
max_length=10,
|
|
choices=OBJECT_FIT_CHOICES,
|
|
default=OBJECT_FIT_COVER,
|
|
)
|
|
background_color = models.CharField(
|
|
max_length=7, default="#070713", validators=[hex_color_validator]
|
|
)
|
|
surface_color = models.CharField(
|
|
max_length=7, default="#10102a", validators=[hex_color_validator]
|
|
)
|
|
primary_color = models.CharField(
|
|
max_length=7, default="#4f8ef7", validators=[hex_color_validator]
|
|
)
|
|
secondary_color = models.CharField(
|
|
max_length=7, default="#a78bfa", validators=[hex_color_validator]
|
|
)
|
|
accent_color = models.CharField(
|
|
max_length=7, default="#22d3ee", validators=[hex_color_validator]
|
|
)
|
|
text_color = models.CharField(
|
|
max_length=7, default="#eef2ff", validators=[hex_color_validator]
|
|
)
|
|
muted_text_color = models.CharField(
|
|
max_length=7, default="#a6abc2", validators=[hex_color_validator]
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Site Branding"
|
|
verbose_name_plural = "Site Branding"
|
|
|
|
def __str__(self):
|
|
return self.site_name
|
|
|
|
@classmethod
|
|
def load(cls):
|
|
obj, _ = cls.objects.get_or_create(pk=1)
|
|
return obj
|
|
|
|
def icon_style(self, size_px):
|
|
parts = [
|
|
"width:auto",
|
|
f"height:{size_px}px",
|
|
"object-fit:contain",
|
|
]
|
|
if self.show_border:
|
|
parts.append(f"border:{self.border_width}px solid {self.border_color}")
|
|
return ";".join(parts)
|
|
|
|
@property
|
|
def navbar_icon_style(self):
|
|
return self.icon_style(self.navbar_icon_size)
|
|
|
|
@property
|
|
def footer_icon_style(self):
|
|
return self.icon_style(self.footer_icon_size)
|
|
|
|
@property
|
|
def theme_style(self):
|
|
return ";".join(
|
|
(
|
|
f"--bg-primary:{self.background_color}",
|
|
f"--bg-secondary:{self.background_color}",
|
|
f"--bg-surface:{self.surface_color}",
|
|
f"--accent-blue:{self.primary_color}",
|
|
f"--accent-blue-light:{self.primary_color}",
|
|
f"--accent-purple:{self.secondary_color}",
|
|
f"--accent-cyan:{self.accent_color}",
|
|
f"--text-primary:{self.text_color}",
|
|
f"--text-secondary:{self.muted_text_color}",
|
|
f"--text-muted:{self.muted_text_color}",
|
|
f"--text-link:{self.primary_color}",
|
|
f"--gradient-brand:linear-gradient(135deg,{self.primary_color} 0%,{self.secondary_color} 100%)",
|
|
f"--gradient-text:linear-gradient(135deg,{self.primary_color} 0%,{self.secondary_color} 50%,{self.accent_color} 100%)",
|
|
)
|
|
)
|
|
|
|
|
|
class SiteContact(models.Model):
|
|
support_email = models.EmailField(
|
|
blank=True,
|
|
help_text="Shown in the footer and on the contact page. Hidden when empty.",
|
|
)
|
|
discord_url = models.URLField(
|
|
blank=True,
|
|
help_text="Discord invite link. Hidden when empty.",
|
|
)
|
|
discord_label = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
help_text="Button label (e.g. Join Discord). Defaults to “Join Discord” when empty.",
|
|
)
|
|
office_address = models.TextField(
|
|
blank=True,
|
|
help_text="Physical address; one line per row. Hidden when empty.",
|
|
)
|
|
email_card_title = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
help_text="Contact page email card heading. Defaults to “Email Support”.",
|
|
)
|
|
email_card_description = models.CharField(
|
|
max_length=500,
|
|
blank=True,
|
|
help_text="Short text under the email card heading on the contact page.",
|
|
)
|
|
discord_card_title = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
help_text="Contact page Discord card heading. Defaults to “Discord Community”.",
|
|
)
|
|
discord_card_description = models.CharField(
|
|
max_length=500,
|
|
blank=True,
|
|
help_text="Short text under the Discord card heading on the contact page.",
|
|
)
|
|
office_card_title = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
help_text="Contact page office card heading. Defaults to “Office”.",
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Site Contact"
|
|
verbose_name_plural = "Site Contact"
|
|
|
|
def __str__(self):
|
|
return "Site Contact"
|
|
|
|
@classmethod
|
|
def load(cls):
|
|
obj, _ = cls.objects.get_or_create(pk=1)
|
|
return obj
|
|
|
|
@property
|
|
def discord_label_display(self):
|
|
return self.discord_label.strip() or "Join Discord"
|
|
|
|
@property
|
|
def office_address_lines(self):
|
|
if not self.office_address.strip():
|
|
return []
|
|
return [line.strip() for line in self.office_address.splitlines() if line.strip()]
|
|
|
|
@property
|
|
def has_support_email(self):
|
|
return bool(self.support_email)
|
|
|
|
@property
|
|
def has_discord(self):
|
|
return bool(self.discord_url)
|
|
|
|
@property
|
|
def has_office_address(self):
|
|
return bool(self.office_address_lines)
|
|
|
|
@property
|
|
def has_footer_contact(self):
|
|
return self.has_support_email or self.has_office_address
|
|
|
|
@property
|
|
def has_contact_sidebar(self):
|
|
return self.has_support_email or self.has_discord or self.has_office_address
|
|
|
|
@property
|
|
def email_card_title_display(self):
|
|
return self.email_card_title.strip() or "Email Support"
|
|
|
|
@property
|
|
def discord_card_title_display(self):
|
|
return self.discord_card_title.strip() or "Discord Community"
|
|
|
|
@property
|
|
def office_card_title_display(self):
|
|
return self.office_card_title.strip() or "Office"
|