feat: change entire downloads
This commit is contained in:
@@ -226,7 +226,7 @@ FAQ_ENTRIES = [
|
|||||||
"answer": (
|
"answer": (
|
||||||
"Radiuma currently fully supports Windows 10 and above (64-bit). "
|
"Radiuma currently fully supports Windows 10 and above (64-bit). "
|
||||||
"New versions to support macOS and Linux systems are under active development "
|
"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,
|
"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")
|
search_fields = ("question", "answer")
|
||||||
list_editable = ("order", "is_active")
|
list_editable = ("order", "is_active")
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("question", "answer")}),
|
(None, {"fields": ("question",)}),
|
||||||
|
("Answer", {"fields": ("answer_format", "answer")}),
|
||||||
("Settings", {"fields": ("order", "is_active")}),
|
("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 django.db import models
|
||||||
|
|
||||||
|
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_PLAIN, render_content
|
||||||
|
|
||||||
|
|
||||||
class ContactSubmission(models.Model):
|
class ContactSubmission(models.Model):
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
@@ -21,6 +23,9 @@ class ContactSubmission(models.Model):
|
|||||||
class FAQEntry(models.Model):
|
class FAQEntry(models.Model):
|
||||||
question = models.CharField(max_length=500)
|
question = models.CharField(max_length=500)
|
||||||
answer = models.TextField()
|
answer = models.TextField()
|
||||||
|
answer_format = models.CharField(
|
||||||
|
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||||||
|
)
|
||||||
order = models.PositiveIntegerField(default=0)
|
order = models.PositiveIntegerField(default=0)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
@@ -34,6 +39,10 @@ class FAQEntry(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.question
|
return self.question
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rendered_answer(self):
|
||||||
|
return render_content(self.answer, self.answer_format)
|
||||||
|
|
||||||
|
|
||||||
class DownloadItem(models.Model):
|
class DownloadItem(models.Model):
|
||||||
PLATFORM_WINDOWS = "windows"
|
PLATFORM_WINDOWS = "windows"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from apps.pages.models import DownloadItem, FAQEntry
|
from apps.pages.models import FAQEntry
|
||||||
|
|
||||||
|
|
||||||
class HomeViewTest(TestCase):
|
class HomeViewTest(TestCase):
|
||||||
@@ -24,42 +24,6 @@ class AboutViewTest(TestCase):
|
|||||||
self.assertTemplateUsed(response, "pages/about.html")
|
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):
|
class FAQViewTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
FAQEntry.objects.create(
|
FAQEntry.objects.create(
|
||||||
@@ -107,8 +71,12 @@ class NavigationContextTest(TestCase):
|
|||||||
reverse("pages:about"),
|
reverse("pages:about"),
|
||||||
reverse("pages:faq"),
|
reverse("pages:faq"),
|
||||||
reverse("pages:contact"),
|
reverse("pages:contact"),
|
||||||
reverse("pages:downloads"),
|
reverse("products:overview"),
|
||||||
]
|
]
|
||||||
for url in urls:
|
for url in urls:
|
||||||
response = self.client.get(url)
|
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 = [
|
urlpatterns = [
|
||||||
path("", views.HomeView.as_view(), name="home"),
|
path("", views.HomeView.as_view(), name="home"),
|
||||||
path("about/", views.AboutView.as_view(), name="about"),
|
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("faq/", views.FAQView.as_view(), name="faq"),
|
||||||
path("contact/", views.ContactView.as_view(), name="contact"),
|
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 django.views.generic import ListView, TemplateView
|
||||||
|
|
||||||
from .forms import ContactForm
|
from .forms import ContactForm
|
||||||
from .models import ContactSubmission, DownloadItem, FAQEntry
|
from .models import ContactSubmission, FAQEntry
|
||||||
|
|
||||||
|
|
||||||
class HomeView(TemplateView):
|
class HomeView(TemplateView):
|
||||||
@@ -17,24 +17,6 @@ class AboutView(TemplateView):
|
|||||||
template_name = "pages/about.html"
|
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):
|
class FAQView(ListView):
|
||||||
model = FAQEntry
|
model = FAQEntry
|
||||||
template_name = "pages/faq.html"
|
template_name = "pages/faq.html"
|
||||||
|
|||||||
+84
-13
@@ -1,12 +1,12 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Article, ArticleSection, MainProduct, SubProduct
|
from .models import Article, ArticleSection, MainProduct, SubProduct, SubProductVersion
|
||||||
|
|
||||||
|
|
||||||
class ArticleSectionInline(admin.TabularInline):
|
class ArticleSectionInline(admin.TabularInline):
|
||||||
model = ArticleSection
|
model = ArticleSection
|
||||||
extra = 1
|
extra = 1
|
||||||
fields = ("title", "value", "order")
|
fields = ("title", "value_format", "value", "order")
|
||||||
ordering = ("order",)
|
ordering = ("order",)
|
||||||
|
|
||||||
|
|
||||||
@@ -18,10 +18,28 @@ class ArticleInline(admin.StackedInline):
|
|||||||
show_change_link = True
|
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):
|
class SubProductInline(admin.StackedInline):
|
||||||
model = SubProduct
|
model = SubProduct
|
||||||
extra = 0
|
extra = 0
|
||||||
fields = ("name", "slug", "short_description", "image", "order", "is_active")
|
fields = ("name", "slug", "distribution", "short_description", "image", "order", "is_active")
|
||||||
ordering = ("order",)
|
ordering = ("order",)
|
||||||
show_change_link = True
|
show_change_link = True
|
||||||
prepopulated_fields = {"slug": ("name",)}
|
prepopulated_fields = {"slug": ("name",)}
|
||||||
@@ -36,7 +54,8 @@ class MainProductAdmin(admin.ModelAdmin):
|
|||||||
list_editable = ("order", "is_active")
|
list_editable = ("order", "is_active")
|
||||||
inlines = [SubProductInline]
|
inlines = [SubProductInline]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("name", "slug", "short_description", "description")}),
|
(None, {"fields": ("name", "slug", "short_description")}),
|
||||||
|
("Description", {"fields": ("description_format", "description")}),
|
||||||
("Media", {"fields": ("image",)}),
|
("Media", {"fields": ("image",)}),
|
||||||
("Settings", {"fields": ("order", "is_active")}),
|
("Settings", {"fields": ("order", "is_active")}),
|
||||||
)
|
)
|
||||||
@@ -44,18 +63,23 @@ class MainProductAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(SubProduct)
|
@admin.register(SubProduct)
|
||||||
class SubProductAdmin(admin.ModelAdmin):
|
class SubProductAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "main_product", "order", "is_active", "created_at")
|
list_display = (
|
||||||
list_filter = ("is_active", "main_product")
|
"name",
|
||||||
|
"main_product",
|
||||||
|
"distribution",
|
||||||
|
"order",
|
||||||
|
"is_active",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
list_filter = ("is_active", "distribution", "main_product")
|
||||||
search_fields = ("name", "description", "main_product__name")
|
search_fields = ("name", "description", "main_product__name")
|
||||||
prepopulated_fields = {"slug": ("name",)}
|
prepopulated_fields = {"slug": ("name",)}
|
||||||
list_editable = ("order", "is_active")
|
list_editable = ("order", "is_active")
|
||||||
raw_id_fields = ("main_product",)
|
raw_id_fields = ("main_product",)
|
||||||
inlines = [ArticleInline]
|
inlines = [ArticleInline, SubProductVersionInline]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(None, {"fields": ("main_product", "name", "slug", "distribution", "short_description")}),
|
||||||
None,
|
("Description", {"fields": ("description_format", "description")}),
|
||||||
{"fields": ("main_product", "name", "slug", "short_description", "description")},
|
|
||||||
),
|
|
||||||
("Media", {"fields": ("image",)}),
|
("Media", {"fields": ("image",)}),
|
||||||
("Settings", {"fields": ("order", "is_active")}),
|
("Settings", {"fields": ("order", "is_active")}),
|
||||||
)
|
)
|
||||||
@@ -70,14 +94,61 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
raw_id_fields = ("sub_product",)
|
raw_id_fields = ("sub_product",)
|
||||||
inlines = [ArticleSectionInline]
|
inlines = [ArticleSectionInline]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("sub_product", "title", "description")}),
|
(None, {"fields": ("sub_product", "title")}),
|
||||||
|
("Description", {"fields": ("description_format", "description")}),
|
||||||
("Settings", {"fields": ("order",)}),
|
("Settings", {"fields": ("order",)}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ArticleSection)
|
@admin.register(ArticleSection)
|
||||||
class ArticleSectionAdmin(admin.ModelAdmin):
|
class ArticleSectionAdmin(admin.ModelAdmin):
|
||||||
list_display = ("title", "article", "order")
|
list_display = ("title", "article", "value_format", "order")
|
||||||
search_fields = ("title", "value", "article__title")
|
search_fields = ("title", "value", "article__title")
|
||||||
list_editable = ("order",)
|
list_editable = ("order",)
|
||||||
raw_id_fields = ("article",)
|
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.urls import reverse
|
||||||
from django.utils.text import slugify
|
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):
|
class MainProduct(models.Model):
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
slug = models.SlugField(unique=True, blank=True)
|
slug = models.SlugField(unique=True, blank=True)
|
||||||
short_description = models.CharField(max_length=300)
|
short_description = models.CharField(max_length=300)
|
||||||
description = models.TextField()
|
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)
|
image = models.ImageField(upload_to="products/main/", blank=True, null=True)
|
||||||
order = models.PositiveIntegerField(default=0)
|
order = models.PositiveIntegerField(default=0)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
@@ -19,6 +32,10 @@ class MainProduct(models.Model):
|
|||||||
verbose_name = "Main Product"
|
verbose_name = "Main Product"
|
||||||
verbose_name_plural = "Main Products"
|
verbose_name_plural = "Main Products"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rendered_description(self):
|
||||||
|
return render_content(self.description, self.description_format)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@@ -32,6 +49,13 @@ class MainProduct(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class SubProduct(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(
|
main_product = models.ForeignKey(
|
||||||
MainProduct,
|
MainProduct,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@@ -41,9 +65,18 @@ class SubProduct(models.Model):
|
|||||||
slug = models.SlugField(blank=True)
|
slug = models.SlugField(blank=True)
|
||||||
short_description = models.CharField(max_length=300)
|
short_description = models.CharField(max_length=300)
|
||||||
description = models.TextField()
|
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)
|
image = models.ImageField(upload_to="products/sub/", blank=True, null=True)
|
||||||
order = models.PositiveIntegerField(default=0)
|
order = models.PositiveIntegerField(default=0)
|
||||||
is_active = models.BooleanField(default=True)
|
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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
@@ -53,6 +86,10 @@ class SubProduct(models.Model):
|
|||||||
verbose_name = "Sub Product"
|
verbose_name = "Sub Product"
|
||||||
verbose_name_plural = "Sub Products"
|
verbose_name_plural = "Sub Products"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rendered_description(self):
|
||||||
|
return render_content(self.description, self.description_format)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.main_product.name} › {self.name}"
|
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):
|
class Article(models.Model):
|
||||||
sub_product = models.ForeignKey(
|
sub_product = models.ForeignKey(
|
||||||
@@ -79,6 +125,9 @@ class Article(models.Model):
|
|||||||
)
|
)
|
||||||
title = models.CharField(max_length=300)
|
title = models.CharField(max_length=300)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
|
description_format = models.CharField(
|
||||||
|
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||||||
|
)
|
||||||
order = models.PositiveIntegerField(default=0)
|
order = models.PositiveIntegerField(default=0)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -88,10 +137,64 @@ class Article(models.Model):
|
|||||||
verbose_name = "Article"
|
verbose_name = "Article"
|
||||||
verbose_name_plural = "Articles"
|
verbose_name_plural = "Articles"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rendered_description(self):
|
||||||
|
return render_content(self.description, self.description_format)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
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):
|
class ArticleSection(models.Model):
|
||||||
article = models.ForeignKey(
|
article = models.ForeignKey(
|
||||||
Article,
|
Article,
|
||||||
@@ -100,6 +203,9 @@ class ArticleSection(models.Model):
|
|||||||
)
|
)
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
value = models.TextField()
|
value = models.TextField()
|
||||||
|
value_format = models.CharField(
|
||||||
|
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||||||
|
)
|
||||||
order = models.PositiveIntegerField(default=0)
|
order = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -107,5 +213,9 @@ class ArticleSection(models.Model):
|
|||||||
verbose_name = "Article Section"
|
verbose_name = "Article Section"
|
||||||
verbose_name_plural = "Article Sections"
|
verbose_name_plural = "Article Sections"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rendered_value(self):
|
||||||
|
return render_content(self.value, self.value_format)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.article.title} › {self.title}"
|
return f"{self.article.title} › {self.title}"
|
||||||
|
|||||||
@@ -135,3 +135,30 @@ class SubProductDetailViewTest(ProductViewsSetup):
|
|||||||
)
|
)
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 404)
|
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(),
|
views.MainProductDetailView.as_view(),
|
||||||
name="main_product_detail",
|
name="main_product_detail",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"<slug:main_slug>/<slug:sub_slug>/versions/",
|
||||||
|
views.SubProductOlderVersionsView.as_view(),
|
||||||
|
name="sub_product_versions",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:main_slug>/<slug:sub_slug>/",
|
"<slug:main_slug>/<slug:sub_slug>/",
|
||||||
views.SubProductDetailView.as_view(),
|
views.SubProductDetailView.as_view(),
|
||||||
|
|||||||
+119
-5
@@ -1,7 +1,22 @@
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.views.generic import DetailView, ListView, TemplateView
|
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):
|
class ProductOverviewView(ListView):
|
||||||
@@ -43,11 +58,110 @@ class SubProductDetailView(TemplateView):
|
|||||||
context["sub_product"] = sub_product
|
context["sub_product"] = sub_product
|
||||||
context["articles"] = sub_product.articles.prefetch_related("sections").all()
|
context["articles"] = sub_product.articles.prefetch_related("sections").all()
|
||||||
context["siblings"] = (
|
context["siblings"] = (
|
||||||
SubProduct.objects.filter(
|
SubProduct.objects.filter(main_product=main_product, is_active=True)
|
||||||
main_product=main_product,
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
.exclude(pk=sub_product.pk)
|
.exclude(pk=sub_product.pk)
|
||||||
.order_by("order", "name")
|
.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
|
return context
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ gunicorn>=22.0.0
|
|||||||
whitenoise[brotli]>=6.7.0
|
whitenoise[brotli]>=6.7.0
|
||||||
Pillow>=10.4.0
|
Pillow>=10.4.0
|
||||||
python-dotenv>=1.0.1
|
python-dotenv>=1.0.1
|
||||||
|
markdown>=3.6
|
||||||
|
|||||||
@@ -1542,6 +1542,446 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
margin-top: 0.5rem;
|
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 {
|
.info-block {
|
||||||
padding: 2.5rem;
|
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 }}"
|
aria-labelledby="faq-question-{{ entry.pk }}"
|
||||||
hidden
|
hidden
|
||||||
>
|
>
|
||||||
<p>{{ entry.answer }}</p>
|
<div class="rich-content">{{ entry.rendered_answer }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
including radiomics and machine learning analysis.
|
including radiomics and machine learning analysis.
|
||||||
</p>
|
</p>
|
||||||
<div class="hero-actions">
|
<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>
|
<a href="{% url 'pages:about' %}" class="btn-ghost">About Radiuma</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
<li><a href="{% url 'pages:home' %}">Home</a></li>
|
<li><a href="{% url 'pages:home' %}">Home</a></li>
|
||||||
<li><a href="{% url 'pages:about' %}">What is Radiuma</a></li>
|
<li><a href="{% url 'pages:about' %}">What is Radiuma</a></li>
|
||||||
<li><a href="{% url 'products:overview' %}">Products</a></li>
|
<li><a href="{% url 'products:overview' %}">Products</a></li>
|
||||||
<li><a href="{% url 'pages:downloads' %}">Downloads</a></li>
|
|
||||||
<li><a href="{% url 'pages:faq' %}">FAQ</a></li>
|
<li><a href="{% url 'pages:faq' %}">FAQ</a></li>
|
||||||
<li><a href="{% url 'pages:contact' %}">Contact</a></li>
|
<li><a href="{% url 'pages:contact' %}">Contact</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -72,12 +72,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</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">
|
<li class="nav-item">
|
||||||
<a href="{% url 'pages:faq' %}" class="nav-link {% if request.resolver_match.url_name == 'faq' %}active{% endif %}">
|
<a href="{% url 'pages:faq' %}" class="nav-link {% if request.resolver_match.url_name == 'faq' %}active{% endif %}">
|
||||||
FAQ
|
FAQ
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="product-detail-text">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,24 +44,125 @@
|
|||||||
|
|
||||||
<div class="sub-product-desc glass-card fade-in">
|
<div class="sub-product-desc glass-card fade-in">
|
||||||
<h2>About {{ sub_product.name }}</h2>
|
<h2>About {{ sub_product.name }}</h2>
|
||||||
<p>{{ sub_product.description }}</p>
|
<div class="rich-content">{{ sub_product.rendered_description }}</div>
|
||||||
</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 %}
|
{% if articles %}
|
||||||
<section aria-labelledby="articles-heading">
|
<section aria-label="Content sections">
|
||||||
<h2 class="articles-section-title" id="articles-heading">Articles</h2>
|
|
||||||
<div class="articles-list">
|
<div class="articles-list">
|
||||||
{% for article in articles %}
|
{% for article in articles %}
|
||||||
<article class="article-card glass-card fade-in" aria-labelledby="article-{{ article.pk }}-title">
|
<article class="article-card glass-card fade-in" aria-labelledby="article-{{ article.pk }}-title">
|
||||||
<h3 class="article-title" id="article-{{ article.pk }}-title">{{ article.title }}</h3>
|
<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 %}
|
{% if article.sections.all %}
|
||||||
<div class="article-sections">
|
<div class="article-sections">
|
||||||
<dl class="article-sections-list">
|
<dl class="article-sections-list">
|
||||||
{% for section in article.sections.all %}
|
{% for section in article.sections.all %}
|
||||||
<div class="article-section-item">
|
<div class="article-section-item">
|
||||||
<dt class="section-key">{{ section.title }}</dt>
|
<dt class="section-key">{{ section.title }}</dt>
|
||||||
<dd class="section-value">{{ section.value }}</dd>
|
<dd class="section-value rich-content">{{ section.rendered_value }}</dd>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
</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