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 every blog article.", ) article_people_title = models.CharField( max_length=200, default="Organizers & collaborators", help_text="Heading above the people cards on every blog article.", ) 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"