From b8ab67049f1c70b9065a6fc01795046742780b85 Mon Sep 17 00:00:00 2001 From: mohamad Date: Sun, 17 May 2026 18:51:26 +0330 Subject: [PATCH] feat: changes for citation ad dynamic --- apps/core/management/commands/seed_content.py | 117 ++++++- apps/pages/admin.py | 50 ++- .../0004_aboutsection_aboutsectionitem.py | 103 ++++++ .../migrations/0005_homepage_sections.py | 49 +++ ...06_homepagesectionitem_image_supporters.py | 23 ++ apps/pages/models.py | 114 ++++++- apps/pages/views.py | 27 +- apps/products/admin.py | 27 +- .../products/migrations/0005_article_badge.py | 22 ++ ...0006_subproduct_logo_homepage_citations.py | 49 +++ .../0007_article_citation_count_display.py | 18 ++ apps/products/models.py | 32 ++ static/css/main.css | 257 ++++++++++++++- static/js/main.js | 39 +++ templates/pages/about.html | 175 +++++----- templates/pages/contact.html | 2 +- templates/pages/home.html | 302 ++++++++---------- templates/partials/_footer.html | 2 +- templates/partials/_navbar.html | 4 +- templates/products/main_detail.html | 4 +- templates/products/overview.html | 2 +- templates/products/sub_detail.html | 39 ++- 22 files changed, 1170 insertions(+), 287 deletions(-) create mode 100644 apps/pages/migrations/0004_aboutsection_aboutsectionitem.py create mode 100644 apps/pages/migrations/0005_homepage_sections.py create mode 100644 apps/pages/migrations/0006_homepagesectionitem_image_supporters.py create mode 100644 apps/products/migrations/0005_article_badge.py create mode 100644 apps/products/migrations/0006_subproduct_logo_homepage_citations.py create mode 100644 apps/products/migrations/0007_article_citation_count_display.py diff --git a/apps/core/management/commands/seed_content.py b/apps/core/management/commands/seed_content.py index 72f6d8a..a910e1d 100644 --- a/apps/core/management/commands/seed_content.py +++ b/apps/core/management/commands/seed_content.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand 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 MAIN_PRODUCTS = [ @@ -33,6 +33,8 @@ MAIN_PRODUCTS = [ "for consistent, reproducible research outcomes." ), "order": 1, + "show_on_homepage": True, + "homepage_order": 1, "articles": [ { "title": "Image Filtering Techniques", @@ -252,7 +254,7 @@ FAQ_ENTRIES = [ { "question": "Where can I get support or report issues?", "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 " "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 = [ { "name": "Radiuma Desktop", @@ -292,7 +374,7 @@ DOWNLOAD_ITEMS = [ 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): parser.add_argument( @@ -311,14 +393,21 @@ class Command(BaseCommand): MainProduct.objects.all().delete() FAQEntry.objects.all().delete() DownloadItem.objects.all().delete() + HomepageSectionItem.objects.all().delete() + HomepageSection.objects.all().delete() self._seed_products() self._seed_faq() self._seed_downloads() + self._seed_homepage_sections() self.stdout.write(self.style.SUCCESS("Content seeded successfully.")) 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: sub_products_data = product_data.pop("sub_products") main_product, created = MainProduct.objects.get_or_create( @@ -384,6 +473,28 @@ class Command(BaseCommand): action = "Created" if created else "Updated" self.stdout.write(f" {action} FAQ: {faq.question[:60]}...") + def _seed_homepage_sections(self): + for section_data in HOMEPAGE_SECTIONS: + items_data = section_data.pop("items") + 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): for item_data in DOWNLOAD_ITEMS: item, created = DownloadItem.objects.get_or_create( diff --git a/apps/pages/admin.py b/apps/pages/admin.py index 57f4f2c..8d798a0 100644 --- a/apps/pages/admin.py +++ b/apps/pages/admin.py @@ -1,6 +1,54 @@ 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) diff --git a/apps/pages/migrations/0004_aboutsection_aboutsectionitem.py b/apps/pages/migrations/0004_aboutsection_aboutsectionitem.py new file mode 100644 index 0000000..591b009 --- /dev/null +++ b/apps/pages/migrations/0004_aboutsection_aboutsectionitem.py @@ -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"], + }, + ), + ] diff --git a/apps/pages/migrations/0005_homepage_sections.py b/apps/pages/migrations/0005_homepage_sections.py new file mode 100644 index 0000000..e1c7e8f --- /dev/null +++ b/apps/pages/migrations/0005_homepage_sections.py @@ -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'], + }, + ), + ] diff --git a/apps/pages/migrations/0006_homepagesectionitem_image_supporters.py b/apps/pages/migrations/0006_homepagesectionitem_image_supporters.py new file mode 100644 index 0000000..0b27892 --- /dev/null +++ b/apps/pages/migrations/0006_homepagesectionitem_image_supporters.py @@ -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), + ), + ] diff --git a/apps/pages/models.py b/apps/pages/models.py index 737c502..83888ff 100644 --- a/apps/pages/models.py +++ b/apps/pages/models.py @@ -1,6 +1,118 @@ 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): diff --git a/apps/pages/views.py b/apps/pages/views.py index 3f8cd01..29d9480 100644 --- a/apps/pages/views.py +++ b/apps/pages/views.py @@ -5,17 +5,42 @@ from django.shortcuts import render from django.views import View from django.views.generic import ListView, TemplateView +from apps.products.models import SubProduct + from .forms import ContactForm -from .models import ContactSubmission, FAQEntry +from .models import AboutSection, ContactSubmission, FAQEntry, HomepageSection class HomeView(TemplateView): 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): 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): model = FAQEntry diff --git a/apps/products/admin.py b/apps/products/admin.py index a663949..b1d8008 100644 --- a/apps/products/admin.py +++ b/apps/products/admin.py @@ -1,6 +1,13 @@ 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): @@ -13,7 +20,7 @@ class ArticleSectionInline(admin.TabularInline): class ArticleInline(admin.StackedInline): model = Article extra = 0 - fields = ("title", "description", "order") + fields = ("badge", "title", "description", "order") ordering = ("order",) show_change_link = True @@ -39,7 +46,7 @@ class SubProductVersionInline(admin.TabularInline): class SubProductInline(admin.StackedInline): model = SubProduct 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",) show_change_link = True prepopulated_fields = {"slug": ("name",)} @@ -67,20 +74,23 @@ class SubProductAdmin(admin.ModelAdmin): "name", "main_product", "distribution", + "show_on_homepage", + "homepage_order", "order", "is_active", "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") prepopulated_fields = {"slug": ("name",)} - list_editable = ("order", "is_active") + list_editable = ("order", "is_active", "show_on_homepage", "homepage_order") raw_id_fields = ("main_product",) inlines = [ArticleInline, SubProductVersionInline] fieldsets = ( (None, {"fields": ("main_product", "name", "slug", "distribution", "short_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")}), ) @@ -92,10 +102,11 @@ class ArticleAdmin(admin.ModelAdmin): search_fields = ("title", "description") list_editable = ("order",) raw_id_fields = ("sub_product",) - inlines = [ArticleSectionInline] + inlines = [ArticleSectionInline, ArticleCitationInline] fieldsets = ( - (None, {"fields": ("sub_product", "title")}), + (None, {"fields": ("sub_product", "badge", "title")}), ("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",)}), ) diff --git a/apps/products/migrations/0005_article_badge.py b/apps/products/migrations/0005_article_badge.py new file mode 100644 index 0000000..8c6691b --- /dev/null +++ b/apps/products/migrations/0005_article_badge.py @@ -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, + ), + ] diff --git a/apps/products/migrations/0006_subproduct_logo_homepage_citations.py b/apps/products/migrations/0006_subproduct_logo_homepage_citations.py new file mode 100644 index 0000000..a7b9f20 --- /dev/null +++ b/apps/products/migrations/0006_subproduct_logo_homepage_citations.py @@ -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'], + }, + ), + ] diff --git a/apps/products/migrations/0007_article_citation_count_display.py b/apps/products/migrations/0007_article_citation_count_display.py new file mode 100644 index 0000000..d39c310 --- /dev/null +++ b/apps/products/migrations/0007_article_citation_count_display.py @@ -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), + ), + ] diff --git a/apps/products/models.py b/apps/products/models.py index 9b61191..1223561 100644 --- a/apps/products/models.py +++ b/apps/products/models.py @@ -69,8 +69,11 @@ class SubProduct(models.Model): max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN ) 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) 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( max_length=20, choices=DISTRIBUTION_CHOICES, @@ -123,11 +126,21 @@ class Article(models.Model): on_delete=models.CASCADE, 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) description = models.TextField() description_format = models.CharField( 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) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -219,3 +232,22 @@ class ArticleSection(models.Model): def __str__(self): 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}" diff --git a/static/css/main.css b/static/css/main.css index ac92d5b..3b5acab 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -264,14 +264,14 @@ h1, h2, h3, h4, h5, h6 { .blob--1 { 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; animation-delay: 0s, 0s; } .blob--2 { 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; animation-delay: 0.3s, 3s; animation-duration: 1.2s, 18s; @@ -279,7 +279,7 @@ h1, h2, h3, h4, h5, h6 { .blob--3 { 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%; animation-delay: 0.6s, 6s; animation-duration: 1.2s, 20s; @@ -342,14 +342,12 @@ h1, h2, h3, h4, h5, h6 { } .brand-icon { - width: 34px; height: 34px; - display: flex; align-items: center; justify-content: center; - background: var(--gradient-brand); - border-radius: 9px; - font-size: 1rem; - font-weight: 800; - color: #fff; - letter-spacing: -0.03em; + width: 34px; + height: 34px; + border-radius: 8px; + object-fit: cover; + display: block; + flex-shrink: 0; } .brand-name { @@ -822,6 +820,18 @@ h1, h2, h3, h4, h5, h6 { flex: 1; display: flex; 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 { @@ -883,6 +893,57 @@ h1, h2, h3, h4, h5, h6 { 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 ============================================================ */ @@ -1120,7 +1181,119 @@ h1, h2, h3, h4, h5, h6 { } .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 { @@ -1640,11 +1813,21 @@ h1, h2, h3, h4, h5, h6 { .rich-content h2 { font-size: 1.25rem; } .rich-content h3 { font-size: 1.05rem; } -.rich-content ul, .rich-content ol { +.rich-content ul { + list-style-type: disc; padding-left: 1.5em; 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 a { @@ -2868,3 +3051,51 @@ h1, h2, h3, h4, h5, h6 { 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); +} diff --git a/static/js/main.js b/static/js/main.js index a549b29..4c75aaf 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -152,12 +152,51 @@ function initIntersectionObserver() { }); } +function initReadMore() { + var chevronSVG = ''; + + 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() { initNavbarScroll(); initMobileMenu(); initMegaMenu(); initFAQAccordion(); initIntersectionObserver(); + initReadMore(); } if (document.readyState === 'loading') { diff --git a/templates/pages/about.html b/templates/pages/about.html index 7197bc6..1225f0d 100644 --- a/templates/pages/about.html +++ b/templates/pages/about.html @@ -1,11 +1,14 @@ {% extends "base.html" %} {% load static %} -{% block title %}What is Radiuma{% 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 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 content %} +{% for section in about_sections %} + +{% if section.section_type == "hero" %}
-
About
-

