feat: change entire downloads
This commit is contained in:
+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
|
||||
|
||||
Reference in New Issue
Block a user