diff --git a/.env.example b/.env.example index 24af327..f9ae27e 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,18 @@ 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 -ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,radiuma.com,www.radiuma.com -CSRF_TRUSTED_ORIGINS=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=http://localhost:8000,https://com2care.com,https://www.com2care.com -POSTGRES_DB=radiuma -POSTGRES_USER=radiuma_user -POSTGRES_PASSWORD=eS4_WYJH97gywyoHjP6v +POSTGRES_DB=com2care +POSTGRES_USER=com2care_user +POSTGRES_PASSWORD=replace-with-a-strong-database-password POSTGRES_HOST=db POSTGRES_PORT=5432 SECURE_SSL_REDIRECT=False DJANGO_SUPERUSER_USERNAME=admin -DJANGO_SUPERUSER_EMAIL=admin@yourdomain.com -DJANGO_SUPERUSER_PASSWORD=bS7_U_uRTmivj7W-lCR5 +DJANGO_SUPERUSER_EMAIL=admin@com2care.com +DJANGO_SUPERUSER_PASSWORD=cmosV6Tw46Odv7UN +DJANGO_SUPERUSER_SYNC_PASSWORD=False diff --git a/.gitignore b/.gitignore index cf508e6..b902e5a 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,8 @@ venv.bak/ media/ private_uploads/ staticfiles/ -*.DS_Store \ No newline at end of file +*.DS_Store +test_media_releases/ +docs/ +scripts/ +videos/ diff --git a/README.md b/README.md index a001a3e..a06ca90 100644 --- a/README.md +++ b/README.md @@ -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 | -|---|---| -| Backend | Django 5.1 (MVT) | -| Database | PostgreSQL 16 | -| Static files | WhiteNoise (with Brotli compression) | -| Application server | Gunicorn | -| Containerization | Docker + Docker Compose | -| Frontend | Vanilla HTML/CSS/JS (no framework) | - -## Project Structure - -``` -tecvico_website/ -├── config/ # Django project configuration -│ └── settings/ -│ ├── base.py # Shared settings -│ ├── development.py # Dev settings (DEBUG=True, dotenv) -│ └── production.py # Production settings (security headers) -├── apps/ -│ ├── core/ # Context processors, management commands -│ │ └── management/commands/seed_content.py -│ ├── products/ # MainProduct, SubProduct, Article, ArticleSection -│ └── pages/ # FAQEntry, DownloadItem; static pages -├── templates/ # Global templates -│ ├── base.html -│ ├── partials/ -│ └── pages/ & products/ -├── static/ -│ ├── css/main.css # Full design system (dark glass/ice theme) -│ └── js/main.js # Navbar, FAQ accordion, scroll effects -├── Dockerfile -├── docker-compose.yml # Production compose -├── docker-compose.override.yml # Development compose overrides -└── entrypoint.sh # DB wait + migrate on container start -``` - -## URL Map - -| URL | View | Description | -|---|---|---| -| `/` | `HomeView` | Landing page | -| `/about/` | `AboutView` | What is Radiuma | -| `/products/` | `ProductOverviewView` | All main products | -| `/products//` | `MainProductDetailView` | Main product + sub-products | -| `/products///` | `SubProductDetailView` | Sub-product + articles | -| `/downloads/` | `DownloadsView` | Download items by platform | -| `/faq/` | `FAQView` | FAQ entries | -| `/contact/` | `ContactView` | Contact info | -| `/admin/` | Django Admin | Admin panel | - -## 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 +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. + +```bash +docker compose up -d --build +``` + +Open: + +- Website: +- Admin: + +Initial local admin credentials: + +```text +Username: admin +Password: cmosV6Tw46Odv7UN +``` + +Change the password immediately after the first login using **Django Admin → Change password**. +The startup process never resets an existing admin password unless credential synchronization is +explicitly enabled. + +## Automatic startup behavior + +Every web-container start performs these idempotent steps: + +1. Wait for PostgreSQL. +2. Collect static files. +3. Apply Django migrations. +4. Normalize public database text and URLs to the Communication to Care identity. +5. Seed demonstration content only when all public content tables are empty. +6. Create the configured admin account only when it does not exist. +7. Start Django or Gunicorn. + +Demo content includes: + +- A com2care product with five medical-imaging modules. +- Articles, structured specifications, FAQs, and release examples. +- Homepage and About page sections with local images. +- A generated Communication to Care hero image. +- Relevant external YouTube tutorials for DICOM review and image segmentation. +- `support@com2care.com` contact data. + +Content created or edited by an admin is preserved on later container restarts. + +## Optional configuration + +Copy `.env.example` to `.env` only when you want to override the local defaults: ```bash -git clone tecvico_website -cd tecvico_website cp .env.example .env ``` -Edit `.env` and set at minimum: +Important variables: -``` -DJANGO_SECRET_KEY=your-very-secure-random-key -POSTGRES_PASSWORD=choose-a-strong-password -``` +| Variable | Purpose | +|---|---| +| `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 -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 -docker compose exec web python manage.py createsuperuser -``` +The default production host configuration already includes `com2care.com` and +`www.com2care.com`. -### 4. Seed initial content +## Manual development -Populate the database with content scraped and adapted from visera.ca: - -```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 +Requirements: - Python 3.12+ - PostgreSQL 14+ -### 1. Set up virtual environment - ```bash python -m venv .venv -source .venv/bin/activate # Windows: .venv\Scripts\activate +source .venv/bin/activate pip install -r requirements-dev.txt -``` - -### 2. Configure environment - -```bash 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 seed_content -python manage.py createsuperuser -``` - -### 5. Start development server - -```bash +python manage.py seed_content --if-empty +python manage.py ensure_superuser 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 python manage.py test apps ``` -### With pytest (requires `requirements-dev.txt`) +or: ```bash pytest ``` -### With coverage report +## Project structure -```bash -coverage run -m pytest -coverage report -m -coverage html # Generates htmlcov/index.html +```text +config/ Django settings and root URLs +apps/core/ Branding, contact settings, bootstrap commands +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 - -### 1. Build and start production containers - -Remove `docker-compose.override.yml` (or don't override it) and pass production environment variables: - -```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. +- Django +- PostgreSQL 16 +- Gunicorn +- WhiteNoise +- Docker Compose +- Vanilla HTML, CSS, and JavaScript diff --git a/apps/core/admin.py b/apps/core/admin.py index 46d5962..d0e3323 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -20,7 +20,7 @@ class SiteBrandingAdmin(admin.ModelAdmin): ), "description": ( "Manage each placement independently. Removing an upload restores " - "the existing Radiuma asset for that placement." + "the existing Communication to Care asset for that placement." ), }, ), diff --git a/apps/core/management/commands/ensure_superuser.py b/apps/core/management/commands/ensure_superuser.py index a04ec36..cc26725 100644 --- a/apps/core/management/commands/ensure_superuser.py +++ b/apps/core/management/commands/ensure_superuser.py @@ -4,29 +4,53 @@ from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand +DEFAULT_ADMIN_PASSWORD = "cmosV6Tw46Odv7UN" + + 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): User = get_user_model() - username = os.environ.get("DJANGO_SUPERUSER_USERNAME", "admin") - email = os.environ.get("DJANGO_SUPERUSER_EMAIL", "admin@radiuma.com") - password = os.environ.get("DJANGO_SUPERUSER_PASSWORD") + enabled = os.environ.get("DJANGO_SUPERUSER_ENABLED", "True").lower() + if enabled in {"0", "false", "no", "off"}: + 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.style.WARNING( - "DJANGO_SUPERUSER_PASSWORD is not set — skipping superuser creation." + self.style.SUCCESS( + f"Admin '{username}' already exists; its password was preserved." ) ) 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) - 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." + ) + ) diff --git a/apps/core/management/commands/normalize_brand.py b/apps/core/management/commands/normalize_brand.py new file mode 100644 index 0000000..9e47cf3 --- /dev/null +++ b/apps/core/management/commands/normalize_brand.py @@ -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.") diff --git a/apps/core/management/commands/seed_content.py b/apps/core/management/commands/seed_content.py index 3ec612d..815e226 100644 --- a/apps/core/management/commands/seed_content.py +++ b/apps/core/management/commands/seed_content.py @@ -1,24 +1,38 @@ +from django.conf import settings +from django.core.files import File from django.core.management.base import BaseCommand from django.db import transaction -from apps.core.models import SiteContact -from apps.pages.models import DownloadItem, FAQEntry, HeroSection, HomepageSection, HomepageSectionItem -from apps.products.models import Article, ArticleSection, MainProduct, SubProduct +from apps.core.models import SiteBranding, SiteContact +from apps.pages.models import ( + AboutSection, + AboutSectionItem, + DownloadItem, + FAQEntry, + HeroSection, + HomepageSection, + HomepageSectionItem, + PageVideo, +) +from apps.products.models import ( + Article, + ArticleSection, + MainProduct, + ProductVideo, + SubProduct, +) MAIN_PRODUCTS = [ { - "name": "Radiuma", - "slug": "radiuma", - "short_description": "Visualized & Standardized Environment for Radiomics Analysis", + "name": "Communication to Care", + "slug": "com2care", + "short_description": "Collaborative, standardized medical imaging research workflows", "description": ( - "Radiuma is a free, open-source software specialized for visualization, " - "processing, segmentation, registration, fusion and analysis of medical and " - "biomedical images, including radiomics and machine learning analysis. " - "Radiuma is a major, entirely-revamped upgrade to the original SERA " - "(Matlab-based), now built on Python for broader accessibility and community " - "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." + "Communication to Care (com2care) brings medical-image visualization, processing, " + "segmentation, registration, fusion, radiomics, and machine-learning workflows into " + "one research environment. The platform is designed to help multidisciplinary teams " + "discuss findings clearly, build repeatable pipelines, and share analysis context. " + "Its quantitative imaging workflow follows IBSI guidance for reproducible research." ), "order": 1, "show_on_homepage": True, @@ -31,7 +45,7 @@ MAIN_PRODUCTS = [ "description": ( "Advanced image processing capabilities including standardized filtering " "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 " "for consistent, reproducible research outcomes." ), @@ -40,7 +54,7 @@ MAIN_PRODUCTS = [ { "title": "Image Filtering Techniques", "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 " "Initiative (IBSI) phase 2. These filters enable reproducible " "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", "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", "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 " "other combined modality studies. Standardized Uptake Value (SUV) " "conversion is also supported." @@ -78,7 +92,7 @@ MAIN_PRODUCTS = [ "slug": "radiomics-features", "short_description": "IBSI 1.0 compliant handcrafted radiomic feature extraction", "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 " "(IBSI 1.0). Features are computed from segmented regions of interest " "across multiple image modalities, enabling reproducible quantitative " @@ -89,7 +103,7 @@ MAIN_PRODUCTS = [ { "title": "IBSI Compliant Feature Extraction", "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 " "from segmented Regions of Interest (ROIs) and are fully " "reproducible across different platforms and institutions." @@ -113,7 +127,7 @@ MAIN_PRODUCTS = [ "slug": "medical-image-visualization", "short_description": "Professional multi-modality medical image viewer", "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 " "comfortable, intuitive controls for slice navigation, windowing, " "zoom, and annotation, suitable for radiation oncologists, radiologists, " @@ -143,7 +157,7 @@ MAIN_PRODUCTS = [ "slug": "format-conversion", "short_description": "Professional converter for medical imaging file formats", "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, " "NIFTI, NRRD, MHA, and other formats without loss of spatial metadata " "or patient information integrity." @@ -172,7 +186,7 @@ MAIN_PRODUCTS = [ "slug": "workflow-management", "short_description": "Reproducible research workflow creation and sharing", "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 " "processing steps — from image loading and preprocessing to feature " "extraction and machine learning — into reproducible, shareable sequences " @@ -202,34 +216,28 @@ MAIN_PRODUCTS = [ FAQ_ENTRIES = [ { - "question": "What is the Radiuma license?", + "question": "How is Communication to Care licensed?", "answer": ( - "Radiuma is free and open-source for research purposes.\n\n" - "License: CC BY-NC-SA (Creative Commons Attribution-NonCommercial-ShareAlike). " - "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." + "Licensing and deployment terms are provided with each com2care release. " + "Contact support@com2care.com for research, institutional, or evaluation access." ), "order": 1, }, { - "question": "How do I cite Radiuma in my research?", + "question": "How do I acknowledge Communication to Care in my research?", "answer": ( - "Please cite the following reference if you publish results obtained with " - "the help of Radiuma:\n\n" - "M. R. Salmanpour, I. Shiri, M. Hosseinzadeh, H. Zaidi, S. Ashrafinia, " - "M. Oveisi, A. Rahmim. Radiuma: Visualized & Standardized Environment for " - "Radiomics Analysis — A Shareable, Executable, and Reproducible Workflow " - "Generator. Proc. IEEE Medical Imaging Conference, 2023." + "Mention Communication to Care (com2care) and the software version used in your " + "methods section. Release-specific citation guidance can be requested from " + "support@com2care.com." ), "order": 2, }, { - "question": "Which operating systems does Radiuma support?", + "question": "Which operating systems does Communication to Care support?", "answer": ( - "Radiuma currently fully supports Windows 10 and above (64-bit). " - "New versions to support macOS and Linux systems are under active development " - "and coming soon. Follow our Discord or check each product module page for updates." + "Communication to Care currently fully supports Windows 10 and above (64-bit). " + "macOS and Linux packages are represented in this demonstration dataset as upcoming " + "channels. Check each product module page for current release information." ), "order": 3, }, @@ -244,9 +252,9 @@ FAQ_ENTRIES = [ "order": 4, }, { - "question": "Is Radiuma suitable for clinical use?", + "question": "Is Communication to Care suitable for clinical use?", "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 " "qualified medical professionals for clinical decisions." ), @@ -255,9 +263,8 @@ FAQ_ENTRIES = [ { "question": "Where can I get support or report issues?", "answer": ( - "Support is available via email and through our community Discord server " - "(see the Contact page for current details). For bug reports and feature " - "requests, please use the Discord forum or contact us directly by email." + "Use the contact form or email support@com2care.com. Include the software version, " + "operating system, a short reproduction description, and non-sensitive logs when relevant." ), "order": 6, }, @@ -282,9 +289,42 @@ HOMEPAGE_SECTIONS = [ { "section_type": HomepageSection.TYPE_SCREENSHOTS, "badge": "Gallery", - "title": "See Radiuma in Action", - "description": "Explore Radiuma's powerful interface, workflow builder, and multi-modal image viewer.", + "title": "See Communication to Care in Action", + "description": "Explore Communication to Care's powerful interface, workflow builder, and multi-modal image viewer.", "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": [], }, { @@ -292,19 +332,19 @@ HOMEPAGE_SECTIONS = [ "badge": "Our Software", "title": "Products", "description": "Explore our suite of medical imaging and radiomics tools.", - "order": 3, + "order": 4, "items": [], }, { "section_type": HomepageSection.TYPE_PROBLEMS, "badge": "Value Proposition", - "title": "What Problems Does Radiuma Solve?", + "title": "What Problems Does Communication to Care Solve?", "description": "", - "order": 4, + "order": 5, "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": "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": "03", "title": "Flexibility", "content": "Radiuma offers flexibility in terms of tool optimization and workflow customization to match your specific research requirements.", "order": 3}, + {"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": "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": "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}, ], }, @@ -313,48 +353,112 @@ HOMEPAGE_SECTIONS = [ "badge": "Our Story", "title": "More to Know", "description": ( - "Radiuma has been developing since 2021 by the Quantitative Radiomolecular Imaging " - "and Therapy (Qurit) lab & program at the University of British Columbia & " - "BC Cancer Research Institute, Vancouver, BC, Canada." + "Communication to Care is shaped around multidisciplinary research: connect imaging " + "evidence, analysis steps, and team discussion in one understandable workflow." ), "link_text": "Learn More", "link_url": "/about/", - "order": 5, + "order": 6, "items": [], }, { "section_type": HomepageSection.TYPE_SUPPORTERS, - "badge": "Acknowledgements", - "title": "Our Supporters", - "description": "Radiuma is made possible by the support of leading research institutions and organizations.", - "order": 6, + "badge": "Who It Serves", + "title": "Built for Collaborative Teams", + "description": "com2care demo workflows are organized around the people who review, analyze, and communicate medical-imaging evidence.", + "order": 7, "items": [ { - "title": "University of British Columbia", - "content": "Faculty of Medicine and the Department of Integrative Oncology.", + "title": "Imaging Researchers", + "content": "Build standardized pipelines and retain the context behind each processing decision.", "order": 1, }, { - "title": "BC Cancer Research Institute", - "content": "Supporting cutting-edge radiomics and medical imaging research in Vancouver, BC.", + "title": "Clinical Research Teams", + "content": "Review images and quantitative results together without presenting research output as diagnosis.", "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 = [ { - "name": "Radiuma Desktop", + "name": "com2care Desktop", "platform": "windows", "version": "1.0.0", - "download_url": "https://github.com/radiuma/radiuma/releases/latest/download/Radiuma-Setup.exe", - "description": "Windows 10 and above (64-bit). Installer package.", + "download_url": "https://com2care.com/downloads/", + "description": "Demonstration release channel for Windows 10 and above (64-bit).", "is_active": True, "order": 1, }, { - "name": "Radiuma Desktop", + "name": "com2care Desktop", "platform": "macos", "version": "Coming Soon", "download_url": "#", @@ -363,7 +467,7 @@ DOWNLOAD_ITEMS = [ "order": 2, }, { - "name": "Radiuma Desktop", + "name": "com2care Desktop", "platform": "linux", "version": "Coming Soon", "download_url": "#", @@ -375,19 +479,43 @@ DOWNLOAD_ITEMS = [ 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): parser.add_argument( "--flush", 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 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"]: - 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() Article.objects.all().delete() SubProduct.objects.all().delete() @@ -398,66 +526,96 @@ class Command(BaseCommand): HomepageSection.objects.all().delete() HeroSection.objects.all().delete() - self._seed_products() + main_product = self._seed_products() self._seed_faq() self._seed_downloads() self._seed_homepage_sections() + self._seed_about_sections() + self._seed_product_videos(main_product) self._seed_hero() + self._seed_site_branding() 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): + seeded_main_product = None 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( - slug=product_data["slug"], - defaults=product_data, + slug=product_defaults["slug"], + defaults=product_defaults, ) if not created: - for field, value in product_data.items(): - setattr(main_product, field, value) - main_product.save() + self._update_instance(main_product, product_defaults) + seeded_main_product = main_product action = "Created" if created else "Updated" self.stdout.write(f" {action} main product: {main_product.name}") 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( main_product=main_product, - slug=sub_data["slug"], - defaults=sub_data, + slug=sub_defaults["slug"], + defaults=sub_defaults, ) if not sub_created: - for field, value in sub_data.items(): - setattr(sub_product, field, value) - sub_product.save() + self._update_instance(sub_product, sub_defaults) sub_action = "Created" if sub_created else "Updated" self.stdout.write(f" {sub_action} sub-product: {sub_product.name}") 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( sub_product=sub_product, - title=article_data["title"], - defaults=article_data, + title=article_defaults["title"], + defaults=article_defaults, ) if not art_created: - for field, value in article_data.items(): - setattr(article, field, value) - article.save() + self._update_instance(article, article_defaults) art_action = "Created" if art_created else "Updated" self.stdout.write(f" {art_action} article: {article.title}") for section_data in sections_data: - section, _ = ArticleSection.objects.get_or_create( + article_section, section_created = ArticleSection.objects.get_or_create( article=article, title=section_data["title"], defaults=section_data, ) + if not section_created: + self._update_instance(article_section, section_data) + return seeded_main_product def _seed_faq(self): for entry_data in FAQ_ENTRIES: @@ -466,34 +624,92 @@ class Command(BaseCommand): defaults=entry_data, ) if not created: - for field, value in entry_data.items(): - setattr(faq, field, value) - faq.save() + self._update_instance(faq, entry_data) action = "Created" if created else "Updated" self.stdout.write(f" {action} FAQ: {faq.question[:60]}...") def _seed_homepage_sections(self): for section_data in HOMEPAGE_SECTIONS: - items_data = section_data.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_type=section_data["section_type"], - defaults=section_data, + section_type=section_defaults["section_type"], + defaults=section_defaults, ) if not created: - for field, value in section_data.items(): - setattr(section, field, value) - section.save() + self._update_instance(section, section_defaults) action = "Created" if created else "Updated" self.stdout.write(f" {action} homepage section: {section}") 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, - title=item_data["title"], - defaults=item_data, + 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_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): for item_data in DOWNLOAD_ITEMS: @@ -503,73 +719,52 @@ class Command(BaseCommand): defaults=item_data, ) if not created: - for field, value in item_data.items(): - setattr(item, field, value) - item.save() + self._update_instance(item, item_data) action = "Created" if created else "Updated" self.stdout.write(f" {action} download: {item}") def _seed_hero(self): data = { - "badge": "Developing since 2021", - "title": "Radiuma,", - "title_highlight": "A Powerful Workflow Generator", - "subtitle": "for Standardized Radiomics Analysis and Medical Image Visualization", + "badge": "Communication-first medical imaging", + "title": "Communication to Care,", + "title_highlight": "From Images to Shared Understanding", + "subtitle": "Collaborative medical imaging, radiomics, and reproducible research workflows", "description": ( - "Radiuma is a free, open-source software specialized for visualization, processing, " - "segmentation, registration, fusion and analysis of medical and biomedical images, " - "including radiomics and machine learning analysis." + "com2care helps multidisciplinary teams explore complex imaging evidence, " + "document analysis choices, and communicate results with clearer context." ), - "primary_cta_text": "Get Radiuma", + "primary_cta_text": "Explore com2care", "primary_cta_url": "/products/", - "secondary_cta_text": "About Radiuma", + "secondary_cta_text": "Our Mission", "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() if hero is None: - HeroSection.objects.create(**data) - self.stdout.write(" Created hero section") + hero = HeroSection.objects.create(**data) else: - for field, value in data.items(): - if field == "image": - continue - setattr(hero, field, value) - hero.save() - self.stdout.write(" Updated hero section") + self._update_instance(hero, data) + self._attach_static_image(hero, "image", "com2care-care-team.jpg") + + def _seed_site_branding(self): + branding = SiteBranding.load() + 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): - contact, created = SiteContact.objects.get_or_create( - pk=1, - defaults={ - "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" - ), - }, - ) + data = { + "support_email": "support@com2care.com", + "discord_url": "", + "discord_label": "", + "email_card_title": "Email com2care Support", + "email_card_description": "For product, evaluation, and research questions:", + "discord_card_title": "", + "discord_card_description": "", + "office_card_title": "", + "office_address": "", + } + contact, created = SiteContact.objects.get_or_create(pk=1, defaults=data) if not created: - updates = { - "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") + self._update_instance(contact, data) diff --git a/apps/core/migrations/0004_alter_sitebranding_hero_logo_alt_and_more.py b/apps/core/migrations/0004_alter_sitebranding_hero_logo_alt_and_more.py new file mode 100644 index 0000000..a716268 --- /dev/null +++ b/apps/core/migrations/0004_alter_sitebranding_hero_logo_alt_and_more.py @@ -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'))]), + ), + ] diff --git a/apps/core/models.py b/apps/core/models.py index 875f408..4d7e1ee 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -33,7 +33,7 @@ class SiteBranding(models.Model): ], 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 Radiuma icons." + "JPG, or WebP file. Leave empty to use the default Communication to Care icons." ), ) hero_logo = models.ImageField( @@ -48,7 +48,7 @@ class SiteBranding(models.Model): hero_logo_alt = models.CharField( max_length=200, blank=True, - default="Radiuma", + default="Communication to Care", help_text="Accessible description for the homepage hero logo.", ) navbar_icon_size = models.PositiveSmallIntegerField( diff --git a/apps/core/tests/test_bootstrap_commands.py b/apps/core/tests/test_bootstrap_commands.py new file mode 100644 index 0000000..adebb40 --- /dev/null +++ b/apps/core/tests/test_bootstrap_commands.py @@ -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") diff --git a/apps/core/tests/test_site_branding.py b/apps/core/tests/test_site_branding.py index 4e16cd3..4054544 100644 --- a/apps/core/tests/test_site_branding.py +++ b/apps/core/tests/test_site_branding.py @@ -44,7 +44,7 @@ class SiteBrandingTemplateTests(TestCase): branding.icon = "branding/navigation-logo.png" branding.website_icon = "branding/icons/site-icon.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() 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, 'href="/media/branding/icons/site-icon.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): response = self.client.get(reverse("pages:home")) - self.assertContains(response, "images/favicon-16.png") - self.assertContains(response, "images/screenshot-1.jpg") + self.assertContains(response, "images/com2care-mark.svg") + self.assertContains(response, "images/com2care-care-team.jpg") diff --git a/apps/pages/migrations/0018_alter_herosection_title.py b/apps/pages/migrations/0018_alter_herosection_title.py new file mode 100644 index 0000000..a49cc35 --- /dev/null +++ b/apps/pages/migrations/0018_alter_herosection_title.py @@ -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), + ), + ] diff --git a/apps/pages/models.py b/apps/pages/models.py index 6f34358..e8ea274 100644 --- a/apps/pages/models.py +++ b/apps/pages/models.py @@ -23,7 +23,7 @@ RESERVED_PAGE_SLUGS = frozenset({ class HeroSection(models.Model): 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').") 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.") diff --git a/apps/pages/tests/test_video.py b/apps/pages/tests/test_video.py index 601df72..8ab71a0 100644 --- a/apps/pages/tests/test_video.py +++ b/apps/pages/tests/test_video.py @@ -156,8 +156,8 @@ class PageVideoTest(TestCase): class ProductVideoTest(TestCase): def setUp(self): self.main_product = MainProduct.objects.create( - name="Radiuma", - slug="radiuma", + name="Communication to Care", + slug="com2care", short_description="Short", description="Long", is_active=True, @@ -173,7 +173,7 @@ class ProductVideoTest(TestCase): is_active=True, ) 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, "video-block--full") diff --git a/apps/pages/views.py b/apps/pages/views.py index 26203c7..d0fe56f 100644 --- a/apps/pages/views.py +++ b/apps/pages/views.py @@ -207,7 +207,7 @@ class PageVideoArchiveView(ListView): PAGE_CONFIG = { PageVideo.PAGE_CONTACT: ( "Contact videos", - "Guides and updates from the Radiuma team.", + "Guides and updates from the Communication to Care team.", "pages:contact", ), PageVideo.PAGE_FAQ: ( diff --git a/apps/products/tests/test_views.py b/apps/products/tests/test_views.py index 3870e72..ba38df5 100644 --- a/apps/products/tests/test_views.py +++ b/apps/products/tests/test_views.py @@ -11,8 +11,8 @@ from apps.products.models import Article, ArticleSection, MainProduct, SubProduc class ProductViewsSetup(TestCase): def setUp(self): self.main_product = MainProduct.objects.create( - name="Radiuma", - slug="radiuma", + name="Communication to Care", + slug="com2care", short_description="Radiomics software", description="Full description.", ) @@ -61,17 +61,17 @@ class ProductOverviewViewTest(ProductViewsSetup): class MainProductDetailViewTest(ProductViewsSetup): 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) self.assertEqual(response.status_code, 200) 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) self.assertTemplateUsed(response, "products/main_detail.html") 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) self.assertEqual(response.context["main_product"], self.main_product) @@ -83,7 +83,7 @@ class MainProductDetailViewTest(ProductViewsSetup): def test_inactive_product_returns_404(self): self.main_product.is_active = False 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) self.assertEqual(response.status_code, 404) @@ -93,7 +93,7 @@ class MainProductDetailViewTest(ProductViewsSetup): title="Overview", 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) self.assertIn("articles", response.context) self.assertIn(main_article, list(response.context["articles"])) @@ -104,7 +104,7 @@ class SubProductDetailViewTest(ProductViewsSetup): def test_sub_detail_returns_200(self): url = reverse( "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) self.assertEqual(response.status_code, 200) @@ -112,7 +112,7 @@ class SubProductDetailViewTest(ProductViewsSetup): def test_sub_detail_uses_correct_template(self): url = reverse( "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) self.assertTemplateUsed(response, "products/sub_detail.html") @@ -120,7 +120,7 @@ class SubProductDetailViewTest(ProductViewsSetup): def test_sub_detail_context_keys(self): url = reverse( "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) self.assertIn("main_product", response.context) @@ -131,7 +131,7 @@ class SubProductDetailViewTest(ProductViewsSetup): def test_sub_detail_articles_in_context(self): url = reverse( "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) self.assertIn(self.article, list(response.context["articles"])) @@ -139,7 +139,7 @@ class SubProductDetailViewTest(ProductViewsSetup): def test_nonexistent_sub_slug_returns_404(self): url = reverse( "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) self.assertEqual(response.status_code, 404) @@ -174,7 +174,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup): ) url = reverse( "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) self.assertEqual(response.status_code, 200) @@ -198,7 +198,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup): ) url = reverse( "products:main_product_versions", - kwargs={"main_slug": "radiuma"}, + kwargs={"main_slug": "com2care"}, ) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -213,7 +213,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup): windows_download_url="https://example.com/win.exe", 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) self.assertTrue(response.context["show_releases_section"]) @@ -236,7 +236,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup): windows_download_url="https://example.com/prev.exe", 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) self.assertContains(response, "Beta") self.assertContains(response, "Previous") @@ -254,7 +254,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup): windows_download_url="https://example.com/ea.exe", 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) self.assertContains(response, "Early Access") self.assertNotContains(response, ">Stable<") @@ -301,7 +301,7 @@ class ReleaseAssetDownloadViewTest(ProductViewsSetup): windows_download_file=SimpleUploadedFile("win.tar.gz", b"gz"), 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) download_url = reverse( "products:release_asset_download", diff --git a/config/settings/base.py b/config/settings/base.py index b6d59bb..2f0a94e 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -67,8 +67,8 @@ WSGI_APPLICATION = "config.wsgi.application" DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ.get("POSTGRES_DB", "tecvico"), - "USER": os.environ.get("POSTGRES_USER", "tecvico_user"), + "NAME": os.environ.get("POSTGRES_DB", "com2care"), + "USER": os.environ.get("POSTGRES_USER", "com2care_user"), "PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""), "HOST": os.environ.get("POSTGRES_HOST", "localhost"), "PORT": os.environ.get("POSTGRES_PORT", "5432"), diff --git a/config/urls.py b/config/urls.py index 25d1759..a9f2137 100644 --- a/config/urls.py +++ b/config/urls.py @@ -5,9 +5,9 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path -admin.site.site_header = "Radiuma Administration" -admin.site.site_title = "Radiuma Admin" -admin.site.index_title = "Welcome to Radiuma Administration" +admin.site.site_header = "Communication to Care Administration" +admin.site.site_title = "Communication to Care Admin" +admin.site.index_title = "Welcome to Communication to Care Administration" urlpatterns = [ path("admin/", admin.site.urls), diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 6efdf33..61f460d 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,4 +1,8 @@ services: + db: + ports: + - "5433:5432" + web: build: . environment: diff --git a/docker-compose.yml b/docker-compose.yml index 70c2576..c42db3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,14 +4,12 @@ services: restart: unless-stopped volumes: - postgres_data:/var/lib/postgresql/data - env_file: - - .env environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-com2care} + POSTGRES_USER: ${POSTGRES_USER:-com2care_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-com2care_local_password} 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 timeout: 5s retries: 5 @@ -21,11 +19,22 @@ services: restart: unless-stopped ports: - "8000:8000" - env_file: - - .env 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_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: - ./media:/app/media - ./staticfiles:/app/staticfiles @@ -35,3 +44,4 @@ services: volumes: postgres_data: + media_data: diff --git a/entrypoint.sh b/entrypoint.sh index 54c1211..e324c01 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,8 +6,8 @@ until python -c " import os, psycopg2, sys try: psycopg2.connect( - dbname=os.environ.get('POSTGRES_DB', 'tecvico'), - user=os.environ.get('POSTGRES_USER', 'tecvico_user'), + dbname=os.environ.get('POSTGRES_DB', 'com2care'), + user=os.environ.get('POSTGRES_USER', 'com2care_user'), password=os.environ.get('POSTGRES_PASSWORD', ''), host=os.environ.get('POSTGRES_HOST', 'db'), port=os.environ.get('POSTGRES_PORT', '5432'), @@ -28,7 +28,13 @@ python manage.py collectstatic --noinput echo "Running database migrations..." 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 echo "Starting application..." diff --git a/static/images/com2care-care-team.jpg b/static/images/com2care-care-team.jpg new file mode 100644 index 0000000..36d165f Binary files /dev/null and b/static/images/com2care-care-team.jpg differ diff --git a/static/images/com2care-logo.svg b/static/images/com2care-logo.svg new file mode 100644 index 0000000..d3d5378 --- /dev/null +++ b/static/images/com2care-logo.svg @@ -0,0 +1,25 @@ + + com2care + Communication to Care logo + + + + + + + + + + + + + + + + + + + + + com2care + diff --git a/static/images/com2care-mark.svg b/static/images/com2care-mark.svg new file mode 100644 index 0000000..ec0a018 --- /dev/null +++ b/static/images/com2care-mark.svg @@ -0,0 +1,17 @@ + + com2care mark + Two connected conversation shapes forming a care heart + + + + + + + + + + + + + + diff --git a/static/images/favicon-16.png b/static/images/favicon-16.png deleted file mode 100644 index 1569638..0000000 Binary files a/static/images/favicon-16.png and /dev/null differ diff --git a/static/images/favicon-180.png b/static/images/favicon-180.png deleted file mode 100644 index 84a151e..0000000 Binary files a/static/images/favicon-180.png and /dev/null differ diff --git a/static/images/favicon-32.png b/static/images/favicon-32.png deleted file mode 100644 index 6285814..0000000 Binary files a/static/images/favicon-32.png and /dev/null differ diff --git a/static/images/logo.png b/static/images/logo.png deleted file mode 100644 index 9063b4a..0000000 Binary files a/static/images/logo.png and /dev/null differ diff --git a/static/images/screenshot-6.png b/static/images/screenshot-6.png deleted file mode 100644 index b48f818..0000000 Binary files a/static/images/screenshot-6.png and /dev/null differ diff --git a/static/images/screenshot-7.png b/static/images/screenshot-7.png deleted file mode 100644 index f21fcb8..0000000 Binary files a/static/images/screenshot-7.png and /dev/null differ diff --git a/templates/base.html b/templates/base.html index aebed31..f39e8e3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,15 +4,18 @@ - - {% block title %}Radiuma{% endblock %} | Radiuma + + + + + + {% block title %}Communication to Care{% endblock %} | Communication to Care {% if site_branding.website_icon %} {% else %} - - - + + {% endif %} diff --git a/templates/pages/about.html b/templates/pages/about.html index f9dfc53..b86710f 100644 --- a/templates/pages/about.html +++ b/templates/pages/about.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% 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 %} {% include "partials/_page_sections.html" with sections=about_sections %} diff --git a/templates/pages/contact.html b/templates/pages/contact.html index 6826e15..2dec9a5 100644 --- a/templates/pages/contact.html +++ b/templates/pages/contact.html @@ -2,7 +2,7 @@ {% load static %} {% 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 %} @@ -15,7 +15,7 @@
Get in Touch