What is Radiuma?

-

- Visualized & Standardized Environment for Radiomics Analysis -

+ {% if section.badge %}
{{ section.badge }}
{% endif %} + {% if section.title %}

{{ section.title }}

{% endif %} + {% if section.subtitle %}

{{ section.subtitle }}

{% endif %} + {% if section.content %}
{{ section.rendered_content }}
{% endif %}
-
+{% elif section.section_type == "intro" %} +
-

Desktop Software for Medical Imaging Research

-
    -
  • Desktop software to improve usability, reusability, and reproducibility in medical imaging & healthcare research.
  • -
  • Development platform to create reproducible research workflows by connecting different tools.
  • -
  • Useful for collaborative research projects and for ensuring consistency across different studies.
  • -
  • User-friendly for different expertise levels, including radiation oncologists, radiologists, physicists & data scientists.
  • -
+ {% if section.title %}

{{ section.title }}

{% endif %} + {% if section.content %} +
{{ section.rendered_content }}
+ {% endif %}
-
+{% elif section.section_type == "grid" %} +
+ {% if section.badge or section.title %}
-
Standards
-

Standardization

+ {% if section.badge %}
{{ section.badge }}
{% endif %} + {% if section.title %}

{{ section.title }}

{% endif %}
+ {% endif %} + {% if section.content %}
{{ section.rendered_content }}
{% endif %} + {% with items=section.items.all %} + {% if items %}
+ {% for item in items %}
-
IBSI 1.0
-

