feat: a link in download section
This commit is contained in:
@@ -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/'),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
)
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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.",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .development import * # noqa: F401, F403
|
||||
|
||||
DATABASES["default"]["CONN_MAX_AGE"] = 0 # noqa: F405
|
||||
@@ -4,6 +4,9 @@ import sys
|
||||
|
||||
|
||||
def main():
|
||||
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
|
||||
|
||||
+16
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
{% if show_releases_section %}
|
||||
{% if distribution_installable %}
|
||||
<section class="sub-downloads fade-in" aria-labelledby="sub-dl-heading">
|
||||
<div class="sub-downloads-header">
|
||||
<h2 class="sub-downloads-title" id="sub-dl-heading">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
Downloads
|
||||
</h2>
|
||||
{% if downloads_section_link %}
|
||||
<a href="{{ downloads_section_link.url }}" class="sub-downloads-corner-link link-arrow" target="_blank" rel="noopener noreferrer">{{ downloads_section_link.text }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="sub-downloads-channel">
|
||||
{% if featured_version.is_featured_stable %}
|
||||
@@ -61,6 +66,7 @@
|
||||
|
||||
{% elif distribution_package %}
|
||||
<section class="sub-downloads sub-downloads--package fade-in" aria-labelledby="sub-resources-heading">
|
||||
<div class="sub-downloads-header">
|
||||
<h2 class="sub-downloads-title" id="sub-resources-heading">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
@@ -68,6 +74,10 @@
|
||||
</svg>
|
||||
Resources
|
||||
</h2>
|
||||
{% if downloads_section_link %}
|
||||
<a href="{{ downloads_section_link.url }}" class="sub-downloads-corner-link link-arrow" target="_blank" rel="noopener noreferrer">{{ downloads_section_link.text }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<ul class="pkg-version-list" role="list">
|
||||
{% for ver in package_versions %}
|
||||
<li class="pkg-version-row">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
a
|
||||
@@ -0,0 +1 @@
|
||||
binary-payload
|
||||
@@ -0,0 +1 @@
|
||||
x
|
||||
@@ -0,0 +1 @@
|
||||
gz
|
||||
Reference in New Issue
Block a user