feat: rebuild site content management

Replace legacy FAQ, downloads, and release flows with project and blog publishing. Add configurable branding and theme fields plus clean-start Compose seeding support.
This commit is contained in:
mohamad
2026-09-24 02:32:27 +03:30
parent 4fc4282f0f
commit 588d95f889
43 changed files with 703 additions and 1193 deletions
+43
View File
@@ -1,3 +1,4 @@
from django import forms
from django.contrib import admin
from django.http import HttpResponseRedirect
from django.urls import reverse
@@ -5,9 +6,36 @@ from django.urls import reverse
from .models import SiteBranding, SiteContact
class SiteBrandingForm(forms.ModelForm):
class Meta:
model = SiteBranding
fields = "__all__"
widgets = {
field: forms.TextInput(attrs={"type": "color"})
for field in (
"border_color",
"background_color",
"surface_color",
"primary_color",
"secondary_color",
"accent_color",
"text_color",
"muted_text_color",
)
}
@admin.register(SiteBranding)
class SiteBrandingAdmin(admin.ModelAdmin):
form = SiteBrandingForm
fieldsets = (
(
"Site identity",
{
"fields": ("site_name", "short_name", "tagline", "meta_description"),
"description": "Names and text used throughout the public website.",
},
),
(
"Brand assets",
{
@@ -44,6 +72,21 @@ class SiteBrandingAdmin(admin.ModelAdmin):
),
},
),
(
"Site colours",
{
"fields": (
"background_color",
"surface_color",
"primary_color",
"secondary_color",
"accent_color",
"text_color",
"muted_text_color",
),
"description": "Use six-digit hex colours. Changes apply across the public site.",
},
),
)
def has_add_permission(self, request):
+3 -123
View File
@@ -7,8 +7,6 @@ from apps.core.models import SiteBranding, SiteContact
from apps.pages.models import (
AboutSection,
AboutSectionItem,
DownloadItem,
FAQEntry,
HeroSection,
HomepageSection,
HomepageSectionItem,
@@ -214,62 +212,6 @@ MAIN_PRODUCTS = [
},
]
FAQ_ENTRIES = [
{
"question": "How is Communication to Care licensed?",
"answer": (
"Licensing and deployment terms are provided with each com2care release. "
"Contact support@com2care.com for research, institutional, or evaluation access."
),
"order": 1,
},
{
"question": "How do I acknowledge Communication to Care in my research?",
"answer": (
"Mention Communication to Care (com2care) and the software version used in your "
"methods section. Release-specific citation guidance can be requested from "
"support@com2care.com."
),
"order": 2,
},
{
"question": "Which operating systems does Communication to Care support?",
"answer": (
"Communication to Care currently fully supports Windows 10 and above (64-bit). "
"macOS and Linux packages are represented in this demonstration dataset as upcoming "
"channels. Check each product module page for current release information."
),
"order": 3,
},
{
"question": "Can I install a new version over an existing installation?",
"answer": (
"Yes, you can install the new version without removing the previous one. "
"However, if you encounter any problems after upgrading, we recommend "
"uninstalling the old version first, then performing a clean installation "
"of the new release."
),
"order": 4,
},
{
"question": "Is Communication to Care suitable for clinical use?",
"answer": (
"Communication to Care is designed and intended exclusively for research purposes. "
"It is not certified for clinical diagnostic use. Always consult with "
"qualified medical professionals for clinical decisions."
),
"order": 5,
},
{
"question": "Where can I get support or report issues?",
"answer": (
"Use the contact form or email support@com2care.com. Include the software version, "
"operating system, a short reproduction description, and non-sensitive logs when relevant."
),
"order": 6,
},
]
HOMEPAGE_SECTIONS = [
{
"section_type": HomepageSection.TYPE_FEATURES,
@@ -329,8 +271,8 @@ HOMEPAGE_SECTIONS = [
},
{
"section_type": HomepageSection.TYPE_PRODUCTS,
"badge": "Our Software",
"title": "Products",
"badge": "Our Projects",
"title": "Projects",
"description": "Explore our suite of medical imaging and radiomics tools.",
"order": 4,
"items": [],
@@ -447,44 +389,11 @@ ABOUT_SECTIONS = [
},
]
DOWNLOAD_ITEMS = [
{
"name": "com2care Desktop",
"platform": "windows",
"version": "1.0.0",
"download_url": "https://com2care.com/downloads/",
"description": "Demonstration release channel for Windows 10 and above (64-bit).",
"is_active": True,
"order": 1,
},
{
"name": "com2care Desktop",
"platform": "macos",
"version": "Coming Soon",
"download_url": "#",
"description": "macOS version is under development.",
"is_active": False,
"order": 2,
},
{
"name": "com2care Desktop",
"platform": "linux",
"version": "Coming Soon",
"download_url": "#",
"description": "Linux version is under development.",
"is_active": False,
"order": 3,
},
]
class Command(BaseCommand):
help = "Seed a complete com2care demonstration site."
CONTENT_MODELS = (
MainProduct,
FAQEntry,
DownloadItem,
HeroSection,
HomepageSection,
AboutSection,
@@ -520,15 +429,11 @@ class Command(BaseCommand):
Article.objects.all().delete()
SubProduct.objects.all().delete()
MainProduct.objects.all().delete()
FAQEntry.objects.all().delete()
DownloadItem.objects.all().delete()
HomepageSectionItem.objects.all().delete()
HomepageSection.objects.all().delete()
HeroSection.objects.all().delete()
main_product = self._seed_products()
self._seed_faq()
self._seed_downloads()
self._seed_homepage_sections()
self._seed_about_sections()
self._seed_product_videos(main_product)
@@ -617,18 +522,6 @@ class Command(BaseCommand):
self._update_instance(article_section, section_data)
return seeded_main_product
def _seed_faq(self):
for entry_data in FAQ_ENTRIES:
faq, created = FAQEntry.objects.get_or_create(
question=entry_data["question"],
defaults=entry_data,
)
if not created:
self._update_instance(faq, entry_data)
action = "Created" if created else "Updated"
self.stdout.write(f" {action} FAQ: {faq.question[:60]}...")
def _seed_homepage_sections(self):
for section_data in HOMEPAGE_SECTIONS:
items_data = section_data["items"]
@@ -711,19 +604,6 @@ class Command(BaseCommand):
if not created:
self._update_instance(video, data)
def _seed_downloads(self):
for item_data in DOWNLOAD_ITEMS:
item, created = DownloadItem.objects.get_or_create(
name=item_data["name"],
platform=item_data["platform"],
defaults=item_data,
)
if not created:
self._update_instance(item, item_data)
action = "Created" if created else "Updated"
self.stdout.write(f" {action} download: {item}")
def _seed_hero(self):
data = {
"badge": "Communication-first medical imaging",
@@ -735,7 +615,7 @@ class Command(BaseCommand):
"document analysis choices, and communicate results with clearer context."
),
"primary_cta_text": "Explore com2care",
"primary_cta_url": "/products/",
"primary_cta_url": "/projects/",
"secondary_cta_text": "Our Mission",
"secondary_cta_url": "/about/",
"image_alt": "A multidisciplinary care team collaborating around medical imaging",
@@ -0,0 +1,69 @@
# Generated by Django 5.2.16 on 2026-09-23 22:42
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_alter_sitebranding_hero_logo_alt_and_more'),
]
operations = [
migrations.AddField(
model_name='sitebranding',
name='accent_color',
field=models.CharField(default='#22d3ee', max_length=7, validators=[django.core.validators.RegexValidator(message='Enter a six-digit hex colour such as #4f8ef7.', regex='^#[0-9A-Fa-f]{6}$')]),
),
migrations.AddField(
model_name='sitebranding',
name='background_color',
field=models.CharField(default='#070713', max_length=7, validators=[django.core.validators.RegexValidator(message='Enter a six-digit hex colour such as #4f8ef7.', regex='^#[0-9A-Fa-f]{6}$')]),
),
migrations.AddField(
model_name='sitebranding',
name='meta_description',
field=models.CharField(blank=True, default='Communication to Care — Advanced Medical Imaging and Radiomics Solutions', max_length=300),
),
migrations.AddField(
model_name='sitebranding',
name='muted_text_color',
field=models.CharField(default='#a6abc2', max_length=7, validators=[django.core.validators.RegexValidator(message='Enter a six-digit hex colour such as #4f8ef7.', regex='^#[0-9A-Fa-f]{6}$')]),
),
migrations.AddField(
model_name='sitebranding',
name='primary_color',
field=models.CharField(default='#4f8ef7', max_length=7, validators=[django.core.validators.RegexValidator(message='Enter a six-digit hex colour such as #4f8ef7.', regex='^#[0-9A-Fa-f]{6}$')]),
),
migrations.AddField(
model_name='sitebranding',
name='secondary_color',
field=models.CharField(default='#a78bfa', max_length=7, validators=[django.core.validators.RegexValidator(message='Enter a six-digit hex colour such as #4f8ef7.', regex='^#[0-9A-Fa-f]{6}$')]),
),
migrations.AddField(
model_name='sitebranding',
name='short_name',
field=models.CharField(default='com2care', help_text='Compact name used in the navigation bar.', max_length=60),
),
migrations.AddField(
model_name='sitebranding',
name='site_name',
field=models.CharField(default='Communication to Care', max_length=120),
),
migrations.AddField(
model_name='sitebranding',
name='surface_color',
field=models.CharField(default='#10102a', max_length=7, validators=[django.core.validators.RegexValidator(message='Enter a six-digit hex colour such as #4f8ef7.', regex='^#[0-9A-Fa-f]{6}$')]),
),
migrations.AddField(
model_name='sitebranding',
name='tagline',
field=models.CharField(blank=True, default='Turning complex medical-imaging evidence into clearer, reproducible research conversations.', max_length=300),
),
migrations.AddField(
model_name='sitebranding',
name='text_color',
field=models.CharField(default='#eef2ff', max_length=7, validators=[django.core.validators.RegexValidator(message='Enter a six-digit hex colour such as #4f8ef7.', regex='^#[0-9A-Fa-f]{6}$')]),
),
]
+64 -2
View File
@@ -1,8 +1,12 @@
from django.core.validators import FileExtensionValidator
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 = [
@@ -10,6 +14,23 @@ class SiteBranding(models.Model):
(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,
)
icon = models.ImageField(
upload_to="branding/",
blank=True,
@@ -77,13 +98,34 @@ class SiteBranding(models.Model):
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 "Site Branding"
return self.site_name
@classmethod
def load(cls):
@@ -108,6 +150,26 @@ class SiteBranding(models.Model):
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(
+13
View File
@@ -59,3 +59,16 @@ class SiteBrandingTemplateTests(TestCase):
self.assertContains(response, "images/com2care-mark.svg")
self.assertContains(response, "images/com2care-care-team.jpg")
def test_site_identity_and_theme_are_admin_configurable(self):
branding = SiteBranding.load()
branding.site_name = "Care Research Network"
branding.short_name = "CRN"
branding.primary_color = "#123456"
branding.save()
response = self.client.get(reverse("pages:home"))
self.assertContains(response, "Care Research Network")
self.assertContains(response, ">CRN</span>")
self.assertContains(response, "--accent-blue:#123456")
+1 -1
View File
@@ -117,7 +117,7 @@ class VideoBlockMixin(models.Model):
)
video_styled_background = models.BooleanField(
default=False,
help_text="Glass panel with ambient glow (similar to the Downloads section).",
help_text="Show the video in a glass panel with an ambient glow.",
)
description_format = models.CharField(
max_length=20,