Radiomic Feature Extraction

-

- 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). -

-
-
-
IBSI 2.0
-

Image Filtering

-

- Image filters have been standardized against IBSI 2.0 by implementing and - validating several filter options, ensuring reproducibility across research - institutions worldwide. -

-
-
-
Python
-

Open Source

-

- Radiuma is a major, entirely-revamped upgrade to the original SERA (Matlab-based), - now built on Python for broader accessibility and community contribution. -

-
-
-
End-to-End
-

Standardized Workflows

-

- Radiuma employs a number of popular image processing algorithms to create - end-to-end standardized workflows for consistent, reproducible research results. -

+ {% if item.badge %}
{{ item.badge }}
{% endif %} + {% if item.title %}

{{ item.title }}

{% endif %} + {% if item.content %}

{{ item.content }}

{% endif %}
+ {% endfor %}
+ {% endif %} + {% endwith %}
-
-
-
-
Interface
-

Radiuma in Action

-
-
- -
- Radiuma settings panel -
-
- Radiuma analysis results -
-
-
-
- -
+{% elif section.section_type == "history" %} +
-
History
-

Our Origins

-

- 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. -

-

- 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. -

+ {% if section.badge %}
{{ section.badge }}
{% endif %} + {% if section.title %}

{{ section.title }}

