fix: separate logo and hero icon
@@ -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
|
||||||
|
|||||||
@@ -48,4 +48,8 @@ venv.bak/
|
|||||||
media/
|
media/
|
||||||
private_uploads/
|
private_uploads/
|
||||||
staticfiles/
|
staticfiles/
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
|
test_media_releases/
|
||||||
|
docs/
|
||||||
|
scripts/
|
||||||
|
videos/
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -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.")
|
||||||
@@ -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'))]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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: (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
|
db:
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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..."
|
||||||
|
|||||||
|
After Width: | Height: | Size: 291 KiB |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 454 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 834 B |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 47 KiB |
@@ -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,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,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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
|
|||||||
@@ -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">
|
||||||
© {% now "Y" %} Radiuma. All rights reserved.
|
© {% 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>
|
|
||||||
& <a href="https://www.bccrc.ca" target="_blank" rel="noopener noreferrer">BC Cancer Research Institute</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,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
|
|
||||||