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",
|
"distribution",
|
||||||
"package_resource_button_text",
|
"package_resource_button_text",
|
||||||
"package_source_button_text",
|
"package_source_button_text",
|
||||||
|
"downloads_section_link_text",
|
||||||
|
"downloads_section_link_url",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -135,6 +137,8 @@ class SubProductAdmin(admin.ModelAdmin):
|
|||||||
"short_description",
|
"short_description",
|
||||||
"package_resource_button_text",
|
"package_resource_button_text",
|
||||||
"package_source_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",
|
default="Source",
|
||||||
help_text="Label for the source code button on package releases.",
|
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)
|
order = models.PositiveIntegerField(default=0)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
show_on_homepage = models.BooleanField(default=False, help_text="Display this product in the homepage Products section.")
|
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",
|
default="Source",
|
||||||
help_text="Label for the source code button on package releases.",
|
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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ from django.utils.text import get_valid_filename
|
|||||||
|
|
||||||
|
|
||||||
class ReleaseFileStorage(FileSystemStorage):
|
class ReleaseFileStorage(FileSystemStorage):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs.setdefault("allow_overwrite", True)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def get_available_name(self, name, max_length=None):
|
def get_available_name(self, name, max_length=None):
|
||||||
return name
|
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):
|
def featured_version(qs):
|
||||||
ordered = qs.order_by("order", "pk")
|
ordered = qs.order_by("order", "pk")
|
||||||
cand = ordered.filter(is_featured_stable=True).first()
|
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,
|
"show_older_versions_link": False,
|
||||||
"package_versions": [],
|
"package_versions": [],
|
||||||
**package_button_labels(product),
|
**package_button_labels(product),
|
||||||
|
"downloads_section_link": downloads_section_link(product),
|
||||||
}
|
}
|
||||||
|
|
||||||
if distribution == DISTRIBUTION_INSTALLABLE:
|
if distribution == DISTRIBUTION_INSTALLABLE:
|
||||||
|
|||||||
@@ -155,6 +155,33 @@ class ArticleModelTest(TestCase):
|
|||||||
article.full_clean()
|
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):
|
class SubProductVersionModelTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.main_product = MainProduct.objects.create(
|
self.main_product = MainProduct.objects.create(
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ class SubProductOlderVersionsViewTest(ProductViewsSetup):
|
|||||||
self.assertTrue(response.context["show_releases_section"])
|
self.assertTrue(response.context["show_releases_section"])
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(MEDIA_ROOT=settings.BASE_DIR / "test_media_releases")
|
||||||
class ReleaseAssetDownloadViewTest(ProductViewsSetup):
|
class ReleaseAssetDownloadViewTest(ProductViewsSetup):
|
||||||
def test_uploaded_file_download(self):
|
def test_uploaded_file_download(self):
|
||||||
release_file = SimpleUploadedFile("setup.exe", b"binary-payload")
|
release_file = SimpleUploadedFile("setup.exe", b"binary-payload")
|
||||||
@@ -277,7 +278,6 @@ class ReleaseAssetDownloadViewTest(ProductViewsSetup):
|
|||||||
)
|
)
|
||||||
self.assertEqual(self.client.get(url).status_code, 404)
|
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):
|
def test_clearing_file_removes_from_disk(self):
|
||||||
version = SubProductVersion.objects.create(
|
version = SubProductVersion.objects.create(
|
||||||
main_product=self.main_product,
|
main_product=self.main_product,
|
||||||
@@ -294,7 +294,6 @@ class ReleaseAssetDownloadViewTest(ProductViewsSetup):
|
|||||||
self.assertEqual(version.windows_download_filename, "")
|
self.assertEqual(version.windows_download_filename, "")
|
||||||
self.assertFalse(os.path.exists(stored_path))
|
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):
|
def test_deleting_version_removes_files_from_disk(self):
|
||||||
version = SubProductVersion.objects.create(
|
version = SubProductVersion.objects.create(
|
||||||
main_product=self.main_product,
|
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():
|
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")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
|
|||||||
+16
-1
@@ -2124,16 +2124,31 @@ a.citation-count-badge:hover {
|
|||||||
backdrop-filter: blur(12px);
|
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 {
|
.sub-downloads-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 0;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sub-downloads-corner-link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.sub-downloads-channel {
|
.sub-downloads-channel {
|
||||||
margin-bottom: 1.1rem;
|
margin-bottom: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
{% if show_releases_section %}
|
{% if show_releases_section %}
|
||||||
{% if distribution_installable %}
|
{% if distribution_installable %}
|
||||||
<section class="sub-downloads fade-in" aria-labelledby="sub-dl-heading">
|
<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">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
Downloads
|
Downloads
|
||||||
</h2>
|
</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">
|
<div class="sub-downloads-channel">
|
||||||
{% if featured_version.is_featured_stable %}
|
{% if featured_version.is_featured_stable %}
|
||||||
@@ -61,6 +66,7 @@
|
|||||||
|
|
||||||
{% elif distribution_package %}
|
{% elif distribution_package %}
|
||||||
<section class="sub-downloads sub-downloads--package fade-in" aria-labelledby="sub-resources-heading">
|
<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">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
Resources
|
Resources
|
||||||
</h2>
|
</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">
|
<ul class="pkg-version-list" role="list">
|
||||||
{% for ver in package_versions %}
|
{% for ver in package_versions %}
|
||||||
<li class="pkg-version-row">
|
<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