Contact Us

-

Have a question or want to reach the Radiuma team? Fill out the form or contact us directly below.

+

Have a product, evaluation, or research question? Send the com2care team a message below.

diff --git a/templates/pages/faq.html b/templates/pages/faq.html index 6a4aadc..6689d17 100644 --- a/templates/pages/faq.html +++ b/templates/pages/faq.html @@ -2,7 +2,7 @@ {% load static %} {% 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 %} diff --git a/templates/pages/home.html b/templates/pages/home.html index 477d6e9..0fcd045 100644 --- a/templates/pages/home.html +++ b/templates/pages/home.html @@ -2,7 +2,7 @@ {% load static %} {% 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 %} @@ -35,18 +35,16 @@ {% endif %} {% endif %} - {% if site_branding.hero_logo or hero and hero.image or not hero %}
{% if site_branding.hero_logo %} - {{ site_branding.hero_logo_alt|default:'Radiuma' }} + {{ site_branding.hero_logo_alt|default:'Communication to Care' }} {% elif hero and hero.image %} {{ hero.image_alt }} - {% elif not hero %} - Radiuma application — main workflow view + {% else %} + A multidisciplinary care team collaborating around medical imaging {% endif %}
- {% endif %} diff --git a/templates/partials/_brand_icon.html b/templates/partials/_brand_icon.html index e32da22..a5cd26a 100644 --- a/templates/partials/_brand_icon.html +++ b/templates/partials/_brand_icon.html @@ -1,14 +1,14 @@ {% load static %} {% if placement == "footer" %} {% else %} @@ -22,7 +22,7 @@ @@ -55,12 +55,10 @@ diff --git a/templates/partials/_navbar.html b/templates/partials/_navbar.html index afba6c0..167233a 100644 --- a/templates/partials/_navbar.html +++ b/templates/partials/_navbar.html @@ -2,9 +2,9 @@