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:
@@ -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):
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user