feat: rebuild site content management

Replace legacy FAQ, downloads, and release flows with project and blog publishing. Add configurable branding and theme fields plus clean-start Compose seeding support.
This commit is contained in:
mohamad
2026-09-24 02:32:27 +03:30
parent 4fc4282f0f
commit 588d95f889
43 changed files with 703 additions and 1193 deletions
+5 -122
View File
@@ -3,7 +3,7 @@ from django.contrib import admin
from apps.core.admin_video import video_admin_preview
from apps.core.video import VIDEO_ADMIN_FIELDSET
from .models import Article, ArticleCitation, ArticleSection, MainProduct, ProductVideo, SubProduct, SubProductVersion
from .models import Article, ArticleCitation, ArticleSection, MainProduct, ProductVideo, SubProduct
class ArticleCitationInline(admin.TabularInline):
@@ -38,41 +38,6 @@ class MainProductArticleInline(admin.StackedInline):
show_change_link = True
RELEASE_VERSION_INLINE_FIELDS = (
"version",
"release_channel",
"channel_label",
"is_featured",
"show_on_product_page",
"windows_download_url",
"macos_download_url",
"linux_download_url",
"source_code_url",
"package_resource_url",
"release_notes",
"is_active",
"order",
)
class SubProductVersionInline(admin.TabularInline):
model = SubProductVersion
fk_name = "sub_product"
extra = 0
ordering = ("order", "version")
fields = RELEASE_VERSION_INLINE_FIELDS
show_change_link = True
class MainProductVersionInline(admin.TabularInline):
model = SubProductVersion
fk_name = "main_product"
extra = 0
ordering = ("order", "version")
fields = RELEASE_VERSION_INLINE_FIELDS
show_change_link = True
class ProductVideoInline(admin.StackedInline):
model = ProductVideo
fk_name = "main_product"
@@ -98,7 +63,7 @@ class ProductVideoInline(admin.StackedInline):
class SubProductInline(admin.StackedInline):
model = SubProduct
extra = 0
fields = ("name", "slug", "distribution", "short_description", "image", "logo", "show_on_homepage", "homepage_order", "order", "is_active")
fields = ("name", "slug", "short_description", "image", "logo", "show_on_homepage", "homepage_order", "order", "is_active")
ordering = ("order",)
show_change_link = True
prepopulated_fields = {"slug": ("name",)}
@@ -111,7 +76,7 @@ class MainProductAdmin(admin.ModelAdmin):
search_fields = ("name", "description")
prepopulated_fields = {"slug": ("name",)}
list_editable = ("order", "is_active", "show_on_homepage", "homepage_order")
inlines = [MainProductArticleInline, ProductVideoInline, MainProductVersionInline, SubProductInline]
inlines = [MainProductArticleInline, ProductVideoInline, SubProductInline]
fieldsets = (
(
None,
@@ -120,11 +85,6 @@ class MainProductAdmin(admin.ModelAdmin):
"name",
"slug",
"short_description",
"distribution",
"package_resource_button_text",
"package_source_button_text",
"downloads_section_link_text",
"downloads_section_link_url",
)
},
),
@@ -162,19 +122,18 @@ class SubProductAdmin(admin.ModelAdmin):
list_display = (
"name",
"main_product",
"distribution",
"show_on_homepage",
"homepage_order",
"order",
"is_active",
"created_at",
)
list_filter = ("is_active", "distribution", "main_product", "show_on_homepage")
list_filter = ("is_active", "main_product", "show_on_homepage")
search_fields = ("name", "description", "main_product__name")
prepopulated_fields = {"slug": ("name",)}
list_editable = ("order", "is_active", "show_on_homepage", "homepage_order")
raw_id_fields = ("main_product",)
inlines = [SubProductArticleInline, SubProductVideoInline, SubProductVersionInline]
inlines = [SubProductArticleInline, SubProductVideoInline]
fieldsets = (
(
None,
@@ -183,12 +142,7 @@ class SubProductAdmin(admin.ModelAdmin):
"main_product",
"name",
"slug",
"distribution",
"short_description",
"package_resource_button_text",
"package_source_button_text",
"downloads_section_link_text",
"downloads_section_link_url",
)
},
),
@@ -261,74 +215,3 @@ class ArticleSectionAdmin(admin.ModelAdmin):
("Content", {"fields": ("value_format", "value")}),
("Settings", {"fields": ("order",)}),
)
@admin.register(SubProductVersion)
class SubProductVersionAdmin(admin.ModelAdmin):
list_display = (
"parent",
"version",
"release_channel",
"channel_label",
"is_featured",
"show_on_product_page",
"is_active",
"order",
)
list_filter = ("is_active", "release_channel", "is_featured", "show_on_product_page", "main_product", "sub_product__main_product")
search_fields = ("main_product__name", "sub_product__name", "version")
list_editable = ("is_active", "order")
raw_id_fields = ("main_product", "sub_product")
fieldsets = (
(
None,
{
"description": (
"Set a preset channel badge (Stable, Beta, Previous, etc.) or write a custom "
"label. Mark one release as Primary (featured) for the main download block. "
"Enable “Show on product page” for additional inline channels such as beta or "
"previous versions."
),
"fields": (
"main_product",
"sub_product",
"version",
"release_channel",
"channel_label",
"is_featured",
"show_on_product_page",
)
},
),
(
"Installable downloads",
{
"description": (
"For each platform, set an external URL, upload a file, both, or neither. "
"When a URL is set it is used on the site; otherwise an uploaded file is served. "
"To delete an uploaded file from the server, open this release, check Clear next to the file, and Save."
),
"fields": (
"windows_download_url",
"windows_download_file",
"macos_download_url",
"macos_download_file",
"linux_download_url",
"linux_download_file",
"source_code_url",
"source_code_file",
),
},
),
("Package link", {"fields": ("package_resource_url",)}),
("Details", {"fields": ("release_notes",)}),
("Settings", {"fields": ("is_active", "order")}),
)
@admin.display(description="Parent")
def parent(self, obj):
if obj.main_product_id:
return obj.main_product
if obj.sub_product_id:
return obj.sub_product
return "—"
+1 -1
View File
@@ -4,7 +4,7 @@ from django.apps import AppConfig
class ProductsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.products"
verbose_name = "Products"
verbose_name = "Projects"
def ready(self):
from . import signals # noqa: F401
@@ -0,0 +1,90 @@
# Generated by Django 5.2.16 on 2026-09-23 22:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0021_release_channels'),
]
operations = [
migrations.AlterModelOptions(
name='mainproduct',
options={'ordering': ['order', 'name'], 'verbose_name': 'Main Project', 'verbose_name_plural': 'Main Projects'},
),
migrations.AlterModelOptions(
name='productvideo',
options={'ordering': ['order', 'pk'], 'verbose_name': 'Project Video', 'verbose_name_plural': 'Project Videos'},
),
migrations.AlterModelOptions(
name='subproduct',
options={'ordering': ['order', 'name'], 'verbose_name': 'Sub-project', 'verbose_name_plural': 'Sub-projects'},
),
migrations.RemoveField(
model_name='mainproduct',
name='distribution',
),
migrations.RemoveField(
model_name='mainproduct',
name='downloads_section_link_text',
),
migrations.RemoveField(
model_name='mainproduct',
name='downloads_section_link_url',
),
migrations.RemoveField(
model_name='mainproduct',
name='package_resource_button_text',
),
migrations.RemoveField(
model_name='mainproduct',
name='package_source_button_text',
),
migrations.RemoveField(
model_name='subproduct',
name='distribution',
),
migrations.RemoveField(
model_name='subproduct',
name='downloads_section_link_text',
),
migrations.RemoveField(
model_name='subproduct',
name='downloads_section_link_url',
),
migrations.RemoveField(
model_name='subproduct',
name='package_resource_button_text',
),
migrations.RemoveField(
model_name='subproduct',
name='package_source_button_text',
),
migrations.AlterField(
model_name='mainproduct',
name='homepage_order',
field=models.PositiveIntegerField(default=0, help_text='Order within the homepage Projects section.'),
),
migrations.AlterField(
model_name='mainproduct',
name='show_on_homepage',
field=models.BooleanField(default=False, help_text='Display this project in the homepage Projects section.'),
),
migrations.AlterField(
model_name='productvideo',
name='video_styled_background',
field=models.BooleanField(default=False, help_text='Show the video in a glass panel with an ambient glow.'),
),
migrations.AlterField(
model_name='subproduct',
name='homepage_order',
field=models.PositiveIntegerField(default=0, help_text='Order within the homepage Projects section.'),
),
migrations.AlterField(
model_name='subproduct',
name='show_on_homepage',
field=models.BooleanField(default=False, help_text='Display this sub-project in the homepage Projects section.'),
),
]
+12 -91
View File
@@ -30,13 +30,6 @@ INSTALLABLE_PLATFORM_SPECS = (
("source_code_url", "Source code", None),
)
DISTRIBUTION_INSTALLABLE = "installable"
DISTRIBUTION_PACKAGE = "package"
DISTRIBUTION_CHOICES = [
(DISTRIBUTION_INSTALLABLE, "Installable application"),
(DISTRIBUTION_PACKAGE, "Package (external / non-installable)"),
]
RELEASE_CHANNEL_STABLE = "stable"
RELEASE_CHANNEL_BETA = "beta"
RELEASE_CHANNEL_RC = "rc"
@@ -66,7 +59,6 @@ RELEASE_CHANNEL_LABELS = {
RELEASE_CHANNEL_PREVIOUS: "Previous",
}
class MainProduct(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True)
@@ -76,42 +68,17 @@ class MainProduct(models.Model):
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
)
image = models.ImageField(upload_to="products/main/", blank=True, null=True)
distribution = models.CharField(
max_length=20,
choices=DISTRIBUTION_CHOICES,
default=DISTRIBUTION_INSTALLABLE,
help_text="Only affects how releases appear on the public site.",
)
package_resource_button_text = models.CharField(
max_length=100,
default="PyPI",
help_text="Label for the package resource button on package releases.",
)
package_source_button_text = models.CharField(
max_length=100,
default="Source",
help_text="Label for the source code button on package releases.",
)
downloads_section_link_url = models.URLField(
blank=True,
help_text="Optional link shown in the top-right corner of the downloads section.",
)
downloads_section_link_text = models.CharField(
max_length=100,
blank=True,
help_text="Label for the optional downloads section link.",
)
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
show_on_homepage = models.BooleanField(default=False, help_text="Display this product in the homepage Products section.")
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Products section.")
show_on_homepage = models.BooleanField(default=False, help_text="Display this project in the homepage Projects section.")
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Projects section.")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["order", "name"]
verbose_name = "Main Product"
verbose_name_plural = "Main Products"
verbose_name = "Main Project"
verbose_name_plural = "Main Projects"
@property
def rendered_description(self):
@@ -128,18 +95,7 @@ class MainProduct(models.Model):
def get_absolute_url(self):
return reverse("products:main_product_detail", kwargs={"main_slug": self.slug})
def get_versions_archive_url(self):
return reverse(
"products:main_product_versions",
kwargs={"main_slug": self.slug},
)
class SubProduct(models.Model):
DISTRIBUTION_INSTALLABLE = DISTRIBUTION_INSTALLABLE
DISTRIBUTION_PACKAGE = DISTRIBUTION_PACKAGE
DISTRIBUTION_CHOICES = DISTRIBUTION_CHOICES
main_product = models.ForeignKey(
MainProduct,
on_delete=models.CASCADE,
@@ -156,41 +112,16 @@ class SubProduct(models.Model):
logo = models.ImageField(upload_to="products/sub_logos/", blank=True, null=True, help_text="Small logo shown as a corner badge on homepage product cards.")
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
show_on_homepage = models.BooleanField(default=False, help_text="Display this sub-product in the homepage Products section.")
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Products section.")
distribution = models.CharField(
max_length=20,
choices=DISTRIBUTION_CHOICES,
default=DISTRIBUTION_INSTALLABLE,
help_text="Only affects how releases appear on the public site.",
)
package_resource_button_text = models.CharField(
max_length=100,
default="PyPI",
help_text="Label for the package resource button on package releases.",
)
package_source_button_text = models.CharField(
max_length=100,
default="Source",
help_text="Label for the source code button on package releases.",
)
downloads_section_link_url = models.URLField(
blank=True,
help_text="Optional link shown in the top-right corner of the downloads section.",
)
downloads_section_link_text = models.CharField(
max_length=100,
blank=True,
help_text="Label for the optional downloads section link.",
)
show_on_homepage = models.BooleanField(default=False, help_text="Display this sub-project in the homepage Projects section.")
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Projects section.")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["order", "name"]
unique_together = [["main_product", "slug"]]
verbose_name = "Sub Product"
verbose_name_plural = "Sub Products"
verbose_name = "Sub-project"
verbose_name_plural = "Sub-projects"
@property
def rendered_description(self):
@@ -213,16 +144,6 @@ 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):
main_product = models.ForeignKey(
MainProduct,
@@ -287,7 +208,7 @@ class Article(models.Model):
has_sub = self.sub_product_id is not None
if has_main == has_sub:
raise ValidationError(
"An article must belong to exactly one main product or sub-product."
"An article must belong to exactly one main project or sub-project."
)
def __str__(self):
@@ -572,8 +493,8 @@ class ProductVideo(VideoBlockMixin, models.Model):
class Meta:
ordering = ["order", "pk"]
verbose_name = "Product Video"
verbose_name_plural = "Product Videos"
verbose_name = "Project Video"
verbose_name_plural = "Project Videos"
constraints = [
models.CheckConstraint(
check=(
@@ -590,7 +511,7 @@ class ProductVideo(VideoBlockMixin, models.Model):
has_sub = self.sub_product_id is not None
if has_main == has_sub:
raise ValidationError(
"A product video must belong to exactly one main product or sub-product."
"A project video must belong to exactly one main project or sub-project."
)
self.clean_video_fields(require=True)
+7 -201
View File
@@ -1,11 +1,7 @@
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
from django.test import TestCase
from django.urls import reverse
from apps.products.models import Article, ArticleSection, MainProduct, SubProduct, SubProductVersion
from apps.products.models import Article, ArticleSection, MainProduct, SubProduct
class ProductViewsSetup(TestCase):
@@ -153,198 +149,8 @@ class SubProductDetailViewTest(ProductViewsSetup):
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=True,
release_channel="stable",
windows_download_url="https://example.com/w",
is_active=True,
)
SubProductVersion.objects.create(
sub_product=self.sub_product,
version="9.8",
is_featured=False,
windows_download_url="https://example.com/w2",
is_active=True,
)
url = reverse(
"products:sub_product_versions",
kwargs={"main_slug": "com2care", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "products/versions_archive.html")
def test_main_versions_returns_200(self):
from apps.products.models import SubProductVersion
SubProductVersion.objects.create(
main_product=self.main_product,
version="9.9",
is_featured=True,
release_channel="stable",
windows_download_url="https://example.com/win.exe",
is_active=True,
)
SubProductVersion.objects.create(
main_product=self.main_product,
version="9.8",
is_active=True,
)
url = reverse(
"products:main_product_versions",
kwargs={"main_slug": "com2care"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "products/versions_archive.html")
def test_main_detail_releases_in_context(self):
from apps.products.models import SubProductVersion
SubProductVersion.objects.create(
main_product=self.main_product,
version="1.0",
windows_download_url="https://example.com/win.exe",
is_active=True,
)
url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url)
self.assertTrue(response.context["show_releases_section"])
def test_installable_shows_release_channel_badges(self):
from apps.products.models import RELEASE_CHANNEL_BETA, RELEASE_CHANNEL_PREVIOUS, SubProductVersion
SubProductVersion.objects.create(
main_product=self.main_product,
version="2.0",
release_channel=RELEASE_CHANNEL_BETA,
is_featured=True,
windows_download_url="https://example.com/beta.exe",
is_active=True,
)
SubProductVersion.objects.create(
main_product=self.main_product,
version="1.9",
release_channel=RELEASE_CHANNEL_PREVIOUS,
show_on_product_page=True,
windows_download_url="https://example.com/prev.exe",
is_active=True,
)
url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url)
self.assertContains(response, "Beta")
self.assertContains(response, "Previous")
self.assertEqual(len(response.context["inline_download_channels"]), 1)
def test_custom_channel_label_overrides_preset(self):
from apps.products.models import RELEASE_CHANNEL_STABLE, SubProductVersion
SubProductVersion.objects.create(
main_product=self.main_product,
version="3.0",
release_channel=RELEASE_CHANNEL_STABLE,
channel_label="Early Access",
is_featured=True,
windows_download_url="https://example.com/ea.exe",
is_active=True,
)
url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(url)
self.assertContains(response, "Early Access")
self.assertNotContains(response, ">Stable<")
@override_settings(MEDIA_ROOT=settings.BASE_DIR / "test_media_releases")
class ReleaseAssetDownloadViewTest(ProductViewsSetup):
def test_uploaded_file_download(self):
release_file = SimpleUploadedFile("setup.exe", b"binary-payload")
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="2.0",
windows_download_file=release_file,
is_active=True,
)
url = reverse(
"products:release_asset_download",
kwargs={"version_id": version.pk, "asset": "windows"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(b"".join(response.streaming_content), b"binary-payload")
self.assertIn("attachment", response["Content-Disposition"])
self.assertIn("setup.exe", response["Content-Disposition"])
def test_external_url_blocks_file_download(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="2.1",
windows_download_url="https://example.com/win.exe",
windows_download_file=SimpleUploadedFile("local.exe", b"x"),
is_active=True,
)
url = reverse(
"products:release_asset_download",
kwargs={"version_id": version.pk, "asset": "windows"},
)
self.assertEqual(self.client.get(url).status_code, 404)
def test_product_page_uses_file_when_no_url(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="3.0",
windows_download_file=SimpleUploadedFile("win.tar.gz", b"gz"),
is_active=True,
)
detail_url = reverse("products:main_product_detail", kwargs={"main_slug": "com2care"})
response = self.client.get(detail_url)
download_url = reverse(
"products:release_asset_download",
kwargs={"version_id": version.pk, "asset": "windows"},
)
self.assertContains(response, download_url)
def test_inactive_version_returns_404(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="0.1",
windows_download_file=SimpleUploadedFile("a.exe", b"a"),
is_active=False,
)
url = reverse(
"products:release_asset_download",
kwargs={"version_id": version.pk, "asset": "windows"},
)
self.assertEqual(self.client.get(url).status_code, 404)
def test_clearing_file_removes_from_disk(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="4.0",
windows_download_file=SimpleUploadedFile("remove-me.exe", b"data"),
is_active=True,
)
stored_path = version.windows_download_file.path
self.assertTrue(stored_path)
version.windows_download_file = ""
version.save()
version.refresh_from_db()
self.assertFalse(version.windows_download_file)
self.assertEqual(version.windows_download_filename, "")
self.assertFalse(os.path.exists(stored_path))
def test_deleting_version_removes_files_from_disk(self):
version = SubProductVersion.objects.create(
main_product=self.main_product,
version="5.0",
windows_download_file=SimpleUploadedFile("gone.exe", b"data"),
is_active=True,
)
stored_path = version.windows_download_file.path
version.delete()
self.assertFalse(os.path.exists(stored_path))
class RemovedDistributionViewsTest(ProductViewsSetup):
def test_project_pages_do_not_expose_downloads_or_versions(self):
response = self.client.get(self.main_product.get_absolute_url())
self.assertNotContains(response, "Downloads")
self.assertNotContains(response, "Older releases")
-15
View File
@@ -6,11 +6,6 @@ app_name = "products"
urlpatterns = [
path("", views.ProductOverviewView.as_view(), name="overview"),
path(
"releases/<int:version_id>/<slug:asset>/download/",
views.ReleaseAssetDownloadView.as_view(),
name="release_asset_download",
),
path(
"<slug:main_slug>/videos/",
views.ProductVideoArchiveView.as_view(),
@@ -26,16 +21,6 @@ urlpatterns = [
views.MainProductDetailView.as_view(),
name="main_product_detail",
),
path(
"<slug:main_slug>/versions/",
views.MainProductOlderVersionsView.as_view(),
name="main_product_versions",
),
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(),
+2 -95
View File
@@ -1,14 +1,9 @@
import mimetypes
from django.http import FileResponse, Http404
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views import View
from django.views.generic import DetailView, ListView, TemplateView
from .models import Article, ArticleCitation, ArticleSection, MainProduct, ProductVideo, SubProduct, SubProductVersion
from .release_assets import FILE_FIELD_BY_ASSET_KEY, RELEASE_ASSET_KEYS, URL_FIELD_BY_ASSET_KEY
from .release_context import build_archive_context, build_release_context
from .models import MainProduct, ProductVideo, SubProduct
VIDEO_PREVIEW_LIMIT = 3
VIDEO_ARCHIVE_PAGE_SIZE = 6
@@ -23,31 +18,6 @@ def _product_video_preview(queryset, archive_url):
}
class ReleaseAssetDownloadView(View):
def get(self, request, version_id, asset):
if asset not in RELEASE_ASSET_KEYS:
raise Http404
version = get_object_or_404(SubProductVersion, pk=version_id, is_active=True)
parent = version.sub_product or version.main_product
if parent is None or not parent.is_active:
raise Http404
url_field = URL_FIELD_BY_ASSET_KEY[asset]
if (getattr(version, url_field) or "").strip():
raise Http404
file_field = FILE_FIELD_BY_ASSET_KEY[asset]
release_file = getattr(version, file_field)
if not release_file:
raise Http404
filename = version.release_download_filename(asset)
content_type, _ = mimetypes.guess_type(filename)
return FileResponse(
release_file.open("rb"),
as_attachment=True,
filename=filename,
content_type=content_type or "application/octet-stream",
)
class ProductOverviewView(ListView):
model = MainProduct
template_name = "products/overview.html"
@@ -81,35 +51,6 @@ class MainProductDetailView(DetailView):
),
)
)
release_context = build_release_context(
self.object.distribution,
self.object.versions.filter(is_active=True),
self.object,
)
release_context["versions_archive_url"] = self.object.get_versions_archive_url()
context.update(release_context)
return context
class MainProductOlderVersionsView(TemplateView):
template_name = "products/versions_archive.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,
)
active_versions = main_product.versions.filter(is_active=True)
context["main_product"] = main_product
context["sub_product"] = None
context["product_detail_url"] = main_product.get_absolute_url()
context.update(
build_archive_context(
main_product.distribution, active_versions, main_product
)
)
return context
@@ -152,12 +93,6 @@ class SubProductDetailView(TemplateView):
.exclude(pk=sub_product.pk)
.order_by("order", "name")
)
active_versions = sub_product.versions.filter(is_active=True)
release_context = build_release_context(
sub_product.distribution, active_versions, sub_product
)
release_context["versions_archive_url"] = sub_product.get_versions_archive_url()
context.update(release_context)
return context
@@ -203,31 +138,3 @@ class ProductVideoArchiveView(ListView):
}
)
return context
class SubProductOlderVersionsView(TemplateView):
template_name = "products/versions_archive.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)
context["main_product"] = main_product
context["sub_product"] = sub_product
context["product_detail_url"] = sub_product.get_absolute_url()
context.update(
build_archive_context(
sub_product.distribution, active_versions, sub_product
)
)
return context