feat: change entire downloads
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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}",
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user