{% endif %} + {% if section.content %}
{{ section.rendered_content }}
{% endif %} + {% with links=section.items.all %} + {% if links %} + {% endif %} + {% endwith %}
+{% elif section.section_type == "custom" %} +
+
+ {% if section.badge or section.title %} +
+ {% if section.badge %}
{{ section.badge }}
{% endif %} + {% if section.title %}

{{ section.title }}

{% endif %} +
+ {% endif %} + {% if section.content %} +
+
{{ section.rendered_content }}
+
+ {% endif %} + {% with items=section.items.all %} + {% if items %} +
+ {% for item in items %} +
+ {% if item.badge %}
{{ item.badge }}
{% endif %} + {% if item.title %}

{{ item.title }}

{% endif %} + {% if item.content %}

{{ item.content }}

{% endif %} + {% if item.url %}{{ item.title }}{% endif %} +
+ {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+ +{% endif %} +{% empty %} +
+
+

+ No content configured yet. Add sections in the admin panel. +

+
+
+{% endfor %} + {% endblock %} diff --git a/templates/pages/contact.html b/templates/pages/contact.html index da04fdc..d36c182 100644 --- a/templates/pages/contact.html +++ b/templates/pages/contact.html @@ -85,7 +85,7 @@

Email Support

For direct software support:

- support@visera.ca + support@radiuma.ca
diff --git a/templates/pages/home.html b/templates/pages/home.html index d7a8b46..365875e 100644 --- a/templates/pages/home.html +++ b/templates/pages/home.html @@ -42,181 +42,157 @@
-
-
-
-
Capabilities
-

Important Features

-

- Comprehensive tools for medical imaging research, standardized and reproducible. -

-
-
- {% for feature in features %} -
- -

{{ feature.title }}

-

{{ feature.desc }}

-
- {% empty %} -
- -

Image Filtering

-

Standardized image filtering techniques compliant with IBSI 2.0 guidelines.

-
-
- -

Professional Viewer

-

Comfortable, professional medical image viewer with multi-modality support.

-
-
- -

Radiomics Features

-

Handcrafted radiomics feature generation standardized by IBSI 1.0.

-
-
- -

Format Support

-

NIFTI, DICOM, NRRD, and more — comprehensive multi-format support.

-
-
- -

Image Registration

-

Advanced image registration, fusion, and standardized SUV conversion.

-
-
- -

RT Struct Support

-

Full RT struct support for radiation oncology workflows and research.

-
- {% endfor %} -
-
-
+{% for section in homepage_sections %} -
-
-
-
Gallery
-

See Radiuma in Action

-

- Explore Radiuma's powerful interface, workflow builder, and multi-modal image viewer. -

-
-
-
- Radiuma full workflow interface + {% if section.section_type == "features" %} +
+
+
+ {% if section.badge %}
{{ section.badge }}
{% endif %} +

{{ section.title }}

+ {% if section.description %}

{{ section.description }}

{% endif %}
-
- Radiuma panel view -
-
- Radiuma segmentation view -
-
- Radiuma radiomics results -
-
-
-
- -{% if nav_main_products %} -
-
+ + {% elif section.section_type == "screenshots" %} +
+ - -
-{% endif %} +
-
-
-
-
Value Proposition
-

What Problems Does Radiuma Solve?

-
-
-
- -

Accessibility

-

- 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. -

+ {% elif section.section_type == "products" %} + {% if sub_products %} +
+
+
+ {% if section.badge %}
{{ section.badge }}
{% endif %} +

{{ section.title }}

+ {% if section.description %}

{{ section.description }}

{% endif %}
-
- -

Integrated Tools

-

- Radiuma integrates a vast collection of tools and resources from various domains - of healthcare and medical imaging research in a common, unified environment. -

-
-
- -

Flexibility

-

- Radiuma offers flexibility in terms of tool optimization and workflow customization - to match your specific research requirements. -

-
-
- -

Reproducibility

-

- Improve usability, reusability, and reproducibility (URR) through a workflow - management system that allows researchers to easily create, share, and reuse - analysis pipelines. -

+
-
-
+
+ {% endif %} -
-
-
-
-
Our Story
-

More to Know

-

- 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. -

- Learn More + {% elif section.section_type == "problems" %} +
+
+
+ {% if section.badge %}
{{ section.badge }}
{% endif %} +

{{ section.title }}

+ {% if section.description %}

{{ section.description }}

{% endif %}
- -
-
+
+ + {% elif section.section_type == "supporters" %} +
+
+
+ {% if section.badge %}
{{ section.badge }}
{% endif %} +

{{ section.title }}

+ {% if section.description %}

{{ section.description }}

{% endif %} +
+
+ {% for item in section.items.all %} +
+ {% if item.image %} + + {% endif %} +
+

{{ item.title }}

+ {% if item.content %}

{{ item.content }}

{% endif %} +
+
+ {% endfor %} +
+
+
+ + {% elif section.section_type == "about_strip" %} +
+
+
+
+ {% if section.badge %}
{{ section.badge }}
{% endif %} +

{{ section.title }}

+ {% if section.description %}

{{ section.description }}

{% endif %} + {% if section.link_text and section.link_url %} + {{ section.link_text }} + {% endif %} +
+ +
+
+
+ + {% endif %} +{% endfor %} {% endblock %} diff --git a/templates/partials/_footer.html b/templates/partials/_footer.html index baf7a40..4bb957a 100644 --- a/templates/partials/_footer.html +++ b/templates/partials/_footer.html @@ -52,7 +52,7 @@

675 West 10th Ave, Office 6-112

Vancouver, BC, V5Z 1L3

- support@visera.ca + support@radiuma.ca diff --git a/templates/partials/_navbar.html b/templates/partials/_navbar.html index cef9433..5c156b9 100644 --- a/templates/partials/_navbar.html +++ b/templates/partials/_navbar.html @@ -3,7 +3,7 @@
-
{{ main_product.rendered_description }}
+
{{ main_product.rendered_description }}
@@ -71,7 +71,7 @@ {% endif %}

{{ sub.name }}

-

{{ sub.short_description }}

+

{{ sub.short_description }}

Learn More

{{ product.name }}

{{ product.short_description }}

-

{{ product.description }}

+

{{ product.description }}

Explore {{ product.name }}
{% with subs=product.sub_products.all %} diff --git a/templates/products/sub_detail.html b/templates/products/sub_detail.html index 64591e1..9445c7c 100644 --- a/templates/products/sub_detail.html +++ b/templates/products/sub_detail.html @@ -44,7 +44,7 @@

About {{ sub_product.name }}

-
{{ sub_product.rendered_description }}
+
{{ sub_product.rendered_description }}
{% if show_releases_section %} @@ -154,20 +154,53 @@
{% for article in articles %}
+ {% if article.badge %}
{{ article.badge }}
{% endif %}

{{ article.title }}

-
{{ article.rendered_description }}
+
{{ article.rendered_description }}
{% if article.sections.all %}
{% for section in article.sections.all %}
{{ section.title }}
-
{{ section.rendered_value }}
+
{{ section.rendered_value }}
{% endfor %}
{% endif %} + {% if article.citations.all %} +
+

Citations

+
    + {% for citation in article.citations.all %} +
  1. + {{ citation.text }} + {% if citation.url %} + + + + {% endif %} +
  2. + {% endfor %} +
+
+ {% endif %} + {% if article.citation_count_display is not None %} + {% if article.citations.exists %} + + {{ article.citation_count_display }} + cited + + {% else %} +
+ {{ article.citation_count_display }} + cited +
+ {% endif %} + {% endif %}
{% endfor %}