feat: change entire downloads

This commit is contained in:
mohamad
2026-05-16 19:13:32 +03:30
parent eb880e3e8b
commit f0a9d5832b
26 changed files with 1283 additions and 213 deletions
@@ -226,7 +226,7 @@ FAQ_ENTRIES = [
"answer": (
"Radiuma currently fully supports Windows 10 and above (64-bit). "
"New versions to support macOS and Linux systems are under active development "
"and coming soon. Follow our Discord or check the Downloads page for updates."
"and coming soon. Follow our Discord or check each product module page for updates."
),
"order": 3,
},
+34
View File
@@ -0,0 +1,34 @@
import markdown as _md
from django.utils.html import escape, mark_safe
FORMAT_PLAIN = "plain"
FORMAT_MARKDOWN = "markdown"
FORMAT_HTML = "html"
CONTENT_FORMAT_CHOICES = [
(FORMAT_PLAIN, "Plain Text"),
(FORMAT_MARKDOWN, "Markdown"),
(FORMAT_HTML, "HTML"),
]
_MD_EXTENSIONS = ["extra", "nl2br", "sane_lists"]
def render_content(text: str, fmt: str) -> str:
if not text:
return mark_safe("")
if fmt == FORMAT_HTML:
return mark_safe(text)
if fmt == FORMAT_MARKDOWN:
return mark_safe(_md.markdown(text, extensions=_MD_EXTENSIONS))
paragraphs = text.split("\n\n")
parts = []
for para in paragraphs:
para = para.strip()
if para:
lines = escape(para).split("\n")
parts.append("<p>" + "<br>".join(lines) + "</p>")
return mark_safe("".join(parts) if parts else f"<p>{escape(text)}</p>")
+2 -1
View File
@@ -23,7 +23,8 @@ class FAQEntryAdmin(admin.ModelAdmin):
search_fields = ("question", "answer")
list_editable = ("order", "is_active")
fieldsets = (
(None, {"fields": ("question", "answer")}),
(None, {"fields": ("question",)}),
("Answer", {"fields": ("answer_format", "answer")}),
("Settings", {"fields": ("order", "is_active")}),
)
@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2026-05-14 05:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pages', '0002_contactsubmission'),
]
operations = [
migrations.AddField(
model_name='faqentry',
name='answer_format',
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
),
]
+9
View File
@@ -1,5 +1,7 @@
from django.db import models
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_PLAIN, render_content
class ContactSubmission(models.Model):
name = models.CharField(max_length=200)
@@ -21,6 +23,9 @@ class ContactSubmission(models.Model):
class FAQEntry(models.Model):
question = models.CharField(max_length=500)
answer = models.TextField()
answer_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
)
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
@@ -34,6 +39,10 @@ class FAQEntry(models.Model):
def __str__(self):
return self.question
@property
def rendered_answer(self):
return render_content(self.answer, self.answer_format)
class DownloadItem(models.Model):
PLATFORM_WINDOWS = "windows"
+7 -39
View File
@@ -1,7 +1,7 @@
from django.test import TestCase
from django.urls import reverse
from apps.pages.models import DownloadItem, FAQEntry
from apps.pages.models import FAQEntry
class HomeViewTest(TestCase):
@@ -24,42 +24,6 @@ class AboutViewTest(TestCase):
self.assertTemplateUsed(response, "pages/about.html")
class DownloadsViewTest(TestCase):
def setUp(self):
DownloadItem.objects.create(
name="Radiuma Desktop",
platform="windows",
version="1.0",
download_url="https://example.com/windows",
is_active=True,
)
DownloadItem.objects.create(
name="Radiuma Desktop",
platform="macos",
version="Coming Soon",
download_url="#",
is_active=False,
)
def test_downloads_returns_200(self):
response = self.client.get(reverse("pages:downloads"))
self.assertEqual(response.status_code, 200)
def test_downloads_uses_correct_template(self):
response = self.client.get(reverse("pages:downloads"))
self.assertTemplateUsed(response, "pages/downloads.html")
def test_downloads_context_has_platform_keys(self):
response = self.client.get(reverse("pages:downloads"))
self.assertIn("windows_items", response.context)
self.assertIn("macos_items", response.context)
self.assertIn("linux_items", response.context)
def test_windows_item_in_context(self):
response = self.client.get(reverse("pages:downloads"))
self.assertEqual(response.context["windows_items"].count(), 1)
class FAQViewTest(TestCase):
def setUp(self):
FAQEntry.objects.create(
@@ -107,8 +71,12 @@ class NavigationContextTest(TestCase):
reverse("pages:about"),
reverse("pages:faq"),
reverse("pages:contact"),
reverse("pages:downloads"),
reverse("products:overview"),
]
for url in urls:
response = self.client.get(url)
self.assertIn("nav_main_products", response.context, f"Missing nav_main_products at {url}")
self.assertIn(
"nav_main_products",
response.context,
f"Missing nav_main_products at {url}",
)
-1
View File
@@ -7,7 +7,6 @@ app_name = "pages"
urlpatterns = [
path("", views.HomeView.as_view(), name="home"),
path("about/", views.AboutView.as_view(), name="about"),
path("downloads/", views.DownloadsView.as_view(), name="downloads"),
path("faq/", views.FAQView.as_view(), name="faq"),
path("contact/", views.ContactView.as_view(), name="contact"),
]
+1 -19
View File
@@ -6,7 +6,7 @@ from django.views import View
from django.views.generic import ListView, TemplateView
from .forms import ContactForm
from .models import ContactSubmission, DownloadItem, FAQEntry
from .models import ContactSubmission, FAQEntry
class HomeView(TemplateView):
@@ -17,24 +17,6 @@ class AboutView(TemplateView):
template_name = "pages/about.html"
class DownloadsView(ListView):
template_name = "pages/downloads.html"
context_object_name = "download_items"
def get_queryset(self):
return DownloadItem.objects.filter(is_active=True).order_by("order", "platform")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
all_items = DownloadItem.objects.order_by("order", "platform")
context["windows_items"] = all_items.filter(
platform=DownloadItem.PLATFORM_WINDOWS
)
context["macos_items"] = all_items.filter(platform=DownloadItem.PLATFORM_MACOS)
context["linux_items"] = all_items.filter(platform=DownloadItem.PLATFORM_LINUX)
return context
class FAQView(ListView):
model = FAQEntry
template_name = "pages/faq.html"
+84 -13
View File
@@ -1,12 +1,12 @@
from django.contrib import admin
from .models import Article, ArticleSection, MainProduct, SubProduct
from .models import Article, ArticleSection, MainProduct, SubProduct, SubProductVersion
class ArticleSectionInline(admin.TabularInline):
model = ArticleSection
extra = 1
fields = ("title", "value", "order")
fields = ("title", "value_format", "value", "order")
ordering = ("order",)
@@ -18,10 +18,28 @@ class ArticleInline(admin.StackedInline):
show_change_link = True
class SubProductVersionInline(admin.TabularInline):
model = SubProductVersion
extra = 0
ordering = ("order", "version")
fields = (
"version",
"is_featured_stable",
"windows_download_url",
"macos_download_url",
"linux_download_url",
"source_code_url",
"package_resource_url",
"release_notes",
"is_active",
"order",
)
class SubProductInline(admin.StackedInline):
model = SubProduct
extra = 0
fields = ("name", "slug", "short_description", "image", "order", "is_active")
fields = ("name", "slug", "distribution", "short_description", "image", "order", "is_active")
ordering = ("order",)
show_change_link = True
prepopulated_fields = {"slug": ("name",)}
@@ -36,7 +54,8 @@ class MainProductAdmin(admin.ModelAdmin):
list_editable = ("order", "is_active")
inlines = [SubProductInline]
fieldsets = (
(None, {"fields": ("name", "slug", "short_description", "description")}),
(None, {"fields": ("name", "slug", "short_description")}),
("Description", {"fields": ("description_format", "description")}),
("Media", {"fields": ("image",)}),
("Settings", {"fields": ("order", "is_active")}),
)
@@ -44,18 +63,23 @@ class MainProductAdmin(admin.ModelAdmin):
@admin.register(SubProduct)
class SubProductAdmin(admin.ModelAdmin):
list_display = ("name", "main_product", "order", "is_active", "created_at")
list_filter = ("is_active", "main_product")
list_display = (
"name",
"main_product",
"distribution",
"order",
"is_active",
"created_at",
)
list_filter = ("is_active", "distribution", "main_product")
search_fields = ("name", "description", "main_product__name")
prepopulated_fields = {"slug": ("name",)}
list_editable = ("order", "is_active")
raw_id_fields = ("main_product",)
inlines = [ArticleInline]
inlines = [ArticleInline, SubProductVersionInline]
fieldsets = (
(
None,
{"fields": ("main_product", "name", "slug", "short_description", "description")},
),
(None, {"fields": ("main_product", "name", "slug", "distribution", "short_description")}),
("Description", {"fields": ("description_format", "description")}),
("Media", {"fields": ("image",)}),
("Settings", {"fields": ("order", "is_active")}),
)
@@ -70,14 +94,61 @@ class ArticleAdmin(admin.ModelAdmin):
raw_id_fields = ("sub_product",)
inlines = [ArticleSectionInline]
fieldsets = (
(None, {"fields": ("sub_product", "title", "description")}),
(None, {"fields": ("sub_product", "title")}),
("Description", {"fields": ("description_format", "description")}),
("Settings", {"fields": ("order",)}),
)
@admin.register(ArticleSection)
class ArticleSectionAdmin(admin.ModelAdmin):
list_display = ("title", "article", "order")
list_display = ("title", "article", "value_format", "order")
search_fields = ("title", "value", "article__title")
list_editable = ("order",)
raw_id_fields = ("article",)
fieldsets = (
(None, {"fields": ("article", "title")}),
("Content", {"fields": ("value_format", "value")}),
("Settings", {"fields": ("order",)}),
)
@admin.register(SubProductVersion)
class SubProductVersionAdmin(admin.ModelAdmin):
list_display = (
"sub_product",
"version",
"is_featured_stable",
"is_active",
"order",
)
list_filter = ("is_active", "is_featured_stable", "sub_product__distribution")
search_fields = ("sub_product__name", "version")
list_editable = ("is_active", "order")
raw_id_fields = ("sub_product",)
fieldsets = (
(
None,
{
"fields": (
"sub_product",
"version",
"is_featured_stable",
)
},
),
(
"Installable downloads",
{
"fields": (
"windows_download_url",
"macos_download_url",
"linux_download_url",
"source_code_url",
)
},
),
("Package link", {"fields": ("package_resource_url",)}),
("Details", {"fields": ("release_notes",)}),
("Settings", {"fields": ("is_active", "order")}),
)
@@ -0,0 +1,34 @@
# Generated by Django 5.0.2 on 2026-05-14 04:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='SubProductRelease',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('platform', models.CharField(choices=[('windows', 'Windows'), ('macos', 'macOS'), ('linux', 'Linux')], max_length=20)),
('release_type', models.CharField(choices=[('stable', 'Stable'), ('previous', 'Previous')], default='stable', max_length=20)),
('version', models.CharField(help_text='e.g. 2.1.0', max_length=50)),
('download_url', models.URLField()),
('release_notes', models.TextField(blank=True)),
('is_active', models.BooleanField(default=True)),
('order', models.PositiveIntegerField(default=0)),
('sub_product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='products.subproduct')),
],
options={
'verbose_name': 'Release',
'verbose_name_plural': 'Releases',
'ordering': ['release_type', 'platform', 'order'],
'unique_together': {('sub_product', 'platform', 'release_type')},
},
),
]
@@ -0,0 +1,33 @@
# Generated by Django 5.0.2 on 2026-05-14 05:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0002_subproductrelease'),
]
operations = [
migrations.AddField(
model_name='article',
name='description_format',
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
),
migrations.AddField(
model_name='articlesection',
name='value_format',
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
),
migrations.AddField(
model_name='mainproduct',
name='description_format',
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
),
migrations.AddField(
model_name='subproduct',
name='description_format',
field=models.CharField(choices=[('plain', 'Plain Text'), ('markdown', 'Markdown'), ('html', 'HTML')], default='plain', max_length=20),
),
]
@@ -0,0 +1,145 @@
from django.db import migrations, models
import django.db.models.deletion
def migrate_legacy_releases_to_versions(apps, schema_editor):
OldRelease = apps.get_model("products", "SubProductRelease")
Version = apps.get_model("products", "SubProductVersion")
PLATFORM_MAP = {
"windows": "windows_download_url",
"macos": "macos_download_url",
"linux": "linux_download_url",
}
sub_ids = (
OldRelease.objects.values_list("sub_product_id", flat=True).distinct().order_by()
)
for sub_id in sub_ids:
used_labels = set()
for release_type in ("stable", "previous"):
slab = OldRelease.objects.filter(
sub_product_id=sub_id, release_type=release_type
).order_by("order", "pk")
if not slab.exists():
continue
urls = {}
labels = []
orders = []
notes = []
any_active = False
for r in slab:
orders.append(r.order)
if r.is_active:
any_active = True
f = PLATFORM_MAP.get(r.platform)
if f and r.download_url:
urls[f] = r.download_url
labels.append(r.version or "")
if (r.release_notes or "").strip():
notes.append((r.release_notes or "").strip())
vn = next((x for x in labels if x), None) or "1.0"
if vn in used_labels:
suffix = "older" if release_type == "previous" else "alternate"
candidate = f"{vn} ({suffix})"
n = 2
while candidate in used_labels:
candidate = f"{vn} ({suffix} {n})"
n += 1
vn = candidate
used_labels.add(vn)
Version.objects.create(
sub_product_id=sub_id,
version=vn,
is_featured_stable=(release_type == "stable"),
windows_download_url=urls.get("windows_download_url", ""),
macos_download_url=urls.get("macos_download_url", ""),
linux_download_url=urls.get("linux_download_url", ""),
source_code_url="",
package_resource_url="",
release_notes="\n\n".join(dict.fromkeys(notes)),
is_active=any_active,
order=min(orders) if orders else 0,
)
def noop_reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("products", "0003_article_description_format_and_more"),
]
operations = [
migrations.AddField(
model_name="subproduct",
name="distribution",
field=models.CharField(
choices=[
("installable", "Installable application"),
("package", "Package (external / non-installable)"),
],
default="installable",
help_text="Only affects how releases appear on the public site.",
max_length=20,
),
),
migrations.CreateModel(
name="SubProductVersion",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("version", models.CharField(max_length=80)),
(
"is_featured_stable",
models.BooleanField(
default=False,
help_text="Highlighted as the main release on the product page.",
),
),
("windows_download_url", models.URLField(blank=True)),
("macos_download_url", models.URLField(blank=True)),
("linux_download_url", models.URLField(blank=True)),
("source_code_url", models.URLField(blank=True)),
(
"package_resource_url",
models.URLField(
blank=True,
help_text="For package-type modules: external link for this release.",
),
),
("release_notes", models.TextField(blank=True)),
("is_active", models.BooleanField(default=True)),
("order", models.PositiveIntegerField(default=0)),
(
"sub_product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="versions",
to="products.subproduct",
),
),
],
options={
"verbose_name": "Release version",
"verbose_name_plural": "Release versions",
"ordering": ["order", "version", "pk"],
"unique_together": {("sub_product", "version")},
},
),
migrations.RunPython(migrate_legacy_releases_to_versions, noop_reverse),
migrations.DeleteModel(
name="SubProductRelease",
),
]
+110
View File
@@ -2,12 +2,25 @@ from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_PLAIN, render_content
INSTALLABLE_PLATFORM_SPECS = (
("windows_download_url", "Windows", "images/icon-windows.svg"),
("macos_download_url", "macOS", "images/icon-macos.svg"),
("linux_download_url", "Linux", "images/icon-linux.svg"),
("source_code_url", "Source code", None),
)
class MainProduct(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True)
short_description = models.CharField(max_length=300)
description = models.TextField()
description_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
)
image = models.ImageField(upload_to="products/main/", blank=True, null=True)
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
@@ -19,6 +32,10 @@ class MainProduct(models.Model):
verbose_name = "Main Product"
verbose_name_plural = "Main Products"
@property
def rendered_description(self):
return render_content(self.description, self.description_format)
def __str__(self):
return self.name
@@ -32,6 +49,13 @@ class MainProduct(models.Model):
class SubProduct(models.Model):
DISTRIBUTION_INSTALLABLE = "installable"
DISTRIBUTION_PACKAGE = "package"
DISTRIBUTION_CHOICES = [
(DISTRIBUTION_INSTALLABLE, "Installable application"),
(DISTRIBUTION_PACKAGE, "Package (external / non-installable)"),
]
main_product = models.ForeignKey(
MainProduct,
on_delete=models.CASCADE,
@@ -41,9 +65,18 @@ class SubProduct(models.Model):
slug = models.SlugField(blank=True)
short_description = models.CharField(max_length=300)
description = models.TextField()
description_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
)
image = models.ImageField(upload_to="products/sub/", blank=True, null=True)
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
distribution = models.CharField(
max_length=20,
choices=DISTRIBUTION_CHOICES,
default=DISTRIBUTION_INSTALLABLE,
help_text="Only affects how releases appear on the public site.",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -53,6 +86,10 @@ class SubProduct(models.Model):
verbose_name = "Sub Product"
verbose_name_plural = "Sub Products"
@property
def rendered_description(self):
return render_content(self.description, self.description_format)
def __str__(self):
return f"{self.main_product.name} {self.name}"
@@ -70,6 +107,15 @@ class SubProduct(models.Model):
},
)
def get_versions_archive_url(self):
return reverse(
"products:sub_product_versions",
kwargs={
"main_slug": self.main_product.slug,
"sub_slug": self.slug,
},
)
class Article(models.Model):
sub_product = models.ForeignKey(
@@ -79,6 +125,9 @@ class Article(models.Model):
)
title = models.CharField(max_length=300)
description = models.TextField()
description_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
)
order = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -88,10 +137,64 @@ class Article(models.Model):
verbose_name = "Article"
verbose_name_plural = "Articles"
@property
def rendered_description(self):
return render_content(self.description, self.description_format)
def __str__(self):
return self.title
class SubProductVersion(models.Model):
sub_product = models.ForeignKey(
SubProduct,
on_delete=models.CASCADE,
related_name="versions",
)
version = models.CharField(max_length=80)
is_featured_stable = models.BooleanField(
default=False,
help_text="Highlighted as the main release on the product page.",
)
windows_download_url = models.URLField(blank=True)
macos_download_url = models.URLField(blank=True)
linux_download_url = models.URLField(blank=True)
source_code_url = models.URLField(blank=True)
package_resource_url = 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.",
)
release_notes = models.TextField(blank=True)
is_active = models.BooleanField(default=True)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["order", "version", "pk"]
unique_together = [["sub_product", "version"]]
verbose_name = "Release version"
verbose_name_plural = "Release versions"
def __str__(self):
return f"{self.sub_product.name} v{self.version}"
def install_urls_for_specs(self, specs):
out = []
for field_name, label, icon in specs:
url = getattr(self, field_name, "") or ""
if url.strip():
out.append({"field": field_name, "label": label, "icon": icon, "url": url})
return out
def has_any_install_asset(self):
return any(
(getattr(self, f[0]) or "").strip()
for f in INSTALLABLE_PLATFORM_SPECS
)
def has_package_link(self):
return bool((self.package_resource_url or "").strip())
class ArticleSection(models.Model):
article = models.ForeignKey(
Article,
@@ -100,6 +203,9 @@ class ArticleSection(models.Model):
)
title = models.CharField(max_length=200)
value = models.TextField()
value_format = models.CharField(
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
)
order = models.PositiveIntegerField(default=0)
class Meta:
@@ -107,5 +213,9 @@ class ArticleSection(models.Model):
verbose_name = "Article Section"
verbose_name_plural = "Article Sections"
@property
def rendered_value(self):
return render_content(self.value, self.value_format)
def __str__(self):
return f"{self.article.title} {self.title}"
+27
View File
@@ -135,3 +135,30 @@ class SubProductDetailViewTest(ProductViewsSetup):
)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
class SubProductOlderVersionsViewTest(ProductViewsSetup):
def test_versions_returns_200(self):
from apps.products.models import SubProductVersion
SubProductVersion.objects.create(
sub_product=self.sub_product,
version="9.9",
is_featured_stable=True,
windows_download_url="https://example.com/w",
is_active=True,
)
SubProductVersion.objects.create(
sub_product=self.sub_product,
version="9.8",
is_featured_stable=False,
windows_download_url="https://example.com/w2",
is_active=True,
)
url = reverse(
"products:sub_product_versions",
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "products/sub_versions.html")
+5
View File
@@ -11,6 +11,11 @@ urlpatterns = [
views.MainProductDetailView.as_view(),
name="main_product_detail",
),
path(
"<slug:main_slug>/<slug:sub_slug>/versions/",
views.SubProductOlderVersionsView.as_view(),
name="sub_product_versions",
),
path(
"<slug:main_slug>/<slug:sub_slug>/",
views.SubProductDetailView.as_view(),
+119 -5
View File
@@ -1,7 +1,22 @@
from django.shortcuts import get_object_or_404
from django.views.generic import DetailView, ListView, TemplateView
from .models import MainProduct, SubProduct
from .models import INSTALLABLE_PLATFORM_SPECS, MainProduct, SubProduct
def _featured_version(qs):
ordered = qs.order_by("order", "pk")
cand = ordered.filter(is_featured_stable=True).first()
return cand if cand else ordered.first()
def _install_specs_from_versions(version_list):
present = set()
for ver in version_list:
for field_name, *_ in INSTALLABLE_PLATFORM_SPECS:
if (getattr(ver, field_name) or "").strip():
present.add(field_name)
return tuple(s for s in INSTALLABLE_PLATFORM_SPECS if s[0] in present)
class ProductOverviewView(ListView):
@@ -43,11 +58,110 @@ class SubProductDetailView(TemplateView):
context["sub_product"] = sub_product
context["articles"] = sub_product.articles.prefetch_related("sections").all()
context["siblings"] = (
SubProduct.objects.filter(
main_product=main_product,
is_active=True,
)
SubProduct.objects.filter(main_product=main_product, is_active=True)
.exclude(pk=sub_product.pk)
.order_by("order", "name")
)
active_versions = sub_product.versions.filter(is_active=True)
context["distribution_installable"] = (
sub_product.distribution == SubProduct.DISTRIBUTION_INSTALLABLE
)
context["distribution_package"] = (
sub_product.distribution == SubProduct.DISTRIBUTION_PACKAGE
)
context["show_releases_section"] = False
context["featured_version"] = None
context["featured_install_cells"] = []
context["show_older_versions_link"] = False
context["package_versions"] = []
if sub_product.distribution == SubProduct.DISTRIBUTION_INSTALLABLE:
featured = _featured_version(active_versions)
if (
featured
and not featured.has_any_install_asset()
and not (featured.package_resource_url or "").strip()
):
for cand in active_versions.exclude(pk=featured.pk).order_by("order", "pk"):
if cand.has_any_install_asset() or (cand.package_resource_url or "").strip():
featured = cand
break
context["featured_version"] = featured
if featured and (
featured.has_any_install_asset()
or (featured.package_resource_url or "").strip()
):
if featured.has_any_install_asset():
specs = _install_specs_from_versions([featured])
context["featured_install_cells"] = featured.install_urls_for_specs(specs)
context["show_releases_section"] = True
older_list = list(
active_versions.exclude(pk=featured.pk).order_by("order", "pk")
if featured else active_versions.order_by("order", "pk")
)
context["show_older_versions_link"] = len(older_list) > 0
elif sub_product.distribution == SubProduct.DISTRIBUTION_PACKAGE:
pkg_versions = list(active_versions.order_by("order", "pk"))
context["package_versions"] = pkg_versions
context["show_releases_section"] = len(pkg_versions) > 0
return context
class SubProductOlderVersionsView(TemplateView):
template_name = "products/sub_versions.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
main_product = get_object_or_404(
MainProduct,
slug=self.kwargs["main_slug"],
is_active=True,
)
sub_product = get_object_or_404(
SubProduct,
slug=self.kwargs["sub_slug"],
main_product=main_product,
is_active=True,
)
active_versions = sub_product.versions.filter(is_active=True)
featured = _featured_version(active_versions)
archive = list(
active_versions.exclude(pk=featured.pk).order_by("order", "pk")
if featured
else active_versions.order_by("order", "pk")
)
context["main_product"] = main_product
context["sub_product"] = sub_product
context["featured_version"] = featured
context["archive_versions"] = archive
if sub_product.distribution == SubProduct.DISTRIBUTION_INSTALLABLE:
specs = _install_specs_from_versions(archive)
context["archive_specs"] = specs
archive_rows_installable = []
for ver in archive:
cells = []
for field_name, label, icon in specs:
u = getattr(ver, field_name, "") or ""
u = u.strip()
cells.append(
{"field": field_name, "label": label, "icon": icon, "url": u}
)
archive_rows_installable.append(
{"version_obj": ver, "cells": cells}
)
context["archive_rows_installable"] = archive_rows_installable
context["archive_rows_package"] = False
context["distribution_installable"] = True
context["distribution_package"] = False
else:
context["archive_specs"] = ()
context["archive_rows_installable"] = []
context["archive_rows_package"] = True
context["distribution_installable"] = False
context["distribution_package"] = True
return context
+1
View File
@@ -4,3 +4,4 @@ gunicorn>=22.0.0
whitenoise[brotli]>=6.7.0
Pillow>=10.4.0
python-dotenv>=1.0.1
markdown>=3.6
+440
View File
@@ -1542,6 +1542,446 @@ h1, h2, h3, h4, h5, h6 {
margin-top: 0.5rem;
}
.release-block {
margin-bottom: 3.5rem;
}
.release-block-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--glass-border);
}
.release-product-parent {
display: block;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent-cyan);
margin-bottom: 0.2rem;
}
.release-product-name {
font-size: 1.35rem;
font-weight: 700;
}
.release-channel {
margin-bottom: 1.25rem;
}
.release-channel--previous {
opacity: 0.82;
}
.release-channel-label {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 0.28rem 0.85rem;
border-radius: var(--radius-pill);
margin-bottom: 1rem;
}
.release-channel-label--stable {
color: rgb(134, 239, 172);
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
}
.release-channel-label--previous {
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
}
.download-card--previous {
opacity: 0.78;
}
.download-btn--prev {
opacity: 0.9;
}
.empty-state {
padding: 3rem;
text-align: center;
color: var(--text-secondary);
}
/* ============================================================
Rich Content (rendered Markdown / HTML / plain descriptions)
============================================================ */
.rich-content p {
margin-bottom: 0.85em;
line-height: 1.75;
}
.rich-content p:last-child { margin-bottom: 0; }
.rich-content h1, .rich-content h2, .rich-content h3,
.rich-content h4, .rich-content h5 {
font-weight: 700;
margin: 1.25em 0 0.5em;
line-height: 1.25;
}
.rich-content h1 { font-size: 1.5rem; }
.rich-content h2 { font-size: 1.25rem; }
.rich-content h3 { font-size: 1.05rem; }
.rich-content ul, .rich-content ol {
padding-left: 1.5em;
margin-bottom: 0.85em;
}
.rich-content li { margin-bottom: 0.3em; line-height: 1.65; }
.rich-content a {
color: var(--accent-blue-light);
text-decoration: underline;
text-underline-offset: 3px;
}
.rich-content a:hover { color: var(--accent-cyan); }
.rich-content code {
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.88em;
background: rgba(79, 142, 247, 0.1);
border: 1px solid rgba(79, 142, 247, 0.15);
padding: 0.15em 0.45em;
border-radius: 4px;
}
.rich-content pre {
background: rgba(0, 0, 0, 0.35);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
padding: 1rem 1.25rem;
overflow-x: auto;
margin-bottom: 1em;
}
.rich-content pre code {
background: none;
border: none;
padding: 0;
font-size: 0.88rem;
}
.rich-content blockquote {
border-left: 3px solid rgba(79, 142, 247, 0.45);
padding-left: 1rem;
color: var(--text-secondary);
margin: 1em 0;
font-style: italic;
}
.rich-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
font-size: 0.9rem;
}
.rich-content th, .rich-content td {
padding: 0.6rem 0.9rem;
border: 1px solid var(--glass-border);
text-align: left;
}
.rich-content th {
background: rgba(79, 142, 247, 0.08);
font-weight: 600;
}
/* ============================================================
Sub-Product Inline Downloads
============================================================ */
.sub-downloads {
margin-top: 2rem;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: 1.5rem;
backdrop-filter: blur(12px);
}
.sub-downloads-title {
font-size: 1rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.25rem;
color: var(--text-primary);
}
.sub-downloads-channel {
margin-bottom: 1.1rem;
}
.sub-downloads-channel--prev {
opacity: 0.78;
}
.sub-downloads-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.85rem;
margin-top: 0.6rem;
align-items: stretch;
}
.sub-dl-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.45rem;
padding: 0.9rem 0.5rem;
border-radius: var(--radius-sm);
border: 1px solid var(--glass-border);
background: rgba(255, 255, 255, 0.03);
text-align: center;
}
.sub-dl-item--na {
opacity: 0.5;
}
.sub-dl-icon-wrap {
width: 100%;
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.sub-dl-icon-wrap .sub-dl-icon,
.sub-dl-icon-wrap .sub-dl-source-svg {
display: block;
}
.sub-dl-item .btn-primary.btn--sm {
margin-top: auto;
}
.sub-dl-source-mark {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted, #8892a6);
}
.sub-dl-icon {
width: 32px;
height: 32px;
}
.sub-dl-source-svg {
color: var(--text-secondary, #aab4c5);
}
.sub-dl-platform {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-secondary);
}
.pkg-version-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.pkg-version-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem 1rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--glass-border);
}
.pkg-version-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.pkg-version-row:first-child {
padding-top: 0.5rem;
}
.pkg-version-meta {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.pkg-stable-inline {
font-size: 0.7rem;
padding: 0.2rem 0.55rem;
}
.pkg-version-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-left: auto;
}
.pkg-version-notes {
width: 100%;
margin: 0.35rem 0 0;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.6;
}
.sub-downloads--package .sub-package-stable {
margin-top: 0.6rem;
}
.sub-package-row {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
margin-top: 0.75rem;
}
.sub-release-notes {
margin-top: 1rem;
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.65;
}
.sub-install-extra-resource {
margin-top: 0.75rem;
margin-bottom: 0;
font-size: 0.92rem;
}
.sub-install-extra-resource a {
font-weight: 600;
}
.sub-older-versions-inner {
margin-top: 1rem;
margin-bottom: 0;
padding-top: 1rem;
border-top: 1px solid var(--glass-border);
}
.link-arrow {
font-weight: 600;
color: var(--accent-strong, var(--accent, #6b9dff));
}
.versions-archive-section {
padding-top: 0;
}
.versions-empty {
padding: 2rem 1.5rem;
}
.versions-table-wrap {
padding: 0;
overflow-x: auto;
}
.versions-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.versions-table th,
.versions-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--glass-border);
}
.versions-table thead th {
background: rgba(79, 142, 247, 0.06);
font-weight: 600;
}
.versions-table-link {
display: inline-flex;
align-items: center;
}
.versions-table-icon {
display: block;
}
.versions-table-text-link {
font-weight: 600;
}
.versions-notes-row td {
background: rgba(0, 0, 0, 0.06);
}
.versions-notes-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted, #8892a6);
display: block;
margin-bottom: 0.35rem;
}
.versions-notes-body {
margin: 0;
font-size: 0.87rem;
color: var(--text-secondary);
line-height: 1.65;
}
.versions-package-list {
display: flex;
flex-direction: column;
gap: 1rem;
list-style: none;
padding: 0;
margin: 0;
}
.versions-package-card {
padding: 1.15rem 1.35rem;
}
.versions-package-head {
display: flex;
align-items: center;
gap: 1rem;
}
.versions-na {
opacity: 0.45;
}
.info-block {
padding: 2.5rem;
}
-119
View File
@@ -1,119 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Downloads{% endblock %}
{% block meta_description %}Download Radiuma by Radiuma — available for Windows, macOS, and Linux.{% endblock %}
{% block content %}
<section class="page-hero" aria-labelledby="downloads-heading">
<div class="page-hero-blobs" aria-hidden="true">
<div class="blob blob--1"></div>
<div class="blob blob--2"></div>
<img src="{% static 'images/modern-shape.svg' %}" class="blob-img blob-img--downloads" alt="" />
</div>
<div class="container">
<div class="page-hero-content">
<div class="section-badge">Software</div>
<h1 class="page-hero-title" id="downloads-heading">Downloads</h1>
<p class="page-hero-subtitle">Download the latest version of Radiuma for your platform.</p>
</div>
</div>
</section>
<section class="section" aria-labelledby="download-platforms-heading">
<div class="container">
<h2 class="sr-only" id="download-platforms-heading">Available Platforms</h2>
<div class="downloads-grid">
<div class="download-card glass-card fade-in {% if not windows_items %}download-card--unavailable{% endif %}">
<div class="download-platform-icon" aria-hidden="true">
<img src="{% static 'images/icon-windows.svg' %}" alt="Windows" width="56" height="56" class="platform-icon" />
</div>
<h3 class="download-platform-name">Windows</h3>
<p class="download-platform-desc">Windows 10 and up (64-bit)</p>
{% if windows_items %}
{% for item in windows_items %}
<div class="download-item">
<span class="download-version">v{{ item.version }}</span>
{% if item.description %}<p class="download-item-desc">{{ item.description }}</p>{% endif %}
{% if item.download_url and item.download_url != '#' %}
<a href="{{ item.download_url }}" class="btn-primary download-btn" download>
<img src="{% static 'images/icon-download.svg' %}" width="18" height="18" alt="" aria-hidden="true" class="btn-icon-img" />
Download
</a>
{% endif %}
</div>
{% endfor %}
{% else %}
<span class="coming-soon-badge">Coming Soon</span>
{% endif %}
</div>
<div class="download-card glass-card fade-in {% if not macos_items %}download-card--unavailable{% endif %}">
<div class="download-platform-icon" aria-hidden="true">
<img src="{% static 'images/icon-macos.svg' %}" alt="macOS" width="56" height="56" class="platform-icon" />
</div>
<h3 class="download-platform-name">macOS</h3>
<p class="download-platform-desc">macOS 11 Big Sur and up</p>
{% if macos_items %}
{% for item in macos_items %}
<div class="download-item">
<span class="download-version">v{{ item.version }}</span>
{% if item.description %}<p class="download-item-desc">{{ item.description }}</p>{% endif %}
{% if item.download_url and item.download_url != '#' %}
<a href="{{ item.download_url }}" class="btn-primary download-btn" download>
<img src="{% static 'images/icon-download.svg' %}" width="18" height="18" alt="" aria-hidden="true" class="btn-icon-img" />
Download
</a>
{% endif %}
</div>
{% endfor %}
{% else %}
<span class="coming-soon-badge">Coming Soon</span>
{% endif %}
</div>
<div class="download-card glass-card fade-in {% if not linux_items %}download-card--unavailable{% endif %}">
<div class="download-platform-icon" aria-hidden="true">
<img src="{% static 'images/icon-linux.svg' %}" alt="Linux" width="56" height="56" class="platform-icon" />
</div>
<h3 class="download-platform-name">Linux</h3>
<p class="download-platform-desc">Ubuntu 20.04+ and compatible distributions</p>
{% if linux_items %}
{% for item in linux_items %}
<div class="download-item">
<span class="download-version">v{{ item.version }}</span>
{% if item.description %}<p class="download-item-desc">{{ item.description }}</p>{% endif %}
{% if item.download_url and item.download_url != '#' %}
<a href="{{ item.download_url }}" class="btn-primary download-btn" download>
<img src="{% static 'images/icon-download.svg' %}" width="18" height="18" alt="" aria-hidden="true" class="btn-icon-img" />
Download
</a>
{% endif %}
</div>
{% endfor %}
{% else %}
<span class="coming-soon-badge">Coming Soon</span>
{% endif %}
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="reinstall-heading">
<div class="container">
<div class="info-block glass-card fade-in">
<h2 id="reinstall-heading">Installation Notes</h2>
<p>
If you have installed an older version of Radiuma, you can install the new version
over it without removing the previous installation. However, if you encounter any
problems, please remove the old version first before reinstalling.
</p>
<a href="{% url 'pages:faq' %}" class="btn-ghost">View Full FAQ</a>
</div>
</div>
</section>
{% endblock %}
+1 -1
View File
@@ -45,7 +45,7 @@
aria-labelledby="faq-question-{{ entry.pk }}"
hidden
>
<p>{{ entry.answer }}</p>
<div class="rich-content">{{ entry.rendered_answer }}</div>
</div>
</div>
{% endfor %}
+1 -1
View File
@@ -31,7 +31,7 @@
including radiomics and machine learning analysis.
</p>
<div class="hero-actions">
<a href="{% url 'pages:downloads' %}" class="btn-primary">Download Now</a>
<a href="{% url 'products:overview' %}" class="btn-primary">Get Radiuma</a>
<a href="{% url 'pages:about' %}" class="btn-ghost">About Radiuma</a>
</div>
</div>
-1
View File
@@ -29,7 +29,6 @@
<li><a href="{% url 'pages:home' %}">Home</a></li>
<li><a href="{% url 'pages:about' %}">What is Radiuma</a></li>
<li><a href="{% url 'products:overview' %}">Products</a></li>
<li><a href="{% url 'pages:downloads' %}">Downloads</a></li>
<li><a href="{% url 'pages:faq' %}">FAQ</a></li>
<li><a href="{% url 'pages:contact' %}">Contact</a></li>
</ul>
-6
View File
@@ -72,12 +72,6 @@
</a>
</li>
<li class="nav-item">
<a href="{% url 'pages:downloads' %}" class="nav-link {% if request.resolver_match.url_name == 'downloads' %}active{% endif %}">
Downloads
</a>
</li>
<li class="nav-item">
<a href="{% url 'pages:faq' %}" class="nav-link {% if request.resolver_match.url_name == 'faq' %}active{% endif %}">
FAQ
+1 -1
View File
@@ -43,7 +43,7 @@
{% endif %}
</div>
<div class="product-detail-text">
<p class="product-detail-desc">{{ main_product.description }}</p>
<div class="product-detail-desc rich-content">{{ main_product.rendered_description }}</div>
</div>
</div>
</div>
+106 -5
View File
@@ -44,24 +44,125 @@
<div class="sub-product-desc glass-card fade-in">
<h2>About {{ sub_product.name }}</h2>
<p>{{ sub_product.description }}</p>
<div class="rich-content">{{ sub_product.rendered_description }}</div>
</div>
{% if show_releases_section %}
{% if distribution_installable %}
<section class="sub-downloads fade-in" aria-labelledby="sub-dl-heading">
<h2 class="sub-downloads-title" id="sub-dl-heading">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Downloads
</h2>
<div class="sub-downloads-channel">
{% if featured_version.is_featured_stable %}
<span class="release-channel-label release-channel-label--stable">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
Stable
</span>
{% endif %}
{% if featured_install_cells %}
<div class="sub-downloads-grid">
{% for cell in featured_install_cells %}
<div class="sub-dl-item">
<div class="sub-dl-icon-wrap">
{% if cell.icon %}
<img src="{% static cell.icon %}" alt="" width="32" height="32" class="platform-icon sub-dl-icon" aria-hidden="true" />
{% else %}
<span class="sub-dl-source-mark" aria-hidden="true">
<svg class="sub-dl-source-svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round">
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
</svg>
</span>
{% endif %}
</div>
<span class="sub-dl-platform">{{ cell.label }}</span>
<span class="download-version">v{{ featured_version.version }}</span>
<a href="{{ cell.url }}" class="btn-primary btn--sm"{% if cell.label == "Source code" %} target="_blank" rel="noopener noreferrer"{% endif %}>{% if cell.label == "Source code" %}Source{% else %}Download{% endif %}</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% if featured_version.package_resource_url %}
<p class="sub-install-extra-resource">
<a href="{{ featured_version.package_resource_url }}" target="_blank" rel="noopener noreferrer">Package resource</a>
</p>
{% endif %}
{% if featured_version.release_notes %}
<p class="sub-release-notes">{{ featured_version.release_notes }}</p>
{% endif %}
{% if show_older_versions_link %}
<p class="sub-older-versions-inner">
<a href="{{ sub_product.get_versions_archive_url }}" class="link-arrow">Older releases</a>
</p>
{% endif %}
</section>
{% elif distribution_package %}
<section class="sub-downloads sub-downloads--package fade-in" aria-labelledby="sub-resources-heading">
<h2 class="sub-downloads-title" id="sub-resources-heading">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
Resources
</h2>
<ul class="pkg-version-list" role="list">
{% for ver in package_versions %}
<li class="pkg-version-row">
<div class="pkg-version-meta">
<span class="download-version">v{{ ver.version }}</span>
{% if ver.is_featured_stable %}
<span class="release-channel-label release-channel-label--stable pkg-stable-inline">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
Stable
</span>
{% endif %}
</div>
<div class="pkg-version-actions">
{% if ver.package_resource_url %}
<a href="{{ ver.package_resource_url }}" class="btn-primary btn--sm" target="_blank" rel="noopener noreferrer">Open</a>
{% endif %}
{% if ver.source_code_url %}
<a href="{{ ver.source_code_url }}" class="btn-ghost btn--sm" target="_blank" rel="noopener noreferrer">Source</a>
{% endif %}
</div>
{% if ver.release_notes %}
<p class="pkg-version-notes">{{ ver.release_notes }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
</section>
{% endif %}
{% endif %}
{% if articles %}
<section aria-labelledby="articles-heading">
<h2 class="articles-section-title" id="articles-heading">Articles</h2>
<section aria-label="Content sections">
<div class="articles-list">
{% for article in articles %}
<article class="article-card glass-card fade-in" aria-labelledby="article-{{ article.pk }}-title">
<h3 class="article-title" id="article-{{ article.pk }}-title">{{ article.title }}</h3>
<p class="article-description">{{ article.description }}</p>
<div class="article-description rich-content">{{ article.rendered_description }}</div>
{% if article.sections.all %}
<div class="article-sections">
<dl class="article-sections-list">
{% for section in article.sections.all %}
<div class="article-section-item">
<dt class="section-key">{{ section.title }}</dt>
<dd class="section-value">{{ section.value }}</dd>
<dd class="section-value rich-content">{{ section.rendered_value }}</dd>
</div>
{% endfor %}
</dl>
+104
View File
@@ -0,0 +1,104 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Older releases — {{ sub_product.name }} — {{ main_product.name }}{% endblock %}
{% block meta_description %}Archived releases for {{ sub_product.name }}.{% endblock %}
{% block content %}
<section class="page-hero" aria-labelledby="versions-heading">
<div class="page-hero-blobs" aria-hidden="true">
<div class="blob blob--1"></div>
<div class="blob blob--2"></div>
</div>
<div class="container">
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol class="breadcrumb-list" role="list">
<li><a href="{% url 'pages:home' %}">Home</a></li>
<li aria-hidden="true" class="breadcrumb-sep"></li>
<li><a href="{% url 'products:overview' %}">Products</a></li>
<li aria-hidden="true" class="breadcrumb-sep"></li>
<li><a href="{{ main_product.get_absolute_url }}">{{ main_product.name }}</a></li>
<li aria-hidden="true" class="breadcrumb-sep"></li>
<li><a href="{{ sub_product.get_absolute_url }}">{{ sub_product.name }}</a></li>
<li aria-hidden="true" class="breadcrumb-sep"></li>
<li aria-current="page">Older releases</li>
</ol>
</nav>
<div class="page-hero-content">
<div class="section-badge">{{ sub_product.name }}</div>
<h1 class="page-hero-title" id="versions-heading">Older releases</h1>
<p class="page-hero-subtitle">Archived versions of this module.</p>
</div>
</div>
</section>
<section class="section versions-archive-section">
<div class="container">
{% if not archive_versions %}
<p class="versions-empty glass-card">There are no archived releases for this module.</p>
{% elif distribution_installable %}
<div class="versions-table-wrap glass-card fade-in">
<table class="versions-table">
<thead>
<tr>
<th scope="col">Version</th>
{% for fname, label, icon in archive_specs %}
<th scope="col">{{ label }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in archive_rows_installable %}
<tr>
<th scope="row">{{ row.version_obj.version }}</th>
{% for cell in row.cells %}
<td>
{% if cell.url %}
{% if cell.icon %}
<a href="{{ cell.url }}" class="versions-table-link">
<img src="{% static cell.icon %}" alt="" width="22" height="22" class="versions-table-icon" aria-hidden="true" />
<span class="sr-only">{{ cell.label }}</span>
</a>
{% else %}
<a href="{{ cell.url }}" class="versions-table-text-link" rel="noopener noreferrer">Source</a>
{% endif %}
{% else %}
<span class="versions-na"></span>
{% endif %}
</td>
{% endfor %}
</tr>
{% if row.version_obj.release_notes %}
<tr class="versions-notes-row">
<td colspan="{{ archive_specs|length|add:1 }}">
<span class="versions-notes-label">Notes</span>
<p class="versions-notes-body">{{ row.version_obj.release_notes }}</p>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<ul class="versions-package-list" role="list">
{% for ver in archive_versions %}
<li class="versions-package-card glass-card fade-in">
<div class="versions-package-head">
<span class="download-version">v{{ ver.version }}</span>
{% if ver.package_resource_url %}
<a href="{{ ver.package_resource_url }}" class="btn-ghost btn--sm" target="_blank" rel="noopener noreferrer">Open</a>
{% endif %}
</div>
{% if ver.release_notes %}
<p class="versions-notes-body">{{ ver.release_notes }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</section>
{% endblock %}