fix: separate logo and hero icon

This commit is contained in:
mohamad
2026-08-03 16:29:25 +03:30
parent 04b6e9049e
commit 4fc4282f0f
45 changed files with 900 additions and 495 deletions
+9 -8
View File
@@ -1,17 +1,18 @@
DJANGO_SETTINGS_MODULE=config.settings.production DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=uqgb2wi9@1dr8alhhx$rp_tx!%_en$k7w6yjbu7wz-qr3$&3-w DJANGO_SECRET_KEY=replace-with-a-long-random-production-secret
DEBUG=False DEBUG=False
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,radiuma.com,www.radiuma.com ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,com2care.com,www.com2care.com
CSRF_TRUSTED_ORIGINS=localhost,127.0.0.1,0.0.0.0,radiuma.com,www.radiuma.com CSRF_TRUSTED_ORIGINS=http://localhost:8000,https://com2care.com,https://www.com2care.com
POSTGRES_DB=radiuma POSTGRES_DB=com2care
POSTGRES_USER=radiuma_user POSTGRES_USER=com2care_user
POSTGRES_PASSWORD=eS4_WYJH97gywyoHjP6v POSTGRES_PASSWORD=replace-with-a-strong-database-password
POSTGRES_HOST=db POSTGRES_HOST=db
POSTGRES_PORT=5432 POSTGRES_PORT=5432
SECURE_SSL_REDIRECT=False SECURE_SSL_REDIRECT=False
DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_EMAIL=admin@yourdomain.com DJANGO_SUPERUSER_EMAIL=admin@com2care.com
DJANGO_SUPERUSER_PASSWORD=bS7_U_uRTmivj7W-lCR5 DJANGO_SUPERUSER_PASSWORD=cmosV6Tw46Odv7UN
DJANGO_SUPERUSER_SYNC_PASSWORD=False
+5 -1
View File
@@ -48,4 +48,8 @@ venv.bak/
media/ media/
private_uploads/ private_uploads/
staticfiles/ staticfiles/
*.DS_Store *.DS_Store
test_media_releases/
docs/
scripts/
videos/
+125 -231
View File
@@ -1,278 +1,172 @@
# Radiuma Website # Communication to Care
Django MVT informational website for **Radiuma**, showcasing the **Radiuma** medical imaging and radiomics software suite. The Django website for **Communication to Care**, published at **com2care.com**. It presents the
com2care medical-imaging research platform, product modules, learning videos, downloads, FAQs,
and support information.
## Tech Stack ## Start with Docker
| Layer | Technology | Docker Compose includes working local defaults, database migrations, demo content, and the first
|---|---| admin account. No setup command or `.env` file is required for a local demonstration.
| Backend | Django 5.1 (MVT) |
| Database | PostgreSQL 16 | ```bash
| Static files | WhiteNoise (with Brotli compression) | docker compose up -d --build
| Application server | Gunicorn | ```
| Containerization | Docker + Docker Compose |
| Frontend | Vanilla HTML/CSS/JS (no framework) | Open:
## Project Structure - Website: <http://localhost:8000>
- Admin: <http://localhost:8000/admin/>
```
tecvico_website/ Initial local admin credentials:
├── config/ # Django project configuration
│ └── settings/ ```text
│ ├── base.py # Shared settings Username: admin
│ ├── development.py # Dev settings (DEBUG=True, dotenv) Password: cmosV6Tw46Odv7UN
│ └── production.py # Production settings (security headers) ```
├── apps/
│ ├── core/ # Context processors, management commands Change the password immediately after the first login using **Django Admin → Change password**.
│ │ └── management/commands/seed_content.py The startup process never resets an existing admin password unless credential synchronization is
│ ├── products/ # MainProduct, SubProduct, Article, ArticleSection explicitly enabled.
│ └── pages/ # FAQEntry, DownloadItem; static pages
├── templates/ # Global templates ## Automatic startup behavior
│ ├── base.html
│ ├── partials/ Every web-container start performs these idempotent steps:
│ └── pages/ & products/
├── static/ 1. Wait for PostgreSQL.
│ ├── css/main.css # Full design system (dark glass/ice theme) 2. Collect static files.
│ └── js/main.js # Navbar, FAQ accordion, scroll effects 3. Apply Django migrations.
├── Dockerfile 4. Normalize public database text and URLs to the Communication to Care identity.
├── docker-compose.yml # Production compose 5. Seed demonstration content only when all public content tables are empty.
├── docker-compose.override.yml # Development compose overrides 6. Create the configured admin account only when it does not exist.
└── entrypoint.sh # DB wait + migrate on container start 7. Start Django or Gunicorn.
```
Demo content includes:
## URL Map
- A com2care product with five medical-imaging modules.
| URL | View | Description | - Articles, structured specifications, FAQs, and release examples.
|---|---|---| - Homepage and About page sections with local images.
| `/` | `HomeView` | Landing page | - A generated Communication to Care hero image.
| `/about/` | `AboutView` | What is Radiuma | - Relevant external YouTube tutorials for DICOM review and image segmentation.
| `/products/` | `ProductOverviewView` | All main products | - `support@com2care.com` contact data.
| `/products/<main-slug>/` | `MainProductDetailView` | Main product + sub-products |
| `/products/<main-slug>/<sub-slug>/` | `SubProductDetailView` | Sub-product + articles | Content created or edited by an admin is preserved on later container restarts.
| `/downloads/` | `DownloadsView` | Download items by platform |
| `/faq/` | `FAQView` | FAQ entries | ## Optional configuration
| `/contact/` | `ContactView` | Contact info |
| `/admin/` | Django Admin | Admin panel | Copy `.env.example` to `.env` only when you want to override the local defaults:
## Data Model
```
MainProduct
└── SubProduct (FK → MainProduct)
└── Article (FK → SubProduct)
└── ArticleSection (FK → Article) ← key/value metadata
FAQEntry ← admin-managed FAQ items
DownloadItem ← admin-managed download links per platform
```
---
## Local Development (with Docker)
### Prerequisites
- Docker Desktop (or Docker Engine + Compose plugin)
### 1. Clone & configure
```bash ```bash
git clone <repo-url> tecvico_website
cd tecvico_website
cp .env.example .env cp .env.example .env
``` ```
Edit `.env` and set at minimum: Important variables:
``` | Variable | Purpose |
DJANGO_SECRET_KEY=your-very-secure-random-key |---|---|
POSTGRES_PASSWORD=choose-a-strong-password | `DJANGO_SECRET_KEY` | Required strong secret for production |
``` | `ALLOWED_HOSTS` | Hostnames, including `com2care.com` |
| `CSRF_TRUSTED_ORIGINS` | Full trusted origins with `https://` |
| `POSTGRES_DB` | PostgreSQL database name |
| `POSTGRES_USER` | PostgreSQL user |
| `POSTGRES_PASSWORD` | PostgreSQL password |
| `DJANGO_SUPERUSER_ENABLED` | Set `False` to disable automatic admin creation |
| `DJANGO_SUPERUSER_USERNAME` | First admin username |
| `DJANGO_SUPERUSER_EMAIL` | First admin email |
| `DJANGO_SUPERUSER_PASSWORD` | First admin password |
| `DJANGO_SUPERUSER_SYNC_PASSWORD` | Set `True` for one restart to rotate an existing admin password from environment values |
### 2. Start services (development mode) For a server-side password rotation, set the new `DJANGO_SUPERUSER_PASSWORD`, temporarily set
`DJANGO_SUPERUSER_SYNC_PASSWORD=True`, restart the web service once, then restore it to `False`.
An admin can always change their own password through Django Admin without changing server
configuration.
The `docker-compose.override.yml` automatically activates when you run `docker compose up`, mounting the source code and using the development settings. ## Production
Use strong values in `.env`, terminate TLS with a reverse proxy, and start only the production
Compose file:
```bash ```bash
docker compose up --build docker compose -f docker-compose.yml up -d --build
``` ```
The app is available at **http://localhost:8000** At minimum, replace these local defaults in production:
### 3. Create a superuser - `DJANGO_SECRET_KEY`
- `POSTGRES_PASSWORD`
- `DJANGO_SUPERUSER_PASSWORD`
- `SECURE_SSL_REDIRECT=True`
```bash The default production host configuration already includes `com2care.com` and
docker compose exec web python manage.py createsuperuser `www.com2care.com`.
```
### 4. Seed initial content ## Manual development
Populate the database with content scraped and adapted from visera.ca: Requirements:
```bash
docker compose exec web python manage.py seed_content
```
To flush and re-seed from scratch:
```bash
docker compose exec web python manage.py seed_content --flush
```
---
## Local Development (without Docker)
### Prerequisites
- Python 3.12+ - Python 3.12+
- PostgreSQL 14+ - PostgreSQL 14+
### 1. Set up virtual environment
```bash ```bash
python -m venv .venv python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate source .venv/bin/activate
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
```
### 2. Configure environment
```bash
cp .env.example .env cp .env.example .env
```
Edit `.env`:
```
DJANGO_SECRET_KEY=your-key
POSTGRES_DB=tecvico
POSTGRES_USER=your_pg_user
POSTGRES_PASSWORD=your_pg_password
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
```
### 3. Create the database
```bash
createdb tecvico
```
### 4. Run migrations & seed
```bash
python manage.py migrate python manage.py migrate
python manage.py seed_content python manage.py seed_content --if-empty
python manage.py createsuperuser python manage.py ensure_superuser
```
### 5. Start development server
```bash
python manage.py runserver python manage.py runserver
``` ```
--- ## Management commands
## Running Tests ```bash
# Add demo data only to a completely empty public database
python manage.py seed_content --if-empty
### With Django test runner # Intentionally replace all public content with the demo dataset
python manage.py seed_content --flush
# Normalize existing public text, slugs, URLs, and email addresses
python manage.py normalize_brand
# Create or optionally synchronize the configured admin
python manage.py ensure_superuser
```
## Tests
```bash ```bash
python manage.py test apps python manage.py test apps
``` ```
### With pytest (requires `requirements-dev.txt`) or:
```bash ```bash
pytest pytest
``` ```
### With coverage report ## Project structure
```bash ```text
coverage run -m pytest config/ Django settings and root URLs
coverage report -m apps/core/ Branding, contact settings, bootstrap commands
coverage html # Generates htmlcov/index.html apps/pages/ Home, About, FAQ, Contact, custom pages, videos
apps/products/ Products, articles, releases, product videos
templates/ Django templates
static/css/main.css Site design system
static/images/ com2care brand and editorial assets
static/js/main.js Navigation and interaction behavior
docker-compose.yml PostgreSQL and production web service
docker-compose.override.yml Local development override
entrypoint.sh Automated database and content bootstrap
``` ```
--- ## Technology
## Production Deployment - Django
- PostgreSQL 16
### 1. Build and start production containers - Gunicorn
- WhiteNoise
Remove `docker-compose.override.yml` (or don't override it) and pass production environment variables: - Docker Compose
- Vanilla HTML, CSS, and JavaScript
```bash
DJANGO_SETTINGS_MODULE=config.settings.production \
docker compose -f docker-compose.yml up --build -d
```
### 2. Production `.env` checklist
| Variable | Notes |
|---|---|
| `DJANGO_SECRET_KEY` | Use `python -c "import secrets; print(secrets.token_urlsafe(50))"` |
| `DEBUG` | Must be `False` |
| `ALLOWED_HOSTS` | Comma-separated: `yourdomain.com,www.yourdomain.com` |
| `CSRF_TRUSTED_ORIGINS` | `https://yourdomain.com` |
| `POSTGRES_PASSWORD` | Strong random password |
| `SECURE_SSL_REDIRECT` | `True` when behind TLS termination |
### 3. Reverse proxy (recommended)
Place an Nginx or Caddy reverse proxy in front of Gunicorn for TLS termination and serving static files (or let WhiteNoise handle statics directly).
---
## Admin Panel
Access Django Admin at `/admin/` with superuser credentials.
### What you can manage
| Model | Description |
|---|---|
| **Main Products** | Top-level products with nested sub-products inline |
| **Sub Products** | Modules within a main product; articles editable inline |
| **Articles** | Article entries with section key/values inline |
| **Article Sections** | Individual key-value metadata rows |
| **FAQ Entries** | Accordion FAQ items (order, active toggle) |
| **Download Items** | Platform download links (Windows/macOS/Linux) |
---
## Seed Content
The `seed_content` command populates:
- **Radiuma** (MainProduct) with 5 sub-products:
- Image Processing
- Radiomics Features
- Medical Image Visualization
- Format Conversion
- Workflow Management
- Articles and sections for each sub-product
- 6 FAQ entries
- 3 download items (Windows active, macOS/Linux coming soon)
Content is adapted from the original [visera.ca](https://visera.ca) website.
---
## Design System
The website uses a custom dark-glass design combining:
- **visera.ca** aesthetic — dark background, organic blob animations, blue/teal accents
- **Apple visionOS Ice** aesthetic — frosted glass panels (`backdrop-filter: blur`), translucent cards, soft gradients
Key CSS custom properties are in `static/css/main.css` under `:root`.
---
## License
Content is adapted from visera.ca under CC BY-NC-SA. Software code is proprietary to Radiuma.
+1 -1
View File
@@ -20,7 +20,7 @@ class SiteBrandingAdmin(admin.ModelAdmin):
), ),
"description": ( "description": (
"Manage each placement independently. Removing an upload restores " "Manage each placement independently. Removing an upload restores "
"the existing Radiuma asset for that placement." "the existing Communication to Care asset for that placement."
), ),
}, },
), ),
@@ -4,29 +4,53 @@ from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
DEFAULT_ADMIN_PASSWORD = "cmosV6Tw46Odv7UN"
class Command(BaseCommand): class Command(BaseCommand):
help = "Create a superuser from environment variables if one does not already exist." help = "Create the first admin account from environment variables when needed."
def handle(self, *args, **options): def handle(self, *args, **options):
User = get_user_model() User = get_user_model()
username = os.environ.get("DJANGO_SUPERUSER_USERNAME", "admin") enabled = os.environ.get("DJANGO_SUPERUSER_ENABLED", "True").lower()
email = os.environ.get("DJANGO_SUPERUSER_EMAIL", "admin@radiuma.com") if enabled in {"0", "false", "no", "off"}:
password = os.environ.get("DJANGO_SUPERUSER_PASSWORD") self.stdout.write("Automatic admin creation is disabled.")
return
if not password: username = os.environ.get("DJANGO_SUPERUSER_USERNAME", "admin")
email = os.environ.get("DJANGO_SUPERUSER_EMAIL", "admin@com2care.com")
password = os.environ.get("DJANGO_SUPERUSER_PASSWORD", DEFAULT_ADMIN_PASSWORD)
sync_password = os.environ.get(
"DJANGO_SUPERUSER_SYNC_PASSWORD", "False"
).lower() in {"1", "true", "yes", "on"}
user = User.objects.filter(username=username).first()
if user:
if sync_password:
user.email = email
user.is_staff = True
user.is_superuser = True
user.set_password(password)
user.save(
update_fields=["email", "is_staff", "is_superuser", "password"]
)
self.stdout.write(
self.style.SUCCESS(
f"Admin '{username}' credentials synchronized from the environment."
)
)
return
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.SUCCESS(
"DJANGO_SUPERUSER_PASSWORD is not set — skipping superuser creation." f"Admin '{username}' already exists; its password was preserved."
) )
) )
return return
if User.objects.filter(username=username).exists():
self.stdout.write(
self.style.SUCCESS(f"Superuser '{username}' already exists — skipping.")
)
return
User.objects.create_superuser(username=username, email=email, password=password) User.objects.create_superuser(username=username, email=email, password=password)
self.stdout.write(self.style.SUCCESS(f"Superuser '{username}' created successfully.")) self.stdout.write(
self.style.SUCCESS(
f"Admin '{username}' created. Change the password in Django Admin after first login."
)
)
@@ -0,0 +1,68 @@
import re
from django.apps import apps
from django.core.management.base import BaseCommand
from django.db import models, transaction
LEGACY_NAME = "".join(("Radi", "uma"))
LEGACY_PATTERN = re.compile(re.escape(LEGACY_NAME), re.IGNORECASE)
LEGACY_DOMAIN_PATTERN = re.compile(
rf"{re.escape(LEGACY_NAME)}\.com", re.IGNORECASE
)
PUBLIC_APP_LABELS = frozenset({"core", "pages", "products"})
def branded_value(field, value):
if not isinstance(value, str) or not LEGACY_PATTERN.search(value):
return value
value = LEGACY_DOMAIN_PATTERN.sub("com2care.com", value)
if isinstance(field, (models.EmailField, models.URLField, models.SlugField)):
return LEGACY_PATTERN.sub("com2care", value)
return LEGACY_PATTERN.sub("Communication to Care", value)
class Command(BaseCommand):
help = "Normalize legacy public content to the current com2care identity."
@transaction.atomic
def handle(self, *args, **options):
updated_rows = 0
for model in apps.get_models():
if model._meta.app_label not in PUBLIC_APP_LABELS:
continue
text_fields = [
field
for field in model._meta.concrete_fields
if isinstance(field, (models.CharField, models.TextField))
and not field.primary_key
]
if not text_fields:
continue
field_names = [field.name for field in text_fields]
for instance in model._default_manager.all().only("pk", *field_names).iterator():
changed_fields = []
for field in text_fields:
current = getattr(instance, field.name)
updated = branded_value(field, current)
if updated == current:
continue
if isinstance(field, models.SlugField):
conflict = model._default_manager.exclude(pk=instance.pk).filter(
**{field.name: updated}
).exists()
if conflict:
continue
setattr(instance, field.name, updated)
changed_fields.append(field.name)
if changed_fields:
instance.save(update_fields=changed_fields)
updated_rows += 1
if updated_rows:
self.stdout.write(
self.style.SUCCESS(f"Normalized {updated_rows} public content row(s).")
)
else:
self.stdout.write("Public content already uses the com2care identity.")
+354 -159
View File
@@ -1,24 +1,38 @@
from django.conf import settings
from django.core.files import File
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from apps.core.models import SiteContact from apps.core.models import SiteBranding, SiteContact
from apps.pages.models import DownloadItem, FAQEntry, HeroSection, HomepageSection, HomepageSectionItem from apps.pages.models import (
from apps.products.models import Article, ArticleSection, MainProduct, SubProduct AboutSection,
AboutSectionItem,
DownloadItem,
FAQEntry,
HeroSection,
HomepageSection,
HomepageSectionItem,
PageVideo,
)
from apps.products.models import (
Article,
ArticleSection,
MainProduct,
ProductVideo,
SubProduct,
)
MAIN_PRODUCTS = [ MAIN_PRODUCTS = [
{ {
"name": "Radiuma", "name": "Communication to Care",
"slug": "radiuma", "slug": "com2care",
"short_description": "Visualized & Standardized Environment for Radiomics Analysis", "short_description": "Collaborative, standardized medical imaging research workflows",
"description": ( "description": (
"Radiuma is a free, open-source software specialized for visualization, " "Communication to Care (com2care) brings medical-image visualization, processing, "
"processing, segmentation, registration, fusion and analysis of medical and " "segmentation, registration, fusion, radiomics, and machine-learning workflows into "
"biomedical images, including radiomics and machine learning analysis. " "one research environment. The platform is designed to help multidisciplinary teams "
"Radiuma is a major, entirely-revamped upgrade to the original SERA " "discuss findings clearly, build repeatable pipelines, and share analysis context. "
"(Matlab-based), now built on Python for broader accessibility and community " "Its quantitative imaging workflow follows IBSI guidance for reproducible research."
"contribution. It enables standardized and reproducible radiomic feature "
"extraction in compliance with the Image Biomarker Standardization Initiative "
"(IBSI 1.0), and implements image filters standardized against IBSI 2.0."
), ),
"order": 1, "order": 1,
"show_on_homepage": True, "show_on_homepage": True,
@@ -31,7 +45,7 @@ MAIN_PRODUCTS = [
"description": ( "description": (
"Advanced image processing capabilities including standardized filtering " "Advanced image processing capabilities including standardized filtering "
"techniques compliant with IBSI 2.0, image registration, fusion, and " "techniques compliant with IBSI 2.0, image registration, fusion, and "
"Standardized Uptake Value (SUV) conversion. Radiuma employs popular " "Standardized Uptake Value (SUV) conversion. Communication to Care employs popular "
"image processing algorithms to create end-to-end standardized workflows " "image processing algorithms to create end-to-end standardized workflows "
"for consistent, reproducible research outcomes." "for consistent, reproducible research outcomes."
), ),
@@ -40,7 +54,7 @@ MAIN_PRODUCTS = [
{ {
"title": "Image Filtering Techniques", "title": "Image Filtering Techniques",
"description": ( "description": (
"Radiuma implements a comprehensive set of image filtering techniques " "Communication to Care implements a comprehensive set of image filtering techniques "
"fully standardized against the Image Biomarker Standardization " "fully standardized against the Image Biomarker Standardization "
"Initiative (IBSI) phase 2. These filters enable reproducible " "Initiative (IBSI) phase 2. These filters enable reproducible "
"preprocessing across institutions and studies." "preprocessing across institutions and studies."
@@ -53,13 +67,13 @@ MAIN_PRODUCTS = [
"value": "Mean, Gaussian, Laplacian of Gaussian (LoG), Laws kernels, Gabor, Wavelets (PyWavelets), Log-Sigma", "value": "Mean, Gaussian, Laplacian of Gaussian (LoG), Laws kernels, Gabor, Wavelets (PyWavelets), Log-Sigma",
"order": 2, "order": 2,
}, },
{"title": "Author", "value": "Radiuma R&D Team", "order": 3}, {"title": "Author", "value": "Communication to Care R&D Team", "order": 3},
], ],
}, },
{ {
"title": "Image Registration & Fusion", "title": "Image Registration & Fusion",
"description": ( "description": (
"Radiuma provides robust image registration and fusion methods, " "Communication to Care provides robust image registration and fusion methods, "
"enabling multi-modal image alignment for PET/CT, PET/MRI, and " "enabling multi-modal image alignment for PET/CT, PET/MRI, and "
"other combined modality studies. Standardized Uptake Value (SUV) " "other combined modality studies. Standardized Uptake Value (SUV) "
"conversion is also supported." "conversion is also supported."
@@ -78,7 +92,7 @@ MAIN_PRODUCTS = [
"slug": "radiomics-features", "slug": "radiomics-features",
"short_description": "IBSI 1.0 compliant handcrafted radiomic feature extraction", "short_description": "IBSI 1.0 compliant handcrafted radiomic feature extraction",
"description": ( "description": (
"Radiuma provides comprehensive handcrafted radiomic feature extraction " "Communication to Care provides comprehensive handcrafted radiomic feature extraction "
"fully standardized by the Image Biomarker Standardization Initiative " "fully standardized by the Image Biomarker Standardization Initiative "
"(IBSI 1.0). Features are computed from segmented regions of interest " "(IBSI 1.0). Features are computed from segmented regions of interest "
"across multiple image modalities, enabling reproducible quantitative " "across multiple image modalities, enabling reproducible quantitative "
@@ -89,7 +103,7 @@ MAIN_PRODUCTS = [
{ {
"title": "IBSI Compliant Feature Extraction", "title": "IBSI Compliant Feature Extraction",
"description": ( "description": (
"Radiuma computes a comprehensive set of radiomic features " "Communication to Care computes a comprehensive set of radiomic features "
"covering all IBSI 1.0 feature classes. Features are extracted " "covering all IBSI 1.0 feature classes. Features are extracted "
"from segmented Regions of Interest (ROIs) and are fully " "from segmented Regions of Interest (ROIs) and are fully "
"reproducible across different platforms and institutions." "reproducible across different platforms and institutions."
@@ -113,7 +127,7 @@ MAIN_PRODUCTS = [
"slug": "medical-image-visualization", "slug": "medical-image-visualization",
"short_description": "Professional multi-modality medical image viewer", "short_description": "Professional multi-modality medical image viewer",
"description": ( "description": (
"Radiuma includes a professional medical image viewer that supports " "Communication to Care includes a professional medical image viewer that supports "
"multiple imaging modalities and file formats. The viewer provides " "multiple imaging modalities and file formats. The viewer provides "
"comfortable, intuitive controls for slice navigation, windowing, " "comfortable, intuitive controls for slice navigation, windowing, "
"zoom, and annotation, suitable for radiation oncologists, radiologists, " "zoom, and annotation, suitable for radiation oncologists, radiologists, "
@@ -143,7 +157,7 @@ MAIN_PRODUCTS = [
"slug": "format-conversion", "slug": "format-conversion",
"short_description": "Professional converter for medical imaging file formats", "short_description": "Professional converter for medical imaging file formats",
"description": ( "description": (
"Radiuma provides a professional image format converter supporting all " "Communication to Care provides a professional image format converter supporting all "
"major medical imaging standards. Seamlessly convert between DICOM, " "major medical imaging standards. Seamlessly convert between DICOM, "
"NIFTI, NRRD, MHA, and other formats without loss of spatial metadata " "NIFTI, NRRD, MHA, and other formats without loss of spatial metadata "
"or patient information integrity." "or patient information integrity."
@@ -172,7 +186,7 @@ MAIN_PRODUCTS = [
"slug": "workflow-management", "slug": "workflow-management",
"short_description": "Reproducible research workflow creation and sharing", "short_description": "Reproducible research workflow creation and sharing",
"description": ( "description": (
"Radiuma's workflow management system allows researchers to design, save, " "Communication to Care's workflow management system allows researchers to design, save, "
"share, and reuse analysis pipelines. Workflows connect individual " "share, and reuse analysis pipelines. Workflows connect individual "
"processing steps — from image loading and preprocessing to feature " "processing steps — from image loading and preprocessing to feature "
"extraction and machine learning — into reproducible, shareable sequences " "extraction and machine learning — into reproducible, shareable sequences "
@@ -202,34 +216,28 @@ MAIN_PRODUCTS = [
FAQ_ENTRIES = [ FAQ_ENTRIES = [
{ {
"question": "What is the Radiuma license?", "question": "How is Communication to Care licensed?",
"answer": ( "answer": (
"Radiuma is free and open-source for research purposes.\n\n" "Licensing and deployment terms are provided with each com2care release. "
"License: CC BY-NC-SA (Creative Commons Attribution-NonCommercial-ShareAlike). " "Contact support@com2care.com for research, institutional, or evaluation access."
"This means you may use, share, and adapt the software for non-commercial "
"research purposes, provided you give appropriate credit and distribute "
"derivatives under the same license."
), ),
"order": 1, "order": 1,
}, },
{ {
"question": "How do I cite Radiuma in my research?", "question": "How do I acknowledge Communication to Care in my research?",
"answer": ( "answer": (
"Please cite the following reference if you publish results obtained with " "Mention Communication to Care (com2care) and the software version used in your "
"the help of Radiuma:\n\n" "methods section. Release-specific citation guidance can be requested from "
"M. R. Salmanpour, I. Shiri, M. Hosseinzadeh, H. Zaidi, S. Ashrafinia, " "support@com2care.com."
"M. Oveisi, A. Rahmim. Radiuma: Visualized & Standardized Environment for "
"Radiomics Analysis — A Shareable, Executable, and Reproducible Workflow "
"Generator. Proc. IEEE Medical Imaging Conference, 2023."
), ),
"order": 2, "order": 2,
}, },
{ {
"question": "Which operating systems does Radiuma support?", "question": "Which operating systems does Communication to Care support?",
"answer": ( "answer": (
"Radiuma currently fully supports Windows 10 and above (64-bit). " "Communication to Care currently fully supports Windows 10 and above (64-bit). "
"New versions to support macOS and Linux systems are under active development " "macOS and Linux packages are represented in this demonstration dataset as upcoming "
"and coming soon. Follow our Discord or check each product module page for updates." "channels. Check each product module page for current release information."
), ),
"order": 3, "order": 3,
}, },
@@ -244,9 +252,9 @@ FAQ_ENTRIES = [
"order": 4, "order": 4,
}, },
{ {
"question": "Is Radiuma suitable for clinical use?", "question": "Is Communication to Care suitable for clinical use?",
"answer": ( "answer": (
"Radiuma is designed and intended exclusively for research purposes. " "Communication to Care is designed and intended exclusively for research purposes. "
"It is not certified for clinical diagnostic use. Always consult with " "It is not certified for clinical diagnostic use. Always consult with "
"qualified medical professionals for clinical decisions." "qualified medical professionals for clinical decisions."
), ),
@@ -255,9 +263,8 @@ FAQ_ENTRIES = [
{ {
"question": "Where can I get support or report issues?", "question": "Where can I get support or report issues?",
"answer": ( "answer": (
"Support is available via email and through our community Discord server " "Use the contact form or email support@com2care.com. Include the software version, "
"(see the Contact page for current details). For bug reports and feature " "operating system, a short reproduction description, and non-sensitive logs when relevant."
"requests, please use the Discord forum or contact us directly by email."
), ),
"order": 6, "order": 6,
}, },
@@ -282,9 +289,42 @@ HOMEPAGE_SECTIONS = [
{ {
"section_type": HomepageSection.TYPE_SCREENSHOTS, "section_type": HomepageSection.TYPE_SCREENSHOTS,
"badge": "Gallery", "badge": "Gallery",
"title": "See Radiuma in Action", "title": "See Communication to Care in Action",
"description": "Explore Radiuma's powerful interface, workflow builder, and multi-modal image viewer.", "description": "Explore Communication to Care's powerful interface, workflow builder, and multi-modal image viewer.",
"order": 2, "order": 2,
"items": [
{
"title": "Visual workflow builder",
"content": "Connect image-processing steps into a repeatable analysis pipeline.",
"static_image": "screenshot-3.png",
"order": 1,
},
{
"title": "Multi-planar image review",
"content": "Inspect imaging and segmentation context across synchronized views.",
"static_image": "screenshot-4.png",
"order": 2,
},
{
"title": "Radiomics configuration",
"content": "Review quantitative feature settings before a reproducible run.",
"static_image": "screenshot-5.png",
"order": 3,
},
],
},
{
"section_type": HomepageSection.TYPE_VIDEO,
"badge": "Learning Library",
"title": "Medical image segmentation essentials",
"description": (
"A practical introduction to thresholding, drawing, erasing, and 3D review in a "
"medical-image segmentation workflow."
),
"video_url": "https://www.youtube.com/watch?v=_9J3i883yA4",
"video_styled_background": True,
"video_size": "lg",
"order": 3,
"items": [], "items": [],
}, },
{ {
@@ -292,19 +332,19 @@ HOMEPAGE_SECTIONS = [
"badge": "Our Software", "badge": "Our Software",
"title": "Products", "title": "Products",
"description": "Explore our suite of medical imaging and radiomics tools.", "description": "Explore our suite of medical imaging and radiomics tools.",
"order": 3, "order": 4,
"items": [], "items": [],
}, },
{ {
"section_type": HomepageSection.TYPE_PROBLEMS, "section_type": HomepageSection.TYPE_PROBLEMS,
"badge": "Value Proposition", "badge": "Value Proposition",
"title": "What Problems Does Radiuma Solve?", "title": "What Problems Does Communication to Care Solve?",
"description": "", "description": "",
"order": 4, "order": 5,
"items": [ "items": [
{"icon": "01", "title": "Accessibility", "content": "Radiuma provides a user-friendly interface and a wide range of tools, allowing researchers to perform complex data analysis without extensive technical knowledge or programming expertise.", "order": 1}, {"icon": "01", "title": "Accessibility", "content": "Communication to Care provides a user-friendly interface and a wide range of tools, allowing researchers to perform complex data analysis without extensive technical knowledge or programming expertise.", "order": 1},
{"icon": "02", "title": "Integrated Tools", "content": "Radiuma integrates a vast collection of tools and resources from various domains of healthcare and medical imaging research in a common, unified environment.", "order": 2}, {"icon": "02", "title": "Integrated Tools", "content": "Communication to Care integrates a vast collection of tools and resources from various domains of healthcare and medical imaging research in a common, unified environment.", "order": 2},
{"icon": "03", "title": "Flexibility", "content": "Radiuma offers flexibility in terms of tool optimization and workflow customization to match your specific research requirements.", "order": 3}, {"icon": "03", "title": "Flexibility", "content": "Communication to Care offers flexibility in terms of tool optimization and workflow customization to match your specific research requirements.", "order": 3},
{"icon": "04", "title": "Reproducibility", "content": "Improve usability, reusability, and reproducibility (URR) through a workflow management system that allows researchers to easily create, share, and reuse analysis pipelines.", "order": 4}, {"icon": "04", "title": "Reproducibility", "content": "Improve usability, reusability, and reproducibility (URR) through a workflow management system that allows researchers to easily create, share, and reuse analysis pipelines.", "order": 4},
], ],
}, },
@@ -313,48 +353,112 @@ HOMEPAGE_SECTIONS = [
"badge": "Our Story", "badge": "Our Story",
"title": "More to Know", "title": "More to Know",
"description": ( "description": (
"Radiuma has been developing since 2021 by the Quantitative Radiomolecular Imaging " "Communication to Care is shaped around multidisciplinary research: connect imaging "
"and Therapy (Qurit) lab & program at the University of British Columbia & " "evidence, analysis steps, and team discussion in one understandable workflow."
"BC Cancer Research Institute, Vancouver, BC, Canada."
), ),
"link_text": "Learn More", "link_text": "Learn More",
"link_url": "/about/", "link_url": "/about/",
"order": 5, "order": 6,
"items": [], "items": [],
}, },
{ {
"section_type": HomepageSection.TYPE_SUPPORTERS, "section_type": HomepageSection.TYPE_SUPPORTERS,
"badge": "Acknowledgements", "badge": "Who It Serves",
"title": "Our Supporters", "title": "Built for Collaborative Teams",
"description": "Radiuma is made possible by the support of leading research institutions and organizations.", "description": "com2care demo workflows are organized around the people who review, analyze, and communicate medical-imaging evidence.",
"order": 6, "order": 7,
"items": [ "items": [
{ {
"title": "University of British Columbia", "title": "Imaging Researchers",
"content": "Faculty of Medicine and the Department of Integrative Oncology.", "content": "Build standardized pipelines and retain the context behind each processing decision.",
"order": 1, "order": 1,
}, },
{ {
"title": "BC Cancer Research Institute", "title": "Clinical Research Teams",
"content": "Supporting cutting-edge radiomics and medical imaging research in Vancouver, BC.", "content": "Review images and quantitative results together without presenting research output as diagnosis.",
"order": 2, "order": 2,
}, },
], ],
}, },
] ]
ABOUT_SECTIONS = [
{
"section_type": AboutSection.TYPE_HERO,
"badge": "Our Mission",
"title": "Better imaging conversations, clearer research decisions",
"subtitle": "Communication to Care",
"content": (
"com2care is built around a simple idea: complex medical-imaging evidence becomes "
"more useful when researchers, clinicians, engineers, and data teams can examine it "
"together in a shared, reproducible workflow."
),
"order": 1,
"items": [
{
"title": "A collaborative care and research team",
"image_alt": "Healthcare and imaging researchers reviewing medical images together",
"static_image": "com2care-care-team.jpg",
"is_featured": True,
"order": 1,
}
],
},
{
"section_type": AboutSection.TYPE_GRID,
"badge": "How We Work",
"title": "Designed for shared understanding",
"content": "Every part of the platform supports transparent, repeatable research communication.",
"order": 2,
"items": [
{
"icon": "01",
"title": "Clinical context",
"content": "Keep imaging evidence and analysis choices visible to the whole team.",
"order": 1,
},
{
"icon": "02",
"title": "Reproducible workflows",
"content": "Save processing steps so collaborators can review and repeat the same pipeline.",
"order": 2,
},
{
"icon": "03",
"title": "Responsible research",
"content": "Separate research exploration from clinical diagnosis and protect patient privacy.",
"order": 3,
},
],
},
{
"section_type": AboutSection.TYPE_VIDEO,
"badge": "Practical Learning",
"title": "Viewing DICOM studies with an open medical-imaging workflow",
"content": (
"This independent tutorial demonstrates how researchers can import and inspect DICOM "
"studies in 3D Slicer—skills that complement the workflows presented on com2care."
),
"video_url": "https://www.youtube.com/watch?v=EV8tAjAHeac",
"video_styled_background": True,
"video_size": "lg",
"order": 3,
"items": [],
},
]
DOWNLOAD_ITEMS = [ DOWNLOAD_ITEMS = [
{ {
"name": "Radiuma Desktop", "name": "com2care Desktop",
"platform": "windows", "platform": "windows",
"version": "1.0.0", "version": "1.0.0",
"download_url": "https://github.com/radiuma/radiuma/releases/latest/download/Radiuma-Setup.exe", "download_url": "https://com2care.com/downloads/",
"description": "Windows 10 and above (64-bit). Installer package.", "description": "Demonstration release channel for Windows 10 and above (64-bit).",
"is_active": True, "is_active": True,
"order": 1, "order": 1,
}, },
{ {
"name": "Radiuma Desktop", "name": "com2care Desktop",
"platform": "macos", "platform": "macos",
"version": "Coming Soon", "version": "Coming Soon",
"download_url": "#", "download_url": "#",
@@ -363,7 +467,7 @@ DOWNLOAD_ITEMS = [
"order": 2, "order": 2,
}, },
{ {
"name": "Radiuma Desktop", "name": "com2care Desktop",
"platform": "linux", "platform": "linux",
"version": "Coming Soon", "version": "Coming Soon",
"download_url": "#", "download_url": "#",
@@ -375,19 +479,43 @@ DOWNLOAD_ITEMS = [
class Command(BaseCommand): class Command(BaseCommand):
help = "Seed the database with initial Radiuma / Radiuma content from radiuma.com" help = "Seed a complete com2care demonstration site."
CONTENT_MODELS = (
MainProduct,
FAQEntry,
DownloadItem,
HeroSection,
HomepageSection,
AboutSection,
PageVideo,
ProductVideo,
)
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
"--flush", "--flush",
action="store_true", action="store_true",
help="Delete all existing seed data before re-seeding", help="Delete existing public content before re-seeding.",
)
parser.add_argument(
"--if-empty",
action="store_true",
help="Seed only when every public content table is empty.",
) )
@transaction.atomic @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
if options["if_empty"] and not options["flush"] and self._content_exists():
self.stdout.write("Public content already exists; demo seed skipped.")
return
if options["flush"]: if options["flush"]:
self.stdout.write("Flushing existing seed data...") self.stdout.write("Flushing existing public content...")
ProductVideo.objects.all().delete()
PageVideo.objects.all().delete()
AboutSectionItem.objects.all().delete()
AboutSection.objects.all().delete()
ArticleSection.objects.all().delete() ArticleSection.objects.all().delete()
Article.objects.all().delete() Article.objects.all().delete()
SubProduct.objects.all().delete() SubProduct.objects.all().delete()
@@ -398,66 +526,96 @@ class Command(BaseCommand):
HomepageSection.objects.all().delete() HomepageSection.objects.all().delete()
HeroSection.objects.all().delete() HeroSection.objects.all().delete()
self._seed_products() main_product = self._seed_products()
self._seed_faq() self._seed_faq()
self._seed_downloads() self._seed_downloads()
self._seed_homepage_sections() self._seed_homepage_sections()
self._seed_about_sections()
self._seed_product_videos(main_product)
self._seed_hero() self._seed_hero()
self._seed_site_branding()
self._seed_site_contact() self._seed_site_contact()
self.stdout.write(self.style.SUCCESS("Content seeded successfully.")) self.stdout.write(self.style.SUCCESS("com2care demonstration content seeded."))
def _content_exists(self):
return any(model.objects.exists() for model in self.CONTENT_MODELS)
@staticmethod
def _update_instance(instance, values):
for field, value in values.items():
setattr(instance, field, value)
instance.save()
@staticmethod
def _attach_static_image(instance, field_name, image_name):
if not image_name or getattr(instance, field_name):
return
source = settings.BASE_DIR / "static" / "images" / image_name
if not source.exists():
return
with source.open("rb") as handle:
getattr(instance, field_name).save(source.name, File(handle), save=True)
def _seed_products(self): def _seed_products(self):
seeded_main_product = None
for product_data in MAIN_PRODUCTS: for product_data in MAIN_PRODUCTS:
sub_products_data = product_data.pop("sub_products") sub_products_data = product_data["sub_products"]
product_defaults = {
key: value for key, value in product_data.items() if key != "sub_products"
}
main_product, created = MainProduct.objects.get_or_create( main_product, created = MainProduct.objects.get_or_create(
slug=product_data["slug"], slug=product_defaults["slug"],
defaults=product_data, defaults=product_defaults,
) )
if not created: if not created:
for field, value in product_data.items(): self._update_instance(main_product, product_defaults)
setattr(main_product, field, value) seeded_main_product = main_product
main_product.save()
action = "Created" if created else "Updated" action = "Created" if created else "Updated"
self.stdout.write(f" {action} main product: {main_product.name}") self.stdout.write(f" {action} main product: {main_product.name}")
for sub_data in sub_products_data: for sub_data in sub_products_data:
articles_data = sub_data.pop("articles") articles_data = sub_data["articles"]
sub_defaults = {
key: value for key, value in sub_data.items() if key != "articles"
}
sub_product, sub_created = SubProduct.objects.get_or_create( sub_product, sub_created = SubProduct.objects.get_or_create(
main_product=main_product, main_product=main_product,
slug=sub_data["slug"], slug=sub_defaults["slug"],
defaults=sub_data, defaults=sub_defaults,
) )
if not sub_created: if not sub_created:
for field, value in sub_data.items(): self._update_instance(sub_product, sub_defaults)
setattr(sub_product, field, value)
sub_product.save()
sub_action = "Created" if sub_created else "Updated" sub_action = "Created" if sub_created else "Updated"
self.stdout.write(f" {sub_action} sub-product: {sub_product.name}") self.stdout.write(f" {sub_action} sub-product: {sub_product.name}")
for article_data in articles_data: for article_data in articles_data:
sections_data = article_data.pop("sections") sections_data = article_data["sections"]
article_defaults = {
key: value for key, value in article_data.items() if key != "sections"
}
article, art_created = Article.objects.get_or_create( article, art_created = Article.objects.get_or_create(
sub_product=sub_product, sub_product=sub_product,
title=article_data["title"], title=article_defaults["title"],
defaults=article_data, defaults=article_defaults,
) )
if not art_created: if not art_created:
for field, value in article_data.items(): self._update_instance(article, article_defaults)
setattr(article, field, value)
article.save()
art_action = "Created" if art_created else "Updated" art_action = "Created" if art_created else "Updated"
self.stdout.write(f" {art_action} article: {article.title}") self.stdout.write(f" {art_action} article: {article.title}")
for section_data in sections_data: for section_data in sections_data:
section, _ = ArticleSection.objects.get_or_create( article_section, section_created = ArticleSection.objects.get_or_create(
article=article, article=article,
title=section_data["title"], title=section_data["title"],
defaults=section_data, defaults=section_data,
) )
if not section_created:
self._update_instance(article_section, section_data)
return seeded_main_product
def _seed_faq(self): def _seed_faq(self):
for entry_data in FAQ_ENTRIES: for entry_data in FAQ_ENTRIES:
@@ -466,34 +624,92 @@ class Command(BaseCommand):
defaults=entry_data, defaults=entry_data,
) )
if not created: if not created:
for field, value in entry_data.items(): self._update_instance(faq, entry_data)
setattr(faq, field, value)
faq.save()
action = "Created" if created else "Updated" action = "Created" if created else "Updated"
self.stdout.write(f" {action} FAQ: {faq.question[:60]}...") self.stdout.write(f" {action} FAQ: {faq.question[:60]}...")
def _seed_homepage_sections(self): def _seed_homepage_sections(self):
for section_data in HOMEPAGE_SECTIONS: for section_data in HOMEPAGE_SECTIONS:
items_data = section_data.pop("items") items_data = section_data["items"]
section_defaults = {
key: value for key, value in section_data.items() if key != "items"
}
section, created = HomepageSection.objects.get_or_create( section, created = HomepageSection.objects.get_or_create(
section_type=section_data["section_type"], section_type=section_defaults["section_type"],
defaults=section_data, defaults=section_defaults,
) )
if not created: if not created:
for field, value in section_data.items(): self._update_instance(section, section_defaults)
setattr(section, field, value)
section.save()
action = "Created" if created else "Updated" action = "Created" if created else "Updated"
self.stdout.write(f" {action} homepage section: {section}") self.stdout.write(f" {action} homepage section: {section}")
for item_data in items_data: for item_data in items_data:
item, _ = HomepageSectionItem.objects.get_or_create( image_name = item_data.get("static_image", "")
item_defaults = {
key: value for key, value in item_data.items() if key != "static_image"
}
item, item_created = HomepageSectionItem.objects.get_or_create(
section=section, section=section,
title=item_data["title"], title=item_defaults["title"],
defaults=item_data, defaults=item_defaults,
) )
if not item_created:
self._update_instance(item, item_defaults)
self._attach_static_image(item, "image", image_name)
def _seed_about_sections(self):
for section_data in ABOUT_SECTIONS:
items_data = section_data["items"]
section_defaults = {
key: value for key, value in section_data.items() if key != "items"
}
section, created = AboutSection.objects.get_or_create(
section_type=section_defaults["section_type"],
title=section_defaults["title"],
defaults=section_defaults,
)
if not created:
self._update_instance(section, section_defaults)
for item_data in items_data:
image_name = item_data.get("static_image", "")
item_defaults = {
key: value for key, value in item_data.items() if key != "static_image"
}
item, item_created = AboutSectionItem.objects.get_or_create(
section=section,
title=item_defaults["title"],
defaults=item_defaults,
)
if not item_created:
self._update_instance(item, item_defaults)
self._attach_static_image(item, "image", image_name)
def _seed_product_videos(self, main_product):
if not main_product:
return
data = {
"badge": "Workflow Tutorial",
"title": "Segmentation workflow from image to 3D review",
"description": (
"An independent demonstration of a guided medical-image segmentation workflow "
"using open research tooling."
),
"video_url": "https://www.youtube.com/watch?v=_7oZygGp2ds",
"video_styled_background": True,
"video_size": "lg",
"order": 1,
"is_active": True,
}
video, created = ProductVideo.objects.get_or_create(
main_product=main_product,
title=data["title"],
defaults=data,
)
if not created:
self._update_instance(video, data)
def _seed_downloads(self): def _seed_downloads(self):
for item_data in DOWNLOAD_ITEMS: for item_data in DOWNLOAD_ITEMS:
@@ -503,73 +719,52 @@ class Command(BaseCommand):
defaults=item_data, defaults=item_data,
) )
if not created: if not created:
for field, value in item_data.items(): self._update_instance(item, item_data)
setattr(item, field, value)
item.save()
action = "Created" if created else "Updated" action = "Created" if created else "Updated"
self.stdout.write(f" {action} download: {item}") self.stdout.write(f" {action} download: {item}")
def _seed_hero(self): def _seed_hero(self):
data = { data = {
"badge": "Developing since 2021", "badge": "Communication-first medical imaging",
"title": "Radiuma,", "title": "Communication to Care,",
"title_highlight": "A Powerful Workflow Generator", "title_highlight": "From Images to Shared Understanding",
"subtitle": "for Standardized Radiomics Analysis and Medical Image Visualization", "subtitle": "Collaborative medical imaging, radiomics, and reproducible research workflows",
"description": ( "description": (
"Radiuma is a free, open-source software specialized for visualization, processing, " "com2care helps multidisciplinary teams explore complex imaging evidence, "
"segmentation, registration, fusion and analysis of medical and biomedical images, " "document analysis choices, and communicate results with clearer context."
"including radiomics and machine learning analysis."
), ),
"primary_cta_text": "Get Radiuma", "primary_cta_text": "Explore com2care",
"primary_cta_url": "/products/", "primary_cta_url": "/products/",
"secondary_cta_text": "About Radiuma", "secondary_cta_text": "Our Mission",
"secondary_cta_url": "/about/", "secondary_cta_url": "/about/",
"image_alt": "Radiuma application — main workflow view", "image_alt": "A multidisciplinary care team collaborating around medical imaging",
} }
hero = HeroSection.objects.first() hero = HeroSection.objects.first()
if hero is None: if hero is None:
HeroSection.objects.create(**data) hero = HeroSection.objects.create(**data)
self.stdout.write(" Created hero section")
else: else:
for field, value in data.items(): self._update_instance(hero, data)
if field == "image": self._attach_static_image(hero, "image", "com2care-care-team.jpg")
continue
setattr(hero, field, value) def _seed_site_branding(self):
hero.save() branding = SiteBranding.load()
self.stdout.write(" Updated hero section") branding.icon_alt = "com2care"
branding.hero_logo_alt = "Communication to Care"
branding.save(update_fields=["icon_alt", "hero_logo_alt"])
def _seed_site_contact(self): def _seed_site_contact(self):
contact, created = SiteContact.objects.get_or_create( data = {
pk=1, "support_email": "support@com2care.com",
defaults={ "discord_url": "",
"support_email": "support@radiuma.com", "discord_label": "",
"discord_url": "https://discord.gg/9XxA6pV9hb", "email_card_title": "Email com2care Support",
"email_card_description": "For direct software support:", "email_card_description": "For product, evaluation, and research questions:",
"discord_card_description": "Join for community support and announcements.", "discord_card_title": "",
"office_address": ( "discord_card_description": "",
"BC Cancer Research Center\n" "office_card_title": "",
"675 West 10th Ave, Office 6-112\n" "office_address": "",
"Vancouver, BC, V5Z 1L3\n" }
"Canada" contact, created = SiteContact.objects.get_or_create(pk=1, defaults=data)
),
},
)
if not created: if not created:
updates = { self._update_instance(contact, data)
"support_email": "support@radiuma.com",
"discord_url": "https://discord.gg/9XxA6pV9hb",
"email_card_description": "For direct software support:",
"discord_card_description": "Join for community support and announcements.",
"office_address": (
"BC Cancer Research Center\n"
"675 West 10th Ave, Office 6-112\n"
"Vancouver, BC, V5Z 1L3\n"
"Canada"
),
}
for field, value in updates.items():
setattr(contact, field, value)
contact.save()
action = "Created" if created else "Updated"
self.stdout.write(f" {action} site contact")
@@ -0,0 +1,24 @@
# Generated by Django 5.2.8 on 2026-08-01 15:06
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_sitebranding_hero_logo_sitebranding_hero_logo_alt_and_more'),
]
operations = [
migrations.AlterField(
model_name='sitebranding',
name='hero_logo_alt',
field=models.CharField(blank=True, default='Communication to Care', help_text='Accessible description for the homepage hero logo.', max_length=200),
),
migrations.AlterField(
model_name='sitebranding',
name='website_icon',
field=models.FileField(blank=True, 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.', null=True, upload_to='branding/icons/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=('ico', 'png', 'svg', 'jpg', 'jpeg', 'webp'))]),
),
]
+2 -2
View File
@@ -33,7 +33,7 @@ class SiteBranding(models.Model):
], ],
help_text=( help_text=(
"Icon shown in browser tabs and bookmarks. Use a square ICO, PNG, SVG, " "Icon shown in browser tabs and bookmarks. Use a square ICO, PNG, SVG, "
"JPG, or WebP file. Leave empty to use the default Radiuma icons." "JPG, or WebP file. Leave empty to use the default Communication to Care icons."
), ),
) )
hero_logo = models.ImageField( hero_logo = models.ImageField(
@@ -48,7 +48,7 @@ class SiteBranding(models.Model):
hero_logo_alt = models.CharField( hero_logo_alt = models.CharField(
max_length=200, max_length=200,
blank=True, blank=True,
default="Radiuma", default="Communication to Care",
help_text="Accessible description for the homepage hero logo.", help_text="Accessible description for the homepage hero logo.",
) )
navbar_icon_size = models.PositiveSmallIntegerField( navbar_icon_size = models.PositiveSmallIntegerField(
+120
View File
@@ -0,0 +1,120 @@
import os
from tempfile import TemporaryDirectory
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from apps.core.models import SiteContact
from apps.pages.models import AboutSection, HeroSection, HomepageSection
from apps.products.models import MainProduct, ProductVideo
class DemoSeedCommandTests(TestCase):
def setUp(self):
self.media_directory = TemporaryDirectory()
self.addCleanup(self.media_directory.cleanup)
def _seed_if_empty(self):
with self.settings(MEDIA_ROOT=self.media_directory.name):
call_command("seed_content", "--if-empty", verbosity=0)
def test_seed_populates_complete_demo_when_public_content_is_empty(self):
self._seed_if_empty()
self.assertTrue(MainProduct.objects.filter(slug="com2care").exists())
self.assertTrue(HeroSection.objects.exclude(image="").exists())
self.assertTrue(HomepageSection.objects.filter(section_type="video").exists())
self.assertTrue(AboutSection.objects.filter(section_type="video").exists())
self.assertTrue(ProductVideo.objects.filter(video_url__contains="youtube.com").exists())
self.assertEqual(SiteContact.load().support_email, "support@com2care.com")
def test_if_empty_preserves_existing_admin_content(self):
self._seed_if_empty()
hero = HeroSection.objects.get()
hero.title = "Admin-authored headline"
hero.save(update_fields=["title"])
self._seed_if_empty()
hero.refresh_from_db()
self.assertEqual(hero.title, "Admin-authored headline")
class EnsureSuperuserCommandTests(TestCase):
def test_creates_configurable_first_admin(self):
environment = {
"DJANGO_SUPERUSER_ENABLED": "True",
"DJANGO_SUPERUSER_USERNAME": "siteadmin",
"DJANGO_SUPERUSER_EMAIL": "siteadmin@com2care.com",
"DJANGO_SUPERUSER_PASSWORD": "configurable-test-password",
"DJANGO_SUPERUSER_SYNC_PASSWORD": "False",
}
with patch.dict(os.environ, environment, clear=False):
call_command("ensure_superuser", verbosity=0)
user = get_user_model().objects.get(username="siteadmin")
self.assertTrue(user.is_superuser)
self.assertTrue(user.check_password("configurable-test-password"))
def test_restart_preserves_password_changed_in_admin(self):
user = get_user_model().objects.create_superuser(
username="admin",
email="admin@com2care.com",
password="changed-in-admin",
)
environment = {
"DJANGO_SUPERUSER_USERNAME": "admin",
"DJANGO_SUPERUSER_EMAIL": "admin@com2care.com",
"DJANGO_SUPERUSER_PASSWORD": "environment-password",
"DJANGO_SUPERUSER_SYNC_PASSWORD": "False",
}
with patch.dict(os.environ, environment, clear=False):
call_command("ensure_superuser", verbosity=0)
user.refresh_from_db()
self.assertTrue(user.check_password("changed-in-admin"))
def test_server_operator_can_explicitly_rotate_password(self):
user = get_user_model().objects.create_superuser(
username="admin",
email="old@com2care.com",
password="old-password",
)
environment = {
"DJANGO_SUPERUSER_USERNAME": "admin",
"DJANGO_SUPERUSER_EMAIL": "new@com2care.com",
"DJANGO_SUPERUSER_PASSWORD": "rotated-password",
"DJANGO_SUPERUSER_SYNC_PASSWORD": "True",
}
with patch.dict(os.environ, environment, clear=False):
call_command("ensure_superuser", verbosity=0)
user.refresh_from_db()
self.assertEqual(user.email, "new@com2care.com")
self.assertTrue(user.check_password("rotated-password"))
class NormalizeBrandCommandTests(TestCase):
def test_normalizes_existing_public_content_urls_and_slugs(self):
old_name = "".join(("Radi", "uma"))
old_slug = old_name.lower()
product = MainProduct.objects.create(
name=old_name,
slug=old_slug,
short_description=f"A workflow from {old_name}",
description=f"Learn more at {old_slug}.com.",
)
contact = SiteContact.load()
contact.support_email = f"support@{old_slug}.com"
contact.save(update_fields=["support_email"])
call_command("normalize_brand", verbosity=0)
product.refresh_from_db()
contact.refresh_from_db()
self.assertEqual(product.name, "Communication to Care")
self.assertEqual(product.slug, "com2care")
self.assertEqual(product.description, "Learn more at com2care.com.")
self.assertEqual(contact.support_email, "support@com2care.com")
+4 -4
View File
@@ -44,7 +44,7 @@ class SiteBrandingTemplateTests(TestCase):
branding.icon = "branding/navigation-logo.png" branding.icon = "branding/navigation-logo.png"
branding.website_icon = "branding/icons/site-icon.png" branding.website_icon = "branding/icons/site-icon.png"
branding.hero_logo = "branding/hero/hero-logo.png" branding.hero_logo = "branding/hero/hero-logo.png"
branding.hero_logo_alt = "Radiuma research platform" branding.hero_logo_alt = "Communication to Care research platform"
branding.save() branding.save()
response = self.client.get(reverse("pages:home")) response = self.client.get(reverse("pages:home"))
@@ -52,10 +52,10 @@ class SiteBrandingTemplateTests(TestCase):
self.assertContains(response, 'src="/media/branding/navigation-logo.png"') self.assertContains(response, 'src="/media/branding/navigation-logo.png"')
self.assertContains(response, 'href="/media/branding/icons/site-icon.png"') self.assertContains(response, 'href="/media/branding/icons/site-icon.png"')
self.assertContains(response, 'src="/media/branding/hero/hero-logo.png"') self.assertContains(response, 'src="/media/branding/hero/hero-logo.png"')
self.assertContains(response, 'alt="Radiuma research platform"') self.assertContains(response, 'alt="Communication to Care research platform"')
def test_default_assets_remain_when_custom_assets_are_empty(self): def test_default_assets_remain_when_custom_assets_are_empty(self):
response = self.client.get(reverse("pages:home")) response = self.client.get(reverse("pages:home"))
self.assertContains(response, "images/favicon-16.png") self.assertContains(response, "images/com2care-mark.svg")
self.assertContains(response, "images/screenshot-1.jpg") self.assertContains(response, "images/com2care-care-team.jpg")
@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2026-08-01 15:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pages', '0017_video_description_format'),
]
operations = [
migrations.AlterField(
model_name='herosection',
name='title',
field=models.CharField(blank=True, help_text="Main title line (e.g. 'Communication to Care,').", max_length=300),
),
]
+1 -1
View File
@@ -23,7 +23,7 @@ RESERVED_PAGE_SLUGS = frozenset({
class HeroSection(models.Model): class HeroSection(models.Model):
badge = models.CharField(max_length=200, blank=True, help_text="Small label above the title (e.g. 'Developing since 2021').") 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. 'Radiuma,').") title = models.CharField(max_length=300, blank=True, help_text="Main title line (e.g. 'Communication to Care,').")
title_highlight = models.CharField(max_length=300, blank=True, help_text="Second title line shown in gradient colour (e.g. 'A Powerful Workflow Generator').") 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…').") 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.") description = models.TextField(blank=True, help_text="Longer paragraph below the subtitle.")
+3 -3
View File
@@ -156,8 +156,8 @@ class PageVideoTest(TestCase):
class ProductVideoTest(TestCase): class ProductVideoTest(TestCase):
def setUp(self): def setUp(self):
self.main_product = MainProduct.objects.create( self.main_product = MainProduct.objects.create(
name="Radiuma", name="Communication to Care",
slug="radiuma", slug="com2care",
short_description="Short", short_description="Short",
description="Long", description="Long",
is_active=True, is_active=True,
@@ -173,7 +173,7 @@ class ProductVideoTest(TestCase):
is_active=True, is_active=True,
) )
response = self.client.get( response = self.client.get(
reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
) )
self.assertContains(response, "Product Demo") self.assertContains(response, "Product Demo")
self.assertContains(response, "video-block--full") self.assertContains(response, "video-block--full")
+1 -1
View File
@@ -207,7 +207,7 @@ class PageVideoArchiveView(ListView):
PAGE_CONFIG = { PAGE_CONFIG = {
PageVideo.PAGE_CONTACT: ( PageVideo.PAGE_CONTACT: (
"Contact videos", "Contact videos",
"Guides and updates from the Radiuma team.", "Guides and updates from the Communication to Care team.",
"pages:contact", "pages:contact",
), ),
PageVideo.PAGE_FAQ: ( PageVideo.PAGE_FAQ: (
+18 -18
View File
@@ -11,8 +11,8 @@ from apps.products.models import Article, ArticleSection, MainProduct, SubProduc
class ProductViewsSetup(TestCase): class ProductViewsSetup(TestCase):
def setUp(self): def setUp(self):
self.main_product = MainProduct.objects.create( self.main_product = MainProduct.objects.create(
name="Radiuma", name="Communication to Care",
slug="radiuma", slug="com2care",
short_description="Radiomics software", short_description="Radiomics software",
description="Full description.", description="Full description.",
) )
@@ -61,17 +61,17 @@ class ProductOverviewViewTest(ProductViewsSetup):
class MainProductDetailViewTest(ProductViewsSetup): class MainProductDetailViewTest(ProductViewsSetup):
def test_detail_returns_200(self): def test_detail_returns_200(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_detail_uses_correct_template(self): def test_detail_uses_correct_template(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
self.assertTemplateUsed(response, "products/main_detail.html") self.assertTemplateUsed(response, "products/main_detail.html")
def test_detail_contains_product_in_context(self): def test_detail_contains_product_in_context(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.context["main_product"], self.main_product) self.assertEqual(response.context["main_product"], self.main_product)
@@ -83,7 +83,7 @@ class MainProductDetailViewTest(ProductViewsSetup):
def test_inactive_product_returns_404(self): def test_inactive_product_returns_404(self):
self.main_product.is_active = False self.main_product.is_active = False
self.main_product.save() self.main_product.save()
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@@ -93,7 +93,7 @@ class MainProductDetailViewTest(ProductViewsSetup):
title="Overview", title="Overview",
description="Main product article.", description="Main product article.",
) )
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
self.assertIn("articles", response.context) self.assertIn("articles", response.context)
self.assertIn(main_article, list(response.context["articles"])) self.assertIn(main_article, list(response.context["articles"]))
@@ -104,7 +104,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_returns_200(self): def test_sub_detail_returns_200(self):
url = reverse( url = reverse(
"products:sub_product_detail", "products:sub_product_detail",
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"}, kwargs={"main_slug": "com2care", "sub_slug": "image-processing"},
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -112,7 +112,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_uses_correct_template(self): def test_sub_detail_uses_correct_template(self):
url = reverse( url = reverse(
"products:sub_product_detail", "products:sub_product_detail",
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"}, kwargs={"main_slug": "com2care", "sub_slug": "image-processing"},
) )
response = self.client.get(url) response = self.client.get(url)
self.assertTemplateUsed(response, "products/sub_detail.html") self.assertTemplateUsed(response, "products/sub_detail.html")
@@ -120,7 +120,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_context_keys(self): def test_sub_detail_context_keys(self):
url = reverse( url = reverse(
"products:sub_product_detail", "products:sub_product_detail",
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"}, kwargs={"main_slug": "com2care", "sub_slug": "image-processing"},
) )
response = self.client.get(url) response = self.client.get(url)
self.assertIn("main_product", response.context) self.assertIn("main_product", response.context)
@@ -131,7 +131,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_articles_in_context(self): def test_sub_detail_articles_in_context(self):
url = reverse( url = reverse(
"products:sub_product_detail", "products:sub_product_detail",
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"}, kwargs={"main_slug": "com2care", "sub_slug": "image-processing"},
) )
response = self.client.get(url) response = self.client.get(url)
self.assertIn(self.article, list(response.context["articles"])) self.assertIn(self.article, list(response.context["articles"]))
@@ -139,7 +139,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_nonexistent_sub_slug_returns_404(self): def test_nonexistent_sub_slug_returns_404(self):
url = reverse( url = reverse(
"products:sub_product_detail", "products:sub_product_detail",
kwargs={"main_slug": "radiuma", "sub_slug": "does-not-exist"}, kwargs={"main_slug": "com2care", "sub_slug": "does-not-exist"},
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@@ -174,7 +174,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup):
) )
url = reverse( url = reverse(
"products:sub_product_versions", "products:sub_product_versions",
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"}, kwargs={"main_slug": "com2care", "sub_slug": "image-processing"},
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -198,7 +198,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup):
) )
url = reverse( url = reverse(
"products:main_product_versions", "products:main_product_versions",
kwargs={"main_slug": "radiuma"}, kwargs={"main_slug": "com2care"},
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -213,7 +213,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup):
windows_download_url="https://example.com/win.exe", windows_download_url="https://example.com/win.exe",
is_active=True, is_active=True,
) )
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
self.assertTrue(response.context["show_releases_section"]) self.assertTrue(response.context["show_releases_section"])
@@ -236,7 +236,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup):
windows_download_url="https://example.com/prev.exe", windows_download_url="https://example.com/prev.exe",
is_active=True, is_active=True,
) )
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
self.assertContains(response, "Beta") self.assertContains(response, "Beta")
self.assertContains(response, "Previous") self.assertContains(response, "Previous")
@@ -254,7 +254,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup):
windows_download_url="https://example.com/ea.exe", windows_download_url="https://example.com/ea.exe",
is_active=True, is_active=True,
) )
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url) response = self.client.get(url)
self.assertContains(response, "Early Access") self.assertContains(response, "Early Access")
self.assertNotContains(response, ">Stable<") self.assertNotContains(response, ">Stable<")
@@ -301,7 +301,7 @@ class ReleaseAssetDownloadViewTest(ProductViewsSetup):
windows_download_file=SimpleUploadedFile("win.tar.gz", b"gz"), windows_download_file=SimpleUploadedFile("win.tar.gz", b"gz"),
is_active=True, is_active=True,
) )
detail_url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"}) detail_url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(detail_url) response = self.client.get(detail_url)
download_url = reverse( download_url = reverse(
"products:release_asset_download", "products:release_asset_download",
+2 -2
View File
@@ -67,8 +67,8 @@ WSGI_APPLICATION = "config.wsgi.application"
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql", "ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("POSTGRES_DB", "tecvico"), "NAME": os.environ.get("POSTGRES_DB", "com2care"),
"USER": os.environ.get("POSTGRES_USER", "tecvico_user"), "USER": os.environ.get("POSTGRES_USER", "com2care_user"),
"PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""), "PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""),
"HOST": os.environ.get("POSTGRES_HOST", "localhost"), "HOST": os.environ.get("POSTGRES_HOST", "localhost"),
"PORT": os.environ.get("POSTGRES_PORT", "5432"), "PORT": os.environ.get("POSTGRES_PORT", "5432"),
+3 -3
View File
@@ -5,9 +5,9 @@ from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
admin.site.site_header = "Radiuma Administration" admin.site.site_header = "Communication to Care Administration"
admin.site.site_title = "Radiuma Admin" admin.site.site_title = "Communication to Care Admin"
admin.site.index_title = "Welcome to Radiuma Administration" admin.site.index_title = "Welcome to Communication to Care Administration"
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
+4
View File
@@ -1,4 +1,8 @@
services: services:
db:
ports:
- "5433:5432"
web: web:
build: . build: .
environment: environment:
+19 -9
View File
@@ -4,14 +4,12 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
env_file:
- .env
environment: environment:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB:-com2care}
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER:-com2care_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-com2care_local_password}
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-com2care_user} -d ${POSTGRES_DB:-com2care}"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -21,11 +19,22 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8000:8000" - "8000:8000"
env_file:
- .env
environment: environment:
DJANGO_SETTINGS_MODULE: config.settings.production DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-config.settings.production}
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-django-insecure-local-com2care-change-in-production}
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0,com2care.com,www.com2care.com}
CSRF_TRUSTED_ORIGINS: ${CSRF_TRUSTED_ORIGINS:-http://localhost:8000,https://com2care.com,https://www.com2care.com}
SECURE_SSL_REDIRECT: ${SECURE_SSL_REDIRECT:-False}
POSTGRES_DB: ${POSTGRES_DB:-com2care}
POSTGRES_USER: ${POSTGRES_USER:-com2care_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-com2care_local_password}
POSTGRES_HOST: db POSTGRES_HOST: db
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
DJANGO_SUPERUSER_ENABLED: ${DJANGO_SUPERUSER_ENABLED:-True}
DJANGO_SUPERUSER_USERNAME: ${DJANGO_SUPERUSER_USERNAME:-admin}
DJANGO_SUPERUSER_EMAIL: ${DJANGO_SUPERUSER_EMAIL:-admin@com2care.com}
DJANGO_SUPERUSER_PASSWORD: ${DJANGO_SUPERUSER_PASSWORD:-cmosV6Tw46Odv7UN}
DJANGO_SUPERUSER_SYNC_PASSWORD: ${DJANGO_SUPERUSER_SYNC_PASSWORD:-False}
volumes: volumes:
- ./media:/app/media - ./media:/app/media
- ./staticfiles:/app/staticfiles - ./staticfiles:/app/staticfiles
@@ -35,3 +44,4 @@ services:
volumes: volumes:
postgres_data: postgres_data:
media_data:
+9 -3
View File
@@ -6,8 +6,8 @@ until python -c "
import os, psycopg2, sys import os, psycopg2, sys
try: try:
psycopg2.connect( psycopg2.connect(
dbname=os.environ.get('POSTGRES_DB', 'tecvico'), dbname=os.environ.get('POSTGRES_DB', 'com2care'),
user=os.environ.get('POSTGRES_USER', 'tecvico_user'), user=os.environ.get('POSTGRES_USER', 'com2care_user'),
password=os.environ.get('POSTGRES_PASSWORD', ''), password=os.environ.get('POSTGRES_PASSWORD', ''),
host=os.environ.get('POSTGRES_HOST', 'db'), host=os.environ.get('POSTGRES_HOST', 'db'),
port=os.environ.get('POSTGRES_PORT', '5432'), port=os.environ.get('POSTGRES_PORT', '5432'),
@@ -28,7 +28,13 @@ python manage.py collectstatic --noinput
echo "Running database migrations..." echo "Running database migrations..."
python manage.py migrate --noinput python manage.py migrate --noinput
echo "Creating superuser if not exists..." echo "Normalizing the public brand..."
python manage.py normalize_brand
echo "Adding demonstration content when the database is empty..."
python manage.py seed_content --if-empty
echo "Ensuring the first admin account exists..."
python manage.py ensure_superuser python manage.py ensure_superuser
echo "Starting application..." echo "Starting application..."
Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

+25
View File
@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 88" role="img" aria-labelledby="title desc">
<title id="title">com2care</title>
<desc id="desc">Communication to Care logo</desc>
<defs>
<linearGradient id="logoBubbleA" x1="8" y1="8" x2="48" y2="52" gradientUnits="userSpaceOnUse">
<stop stop-color="#9b7cf6"/>
<stop offset="1" stop-color="#6d5ee7"/>
</linearGradient>
<linearGradient id="logoBubbleB" x1="22" y1="18" x2="58" y2="57" gradientUnits="userSpaceOnUse">
<stop stop-color="#44d6cc"/>
<stop offset="1" stop-color="#239fbd"/>
</linearGradient>
<linearGradient id="wordmark" x1="88" y1="18" x2="388" y2="72" gradientUnits="userSpaceOnUse">
<stop stop-color="#f4f1ff"/>
<stop offset=".55" stop-color="#c9c2f7"/>
<stop offset="1" stop-color="#73e0da"/>
</linearGradient>
</defs>
<g transform="translate(4 12)">
<path fill="url(#logoBubbleA)" d="M8 10.5A8.5 8.5 0 0 1 16.5 2h25A8.5 8.5 0 0 1 50 10.5v18a8.5 8.5 0 0 1-8.5 8.5H28.2L16 47.2V37.1a8.5 8.5 0 0 1-8-8.5z"/>
<path fill="url(#logoBubbleB)" d="M24 24.5a8.5 8.5 0 0 1 8.5-8.5h15a8.5 8.5 0 0 1 8.5 8.5v17a8.5 8.5 0 0 1-8.5 8.5H45v10l-12-10h-.5a8.5 8.5 0 0 1-8.5-8.5z"/>
<path fill="#fff" d="M40 41.9c-1.2-1-7.1-5.6-7.1-10.1 0-2.8 2.1-4.8 4.8-4.8 1.5 0 2.7.7 3.5 1.8A4.3 4.3 0 0 1 44.7 27c2.7 0 4.8 2 4.8 4.8 0 4.5-5.9 9.1-7.1 10.1l-1.2 1z"/>
</g>
<text x="84" y="60" fill="url(#wordmark)" font-family="Inter, Arial, sans-serif" font-size="50" font-weight="700" letter-spacing="-2">com2care</text>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
<title id="title">com2care mark</title>
<desc id="desc">Two connected conversation shapes forming a care heart</desc>
<defs>
<linearGradient id="bubbleA" x1="8" y1="8" x2="48" y2="52" gradientUnits="userSpaceOnUse">
<stop stop-color="#9b7cf6"/>
<stop offset="1" stop-color="#6d5ee7"/>
</linearGradient>
<linearGradient id="bubbleB" x1="22" y1="18" x2="58" y2="57" gradientUnits="userSpaceOnUse">
<stop stop-color="#44d6cc"/>
<stop offset="1" stop-color="#239fbd"/>
</linearGradient>
</defs>
<path fill="url(#bubbleA)" d="M8 10.5A8.5 8.5 0 0 1 16.5 2h25A8.5 8.5 0 0 1 50 10.5v18a8.5 8.5 0 0 1-8.5 8.5H28.2L16 47.2V37.1a8.5 8.5 0 0 1-8-8.5z"/>
<path fill="url(#bubbleB)" d="M24 24.5a8.5 8.5 0 0 1 8.5-8.5h15a8.5 8.5 0 0 1 8.5 8.5v17a8.5 8.5 0 0 1-8.5 8.5H45v10l-12-10h-.5a8.5 8.5 0 0 1-8.5-8.5z"/>
<path fill="#fff" d="M40 41.9c-1.2-1-7.1-5.6-7.1-10.1 0-2.8 2.1-4.8 4.8-4.8 1.5 0 2.7.7 3.5 1.8A4.3 4.3 0 0 1 44.7 27c2.7 0 4.8 2 4.8 4.8 0 4.5-5.9 9.1-7.1 10.1l-1.2 1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

+8 -5
View File
@@ -4,15 +4,18 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{% block meta_description %}Radiuma — Advanced Medical Imaging & Radiomics Solutions{% endblock %}" /> <meta name="description" content="{% block meta_description %}Communication to Care — Advanced Medical Imaging & Radiomics Solutions{% endblock %}" />
<title>{% block title %}Radiuma{% endblock %} | Radiuma</title> <meta property="og:site_name" content="Communication to Care" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://com2care.com{% static 'images/com2care-care-team.jpg' %}" />
<link rel="canonical" href="https://com2care.com{{ request.path }}" />
<title>{% block title %}Communication to Care{% endblock %} | Communication to Care</title>
{% if site_branding.website_icon %} {% if site_branding.website_icon %}
<link rel="icon" href="{{ site_branding.website_icon.url }}" /> <link rel="icon" href="{{ site_branding.website_icon.url }}" />
<link rel="apple-touch-icon" href="{{ site_branding.website_icon.url }}" /> <link rel="apple-touch-icon" href="{{ site_branding.website_icon.url }}" />
{% else %} {% else %}
<link rel="icon" href="{% static 'images/favicon-16.png' %}" type="image/png" sizes="16x16" /> <link rel="icon" href="{% static 'images/com2care-mark.svg' %}" type="image/svg+xml" />
<link rel="icon" href="{% static 'images/favicon-32.png' %}" type="image/png" sizes="32x32" /> <link rel="apple-touch-icon" href="{% static 'images/com2care-mark.svg' %}" />
<link rel="apple-touch-icon" href="{% static 'images/favicon-180.png' %}" sizes="180x180" />
{% endif %} {% endif %}
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+1 -1
View File
@@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}About{% endblock %} {% block title %}About{% endblock %}
{% block meta_description %}Learn about Radiuma — Visualized & Standardized Environment for Radiomics Analysis, developed at UBC and BC Cancer Research Institute.{% endblock %} {% block meta_description %}Learn how Communication to Care helps multidisciplinary teams build clearer, reproducible medical-imaging research workflows.{% endblock %}
{% block content %} {% block content %}
{% include "partials/_page_sections.html" with sections=about_sections %} {% include "partials/_page_sections.html" with sections=about_sections %}
+2 -2
View File
@@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% block title %}Contact{% endblock %} {% block title %}Contact{% endblock %}
{% block meta_description %}Contact Radiuma — support for Radiuma software and general inquiries.{% endblock %} {% block meta_description %}Contact Communication to Care for com2care product support, research evaluation, and general inquiries.{% endblock %}
{% block content %} {% block content %}
@@ -15,7 +15,7 @@
<div class="page-hero-content"> <div class="page-hero-content">
<div class="section-badge">Get in Touch</div> <div class="section-badge">Get in Touch</div>
<h1 class="page-hero-title" id="contact-heading">Contact Us</h1> <h1 class="page-hero-title" id="contact-heading">Contact Us</h1>
<p class="page-hero-subtitle">Have a question or want to reach the Radiuma team? Fill out the form or contact us directly below.</p> <p class="page-hero-subtitle">Have a product, evaluation, or research question? Send the com2care team a message below.</p>
</div> </div>
</div> </div>
</section> </section>
+1 -1
View File
@@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% block title %}FAQ{% endblock %} {% block title %}FAQ{% endblock %}
{% block meta_description %}Frequently asked questions about Radiuma by Radiuma — licensing, citation, system requirements, and more.{% endblock %} {% block meta_description %}Frequently asked questions about Communication to Care licensing, research use, releases, support, and responsible deployment.{% endblock %}
{% block content %} {% block content %}
+4 -6
View File
@@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% block title %}Home{% endblock %} {% block title %}Home{% endblock %}
{% block meta_description %}Radiuma — A Powerful Workflow Generator for Standardized Radiomics Analysis and Medical Image Visualization.{% endblock %} {% block meta_description %}Communication to Care — A Powerful Workflow Generator for Standardized Radiomics Analysis and Medical Image Visualization.{% endblock %}
{% block content %} {% block content %}
@@ -35,18 +35,16 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% if site_branding.hero_logo or hero and hero.image or not hero %}
<div class="hero-app-preview fade-in"> <div class="hero-app-preview fade-in">
<div class="hero-app-preview-glow" aria-hidden="true"></div> <div class="hero-app-preview-glow" aria-hidden="true"></div>
{% if site_branding.hero_logo %} {% if site_branding.hero_logo %}
<img src="{{ site_branding.hero_logo.url }}" alt="{{ site_branding.hero_logo_alt|default:'Radiuma' }}" loading="eager" /> <img src="{{ site_branding.hero_logo.url }}" alt="{{ site_branding.hero_logo_alt|default:'Communication to Care' }}" loading="eager" />
{% elif hero and hero.image %} {% elif hero and hero.image %}
<img src="{{ hero.image.url }}" alt="{{ hero.image_alt }}" loading="eager" /> <img src="{{ hero.image.url }}" alt="{{ hero.image_alt }}" loading="eager" />
{% elif not hero %} {% else %}
<img src="{% static 'images/screenshot-1.jpg' %}" alt="Radiuma application — main workflow view" loading="eager" /> <img src="{% static 'images/com2care-care-team.jpg' %}" alt="A multidisciplinary care team collaborating around medical imaging" loading="eager" />
{% endif %} {% endif %}
</div> </div>
{% endif %}
</div> </div>
</section> </section>
+2 -2
View File
@@ -1,14 +1,14 @@
{% load static %} {% load static %}
{% if placement == "footer" %} {% if placement == "footer" %}
<img <img
{% if site_branding.icon %}src="{{ site_branding.icon.url }}"{% else %}src="{% static 'images/favicon-180.png' %}"{% endif %} {% if site_branding.icon %}src="{{ site_branding.icon.url }}"{% else %}src="{% static 'images/com2care-mark.svg' %}"{% endif %}
{% if site_branding.icon_alt %}alt="{{ site_branding.icon_alt }}"{% else %}alt="" aria-hidden="true"{% endif %} {% if site_branding.icon_alt %}alt="{{ site_branding.icon_alt }}"{% else %}alt="" aria-hidden="true"{% endif %}
class="brand-icon" class="brand-icon"
style="{{ site_branding.footer_icon_style }}" style="{{ site_branding.footer_icon_style }}"
/> />
{% else %} {% else %}
<img <img
{% if site_branding.icon %}src="{{ site_branding.icon.url }}"{% else %}src="{% static 'images/favicon-180.png' %}"{% endif %} {% if site_branding.icon %}src="{{ site_branding.icon.url }}"{% else %}src="{% static 'images/com2care-mark.svg' %}"{% endif %}
{% if site_branding.icon_alt %}alt="{{ site_branding.icon_alt }}"{% else %}alt="" aria-hidden="true"{% endif %} {% if site_branding.icon_alt %}alt="{{ site_branding.icon_alt }}"{% else %}alt="" aria-hidden="true"{% endif %}
class="brand-icon" class="brand-icon"
style="{{ site_branding.navbar_icon_style }}" style="{{ site_branding.navbar_icon_style }}"
+7 -9
View File
@@ -8,12 +8,12 @@
<div class="footer-grid"> <div class="footer-grid">
<div class="footer-brand-col"> <div class="footer-brand-col">
<a href="{% url 'pages:home' %}" class="footer-logo" aria-label="Radiuma home"> <a href="{% url 'pages:home' %}" class="footer-logo" aria-label="Communication to Care home">
{% include "partials/_brand_icon.html" with placement="footer" %} {% include "partials/_brand_icon.html" with placement="footer" %}
<span class="brand-name">Radiuma</span> <span class="brand-name">Communication to Care</span>
</a> </a>
<p class="footer-tagline"> <p class="footer-tagline">
Advancing medical imaging and radiomics research through innovative, standardized software solutions. Turning complex medical-imaging evidence into clearer, reproducible research conversations.
</p> </p>
{% include "partials/_contact_discord.html" with button_class="btn-ghost btn-sm" %} {% include "partials/_contact_discord.html" with button_class="btn-ghost btn-sm" %}
</div> </div>
@@ -22,7 +22,7 @@
<h3 class="footer-heading">Navigation</h3> <h3 class="footer-heading">Navigation</h3>
<ul class="footer-links" role="list"> <ul class="footer-links" role="list">
<li><a href="{% url 'pages:home' %}">Home</a></li> <li><a href="{% url 'pages:home' %}">Home</a></li>
<li><a href="{% url 'pages:about' %}">What is Radiuma</a></li> <li><a href="{% url 'pages:about' %}">What is Communication to Care</a></li>
<li><a href="{% url 'products:overview' %}">Products</a></li> <li><a href="{% url 'products:overview' %}">Products</a></li>
<li><a href="{% url 'pages:faq' %}">FAQ</a></li> <li><a href="{% url 'pages:faq' %}">FAQ</a></li>
<li><a href="{% url 'pages:contact' %}">Contact</a></li> <li><a href="{% url 'pages:contact' %}">Contact</a></li>
@@ -37,7 +37,7 @@
<li><a href="{{ product.get_absolute_url }}">{{ product.name }}</a></li> <li><a href="{{ product.get_absolute_url }}">{{ product.name }}</a></li>
{% endfor %} {% endfor %}
{% if footer_main_products_has_more %} {% if footer_main_products_has_more %}
<li><a href="{% url 'products:main_product_detail' main_slug='radiuma' %}">more</a></li> <li><a href="{% url 'products:overview' %}">More products</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@@ -55,12 +55,10 @@
<div class="footer-bottom"> <div class="footer-bottom">
<p class="footer-copy"> <p class="footer-copy">
&copy; {% now "Y" %} Radiuma. All rights reserved. &copy; {% now "Y" %} Communication to Care. All rights reserved.
</p> </p>
<p class="footer-credit"> <p class="footer-credit">
Developed at <a href="https://www.qurit.ca" target="_blank" rel="noopener noreferrer">Qurit Lab</a>, Research collaboration at <a href="https://com2care.com">com2care.com</a>
<a href="https://www.ubc.ca" target="_blank" rel="noopener noreferrer">University of British Columbia</a>
&amp; <a href="https://www.bccrc.ca" target="_blank" rel="noopener noreferrer">BC Cancer Research Institute</a>
</p> </p>
</div> </div>
+2 -2
View File
@@ -2,9 +2,9 @@
<nav class="navbar" id="navbar" role="navigation" aria-label="Main navigation"> <nav class="navbar" id="navbar" role="navigation" aria-label="Main navigation">
<div class="navbar-container"> <div class="navbar-container">
<a href="{% url 'pages:home' %}" class="navbar-brand" aria-label="Radiuma home"> <a href="{% url 'pages:home' %}" class="navbar-brand" aria-label="Communication to Care home">
{% include "partials/_brand_icon.html" with placement="navbar" %} {% include "partials/_brand_icon.html" with placement="navbar" %}
<span class="brand-name">Radiuma</span> <span class="brand-name">com2care</span>
</a> </a>
<button class="navbar-toggle" id="navbarToggle" aria-expanded="false" aria-controls="navbarMenu" aria-label="Toggle navigation"> <button class="navbar-toggle" id="navbarToggle" aria-expanded="false" aria-controls="navbarMenu" aria-label="Toggle navigation">
+1 -1
View File
@@ -39,7 +39,7 @@
{% if main_product.image %} {% if main_product.image %}
<img src="{{ main_product.image.url }}" alt="{{ main_product.name }}" loading="lazy" /> <img src="{{ main_product.image.url }}" alt="{{ main_product.name }}" loading="lazy" />
{% else %} {% else %}
<img src="{% static 'images/logo.png' %}" alt="{{ main_product.name }} logo" loading="lazy" class="product-logo-img" /> <img src="{% static 'images/com2care-logo.svg' %}" alt="{{ main_product.name }} logo" loading="lazy" class="product-logo-img" />
{% endif %} {% endif %}
</div> </div>
<div class="product-detail-text"> <div class="product-detail-text">
+1 -1
View File
@@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% block title %}Products{% endblock %} {% block title %}Products{% endblock %}
{% block meta_description %}Explore Radiuma's suite of medical imaging and radiomics software products.{% endblock %} {% block meta_description %}Explore com2care tools for collaborative medical imaging, radiomics, reproducible workflows, and research communication.{% endblock %}
{% block content %} {% block content %}
+1 -1
View File
@@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ library_title }}{% endblock %} {% block title %}{{ library_title }}{% endblock %}
{% block meta_description %}Browse {{ library_title|lower }} from Radiuma.{% endblock %} {% block meta_description %}Browse {{ library_title|lower }} selected for the Communication to Care learning library.{% endblock %}
{% block content %} {% block content %}
<section class="page-hero video-library-hero" aria-labelledby="video-library-title"> <section class="page-hero video-library-hero" aria-labelledby="video-library-title">
@@ -1 +0,0 @@
a
@@ -1 +0,0 @@
binary-payload
@@ -1 +0,0 @@
x
@@ -1 +0,0 @@
gz