From 33b34771769b4cd85d550513e1be6cf585e4291f Mon Sep 17 00:00:00 2001 From: mohamad Date: Sun, 7 Jun 2026 17:41:41 +0330 Subject: [PATCH] feat: a link in download section --- ...lter_aboutsection_section_type_and_more.py | 23 ++++++++++ apps/products/admin.py | 4 ++ .../migrations/0016_downloads_section_link.py | 45 +++++++++++++++++++ ...uctversion_linux_download_file_and_more.py | 34 ++++++++++++++ apps/products/models.py | 18 ++++++++ apps/products/release_assets.py | 4 ++ apps/products/release_context.py | 9 ++++ apps/products/tests/test_models.py | 27 +++++++++++ apps/products/tests/test_views.py | 3 +- config/settings/test.py | 3 ++ manage.py | 5 ++- static/css/main.css | 17 ++++++- templates/products/_releases_section.html | 36 +++++++++------ .../releases/radiuma/0.1/windows/a.exe | 1 + .../releases/radiuma/2.0/windows/setup.exe | 1 + .../releases/radiuma/2.1/windows/local.exe | 1 + .../releases/radiuma/3.0/windows/win.tar.gz | 1 + 17 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 apps/pages/migrations/0014_alter_aboutsection_section_type_and_more.py create mode 100644 apps/products/migrations/0016_downloads_section_link.py create mode 100644 apps/products/migrations/0017_alter_subproductversion_linux_download_file_and_more.py create mode 100644 config/settings/test.py create mode 100644 test_media_releases/releases/radiuma/0.1/windows/a.exe create mode 100644 test_media_releases/releases/radiuma/2.0/windows/setup.exe create mode 100644 test_media_releases/releases/radiuma/2.1/windows/local.exe create mode 100644 test_media_releases/releases/radiuma/3.0/windows/win.tar.gz diff --git a/apps/pages/migrations/0014_alter_aboutsection_section_type_and_more.py b/apps/pages/migrations/0014_alter_aboutsection_section_type_and_more.py new file mode 100644 index 0000000..c8a4a65 --- /dev/null +++ b/apps/pages/migrations/0014_alter_aboutsection_section_type_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.13 on 2026-06-07 13:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pages', '0013_contact_submission_attachments'), + ] + + operations = [ + migrations.AlterField( + model_name='aboutsection', + name='section_type', + field=models.CharField(choices=[('hero', 'Hero'), ('intro', 'Intro Card'), ('grid', 'Grid Cards'), ('history', 'History Block'), ('custom', 'Custom Content'), ('supporters', 'Supporters')], default='custom', max_length=30), + ), + migrations.AlterField( + model_name='aboutsectionitem', + name='image', + field=models.ImageField(blank=True, help_text='Logo or image (used for Supporters cards).', null=True, upload_to='about/items/'), + ), + ] diff --git a/apps/products/admin.py b/apps/products/admin.py index ce7cbbb..cc092f1 100644 --- a/apps/products/admin.py +++ b/apps/products/admin.py @@ -95,6 +95,8 @@ class MainProductAdmin(admin.ModelAdmin): "distribution", "package_resource_button_text", "package_source_button_text", + "downloads_section_link_text", + "downloads_section_link_url", ) }, ), @@ -135,6 +137,8 @@ class SubProductAdmin(admin.ModelAdmin): "short_description", "package_resource_button_text", "package_source_button_text", + "downloads_section_link_text", + "downloads_section_link_url", ) }, ), diff --git a/apps/products/migrations/0016_downloads_section_link.py b/apps/products/migrations/0016_downloads_section_link.py new file mode 100644 index 0000000..c98a704 --- /dev/null +++ b/apps/products/migrations/0016_downloads_section_link.py @@ -0,0 +1,45 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("products", "0015_release_original_filenames"), + ] + + operations = [ + migrations.AddField( + model_name="mainproduct", + name="downloads_section_link_text", + field=models.CharField( + blank=True, + help_text="Label for the optional downloads section link.", + max_length=100, + ), + ), + migrations.AddField( + model_name="mainproduct", + name="downloads_section_link_url", + field=models.URLField( + blank=True, + help_text="Optional link shown in the top-right corner of the downloads section.", + ), + ), + migrations.AddField( + model_name="subproduct", + name="downloads_section_link_text", + field=models.CharField( + blank=True, + help_text="Label for the optional downloads section link.", + max_length=100, + ), + ), + migrations.AddField( + model_name="subproduct", + name="downloads_section_link_url", + field=models.URLField( + blank=True, + help_text="Optional link shown in the top-right corner of the downloads section.", + ), + ), + ] diff --git a/apps/products/migrations/0017_alter_subproductversion_linux_download_file_and_more.py b/apps/products/migrations/0017_alter_subproductversion_linux_download_file_and_more.py new file mode 100644 index 0000000..8fa7f7d --- /dev/null +++ b/apps/products/migrations/0017_alter_subproductversion_linux_download_file_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.13 on 2026-06-07 13:49 + +import apps.products.release_assets +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0016_downloads_section_link'), + ] + + operations = [ + 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. 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.', 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. 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.', 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. 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.', 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. 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.', storage=apps.products.release_assets.ReleaseFileStorage(), upload_to=apps.products.release_assets.release_windows_file_upload_to), + ), + ] diff --git a/apps/products/models.py b/apps/products/models.py index 2c6c7fd..207fdc5 100644 --- a/apps/products/models.py +++ b/apps/products/models.py @@ -62,6 +62,15 @@ class MainProduct(models.Model): 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.") @@ -135,6 +144,15 @@ class SubProduct(models.Model): 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.", + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/apps/products/release_assets.py b/apps/products/release_assets.py index 145438d..eadcf0d 100644 --- a/apps/products/release_assets.py +++ b/apps/products/release_assets.py @@ -5,6 +5,10 @@ from django.utils.text import get_valid_filename class ReleaseFileStorage(FileSystemStorage): + def __init__(self, **kwargs): + kwargs.setdefault("allow_overwrite", True) + super().__init__(**kwargs) + def get_available_name(self, name, max_length=None): return name diff --git a/apps/products/release_context.py b/apps/products/release_context.py index 4c8637a..513785a 100644 --- a/apps/products/release_context.py +++ b/apps/products/release_context.py @@ -13,6 +13,14 @@ def package_button_labels(product): } +def downloads_section_link(product): + url = (getattr(product, "downloads_section_link_url", "") or "").strip() + text = (getattr(product, "downloads_section_link_text", "") or "").strip() + if url and text: + return {"url": url, "text": text} + return None + + def featured_version(qs): ordered = qs.order_by("order", "pk") cand = ordered.filter(is_featured_stable=True).first() @@ -38,6 +46,7 @@ def build_release_context(distribution, active_versions, product): "show_older_versions_link": False, "package_versions": [], **package_button_labels(product), + "downloads_section_link": downloads_section_link(product), } if distribution == DISTRIBUTION_INSTALLABLE: diff --git a/apps/products/tests/test_models.py b/apps/products/tests/test_models.py index e54da66..bd2f67d 100644 --- a/apps/products/tests/test_models.py +++ b/apps/products/tests/test_models.py @@ -155,6 +155,33 @@ class ArticleModelTest(TestCase): article.full_clean() +class ReleaseFileStorageTest(TestCase): + def test_save_overwrites_existing_release_file(self): + from django.core.files.uploadedfile import SimpleUploadedFile + + from apps.products.models import MainProduct, SubProductVersion + + main = MainProduct.objects.create( + name="Storage Test", + short_description="s", + description="d", + ) + version = SubProductVersion.objects.create( + main_product=main, + version="1.0", + windows_download_file=SimpleUploadedFile("setup.exe", b"v1"), + is_active=True, + ) + version.windows_download_file.save( + "setup.exe", + SimpleUploadedFile("setup.exe", b"v2"), + save=True, + ) + version.refresh_from_db() + with version.windows_download_file.open("rb") as handle: + self.assertEqual(handle.read(), b"v2") + + class SubProductVersionModelTest(TestCase): def setUp(self): self.main_product = MainProduct.objects.create( diff --git a/apps/products/tests/test_views.py b/apps/products/tests/test_views.py index 275b169..cf3ce06 100644 --- a/apps/products/tests/test_views.py +++ b/apps/products/tests/test_views.py @@ -216,6 +216,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup): self.assertTrue(response.context["show_releases_section"]) +@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") @@ -277,7 +278,6 @@ class ReleaseAssetDownloadViewTest(ProductViewsSetup): ) 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, @@ -294,7 +294,6 @@ class ReleaseAssetDownloadViewTest(ProductViewsSetup): 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, diff --git a/config/settings/test.py b/config/settings/test.py new file mode 100644 index 0000000..9cd8e83 --- /dev/null +++ b/config/settings/test.py @@ -0,0 +1,3 @@ +from .development import * # noqa: F401, F403 + +DATABASES["default"]["CONN_MAX_AGE"] = 0 # noqa: F405 diff --git a/manage.py b/manage.py index 1fb25a0..3b6f940 100644 --- a/manage.py +++ b/manage.py @@ -4,7 +4,10 @@ import sys def main(): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development") + if len(sys.argv) > 1 and sys.argv[1] == "test": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.test") + else: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/static/css/main.css b/static/css/main.css index b7296b7..f9e4e83 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -2124,16 +2124,31 @@ a.citation-count-badge:hover { backdrop-filter: blur(12px); } +.sub-downloads-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1.25rem; +} + .sub-downloads-title { font-size: 1rem; font-weight: 700; display: flex; align-items: center; gap: 0.5rem; - margin-bottom: 1.25rem; + margin-bottom: 0; color: var(--text-primary); } +.sub-downloads-corner-link { + flex-shrink: 0; + font-size: 0.88rem; + white-space: nowrap; +} + .sub-downloads-channel { margin-bottom: 1.1rem; } diff --git a/templates/products/_releases_section.html b/templates/products/_releases_section.html index fa196d1..29ac4bc 100644 --- a/templates/products/_releases_section.html +++ b/templates/products/_releases_section.html @@ -2,12 +2,17 @@ {% if show_releases_section %} {% if distribution_installable %}
-

