feat: add upload and delete option for releases
This commit is contained in:
+12
-1
@@ -55,6 +55,7 @@ class SubProductVersionInline(admin.TabularInline):
|
||||
extra = 0
|
||||
ordering = ("order", "version")
|
||||
fields = RELEASE_VERSION_INLINE_FIELDS
|
||||
show_change_link = True
|
||||
|
||||
|
||||
class MainProductVersionInline(admin.TabularInline):
|
||||
@@ -63,6 +64,7 @@ class MainProductVersionInline(admin.TabularInline):
|
||||
extra = 0
|
||||
ordering = ("order", "version")
|
||||
fields = RELEASE_VERSION_INLINE_FIELDS
|
||||
show_change_link = True
|
||||
|
||||
|
||||
class SubProductInline(admin.StackedInline):
|
||||
@@ -208,12 +210,21 @@ class SubProductVersionAdmin(admin.ModelAdmin):
|
||||
(
|
||||
"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",)}),
|
||||
|
||||
@@ -5,3 +5,6 @@ class ProductsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.products"
|
||||
verbose_name = "Products"
|
||||
|
||||
def ready(self):
|
||||
from . import signals # noqa: F401
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0.2 on 2026-05-31 09:42
|
||||
|
||||
import apps.products.release_assets
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0013_package_release_button_labels'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subproductversion',
|
||||
name='linux_download_file',
|
||||
field=models.FileField(blank=True, help_text='Optional. Served from this site when the matching URL above is empty. Any file type and size are allowed; large uploads may require web server limits.', upload_to=apps.products.release_assets.release_file_upload_to),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subproductversion',
|
||||
name='macos_download_file',
|
||||
field=models.FileField(blank=True, help_text='Optional. Served from this site when the matching URL above is empty. Any file type and size are allowed; large uploads may require web server limits.', upload_to=apps.products.release_assets.release_file_upload_to),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subproductversion',
|
||||
name='source_code_file',
|
||||
field=models.FileField(blank=True, help_text='Optional. Served from this site when the matching URL above is empty. Any file type and size are allowed; large uploads may require web server limits.', upload_to=apps.products.release_assets.release_file_upload_to),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subproductversion',
|
||||
name='windows_download_file',
|
||||
field=models.FileField(blank=True, help_text='Optional. Served from this site when the matching URL above is empty. Any file type and size are allowed; large uploads may require web server limits.', upload_to=apps.products.release_assets.release_file_upload_to),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 5.0.2 on 2026-05-31 09:52
|
||||
|
||||
import apps.products.release_assets
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0014_release_download_files'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subproductversion',
|
||||
name='linux_download_filename',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subproductversion',
|
||||
name='macos_download_filename',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subproductversion',
|
||||
name='source_code_filename',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subproductversion',
|
||||
name='windows_download_filename',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subproductversion',
|
||||
name='linux_download_file',
|
||||
field=models.FileField(blank=True, help_text='Optional. Served from this site when the matching URL above is empty. Any file type and size are allowed; large uploads may require web server limits.', storage=apps.products.release_assets.ReleaseFileStorage(), upload_to=apps.products.release_assets.release_linux_file_upload_to),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subproductversion',
|
||||
name='macos_download_file',
|
||||
field=models.FileField(blank=True, help_text='Optional. Served from this site when the matching URL above is empty. Any file type and size are allowed; large uploads may require web server limits.', storage=apps.products.release_assets.ReleaseFileStorage(), upload_to=apps.products.release_assets.release_macos_file_upload_to),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subproductversion',
|
||||
name='source_code_file',
|
||||
field=models.FileField(blank=True, help_text='Optional. Served from this site when the matching URL above is empty. Any file type and size are allowed; large uploads may require web server limits.', storage=apps.products.release_assets.ReleaseFileStorage(), upload_to=apps.products.release_assets.release_source_file_upload_to),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subproductversion',
|
||||
name='windows_download_file',
|
||||
field=models.FileField(blank=True, help_text='Optional. Served from this site when the matching URL above is empty. Any file type and size are allowed; large uploads may require web server limits.', storage=apps.products.release_assets.ReleaseFileStorage(), upload_to=apps.products.release_assets.release_windows_file_upload_to),
|
||||
),
|
||||
]
|
||||
+105
-7
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@@ -5,6 +7,20 @@ from django.utils.text import slugify
|
||||
|
||||
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_PLAIN, render_content
|
||||
|
||||
from .release_assets import (
|
||||
FILE_FIELD_BY_ASSET_KEY,
|
||||
RELEASE_ASSET_KEY_BY_URL_FIELD,
|
||||
RELEASE_FILE_BY_URL_FIELD,
|
||||
RELEASE_FILE_HELP_TEXT,
|
||||
RELEASE_ORIGINAL_FILENAME_BY_ASSET,
|
||||
RELEASE_ORIGINAL_FILENAME_FIELDS,
|
||||
release_file_storage,
|
||||
release_linux_file_upload_to,
|
||||
release_macos_file_upload_to,
|
||||
release_source_file_upload_to,
|
||||
release_windows_file_upload_to,
|
||||
)
|
||||
|
||||
|
||||
INSTALLABLE_PLATFORM_SPECS = (
|
||||
("windows_download_url", "Windows", "images/icon-windows.svg"),
|
||||
@@ -254,6 +270,34 @@ class SubProductVersion(models.Model):
|
||||
macos_download_url = models.URLField(blank=True)
|
||||
linux_download_url = models.URLField(blank=True)
|
||||
source_code_url = models.URLField(blank=True)
|
||||
windows_download_file = models.FileField(
|
||||
upload_to=release_windows_file_upload_to,
|
||||
storage=release_file_storage,
|
||||
blank=True,
|
||||
help_text=RELEASE_FILE_HELP_TEXT,
|
||||
)
|
||||
macos_download_file = models.FileField(
|
||||
upload_to=release_macos_file_upload_to,
|
||||
storage=release_file_storage,
|
||||
blank=True,
|
||||
help_text=RELEASE_FILE_HELP_TEXT,
|
||||
)
|
||||
linux_download_file = models.FileField(
|
||||
upload_to=release_linux_file_upload_to,
|
||||
storage=release_file_storage,
|
||||
blank=True,
|
||||
help_text=RELEASE_FILE_HELP_TEXT,
|
||||
)
|
||||
source_code_file = models.FileField(
|
||||
upload_to=release_source_file_upload_to,
|
||||
storage=release_file_storage,
|
||||
blank=True,
|
||||
help_text=RELEASE_FILE_HELP_TEXT,
|
||||
)
|
||||
windows_download_filename = models.CharField(max_length=255, blank=True)
|
||||
macos_download_filename = models.CharField(max_length=255, blank=True)
|
||||
linux_download_filename = models.CharField(max_length=255, blank=True)
|
||||
source_code_filename = models.CharField(max_length=255, 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.",
|
||||
@@ -299,19 +343,73 @@ class SubProductVersion(models.Model):
|
||||
parent = self.sub_product or self.main_product
|
||||
return f"{parent.name} v{self.version}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
for file_field, name_field in RELEASE_ORIGINAL_FILENAME_FIELDS.items():
|
||||
field_file = getattr(self, file_field)
|
||||
if field_file and field_file._file is not None:
|
||||
setattr(self, name_field, os.path.basename(field_file.name))
|
||||
elif not field_file:
|
||||
setattr(self, name_field, "")
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def release_download_filename(self, asset):
|
||||
name_field = RELEASE_ORIGINAL_FILENAME_BY_ASSET.get(asset)
|
||||
if name_field:
|
||||
stored = (getattr(self, name_field, "") or "").strip()
|
||||
if stored:
|
||||
return stored
|
||||
file_field = FILE_FIELD_BY_ASSET_KEY.get(asset)
|
||||
if file_field:
|
||||
release_file = getattr(self, file_field)
|
||||
if release_file:
|
||||
return os.path.basename(release_file.name)
|
||||
return "download"
|
||||
|
||||
def has_platform_asset(self, url_field_name):
|
||||
if (getattr(self, url_field_name) or "").strip():
|
||||
return True
|
||||
file_field = RELEASE_FILE_BY_URL_FIELD.get(url_field_name)
|
||||
if not file_field:
|
||||
return False
|
||||
return bool(getattr(self, file_field))
|
||||
|
||||
def resolve_asset_url(self, url_field_name):
|
||||
url = (getattr(self, url_field_name) or "").strip()
|
||||
if url:
|
||||
return url
|
||||
file_field = RELEASE_FILE_BY_URL_FIELD.get(url_field_name)
|
||||
if not file_field or not getattr(self, file_field):
|
||||
return ""
|
||||
if not self.pk:
|
||||
return ""
|
||||
asset = RELEASE_ASSET_KEY_BY_URL_FIELD[url_field_name]
|
||||
return reverse(
|
||||
"products:release_asset_download",
|
||||
kwargs={"version_id": self.pk, "asset": asset},
|
||||
)
|
||||
|
||||
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})
|
||||
url = self.resolve_asset_url(field_name)
|
||||
if url:
|
||||
out.append(
|
||||
{
|
||||
"field": field_name,
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"url": url,
|
||||
"external": bool((getattr(self, field_name) or "").strip()),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
def has_any_install_asset(self):
|
||||
return any(
|
||||
(getattr(self, f[0]) or "").strip()
|
||||
for f in INSTALLABLE_PLATFORM_SPECS
|
||||
)
|
||||
return any(self.has_platform_asset(f[0]) for f in INSTALLABLE_PLATFORM_SPECS)
|
||||
|
||||
@property
|
||||
def resolved_source_code_url(self):
|
||||
return self.resolve_asset_url("source_code_url")
|
||||
|
||||
def has_package_link(self):
|
||||
return bool((self.package_resource_url or "").strip())
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import os
|
||||
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.utils.text import get_valid_filename
|
||||
|
||||
|
||||
class ReleaseFileStorage(FileSystemStorage):
|
||||
def get_available_name(self, name, max_length=None):
|
||||
return name
|
||||
|
||||
|
||||
release_file_storage = ReleaseFileStorage()
|
||||
|
||||
RELEASE_FILE_BY_URL_FIELD = {
|
||||
"windows_download_url": "windows_download_file",
|
||||
"macos_download_url": "macos_download_file",
|
||||
"linux_download_url": "linux_download_file",
|
||||
"source_code_url": "source_code_file",
|
||||
}
|
||||
|
||||
RELEASE_ASSET_KEY_BY_URL_FIELD = {
|
||||
"windows_download_url": "windows",
|
||||
"macos_download_url": "macos",
|
||||
"linux_download_url": "linux",
|
||||
"source_code_url": "source",
|
||||
}
|
||||
|
||||
URL_FIELD_BY_ASSET_KEY = {
|
||||
v: k for k, v in RELEASE_ASSET_KEY_BY_URL_FIELD.items()
|
||||
}
|
||||
|
||||
FILE_FIELD_BY_ASSET_KEY = {
|
||||
asset: RELEASE_FILE_BY_URL_FIELD[url_field]
|
||||
for url_field, asset in RELEASE_ASSET_KEY_BY_URL_FIELD.items()
|
||||
}
|
||||
|
||||
RELEASE_ASSET_KEYS = frozenset(FILE_FIELD_BY_ASSET_KEY.keys())
|
||||
|
||||
RELEASE_FILE_HELP_TEXT = (
|
||||
"Optional. Served from this site when the matching URL above is empty. "
|
||||
"To remove an uploaded file, check Clear, then Save — the file is deleted from the server. "
|
||||
"Any file type and size are allowed; large uploads may require web server limits."
|
||||
)
|
||||
|
||||
RELEASE_ORIGINAL_FILENAME_FIELDS = {
|
||||
"windows_download_file": "windows_download_filename",
|
||||
"macos_download_file": "macos_download_filename",
|
||||
"linux_download_file": "linux_download_filename",
|
||||
"source_code_file": "source_code_filename",
|
||||
}
|
||||
|
||||
RELEASE_ORIGINAL_FILENAME_BY_ASSET = {
|
||||
RELEASE_ASSET_KEY_BY_URL_FIELD[url_field]: RELEASE_ORIGINAL_FILENAME_FIELDS[file_field]
|
||||
for url_field, file_field in RELEASE_FILE_BY_URL_FIELD.items()
|
||||
}
|
||||
|
||||
|
||||
def _release_file_path(instance, filename, platform):
|
||||
if instance.sub_product_id:
|
||||
base = f"{instance.sub_product.main_product.slug}/{instance.sub_product.slug}"
|
||||
else:
|
||||
base = instance.main_product.slug
|
||||
version_part = (instance.version or "release").replace("/", "-")
|
||||
safe_name = get_valid_filename(os.path.basename(filename))
|
||||
return f"releases/{base}/{version_part}/{platform}/{safe_name}"
|
||||
|
||||
|
||||
def release_windows_file_upload_to(instance, filename):
|
||||
return _release_file_path(instance, filename, "windows")
|
||||
|
||||
|
||||
def release_macos_file_upload_to(instance, filename):
|
||||
return _release_file_path(instance, filename, "macos")
|
||||
|
||||
|
||||
def release_linux_file_upload_to(instance, filename):
|
||||
return _release_file_path(instance, filename, "linux")
|
||||
|
||||
|
||||
def release_source_file_upload_to(instance, filename):
|
||||
return _release_file_path(instance, filename, "source")
|
||||
|
||||
|
||||
release_file_upload_to = release_windows_file_upload_to
|
||||
@@ -23,7 +23,7 @@ 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():
|
||||
if ver.has_platform_asset(field_name):
|
||||
present.add(field_name)
|
||||
return tuple(s for s in INSTALLABLE_PLATFORM_SPECS if s[0] in present)
|
||||
|
||||
@@ -95,9 +95,14 @@ def build_archive_context(distribution, active_versions, product):
|
||||
for ver in archive:
|
||||
cells = []
|
||||
for field_name, label, icon in specs:
|
||||
u = (getattr(ver, field_name) or "").strip()
|
||||
cells.append(
|
||||
{"field": field_name, "label": label, "icon": icon, "url": u}
|
||||
{
|
||||
"field": field_name,
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"url": ver.resolve_asset_url(field_name),
|
||||
"external": bool((getattr(ver, field_name) or "").strip()),
|
||||
}
|
||||
)
|
||||
archive_rows_installable.append({"version_obj": ver, "cells": cells})
|
||||
context["archive_rows_installable"] = archive_rows_installable
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
from django.db.models.signals import post_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import SubProductVersion
|
||||
from .release_assets import RELEASE_FILE_BY_URL_FIELD
|
||||
|
||||
|
||||
def _stored_release_file_name(version, file_field):
|
||||
release_file = getattr(version, file_field)
|
||||
return release_file.name if release_file else ""
|
||||
|
||||
|
||||
def _delete_release_file_from_disk(version, file_field):
|
||||
release_file = getattr(version, file_field)
|
||||
if release_file:
|
||||
release_file.delete(save=False)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=SubProductVersion)
|
||||
def delete_replaced_or_cleared_release_files(sender, instance, **kwargs):
|
||||
if not instance.pk:
|
||||
return
|
||||
try:
|
||||
previous = SubProductVersion.objects.get(pk=instance.pk)
|
||||
except SubProductVersion.DoesNotExist:
|
||||
return
|
||||
for file_field in RELEASE_FILE_BY_URL_FIELD.values():
|
||||
old_name = _stored_release_file_name(previous, file_field)
|
||||
new_name = _stored_release_file_name(instance, file_field)
|
||||
if old_name and old_name != new_name:
|
||||
_delete_release_file_from_disk(previous, file_field)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=SubProductVersion)
|
||||
def delete_release_files_on_version_delete(sender, instance, **kwargs):
|
||||
for file_field in RELEASE_FILE_BY_URL_FIELD.values():
|
||||
_delete_release_file_from_disk(instance, file_field)
|
||||
@@ -1,7 +1,11 @@
|
||||
from django.test import TestCase
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.products.models import Article, ArticleSection, MainProduct, SubProduct
|
||||
from apps.products.models import Article, ArticleSection, MainProduct, SubProduct, SubProductVersion
|
||||
|
||||
|
||||
class ProductViewsSetup(TestCase):
|
||||
@@ -210,3 +214,94 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup):
|
||||
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"})
|
||||
response = self.client.get(url)
|
||||
self.assertTrue(response.context["show_releases_section"])
|
||||
|
||||
|
||||
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": "radiuma"})
|
||||
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)
|
||||
|
||||
@override_settings(MEDIA_ROOT=settings.BASE_DIR / "test_media_releases")
|
||||
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))
|
||||
|
||||
@override_settings(MEDIA_ROOT=settings.BASE_DIR / "test_media_releases")
|
||||
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))
|
||||
|
||||
@@ -6,6 +6,11 @@ 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>/",
|
||||
views.MainProductDetailView.as_view(),
|
||||
|
||||
+31
-1
@@ -1,10 +1,40 @@
|
||||
import mimetypes
|
||||
|
||||
from django.http import FileResponse, Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, ListView, TemplateView
|
||||
|
||||
from .models import MainProduct, SubProduct
|
||||
from .models import MainProduct, 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
|
||||
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user