diff --git a/apps/pages/admin.py b/apps/pages/admin.py index 41b636a..8a4f22c 100644 --- a/apps/pages/admin.py +++ b/apps/pages/admin.py @@ -63,10 +63,10 @@ class HomepageSectionAdmin(admin.ModelAdmin): ) -class AboutSectionItemInline(admin.StackedInline): +class AboutSectionItemInline(admin.TabularInline): model = AboutSectionItem extra = 1 - fields = ("badge", "title", "content", "url", "image", "image_alt", "is_featured", "order") + fields = ("icon", "title", "content", "url", "image", "image_alt", "badge", "is_featured", "order") ordering = ("order",) diff --git a/apps/pages/migrations/0012_aboutsectionitem_icon.py b/apps/pages/migrations/0012_aboutsectionitem_icon.py new file mode 100644 index 0000000..a80749f --- /dev/null +++ b/apps/pages/migrations/0012_aboutsectionitem_icon.py @@ -0,0 +1,20 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pages", "0011_custompagesectionitem_page_aboutsectionitem_url"), + ] + + operations = [ + migrations.AddField( + model_name="aboutsectionitem", + name="icon", + field=models.CharField( + blank=True, + help_text="Emoji or short symbol (e.g. ⚗️).", + max_length=20, + ), + ), + ] diff --git a/apps/pages/models.py b/apps/pages/models.py index 37ba61c..0420b56 100644 --- a/apps/pages/models.py +++ b/apps/pages/models.py @@ -98,6 +98,7 @@ class AboutSection(models.Model): TYPE_GRID = "grid" TYPE_HISTORY = "history" TYPE_CUSTOM = "custom" + TYPE_SUPPORTERS = "supporters" TYPE_CHOICES = [ (TYPE_HERO, "Hero"), @@ -105,6 +106,7 @@ class AboutSection(models.Model): (TYPE_GRID, "Grid Cards"), (TYPE_HISTORY, "History Block"), (TYPE_CUSTOM, "Custom Content"), + (TYPE_SUPPORTERS, "Supporters"), ] section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, default=TYPE_CUSTOM) @@ -134,11 +136,17 @@ class AboutSection(models.Model): class AboutSectionItem(models.Model): section = models.ForeignKey(AboutSection, on_delete=models.CASCADE, related_name="items") + icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).") badge = models.CharField(max_length=100, blank=True) title = models.CharField(max_length=300, blank=True) content = models.TextField(blank=True, help_text="Description text or link label for History links.") url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.") - image = models.ImageField(upload_to="about/", blank=True, null=True) + image = models.ImageField( + upload_to="about/items/", + blank=True, + null=True, + help_text="Logo or image (used for Supporters cards).", + ) image_alt = models.CharField(max_length=200, blank=True) is_featured = models.BooleanField(default=False, help_text="Mark as featured item (e.g. large screenshot).") order = models.PositiveIntegerField(default=0) @@ -149,7 +157,7 @@ class AboutSectionItem(models.Model): verbose_name_plural = "About Section Items" def __str__(self): - return f"{self.section} › {self.title or self.badge or '(item)'}" + return f"{self.section} › {self.title or self.icon or self.badge or '(item)'}" class ContactSubmission(models.Model): diff --git a/apps/products/admin.py b/apps/products/admin.py index 38f720c..103d98f 100644 --- a/apps/products/admin.py +++ b/apps/products/admin.py @@ -83,7 +83,19 @@ class MainProductAdmin(admin.ModelAdmin): list_editable = ("order", "is_active", "show_on_homepage", "homepage_order") inlines = [MainProductArticleInline, MainProductVersionInline, SubProductInline] fieldsets = ( - (None, {"fields": ("name", "slug", "short_description", "distribution")}), + ( + None, + { + "fields": ( + "name", + "slug", + "short_description", + "distribution", + "package_resource_button_text", + "package_source_button_text", + ) + }, + ), ("Description", {"fields": ("description_format", "description")}), ("Media", {"fields": ("image",)}), ("Homepage", {"fields": ("show_on_homepage", "homepage_order")}), @@ -110,7 +122,20 @@ class SubProductAdmin(admin.ModelAdmin): raw_id_fields = ("main_product",) inlines = [SubProductArticleInline, SubProductVersionInline] fieldsets = ( - (None, {"fields": ("main_product", "name", "slug", "distribution", "short_description")}), + ( + None, + { + "fields": ( + "main_product", + "name", + "slug", + "distribution", + "short_description", + "package_resource_button_text", + "package_source_button_text", + ) + }, + ), ("Description", {"fields": ("description_format", "description")}), ("Media", {"fields": ("image", "logo")}), ("Homepage", {"fields": ("show_on_homepage", "homepage_order")}), diff --git a/apps/products/migrations/0013_package_release_button_labels.py b/apps/products/migrations/0013_package_release_button_labels.py new file mode 100644 index 0000000..b242665 --- /dev/null +++ b/apps/products/migrations/0013_package_release_button_labels.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.2 on 2026-05-31 09:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0012_migrate_homepage_visibility_to_mainproduct'), + ] + + operations = [ + migrations.AddField( + model_name='mainproduct', + name='package_resource_button_text', + field=models.CharField(default='PyPI', help_text='Label for the package resource button on package releases.', max_length=100), + ), + migrations.AddField( + model_name='mainproduct', + name='package_source_button_text', + field=models.CharField(default='Source', help_text='Label for the source code button on package releases.', max_length=100), + ), + migrations.AddField( + model_name='subproduct', + name='package_resource_button_text', + field=models.CharField(default='PyPI', help_text='Label for the package resource button on package releases.', max_length=100), + ), + migrations.AddField( + model_name='subproduct', + name='package_source_button_text', + field=models.CharField(default='Source', help_text='Label for the source code button on package releases.', max_length=100), + ), + ] diff --git a/apps/products/models.py b/apps/products/models.py index 220f6f2..1924b7a 100644 --- a/apps/products/models.py +++ b/apps/products/models.py @@ -36,6 +36,16 @@ class MainProduct(models.Model): 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.", + ) 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.") @@ -99,6 +109,16 @@ class SubProduct(models.Model): 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.", + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/apps/products/release_context.py b/apps/products/release_context.py index 2a5438a..ffada6e 100644 --- a/apps/products/release_context.py +++ b/apps/products/release_context.py @@ -1,5 +1,17 @@ from .models import DISTRIBUTION_INSTALLABLE, DISTRIBUTION_PACKAGE, INSTALLABLE_PLATFORM_SPECS +DEFAULT_PACKAGE_RESOURCE_BUTTON_TEXT = "PyPI" +DEFAULT_PACKAGE_SOURCE_BUTTON_TEXT = "Source" + + +def package_button_labels(product): + resource = (getattr(product, "package_resource_button_text", "") or "").strip() + source = (getattr(product, "package_source_button_text", "") or "").strip() + return { + "package_resource_button_text": resource or DEFAULT_PACKAGE_RESOURCE_BUTTON_TEXT, + "package_source_button_text": source or DEFAULT_PACKAGE_SOURCE_BUTTON_TEXT, + } + def featured_version(qs): ordered = qs.order_by("order", "pk") @@ -16,7 +28,7 @@ def install_specs_from_versions(version_list): return tuple(s for s in INSTALLABLE_PLATFORM_SPECS if s[0] in present) -def build_release_context(distribution, active_versions): +def build_release_context(distribution, active_versions, product): context = { "distribution_installable": distribution == DISTRIBUTION_INSTALLABLE, "distribution_package": distribution == DISTRIBUTION_PACKAGE, @@ -25,6 +37,7 @@ def build_release_context(distribution, active_versions): "featured_install_cells": [], "show_older_versions_link": False, "package_versions": [], + **package_button_labels(product), } if distribution == DISTRIBUTION_INSTALLABLE: @@ -62,7 +75,7 @@ def build_release_context(distribution, active_versions): return context -def build_archive_context(distribution, active_versions): +def build_archive_context(distribution, active_versions, product): featured = featured_version(active_versions) archive = list( active_versions.exclude(pk=featured.pk).order_by("order", "pk") @@ -72,6 +85,7 @@ def build_archive_context(distribution, active_versions): context = { "featured_version": featured, "archive_versions": archive, + **package_button_labels(product), } if distribution == DISTRIBUTION_INSTALLABLE: diff --git a/apps/products/views.py b/apps/products/views.py index 0cea905..752ca7a 100644 --- a/apps/products/views.py +++ b/apps/products/views.py @@ -31,6 +31,7 @@ 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) @@ -52,7 +53,9 @@ class MainProductOlderVersionsView(TemplateView): context["sub_product"] = None context["product_detail_url"] = main_product.get_absolute_url() context.update( - build_archive_context(main_product.distribution, active_versions) + build_archive_context( + main_product.distribution, active_versions, main_product + ) ) return context @@ -85,7 +88,7 @@ class SubProductDetailView(TemplateView): ) active_versions = sub_product.versions.filter(is_active=True) release_context = build_release_context( - sub_product.distribution, active_versions + sub_product.distribution, active_versions, sub_product ) release_context["versions_archive_url"] = sub_product.get_versions_archive_url() context.update(release_context) @@ -113,6 +116,8 @@ class SubProductOlderVersionsView(TemplateView): context["sub_product"] = sub_product context["product_detail_url"] = sub_product.get_absolute_url() context.update( - build_archive_context(sub_product.distribution, active_versions) + build_archive_context( + sub_product.distribution, active_versions, sub_product + ) ) return context diff --git a/static/css/main.css b/static/css/main.css index 036c20e..6805f6c 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -839,6 +839,12 @@ h1, h2, h3, h4, h5, h6 { font-size: 2rem; margin-bottom: 1rem; display: block; + line-height: 1; +} + +.feature-icon--inline { + font-size: 1.25rem; + margin-bottom: 0.35rem; } .feature-title { @@ -1727,6 +1733,43 @@ a.citation-count-badge:hover { padding: 1.75rem; } +.history-link-icon { + margin-right: 0.35rem; +} + +.history-visual-image { + position: relative; + z-index: 2; + max-width: 100%; + max-height: 160px; + object-fit: contain; + border-radius: var(--radius-sm); +} + +.about-hero-gallery { + margin-top: 2.5rem; +} + +.about-section-screenshots { + margin-top: 1.5rem; +} + +.about-hero-gallery:not(:has(.screenshot-item)) { + display: none; +} + +.about-section-screenshots:not(:has(.screenshot-item)) { + display: none; +} + +.standards-grid:not(:has(.standard-card)) { + display: none; +} + +.history-visual:has(.history-visual-image) .history-year { + display: none; +} + .standard-badge { display: inline-block; font-size: 0.7rem; diff --git a/templates/partials/_page_sections.html b/templates/partials/_page_sections.html index d52c3d1..08571c9 100644 --- a/templates/partials/_page_sections.html +++ b/templates/partials/_page_sections.html @@ -17,6 +17,23 @@ {% if section.subtitle %}
{{ section.subtitle }}
{% endif %} {% if section.content %}{{ item.content }}
{% endif %} - {% include "partials/_linked_card_close.html" with url=item.url %} + {% if item.is_featured and item.image %} + {% else %} + {% include "partials/_standard_item_card.html" with item=item %} + {% endif %} {% endfor %}{{ item.content }}
{% endif %} - {% include "partials/_linked_card_close.html" with url=item.url %} + {% if item.is_featured and item.image %} + {% else %} + {% include "partials/_standard_item_card.html" with item=item %} + {% endif %} {% endfor %}{{ section.description }}
{% endif %} + {% if section.description %}{{ section.description }}
{% elif section.content %}{{ section.description }}
{% endif %} + {% if section.description %}{{ section.description }}
{% elif section.content %}{{ section.description }}
{% endif %} + {% if section.description %}{{ section.description }}
{% elif section.content %}{{ section.description }}
{% endif %} + {% if section.description %}{{ section.description }}
{% elif section.content %}{{ section.description }}
{% endif %} + {% if section.description %}{{ section.description }}
{% elif section.content %}{{ section.description }}
{% endif %} + {% if section.description %}{{ section.description }}
{% elif section.content %}{{ item.content }}
{% endif %} +{{ item.content }}
{% endif %} +{% include "partials/_linked_card_close.html" with url=item.url %} +{% endif %} diff --git a/templates/products/_releases_section.html b/templates/products/_releases_section.html index a649ceb..9b1e701 100644 --- a/templates/products/_releases_section.html +++ b/templates/products/_releases_section.html @@ -34,7 +34,7 @@ {% endif %}