- - Downloads -

+
+

+ + Downloads +

+ {% if downloads_section_link %} + {{ downloads_section_link.text }} + {% endif %} +
{% if featured_version.is_featured_stable %} @@ -61,13 +66,18 @@ {% elif distribution_package %}
-

- - Resources -

+
+

+ + Resources +

+ {% if downloads_section_link %} + {{ downloads_section_link.text }} + {% endif %} +
    {% for ver in package_versions %}
  • diff --git a/test_media_releases/releases/radiuma/0.1/windows/a.exe b/test_media_releases/releases/radiuma/0.1/windows/a.exe new file mode 100644 index 0000000..2e65efe --- /dev/null +++ b/test_media_releases/releases/radiuma/0.1/windows/a.exe @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/test_media_releases/releases/radiuma/2.0/windows/setup.exe b/test_media_releases/releases/radiuma/2.0/windows/setup.exe new file mode 100644 index 0000000..f99364c --- /dev/null +++ b/test_media_releases/releases/radiuma/2.0/windows/setup.exe @@ -0,0 +1 @@ +binary-payload \ No newline at end of file diff --git a/test_media_releases/releases/radiuma/2.1/windows/local.exe b/test_media_releases/releases/radiuma/2.1/windows/local.exe new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/test_media_releases/releases/radiuma/2.1/windows/local.exe @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/test_media_releases/releases/radiuma/3.0/windows/win.tar.gz b/test_media_releases/releases/radiuma/3.0/windows/win.tar.gz new file mode 100644 index 0000000..29f2ec2 --- /dev/null +++ b/test_media_releases/releases/radiuma/3.0/windows/win.tar.gz @@ -0,0 +1 @@ +gz \ No newline at end of file