feat: changes for citation ad dynamic

This commit is contained in:
mohamad
2026-05-17 18:51:26 +03:30
parent f0a9d5832b
commit b8ab67049f
22 changed files with 1170 additions and 287 deletions
+114 -3
View File
@@ -1,7 +1,7 @@
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.pages.models import DownloadItem, FAQEntry from apps.pages.models import DownloadItem, FAQEntry, HomepageSection, HomepageSectionItem
from apps.products.models import Article, ArticleSection, MainProduct, SubProduct from apps.products.models import Article, ArticleSection, MainProduct, SubProduct
MAIN_PRODUCTS = [ MAIN_PRODUCTS = [
@@ -33,6 +33,8 @@ MAIN_PRODUCTS = [
"for consistent, reproducible research outcomes." "for consistent, reproducible research outcomes."
), ),
"order": 1, "order": 1,
"show_on_homepage": True,
"homepage_order": 1,
"articles": [ "articles": [
{ {
"title": "Image Filtering Techniques", "title": "Image Filtering Techniques",
@@ -252,7 +254,7 @@ 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 at support@visera.ca and through our " "Support is available via email at support@radiuma.ca and through our "
"community Discord server. For bug reports and feature requests, please " "community Discord server. For bug reports and feature requests, please "
"use the Discord forum or contact us directly by email." "use the Discord forum or contact us directly by email."
), ),
@@ -260,6 +262,86 @@ FAQ_ENTRIES = [
}, },
] ]
HOMEPAGE_SECTIONS = [
{
"section_type": HomepageSection.TYPE_FEATURES,
"badge": "Capabilities",
"title": "Important Features",
"description": "Comprehensive tools for medical imaging research, standardized and reproducible.",
"order": 1,
"items": [
{"icon": "⚗️", "title": "Image Filtering", "content": "Standardized image filtering techniques compliant with IBSI 2.0 guidelines.", "order": 1},
{"icon": "🖥️", "title": "Professional Viewer", "content": "Comfortable, professional medical image viewer with multi-modality support.", "order": 2},
{"icon": "📊", "title": "Radiomics Features", "content": "Handcrafted radiomics feature generation standardized by IBSI 1.0.", "order": 3},
{"icon": "🔄", "title": "Format Support", "content": "NIFTI, DICOM, NRRD, and more — comprehensive multi-format support.", "order": 4},
{"icon": "🗂️", "title": "Image Registration", "content": "Advanced image registration, fusion, and standardized SUV conversion.", "order": 5},
{"icon": "🔬", "title": "RT Struct Support", "content": "Full RT struct support for radiation oncology workflows and research.", "order": 6},
],
},
{
"section_type": HomepageSection.TYPE_SCREENSHOTS,
"badge": "Gallery",
"title": "See Radiuma in Action",
"description": "Explore Radiuma's powerful interface, workflow builder, and multi-modal image viewer.",
"order": 2,
"items": [],
},
{
"section_type": HomepageSection.TYPE_PRODUCTS,
"badge": "Our Software",
"title": "Products",
"description": "Explore our suite of medical imaging and radiomics tools.",
"order": 3,
"items": [],
},
{
"section_type": HomepageSection.TYPE_PROBLEMS,
"badge": "Value Proposition",
"title": "What Problems Does Radiuma Solve?",
"description": "",
"order": 4,
"items": [
{"icon": "01", "title": "Accessibility", "content": "Radiuma provides a user-friendly interface and a wide range of tools, allowing researchers to perform complex data analysis without extensive technical knowledge or programming expertise.", "order": 1},
{"icon": "02", "title": "Integrated Tools", "content": "Radiuma integrates a vast collection of tools and resources from various domains of healthcare and medical imaging research in a common, unified environment.", "order": 2},
{"icon": "03", "title": "Flexibility", "content": "Radiuma offers flexibility in terms of tool optimization and workflow customization to match your specific research requirements.", "order": 3},
{"icon": "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},
],
},
{
"section_type": HomepageSection.TYPE_ABOUT_STRIP,
"badge": "Our Story",
"title": "More to Know",
"description": (
"Radiuma has been developing since 2021 by the Quantitative Radiomolecular Imaging "
"and Therapy (Qurit) lab & program at the University of British Columbia & "
"BC Cancer Research Institute, Vancouver, BC, Canada."
),
"link_text": "Learn More",
"link_url": "/about/",
"order": 5,
"items": [],
},
{
"section_type": HomepageSection.TYPE_SUPPORTERS,
"badge": "Acknowledgements",
"title": "Our Supporters",
"description": "Radiuma is made possible by the support of leading research institutions and organizations.",
"order": 6,
"items": [
{
"title": "University of British Columbia",
"content": "Faculty of Medicine and the Department of Integrative Oncology.",
"order": 1,
},
{
"title": "BC Cancer Research Institute",
"content": "Supporting cutting-edge radiomics and medical imaging research in Vancouver, BC.",
"order": 2,
},
],
},
]
DOWNLOAD_ITEMS = [ DOWNLOAD_ITEMS = [
{ {
"name": "Radiuma Desktop", "name": "Radiuma Desktop",
@@ -292,7 +374,7 @@ DOWNLOAD_ITEMS = [
class Command(BaseCommand): class Command(BaseCommand):
help = "Seed the database with initial Radiuma / Radiuma content from visera.ca" help = "Seed the database with initial Radiuma / Radiuma content from radiuma.ca"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
@@ -311,14 +393,21 @@ class Command(BaseCommand):
MainProduct.objects.all().delete() MainProduct.objects.all().delete()
FAQEntry.objects.all().delete() FAQEntry.objects.all().delete()
DownloadItem.objects.all().delete() DownloadItem.objects.all().delete()
HomepageSectionItem.objects.all().delete()
HomepageSection.objects.all().delete()
self._seed_products() self._seed_products()
self._seed_faq() self._seed_faq()
self._seed_downloads() self._seed_downloads()
self._seed_homepage_sections()
self.stdout.write(self.style.SUCCESS("Content seeded successfully.")) self.stdout.write(self.style.SUCCESS("Content seeded successfully."))
def _seed_products(self): def _seed_products(self):
SubProduct.objects.filter(show_on_homepage=False, homepage_order=0).update(
show_on_homepage=True,
)
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.pop("sub_products")
main_product, created = MainProduct.objects.get_or_create( main_product, created = MainProduct.objects.get_or_create(
@@ -384,6 +473,28 @@ class Command(BaseCommand):
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):
for section_data in HOMEPAGE_SECTIONS:
items_data = section_data.pop("items")
section, created = HomepageSection.objects.get_or_create(
section_type=section_data["section_type"],
defaults=section_data,
)
if not created:
for field, value in section_data.items():
setattr(section, field, value)
section.save()
action = "Created" if created else "Updated"
self.stdout.write(f" {action} homepage section: {section}")
for item_data in items_data:
item, _ = HomepageSectionItem.objects.get_or_create(
section=section,
title=item_data["title"],
defaults=item_data,
)
def _seed_downloads(self): def _seed_downloads(self):
for item_data in DOWNLOAD_ITEMS: for item_data in DOWNLOAD_ITEMS:
item, created = DownloadItem.objects.get_or_create( item, created = DownloadItem.objects.get_or_create(
+49 -1
View File
@@ -1,6 +1,54 @@
from django.contrib import admin from django.contrib import admin
from .models import ContactSubmission, DownloadItem, FAQEntry from .models import (
AboutSection,
AboutSectionItem,
ContactSubmission,
DownloadItem,
FAQEntry,
HomepageSection,
HomepageSectionItem,
)
class HomepageSectionItemInline(admin.TabularInline):
model = HomepageSectionItem
extra = 1
fields = ("icon", "title", "content", "image", "order")
ordering = ("order",)
@admin.register(HomepageSection)
class HomepageSectionAdmin(admin.ModelAdmin):
list_display = ("section_type", "badge", "title", "order", "is_active")
list_filter = ("section_type", "is_active")
list_editable = ("order", "is_active")
inlines = [HomepageSectionItemInline]
fieldsets = (
(None, {"fields": ("section_type", "badge", "title", "description")}),
("CTA Link (About Strip)", {"fields": ("link_text", "link_url"), "classes": ("collapse",)}),
("Settings", {"fields": ("order", "is_active")}),
)
class AboutSectionItemInline(admin.StackedInline):
model = AboutSectionItem
extra = 1
fields = ("badge", "title", "content", "url", "image", "image_alt", "is_featured", "order")
ordering = ("order",)
@admin.register(AboutSection)
class AboutSectionAdmin(admin.ModelAdmin):
list_display = ("section_type", "badge", "title", "order", "is_active")
list_filter = ("section_type", "is_active")
list_editable = ("order", "is_active")
inlines = [AboutSectionItemInline]
fieldsets = (
(None, {"fields": ("section_type", "badge", "title", "subtitle")}),
("Content", {"fields": ("content_format", "content")}),
("Settings", {"fields": ("order", "is_active")}),
)
@admin.register(ContactSubmission) @admin.register(ContactSubmission)
@@ -0,0 +1,103 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pages", "0003_faqentry_answer_format"),
]
operations = [
migrations.CreateModel(
name="AboutSection",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"section_type",
models.CharField(
choices=[
("hero", "Hero"),
("intro", "Intro Card"),
("grid", "Grid Cards"),
("history", "History Block"),
("custom", "Custom Content"),
],
default="custom",
max_length=30,
),
),
("badge", models.CharField(blank=True, max_length=100)),
("title", models.CharField(blank=True, max_length=300)),
(
"subtitle",
models.CharField(
blank=True,
help_text="Used as subtitle in Hero and year in History.",
max_length=500,
),
),
("content", models.TextField(blank=True)),
(
"content_format",
models.CharField(
choices=[
("plain", "Plain Text"),
("markdown", "Markdown"),
("html", "HTML"),
],
default="markdown",
max_length=20,
),
),
("order", models.PositiveIntegerField(default=0)),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name": "About Section",
"verbose_name_plural": "About Sections",
"ordering": ["order"],
},
),
migrations.CreateModel(
name="AboutSectionItem",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"section",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="items",
to="pages.aboutsection",
),
),
("badge", models.CharField(blank=True, max_length=100)),
("title", models.CharField(blank=True, max_length=300)),
(
"content",
models.TextField(
blank=True,
help_text="Description text or link label for History links.",
),
),
("url", models.URLField(blank=True, help_text="Used for History block links.")),
("image", models.ImageField(blank=True, null=True, upload_to="about/")),
("image_alt", models.CharField(blank=True, max_length=200)),
(
"is_featured",
models.BooleanField(
default=False,
help_text="Mark as featured item (e.g. large screenshot).",
),
),
("order", models.PositiveIntegerField(default=0)),
],
options={
"verbose_name": "About Section Item",
"verbose_name_plural": "About Section Items",
"ordering": ["order"],
},
),
]
@@ -0,0 +1,49 @@
# Generated by Django 5.0.2 on 2026-05-17 14:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pages', '0004_aboutsection_aboutsectionitem'),
]
operations = [
migrations.CreateModel(
name='HomepageSection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('section_type', models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip')], max_length=30, unique=True)),
('badge', models.CharField(blank=True, max_length=100)),
('title', models.CharField(blank=True, max_length=300)),
('description', models.TextField(blank=True)),
('link_text', models.CharField(blank=True, help_text='CTA button label (About Strip).', max_length=100)),
('link_url', models.CharField(blank=True, help_text='CTA button URL (About Strip).', max_length=300)),
('order', models.PositiveIntegerField(default=0)),
('is_active', models.BooleanField(default=True)),
],
options={
'verbose_name': 'Homepage Section',
'verbose_name_plural': 'Homepage Sections',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='HomepageSectionItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('icon', models.CharField(blank=True, help_text='Emoji or short symbol (e.g. ⚗️).', max_length=20)),
('title', models.CharField(blank=True, max_length=300)),
('content', models.TextField(blank=True)),
('order', models.PositiveIntegerField(default=0)),
('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='pages.homepagesection')),
],
options={
'verbose_name': 'Homepage Section Item',
'verbose_name_plural': 'Homepage Section Items',
'ordering': ['order'],
},
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.0.2 on 2026-05-17 14:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pages', '0005_homepage_sections'),
]
operations = [
migrations.AddField(
model_name='homepagesectionitem',
name='image',
field=models.ImageField(blank=True, help_text='Logo or image (used for Supporters cards).', null=True, upload_to='homepage/items/'),
),
migrations.AlterField(
model_name='homepagesection',
name='section_type',
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters')], max_length=30, unique=True),
),
]
+113 -1
View File
@@ -1,6 +1,118 @@
from django.db import models from django.db import models
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_PLAIN, render_content from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_MARKDOWN, FORMAT_PLAIN, render_content
class HomepageSection(models.Model):
TYPE_FEATURES = "features"
TYPE_SCREENSHOTS = "screenshots"
TYPE_PRODUCTS = "products"
TYPE_PROBLEMS = "problems"
TYPE_ABOUT_STRIP = "about_strip"
TYPE_SUPPORTERS = "supporters"
TYPE_CHOICES = [
(TYPE_FEATURES, "Features"),
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
(TYPE_PRODUCTS, "Products"),
(TYPE_PROBLEMS, "Problems / Value Proposition"),
(TYPE_ABOUT_STRIP, "About Strip"),
(TYPE_SUPPORTERS, "Supporters"),
]
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, unique=True)
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
description = models.TextField(blank=True)
link_text = models.CharField(max_length=100, blank=True, help_text="CTA button label (About Strip).")
link_url = models.CharField(max_length=300, blank=True, help_text="CTA button URL (About Strip).")
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["order"]
verbose_name = "Homepage Section"
verbose_name_plural = "Homepage Sections"
def __str__(self):
return f"[{self.get_section_type_display()}] {self.title or self.badge}"
class HomepageSectionItem(models.Model):
section = models.ForeignKey(HomepageSection, on_delete=models.CASCADE, related_name="items")
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
title = models.CharField(max_length=300, blank=True)
content = models.TextField(blank=True)
image = models.ImageField(upload_to="homepage/items/", blank=True, null=True, help_text="Logo or image (used for Supporters cards).")
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["order"]
verbose_name = "Homepage Section Item"
verbose_name_plural = "Homepage Section Items"
def __str__(self):
return f"{self.section} {self.title or self.icon or '(item)'}"
class AboutSection(models.Model):
TYPE_HERO = "hero"
TYPE_INTRO = "intro"
TYPE_GRID = "grid"
TYPE_HISTORY = "history"
TYPE_CUSTOM = "custom"
TYPE_CHOICES = [
(TYPE_HERO, "Hero"),
(TYPE_INTRO, "Intro Card"),
(TYPE_GRID, "Grid Cards"),
(TYPE_HISTORY, "History Block"),
(TYPE_CUSTOM, "Custom Content"),
]
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, default=TYPE_CUSTOM)
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
subtitle = models.CharField(max_length=500, blank=True, help_text="Used as subtitle in Hero and year in History.")
content = models.TextField(blank=True)
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_MARKDOWN)
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["order"]
verbose_name = "About Section"
verbose_name_plural = "About Sections"
@property
def rendered_content(self):
return render_content(self.content, self.content_format)
def __str__(self):
label = self.title or self.badge or self.get_section_type_display()
return f"[{self.get_section_type_display()}] {label}"
class AboutSectionItem(models.Model):
section = models.ForeignKey(AboutSection, on_delete=models.CASCADE, related_name="items")
badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True)
content = models.TextField(blank=True, help_text="Description text or link label for History links.")
url = models.URLField(blank=True, help_text="Used for History block links.")
image = models.ImageField(upload_to="about/", blank=True, null=True)
image_alt = models.CharField(max_length=200, blank=True)
is_featured = models.BooleanField(default=False, help_text="Mark as featured item (e.g. large screenshot).")
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["order"]
verbose_name = "About Section Item"
verbose_name_plural = "About Section Items"
def __str__(self):
return f"{self.section} {self.title or self.badge or '(item)'}"
class ContactSubmission(models.Model): class ContactSubmission(models.Model):
+26 -1
View File
@@ -5,17 +5,42 @@ from django.shortcuts import render
from django.views import View from django.views import View
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
from apps.products.models import SubProduct
from .forms import ContactForm from .forms import ContactForm
from .models import ContactSubmission, FAQEntry from .models import AboutSection, ContactSubmission, FAQEntry, HomepageSection
class HomeView(TemplateView): class HomeView(TemplateView):
template_name = "pages/home.html" template_name = "pages/home.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["homepage_sections"] = (
HomepageSection.objects.filter(is_active=True)
.prefetch_related("items")
.order_by("order")
)
ctx["sub_products"] = (
SubProduct.objects.filter(is_active=True, show_on_homepage=True)
.select_related("main_product")
.order_by("homepage_order", "order")
)
return ctx
class AboutView(TemplateView): class AboutView(TemplateView):
template_name = "pages/about.html" template_name = "pages/about.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["about_sections"] = (
AboutSection.objects.filter(is_active=True)
.prefetch_related("items")
.order_by("order")
)
return ctx
class FAQView(ListView): class FAQView(ListView):
model = FAQEntry model = FAQEntry
+19 -8
View File
@@ -1,6 +1,13 @@
from django.contrib import admin from django.contrib import admin
from .models import Article, ArticleSection, MainProduct, SubProduct, SubProductVersion from .models import Article, ArticleCitation, ArticleSection, MainProduct, SubProduct, SubProductVersion
class ArticleCitationInline(admin.TabularInline):
model = ArticleCitation
extra = 1
fields = ("text", "url", "order")
ordering = ("order",)
class ArticleSectionInline(admin.TabularInline): class ArticleSectionInline(admin.TabularInline):
@@ -13,7 +20,7 @@ class ArticleSectionInline(admin.TabularInline):
class ArticleInline(admin.StackedInline): class ArticleInline(admin.StackedInline):
model = Article model = Article
extra = 0 extra = 0
fields = ("title", "description", "order") fields = ("badge", "title", "description", "order")
ordering = ("order",) ordering = ("order",)
show_change_link = True show_change_link = True
@@ -39,7 +46,7 @@ class SubProductVersionInline(admin.TabularInline):
class SubProductInline(admin.StackedInline): class SubProductInline(admin.StackedInline):
model = SubProduct model = SubProduct
extra = 0 extra = 0
fields = ("name", "slug", "distribution", "short_description", "image", "order", "is_active") fields = ("name", "slug", "distribution", "short_description", "image", "logo", "show_on_homepage", "homepage_order", "order", "is_active")
ordering = ("order",) ordering = ("order",)
show_change_link = True show_change_link = True
prepopulated_fields = {"slug": ("name",)} prepopulated_fields = {"slug": ("name",)}
@@ -67,20 +74,23 @@ class SubProductAdmin(admin.ModelAdmin):
"name", "name",
"main_product", "main_product",
"distribution", "distribution",
"show_on_homepage",
"homepage_order",
"order", "order",
"is_active", "is_active",
"created_at", "created_at",
) )
list_filter = ("is_active", "distribution", "main_product") list_filter = ("is_active", "distribution", "main_product", "show_on_homepage")
search_fields = ("name", "description", "main_product__name") search_fields = ("name", "description", "main_product__name")
prepopulated_fields = {"slug": ("name",)} prepopulated_fields = {"slug": ("name",)}
list_editable = ("order", "is_active") list_editable = ("order", "is_active", "show_on_homepage", "homepage_order")
raw_id_fields = ("main_product",) raw_id_fields = ("main_product",)
inlines = [ArticleInline, SubProductVersionInline] inlines = [ArticleInline, SubProductVersionInline]
fieldsets = ( fieldsets = (
(None, {"fields": ("main_product", "name", "slug", "distribution", "short_description")}), (None, {"fields": ("main_product", "name", "slug", "distribution", "short_description")}),
("Description", {"fields": ("description_format", "description")}), ("Description", {"fields": ("description_format", "description")}),
("Media", {"fields": ("image",)}), ("Media", {"fields": ("image", "logo")}),
("Homepage", {"fields": ("show_on_homepage", "homepage_order")}),
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
) )
@@ -92,10 +102,11 @@ class ArticleAdmin(admin.ModelAdmin):
search_fields = ("title", "description") search_fields = ("title", "description")
list_editable = ("order",) list_editable = ("order",)
raw_id_fields = ("sub_product",) raw_id_fields = ("sub_product",)
inlines = [ArticleSectionInline] inlines = [ArticleSectionInline, ArticleCitationInline]
fieldsets = ( fieldsets = (
(None, {"fields": ("sub_product", "title")}), (None, {"fields": ("sub_product", "badge", "title")}),
("Description", {"fields": ("description_format", "description")}), ("Description", {"fields": ("description_format", "description")}),
("Citation Badge", {"fields": ("citation_count_display",), "description": "Set a number to show the dashed citation circle badge at the bottom-right of the article card. Leave blank to hide it."}),
("Settings", {"fields": ("order",)}), ("Settings", {"fields": ("order",)}),
) )
@@ -0,0 +1,22 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("products", "0004_distribution_and_versions"),
]
operations = [
migrations.AddField(
model_name="article",
name="badge",
field=models.CharField(
blank=True,
default="",
help_text="Optional label shown above the article title (e.g. 'Overview', 'Note').",
max_length=100,
),
preserve_default=False,
),
]
@@ -0,0 +1,49 @@
# Generated by Django 5.0.2 on 2026-05-17 14:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0005_article_badge'),
]
operations = [
migrations.AddField(
model_name='subproduct',
name='homepage_order',
field=models.PositiveIntegerField(default=0, help_text='Order within the homepage Products section.'),
),
migrations.AddField(
model_name='subproduct',
name='logo',
field=models.ImageField(blank=True, help_text='Small logo shown as a corner badge on homepage product cards.', null=True, upload_to='products/sub_logos/'),
),
migrations.AddField(
model_name='subproduct',
name='show_on_homepage',
field=models.BooleanField(default=False, help_text='Display this sub-product in the homepage Products section.'),
),
migrations.AlterField(
model_name='subproductversion',
name='package_resource_url',
field=models.URLField(blank=True, help_text='Shown on the site when set. For Resources it is the main Open link; for Installable it appears under the platform grid.'),
),
migrations.CreateModel(
name='ArticleCitation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField(help_text='Full citation text.')),
('url', models.URLField(blank=True, help_text='Optional link to the cited source.')),
('order', models.PositiveIntegerField(default=0)),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='citations', to='products.article')),
],
options={
'verbose_name': 'Article Citation',
'verbose_name_plural': 'Article Citations',
'ordering': ['order', 'pk'],
},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2026-05-17 15:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0006_subproduct_logo_homepage_citations'),
]
operations = [
migrations.AddField(
model_name='article',
name='citation_count_display',
field=models.PositiveSmallIntegerField(blank=True, help_text='Optional number shown in the dashed citation circle badge at the bottom of the article card. Leave blank to hide the badge.', null=True),
),
]
+32
View File
@@ -69,8 +69,11 @@ class SubProduct(models.Model):
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
) )
image = models.ImageField(upload_to="products/sub/", blank=True, null=True) image = models.ImageField(upload_to="products/sub/", blank=True, null=True)
logo = models.ImageField(upload_to="products/sub_logos/", blank=True, null=True, help_text="Small logo shown as a corner badge on homepage product cards.")
order = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
show_on_homepage = models.BooleanField(default=False, help_text="Display this sub-product in the homepage Products section.")
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Products section.")
distribution = models.CharField( distribution = models.CharField(
max_length=20, max_length=20,
choices=DISTRIBUTION_CHOICES, choices=DISTRIBUTION_CHOICES,
@@ -123,11 +126,21 @@ class Article(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="articles", related_name="articles",
) )
badge = models.CharField(
max_length=100,
blank=True,
help_text="Optional label shown above the article title (e.g. 'Overview', 'Note').",
)
title = models.CharField(max_length=300) title = models.CharField(max_length=300)
description = models.TextField() description = models.TextField()
description_format = models.CharField( description_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
) )
citation_count_display = models.PositiveSmallIntegerField(
null=True,
blank=True,
help_text="Optional number shown in the dashed citation circle badge at the bottom of the article card. Leave blank to hide the badge.",
)
order = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -219,3 +232,22 @@ class ArticleSection(models.Model):
def __str__(self): def __str__(self):
return f"{self.article.title} {self.title}" return f"{self.article.title} {self.title}"
class ArticleCitation(models.Model):
article = models.ForeignKey(
Article,
on_delete=models.CASCADE,
related_name="citations",
)
text = models.TextField(help_text="Full citation text.")
url = models.URLField(blank=True, help_text="Optional link to the cited source.")
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["order", "pk"]
verbose_name = "Article Citation"
verbose_name_plural = "Article Citations"
def __str__(self):
return f"{self.article.title} — citation {self.order or self.pk}"
+244 -13
View File
@@ -264,14 +264,14 @@ h1, h2, h3, h4, h5, h6 {
.blob--1 { .blob--1 {
width: 520px; height: 520px; width: 520px; height: 520px;
background: radial-gradient(circle, rgba(79, 142, 247, 0.4) 0%, rgba(79, 142, 247, 0.1) 70%); background: radial-gradient(circle, rgba(79, 142, 247, 0.18) 0%, rgba(79, 142, 247, 0.05) 70%);
top: -120px; left: -120px; top: -120px; left: -120px;
animation-delay: 0s, 0s; animation-delay: 0s, 0s;
} }
.blob--2 { .blob--2 {
width: 440px; height: 440px; width: 440px; height: 440px;
background: radial-gradient(circle, rgba(167, 139, 250, 0.35) 0%, rgba(167, 139, 250, 0.08) 70%); background: radial-gradient(circle, rgba(167, 139, 250, 0.15) 0%, rgba(167, 139, 250, 0.04) 70%);
top: 80px; right: -100px; top: 80px; right: -100px;
animation-delay: 0.3s, 3s; animation-delay: 0.3s, 3s;
animation-duration: 1.2s, 18s; animation-duration: 1.2s, 18s;
@@ -279,7 +279,7 @@ h1, h2, h3, h4, h5, h6 {
.blob--3 { .blob--3 {
width: 340px; height: 340px; width: 340px; height: 340px;
background: radial-gradient(circle, rgba(34, 211, 238, 0.25) 0%, rgba(34, 211, 238, 0.06) 70%); background: radial-gradient(circle, rgba(34, 211, 238, 0.12) 0%, rgba(34, 211, 238, 0.03) 70%);
bottom: 0; left: 35%; bottom: 0; left: 35%;
animation-delay: 0.6s, 6s; animation-delay: 0.6s, 6s;
animation-duration: 1.2s, 20s; animation-duration: 1.2s, 20s;
@@ -342,14 +342,12 @@ h1, h2, h3, h4, h5, h6 {
} }
.brand-icon { .brand-icon {
width: 34px; height: 34px; width: 34px;
display: flex; align-items: center; justify-content: center; height: 34px;
background: var(--gradient-brand); border-radius: 8px;
border-radius: 9px; object-fit: cover;
font-size: 1rem; display: block;
font-weight: 800; flex-shrink: 0;
color: #fff;
letter-spacing: -0.03em;
} }
.brand-name { .brand-name {
@@ -822,6 +820,18 @@ h1, h2, h3, h4, h5, h6 {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
}
.product-card-logo {
position: absolute;
top: 1rem;
right: 1rem;
width: 32px;
height: 32px;
object-fit: contain;
border-radius: var(--radius-sm);
opacity: 0.85;
} }
.product-card-title { .product-card-title {
@@ -883,6 +893,57 @@ h1, h2, h3, h4, h5, h6 {
line-height: 1.65; line-height: 1.65;
} }
/* ============================================================
Supporters Grid
============================================================ */
.supporters-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.25rem;
}
.supporter-card {
padding: 1.75rem;
display: flex;
align-items: flex-start;
gap: 1rem;
}
.supporter-logo {
flex-shrink: 0;
width: 52px;
height: 52px;
border-radius: var(--radius-sm);
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
}
.supporter-logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
.supporter-body {
min-width: 0;
}
.supporter-name {
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 0.35rem;
color: var(--text-primary);
}
.supporter-desc {
font-size: 0.83rem;
color: var(--text-secondary);
line-height: 1.55;
}
/* ============================================================ /* ============================================================
About Strip About Strip
============================================================ */ ============================================================ */
@@ -1120,7 +1181,119 @@ h1, h2, h3, h4, h5, h6 {
} }
.article-card { .article-card {
padding: 2rem; padding: 2rem 2rem 5.5rem 2rem;
position: relative;
}
.article-card-header {
margin-bottom: 0.5rem;
}
.citation-count-badge {
position: absolute;
bottom: 1.25rem;
right: 1.5rem;
width: 64px;
height: 64px;
border-radius: 50%;
border: 2px dashed rgba(79, 142, 247, 0.4);
background: rgba(79, 142, 247, 0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
opacity: 0.7;
text-decoration: none;
color: var(--accent-blue-light);
transition: opacity 0.2s, border-color 0.2s, background 0.2s;
cursor: default;
}
a.citation-count-badge {
cursor: pointer;
}
a.citation-count-badge:hover {
opacity: 1;
border-color: rgba(79, 142, 247, 0.65);
background: rgba(79, 142, 247, 0.12);
}
.citation-count-number {
font-size: 1.3rem;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
}
.citation-count-label {
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.8;
}
.article-citations {
margin-top: 1.25rem;
padding-top: 1.25rem;
border-top: 1px solid var(--glass-border);
}
.article-citations-title {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--accent-blue-light);
margin-bottom: 0.75rem;
}
.article-citations-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
counter-reset: citations;
}
.article-citation-item {
display: flex;
align-items: baseline;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.55;
counter-increment: citations;
}
.article-citation-item::before {
content: counter(citations) ".";
font-size: 0.72rem;
font-weight: 700;
color: var(--accent-blue-light);
flex-shrink: 0;
min-width: 1.2em;
}
.citation-text {
flex: 1;
}
.citation-link {
flex-shrink: 0;
color: var(--accent-blue-light);
opacity: 0.75;
transition: opacity 0.2s;
display: flex;
align-items: center;
}
.citation-link:hover {
opacity: 1;
} }
.article-title { .article-title {
@@ -1640,11 +1813,21 @@ h1, h2, h3, h4, h5, h6 {
.rich-content h2 { font-size: 1.25rem; } .rich-content h2 { font-size: 1.25rem; }
.rich-content h3 { font-size: 1.05rem; } .rich-content h3 { font-size: 1.05rem; }
.rich-content ul, .rich-content ol { .rich-content ul {
list-style-type: disc;
padding-left: 1.5em; padding-left: 1.5em;
margin-bottom: 0.85em; margin-bottom: 0.85em;
} }
.rich-content ol {
list-style-type: decimal;
padding-left: 1.5em;
margin-bottom: 0.85em;
}
.rich-content ul ul { list-style-type: circle; }
.rich-content ul ul ul { list-style-type: square; }
.rich-content li { margin-bottom: 0.3em; line-height: 1.65; } .rich-content li { margin-bottom: 0.3em; line-height: 1.65; }
.rich-content a { .rich-content a {
@@ -2868,3 +3051,51 @@ h1, h2, h3, h4, h5, h6 {
flex-direction: column; flex-direction: column;
} }
} }
/* ============================================================
Read More / Content Truncation
============================================================ */
.readmore-wrap {
position: relative;
overflow: hidden;
transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.readmore-wrap.is-clamped {
-webkit-mask-image: linear-gradient(to bottom, black 45%, transparent 92%);
mask-image: linear-gradient(to bottom, black 45%, transparent 92%);
}
.readmore-wrap:not(.is-clamped) {
-webkit-mask-image: none;
mask-image: none;
}
.readmore-btn {
display: inline-flex;
align-items: center;
gap: 0.3em;
font-size: 0.8rem;
font-weight: 600;
color: var(--accent-blue-light);
background: none;
border: none;
cursor: pointer;
padding: 0.3rem 0;
margin-top: 0.5rem;
line-height: 1;
transition: color 0.18s ease;
}
.readmore-btn:hover {
color: var(--accent-cyan);
}
.readmore-btn svg {
flex-shrink: 0;
transition: transform 0.22s ease;
}
.readmore-btn.is-open svg {
transform: rotate(180deg);
}
+39
View File
@@ -152,12 +152,51 @@ function initIntersectionObserver() {
}); });
} }
function initReadMore() {
var chevronSVG = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
document.querySelectorAll('[data-readmore]').forEach(function (el) {
var threshold = parseInt(el.getAttribute('data-readmore'), 10) || 120;
var fullHeight = el.scrollHeight;
if (fullHeight <= threshold + 12) return;
var storedHeight = fullHeight;
el.classList.add('readmore-wrap', 'is-clamped');
el.style.maxHeight = threshold + 'px';
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'readmore-btn';
btn.setAttribute('aria-expanded', 'false');
btn.innerHTML = 'Read more ' + chevronSVG;
el.insertAdjacentElement('afterend', btn);
btn.addEventListener('click', function () {
var collapsed = el.classList.toggle('is-clamped');
if (collapsed) {
el.style.maxHeight = threshold + 'px';
btn.innerHTML = 'Read more ' + chevronSVG;
btn.setAttribute('aria-expanded', 'false');
btn.classList.remove('is-open');
} else {
el.style.maxHeight = storedHeight + 'px';
btn.innerHTML = 'Read less ' + chevronSVG;
btn.setAttribute('aria-expanded', 'true');
btn.classList.add('is-open');
}
});
});
}
function init() { function init() {
initNavbarScroll(); initNavbarScroll();
initMobileMenu(); initMobileMenu();
initMegaMenu(); initMegaMenu();
initFAQAccordion(); initFAQAccordion();
initIntersectionObserver(); initIntersectionObserver();
initReadMore();
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
+88 -87
View File
@@ -1,11 +1,14 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% block title %}What is Radiuma{% endblock %} {% block title %}About{% endblock %}
{% block meta_description %}Learn about Radiuma and Radiuma — Visualized & Standardized Environment for Radiomics Analysis, developed at UBC and BC Cancer Research Institute.{% endblock %} {% block meta_description %}Learn about Radiuma — Visualized & Standardized Environment for Radiomics Analysis, developed at UBC and BC Cancer Research Institute.{% endblock %}
{% block content %} {% block content %}
{% for section in about_sections %}
{% if section.section_type == "hero" %}
<section class="page-hero" aria-labelledby="page-hero-heading"> <section class="page-hero" aria-labelledby="page-hero-heading">
<div class="page-hero-blobs" aria-hidden="true"> <div class="page-hero-blobs" aria-hidden="true">
<div class="blob blob--1"></div> <div class="blob blob--1"></div>
@@ -15,125 +18,123 @@
</div> </div>
<div class="container"> <div class="container">
<div class="page-hero-content"> <div class="page-hero-content">
<div class="section-badge">About</div> {% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h1 class="page-hero-title" id="page-hero-heading">What is Radiuma?</h1> {% if section.title %}<h1 class="page-hero-title" id="page-hero-heading">{{ section.title }}</h1>{% endif %}
<p class="page-hero-subtitle"> {% if section.subtitle %}<p class="page-hero-subtitle">{{ section.subtitle }}</p>{% endif %}
Visualized &amp; Standardized Environment for Radiomics Analysis {% if section.content %}<div class="rich-content page-hero-body" data-readmore="160">{{ section.rendered_content }}</div>{% endif %}
</p>
</div> </div>
</div> </div>
</section> </section>
<section class="section" aria-labelledby="about-intro-heading"> {% elif section.section_type == "intro" %}
<section class="section" aria-labelledby="about-intro-{{ section.pk }}-heading">
<div class="container"> <div class="container">
<div class="about-intro glass-card fade-in"> <div class="about-intro glass-card fade-in">
<div class="about-intro-text"> <div class="about-intro-text">
<h2 id="about-intro-heading">Desktop Software for Medical Imaging Research</h2> {% if section.title %}<h2 id="about-intro-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
<ul class="about-list"> {% if section.content %}
<li>Desktop software to improve usability, reusability, and reproducibility in medical imaging &amp; healthcare research.</li> <div class="rich-content" data-readmore="200">{{ section.rendered_content }}</div>
<li>Development platform to create reproducible research workflows by connecting different tools.</li> {% endif %}
<li>Useful for collaborative research projects and for ensuring consistency across different studies.</li>
<li>User-friendly for different expertise levels, including radiation oncologists, radiologists, physicists &amp; data scientists.</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section class="section" aria-labelledby="standardization-heading"> {% elif section.section_type == "grid" %}
<section class="section" aria-labelledby="grid-{{ section.pk }}-heading">
<div class="container"> <div class="container">
{% if section.badge or section.title %}
<div class="section-header"> <div class="section-header">
<div class="section-badge">Standards</div> {% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="standardization-heading">Standardization</h2> {% if section.title %}<h2 class="section-title" id="grid-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
</div> </div>
{% endif %}
{% if section.content %}<div class="rich-content" data-readmore="200">{{ section.rendered_content }}</div>{% endif %}
{% with items=section.items.all %}
{% if items %}
<div class="standards-grid"> <div class="standards-grid">
{% for item in items %}
<div class="standard-card glass-card fade-in"> <div class="standard-card glass-card fade-in">
<div class="standard-badge">IBSI 1.0</div> {% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
<h3 class="standard-title">Radiomic Feature Extraction</h3> {% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %}
<p class="standard-desc"> {% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
Radiuma is a python-based open-source package that enables standardized and
reproducible radiomic feature extraction in compliance with the Image Biomarker
Standardization Initiative (IBSI 1.0).
</p>
</div>
<div class="standard-card glass-card fade-in">
<div class="standard-badge">IBSI 2.0</div>
<h3 class="standard-title">Image Filtering</h3>
<p class="standard-desc">
Image filters have been standardized against IBSI 2.0 by implementing and
validating several filter options, ensuring reproducibility across research
institutions worldwide.
</p>
</div>
<div class="standard-card glass-card fade-in">
<div class="standard-badge">Python</div>
<h3 class="standard-title">Open Source</h3>
<p class="standard-desc">
Radiuma is a major, entirely-revamped upgrade to the original SERA (Matlab-based),
now built on Python for broader accessibility and community contribution.
</p>
</div>
<div class="standard-card glass-card fade-in">
<div class="standard-badge">End-to-End</div>
<h3 class="standard-title">Standardized Workflows</h3>
<p class="standard-desc">
Radiuma employs a number of popular image processing algorithms to create
end-to-end standardized workflows for consistent, reproducible research results.
</p>
</div> </div>
{% endfor %}
</div> </div>
{% endif %}
{% endwith %}
</div> </div>
</section> </section>
<section class="section" aria-labelledby="screenshots-about-heading"> {% elif section.section_type == "history" %}
<div class="container"> <section class="section" aria-labelledby="history-{{ section.pk }}-heading">
<div class="section-header">
<div class="section-badge">Interface</div>
<h2 class="section-title" id="screenshots-about-heading">Radiuma in Action</h2>
</div>
<div class="about-screenshots-grid">
<div class="screenshot-item screenshot-item--featured fade-in">
<img src="{% static 'images/screenshot-11.jpg' %}" alt="Radiuma multi-modal viewer" loading="lazy" />
</div>
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-9.jpg' %}" alt="Radiuma settings panel" loading="lazy" />
</div>
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-10.jpg' %}" alt="Radiuma analysis results" loading="lazy" />
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="history-heading">
<div class="container"> <div class="container">
<div class="history-block glass-card fade-in"> <div class="history-block glass-card fade-in">
<div class="history-content"> <div class="history-content">
<div class="section-badge">History</div> {% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 id="history-heading">Our Origins</h2> {% if section.title %}<h2 id="history-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
<p> {% if section.content %}<div class="rich-content" data-readmore="200">{{ section.rendered_content }}</div>{% endif %}
Radiuma has been developing since 2021 by the {% with links=section.items.all %}
Quantitative Radiomolecular Imaging and Therapy (Qurit) lab &amp; program at the {% if links %}
University of British Columbia &amp; BC Cancer Research Institute, Vancouver, BC, Canada.
</p>
<p>
Radiuma's mission is to bridge the gap between cutting-edge radiomics research and
practical clinical application by providing standardized, reproducible, and
user-friendly software tools.
</p>
<div class="history-links"> <div class="history-links">
<a href="https://www.qurit.ca" class="btn-ghost" target="_blank" rel="noopener noreferrer">Qurit Lab</a> {% for link in links %}
<a href="https://www.ubc.ca" class="btn-ghost" target="_blank" rel="noopener noreferrer">UBC</a> {% if link.url %}
<a href="https://www.bccrc.ca" class="btn-ghost" target="_blank" rel="noopener noreferrer">BC Cancer</a> <a href="{{ link.url }}" class="btn-ghost" target="_blank" rel="noopener noreferrer">{{ link.title }}</a>
{% endif %}
{% endfor %}
</div> </div>
{% endif %}
{% endwith %}
</div> </div>
<div class="history-visual" aria-hidden="true"> <div class="history-visual" aria-hidden="true">
<div class="history-blob history-blob--1"></div> <div class="history-blob history-blob--1"></div>
<div class="history-blob history-blob--2"></div> <div class="history-blob history-blob--2"></div>
<div class="history-year">2021</div> {% if section.subtitle %}<div class="history-year">{{ section.subtitle }}</div>{% endif %}
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{% elif section.section_type == "custom" %}
<section class="section" aria-labelledby="custom-{{ section.pk }}-heading">
<div class="container">
{% if section.badge or section.title %}
<div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
{% if section.title %}<h2 class="section-title" id="custom-{{ section.pk }}-heading">{{ section.title }}</h2>{% endif %}
</div>
{% endif %}
{% if section.content %}
<div class="glass-card fade-in">
<div class="rich-content" style="padding: 2rem;" data-readmore="200">{{ section.rendered_content }}</div>
</div>
{% endif %}
{% with items=section.items.all %}
{% if items %}
<div class="standards-grid" style="margin-top: 1.5rem;">
{% for item in items %}
<div class="standard-card glass-card fade-in">
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
{% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %}
{% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
{% if item.url %}<a href="{{ item.url }}" class="btn-ghost btn--sm" style="margin-top: 0.75rem;" target="_blank" rel="noopener noreferrer">{{ item.title }}</a>{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
</section>
{% endif %}
{% empty %}
<section class="section">
<div class="container">
<p style="color: var(--text-secondary); text-align: center; padding: 4rem 0;">
No content configured yet. Add sections in the admin panel.
</p>
</div>
</section>
{% endfor %}
{% endblock %} {% endblock %}
+1 -1
View File
@@ -85,7 +85,7 @@
</div> </div>
<h3 class="contact-card-title">Email Support</h3> <h3 class="contact-card-title">Email Support</h3>
<p class="contact-card-desc">For direct software support:</p> <p class="contact-card-desc">For direct software support:</p>
<a href="mailto:support@visera.ca" class="contact-email-link">support@visera.ca</a> <a href="mailto:support@radiuma.ca" class="contact-email-link">support@radiuma.ca</a>
</div> </div>
<div class="contact-card glass-card fade-in"> <div class="contact-card glass-card fade-in">
+139 -163
View File
@@ -42,181 +42,157 @@
</div> </div>
</section> </section>
<section class="section features-section" aria-labelledby="features-heading"> {% for section in homepage_sections %}
<div class="container">
<div class="section-header">
<div class="section-badge">Capabilities</div>
<h2 class="section-title" id="features-heading">Important Features</h2>
<p class="section-desc">
Comprehensive tools for medical imaging research, standardized and reproducible.
</p>
</div>
<div class="features-grid">
{% for feature in features %}
<div class="feature-card glass-card fade-in">
<div class="feature-icon" aria-hidden="true">{{ feature.icon }}</div>
<h3 class="feature-title">{{ feature.title }}</h3>
<p class="feature-desc">{{ feature.desc }}</p>
</div>
{% empty %}
<div class="feature-card glass-card fade-in">
<div class="feature-icon" aria-hidden="true">⚗️</div>
<h3 class="feature-title">Image Filtering</h3>
<p class="feature-desc">Standardized image filtering techniques compliant with IBSI 2.0 guidelines.</p>
</div>
<div class="feature-card glass-card fade-in">
<div class="feature-icon" aria-hidden="true">🖥️</div>
<h3 class="feature-title">Professional Viewer</h3>
<p class="feature-desc">Comfortable, professional medical image viewer with multi-modality support.</p>
</div>
<div class="feature-card glass-card fade-in">
<div class="feature-icon" aria-hidden="true">📊</div>
<h3 class="feature-title">Radiomics Features</h3>
<p class="feature-desc">Handcrafted radiomics feature generation standardized by IBSI 1.0.</p>
</div>
<div class="feature-card glass-card fade-in">
<div class="feature-icon" aria-hidden="true">🔄</div>
<h3 class="feature-title">Format Support</h3>
<p class="feature-desc">NIFTI, DICOM, NRRD, and more — comprehensive multi-format support.</p>
</div>
<div class="feature-card glass-card fade-in">
<div class="feature-icon" aria-hidden="true">🗂️</div>
<h3 class="feature-title">Image Registration</h3>
<p class="feature-desc">Advanced image registration, fusion, and standardized SUV conversion.</p>
</div>
<div class="feature-card glass-card fade-in">
<div class="feature-icon" aria-hidden="true">🔬</div>
<h3 class="feature-title">RT Struct Support</h3>
<p class="feature-desc">Full RT struct support for radiation oncology workflows and research.</p>
</div>
{% endfor %}
</div>
</div>
</section>
<section class="section screenshots-section" aria-labelledby="screenshots-heading"> {% if section.section_type == "features" %}
<div class="container"> <section class="section features-section" aria-labelledby="features-heading-{{ section.pk }}">
<div class="section-header"> <div class="container">
<div class="section-badge">Gallery</div> <div class="section-header">
<h2 class="section-title" id="screenshots-heading">See Radiuma in Action</h2> {% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<p class="section-desc"> <h2 class="section-title" id="features-heading-{{ section.pk }}">{{ section.title }}</h2>
Explore Radiuma's powerful interface, workflow builder, and multi-modal image viewer. {% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</p>
</div>
<div class="screenshots-grid">
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-2.jpg' %}" alt="Radiuma full workflow interface" loading="lazy" />
</div> </div>
<div class="screenshot-item fade-in"> <div class="features-grid">
<img src="{% static 'images/screenshot-3.png' %}" alt="Radiuma panel view" loading="lazy" /> {% for item in section.items.all %}
</div> <div class="feature-card glass-card fade-in">
<div class="screenshot-item fade-in"> {% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
<img src="{% static 'images/screenshot-5.png' %}" alt="Radiuma segmentation view" loading="lazy" /> <h3 class="feature-title">{{ item.title }}</h3>
</div> <p class="feature-desc" data-readmore="88">{{ item.content }}</p>
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-8.jpg' %}" alt="Radiuma radiomics results" loading="lazy" />
</div>
</div>
</div>
</section>
{% if nav_main_products %}
<section class="section products-section" aria-labelledby="products-heading">
<div class="container">
<div class="section-header">
<div class="section-badge">Our Software</div>
<h2 class="section-title" id="products-heading">Products</h2>
<p class="section-desc">Explore our suite of medical imaging and radiomics tools.</p>
</div>
<div class="products-grid">
{% for product in nav_main_products %}
<a href="{{ product.get_absolute_url }}" class="product-card glass-card fade-in">
{% if product.image %}
<div class="product-card-image">
<img src="{{ product.image.url }}" alt="{{ product.name }}" loading="lazy" />
</div> </div>
{% endif %} {% endfor %}
<div class="product-card-body"> </div>
<h3 class="product-card-title">{{ product.name }}</h3> </div>
<p class="product-card-desc">{{ product.short_description }}</p> </section>
<span class="product-card-link">
Explore {% elif section.section_type == "screenshots" %}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <section class="section screenshots-section" aria-labelledby="screenshots-heading-{{ section.pk }}">
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <div class="container">
</svg> <div class="section-header">
</span> {% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="screenshots-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
<div class="screenshots-grid">
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-2.jpg' %}" alt="Radiuma full workflow interface" loading="lazy" />
</div> </div>
</a> <div class="screenshot-item fade-in">
{% endfor %} <img src="{% static 'images/screenshot-3.png' %}" alt="Radiuma panel view" loading="lazy" />
</div>
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-5.png' %}" alt="Radiuma segmentation view" loading="lazy" />
</div>
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-8.jpg' %}" alt="Radiuma radiomics results" loading="lazy" />
</div>
</div>
</div> </div>
</div> </section>
</section>
{% endif %}
<section class="section problems-section" aria-labelledby="problems-heading"> {% elif section.section_type == "products" %}
<div class="container"> {% if sub_products %}
<div class="section-header"> <section class="section products-section" aria-labelledby="products-heading-{{ section.pk }}">
<div class="section-badge">Value Proposition</div> <div class="container">
<h2 class="section-title" id="problems-heading">What Problems Does Radiuma Solve?</h2> <div class="section-header">
</div> {% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<div class="problems-grid"> <h2 class="section-title" id="products-heading-{{ section.pk }}">{{ section.title }}</h2>
<div class="problem-item glass-card fade-in"> {% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
<div class="problem-number" aria-hidden="true">01</div>
<h3 class="problem-title">Accessibility</h3>
<p class="problem-desc">
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.
</p>
</div> </div>
<div class="problem-item glass-card fade-in"> <div class="products-grid">
<div class="problem-number" aria-hidden="true">02</div> {% for product in sub_products %}
<h3 class="problem-title">Integrated Tools</h3> <a href="{{ product.get_absolute_url }}" class="product-card glass-card fade-in">
<p class="problem-desc"> {% if product.image %}
Radiuma integrates a vast collection of tools and resources from various domains <div class="product-card-image">
of healthcare and medical imaging research in a common, unified environment. <img src="{{ product.image.url }}" alt="{{ product.name }}" loading="lazy" />
</p> </div>
</div> {% endif %}
<div class="problem-item glass-card fade-in"> <div class="product-card-body">
<div class="problem-number" aria-hidden="true">03</div> {% if product.logo %}
<h3 class="problem-title">Flexibility</h3> <img src="{{ product.logo.url }}" alt="" class="product-card-logo" aria-hidden="true" loading="lazy" />
<p class="problem-desc"> {% endif %}
Radiuma offers flexibility in terms of tool optimization and workflow customization <h3 class="product-card-title">{{ product.name }}</h3>
to match your specific research requirements. <p class="product-card-desc" data-readmore="88">{{ product.short_description }}</p>
</p> <span class="product-card-link">
</div> Explore
<div class="problem-item glass-card fade-in"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<div class="problem-number" aria-hidden="true">04</div> <path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<h3 class="problem-title">Reproducibility</h3> </svg>
<p class="problem-desc"> </span>
Improve usability, reusability, and reproducibility (URR) through a workflow </div>
management system that allows researchers to easily create, share, and reuse </a>
analysis pipelines. {% endfor %}
</p>
</div> </div>
</div> </div>
</div> </section>
</section> {% endif %}
<section class="section about-strip" aria-labelledby="about-strip-heading"> {% elif section.section_type == "problems" %}
<div class="container"> <section class="section problems-section" aria-labelledby="problems-heading-{{ section.pk }}">
<div class="about-strip-inner glass-card"> <div class="container">
<div class="about-strip-content"> <div class="section-header">
<div class="section-badge">Our Story</div> {% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="about-strip-heading">More to Know</h2> <h2 class="section-title" id="problems-heading-{{ section.pk }}">{{ section.title }}</h2>
<p class="about-strip-text"> {% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
Radiuma has been developing since 2021 by the Quantitative Radiomolecular Imaging
and Therapy (Qurit) lab &amp; program at the University of British Columbia &amp;
BC Cancer Research Institute, Vancouver, BC, Canada.
</p>
<a href="{% url 'pages:about' %}" class="btn-primary">Learn More</a>
</div> </div>
<div class="about-strip-blobs" aria-hidden="true"> <div class="problems-grid">
<div class="strip-blob strip-blob--1"></div> {% for item in section.items.all %}
<div class="strip-blob strip-blob--2"></div> <div class="problem-item glass-card fade-in">
{% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %}
<h3 class="problem-title">{{ item.title }}</h3>
<p class="problem-desc" data-readmore="88">{{ item.content }}</p>
</div>
{% endfor %}
</div> </div>
</div> </div>
</div> </section>
</section>
{% elif section.section_type == "supporters" %}
<section class="section supporters-section" aria-labelledby="supporters-heading-{{ section.pk }}">
<div class="container">
<div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="supporters-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
<div class="supporters-grid">
{% for item in section.items.all %}
<div class="supporter-card glass-card fade-in">
{% if item.image %}
<div class="supporter-logo">
<img src="{{ item.image.url }}" alt="{{ item.title }} logo" loading="lazy" />
</div>
{% endif %}
<div class="supporter-body">
<h3 class="supporter-name">{{ item.title }}</h3>
{% if item.content %}<p class="supporter-desc">{{ item.content }}</p>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>
{% elif section.section_type == "about_strip" %}
<section class="section about-strip" aria-labelledby="about-strip-heading-{{ section.pk }}">
<div class="container">
<div class="about-strip-inner glass-card">
<div class="about-strip-content">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="about-strip-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="about-strip-text">{{ section.description }}</p>{% endif %}
{% if section.link_text and section.link_url %}
<a href="{{ section.link_url }}" class="btn-primary">{{ section.link_text }}</a>
{% endif %}
</div>
<div class="about-strip-blobs" aria-hidden="true">
<div class="strip-blob strip-blob--1"></div>
<div class="strip-blob strip-blob--2"></div>
</div>
</div>
</div>
</section>
{% endif %}
{% endfor %}
{% endblock %} {% endblock %}
+1 -1
View File
@@ -52,7 +52,7 @@
<p>675 West 10th Ave, Office 6-112</p> <p>675 West 10th Ave, Office 6-112</p>
<p>Vancouver, BC, V5Z 1L3</p> <p>Vancouver, BC, V5Z 1L3</p>
</address> </address>
<a href="mailto:support@visera.ca" class="footer-email">support@visera.ca</a> <a href="mailto:support@radiuma.ca" class="footer-email">support@radiuma.ca</a>
</div> </div>
</div> </div>
+2 -2
View File
@@ -3,7 +3,7 @@
<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="Radiuma home">
<span class="brand-icon">R</span> <img src="{% static 'images/favicon-180.png' %}" alt="" class="brand-icon" width="34" height="34" aria-hidden="true" />
<span class="brand-name">Radiuma</span> <span class="brand-name">Radiuma</span>
</a> </a>
@@ -68,7 +68,7 @@
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'pages:about' %}" class="nav-link {% if request.resolver_match.url_name == 'about' %}active{% endif %}"> <a href="{% url 'pages:about' %}" class="nav-link {% if request.resolver_match.url_name == 'about' %}active{% endif %}">
What is Radiuma About
</a> </a>
</li> </li>
+2 -2
View File
@@ -43,7 +43,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="product-detail-text"> <div class="product-detail-text">
<div class="product-detail-desc rich-content">{{ main_product.rendered_description }}</div> <div class="product-detail-desc rich-content" data-readmore="220">{{ main_product.rendered_description }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -71,7 +71,7 @@
{% endif %} {% endif %}
<div class="subproduct-card-body"> <div class="subproduct-card-body">
<h3 class="subproduct-card-title">{{ sub.name }}</h3> <h3 class="subproduct-card-title">{{ sub.name }}</h3>
<p class="subproduct-card-desc">{{ sub.short_description }}</p> <p class="subproduct-card-desc" data-readmore="88">{{ sub.short_description }}</p>
<span class="subproduct-card-link"> <span class="subproduct-card-link">
Learn More Learn More
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
+1 -1
View File
@@ -35,7 +35,7 @@
<div class="product-overview-body"> <div class="product-overview-body">
<h2 class="product-overview-title">{{ product.name }}</h2> <h2 class="product-overview-title">{{ product.name }}</h2>
<p class="product-overview-short">{{ product.short_description }}</p> <p class="product-overview-short">{{ product.short_description }}</p>
<p class="product-overview-desc">{{ product.description }}</p> <p class="product-overview-desc" data-readmore="110">{{ product.description }}</p>
<a href="{{ product.get_absolute_url }}" class="btn-primary">Explore {{ product.name }}</a> <a href="{{ product.get_absolute_url }}" class="btn-primary">Explore {{ product.name }}</a>
</div> </div>
{% with subs=product.sub_products.all %} {% with subs=product.sub_products.all %}
+36 -3
View File
@@ -44,7 +44,7 @@
<div class="sub-product-desc glass-card fade-in"> <div class="sub-product-desc glass-card fade-in">
<h2>About {{ sub_product.name }}</h2> <h2>About {{ sub_product.name }}</h2>
<div class="rich-content">{{ sub_product.rendered_description }}</div> <div class="rich-content" data-readmore="220">{{ sub_product.rendered_description }}</div>
</div> </div>
{% if show_releases_section %} {% if show_releases_section %}
@@ -154,20 +154,53 @@
<div class="articles-list"> <div class="articles-list">
{% for article in articles %} {% for article in articles %}
<article class="article-card glass-card fade-in" aria-labelledby="article-{{ article.pk }}-title"> <article class="article-card glass-card fade-in" aria-labelledby="article-{{ article.pk }}-title">
{% if article.badge %}<div class="article-card-header"><div class="section-badge article-badge">{{ article.badge }}</div></div>{% endif %}
<h3 class="article-title" id="article-{{ article.pk }}-title">{{ article.title }}</h3> <h3 class="article-title" id="article-{{ article.pk }}-title">{{ article.title }}</h3>
<div class="article-description rich-content">{{ article.rendered_description }}</div> <div class="article-description rich-content" data-readmore="260">{{ article.rendered_description }}</div>
{% if article.sections.all %} {% if article.sections.all %}
<div class="article-sections"> <div class="article-sections">
<dl class="article-sections-list"> <dl class="article-sections-list">
{% for section in article.sections.all %} {% for section in article.sections.all %}
<div class="article-section-item"> <div class="article-section-item">
<dt class="section-key">{{ section.title }}</dt> <dt class="section-key">{{ section.title }}</dt>
<dd class="section-value rich-content">{{ section.rendered_value }}</dd> <dd class="section-value rich-content" data-readmore="160">{{ section.rendered_value }}</dd>
</div> </div>
{% endfor %} {% endfor %}
</dl> </dl>
</div> </div>
{% endif %} {% endif %}
{% if article.citations.all %}
<div class="article-citations" id="article-{{ article.pk }}-citations">
<h4 class="article-citations-title">Citations</h4>
<ol class="article-citations-list">
{% for citation in article.citations.all %}
<li class="article-citation-item">
<span class="citation-text">{{ citation.text }}</span>
{% if citation.url %}
<a href="{{ citation.url }}" class="citation-link" target="_blank" rel="noopener noreferrer" aria-label="Open citation source">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M7 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1V9M10 2h4m0 0v4m0-4L7 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
{% endif %}
</li>
{% endfor %}
</ol>
</div>
{% endif %}
{% if article.citation_count_display is not None %}
{% if article.citations.exists %}
<a href="#article-{{ article.pk }}-citations" class="citation-count-badge" aria-label="{{ article.citation_count_display }} citation{{ article.citation_count_display|pluralize }}">
<span class="citation-count-number">{{ article.citation_count_display }}</span>
<span class="citation-count-label">cited</span>
</a>
{% else %}
<div class="citation-count-badge" aria-label="{{ article.citation_count_display }} citation{{ article.citation_count_display|pluralize }}">
<span class="citation-count-number">{{ article.citation_count_display }}</span>
<span class="citation-count-label">cited</span>
</div>
{% endif %}
{% endif %}
</article> </article>
{% endfor %} {% endfor %}
</div> </div>