fix: show product in replacement of sub products in homepage
This commit is contained in:
+64
-15
@@ -46,13 +46,13 @@ class HeroSectionAdmin(admin.ModelAdmin):
|
||||
class HomepageSectionItemInline(admin.TabularInline):
|
||||
model = HomepageSectionItem
|
||||
extra = 1
|
||||
fields = ("icon", "title", "content", "image", "order")
|
||||
fields = ("icon", "title", "content", "url", "image", "order")
|
||||
ordering = ("order",)
|
||||
|
||||
|
||||
@admin.register(HomepageSection)
|
||||
class HomepageSectionAdmin(admin.ModelAdmin):
|
||||
list_display = ("section_type", "badge", "title", "order", "is_active")
|
||||
list_display = ("section_type", "title", "badge", "order", "is_active")
|
||||
list_filter = ("section_type", "is_active")
|
||||
list_editable = ("order", "is_active")
|
||||
inlines = [HomepageSectionItemInline]
|
||||
@@ -147,22 +147,33 @@ class CustomPageSectionInline(admin.StackedInline):
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(CustomPage)
|
||||
class CustomPageAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "slug", "show_in_nav", "menu_order", "is_published", "updated_at")
|
||||
list_filter = ("show_in_nav", "is_published")
|
||||
search_fields = ("title", "slug", "menu_label")
|
||||
list_editable = ("show_in_nav", "menu_order", "is_published")
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
inlines = [CustomPageSectionInline]
|
||||
fieldsets = (
|
||||
(None, {"fields": ("title", "slug", "menu_label", "meta_description")}),
|
||||
("Navigation", {"fields": ("show_in_nav", "menu_order")}),
|
||||
("Publishing", {"fields": ("is_published",)}),
|
||||
class CustomPageSectionItemOnPageInline(admin.TabularInline):
|
||||
model = CustomPageSectionItem
|
||||
fk_name = "page"
|
||||
extra = 1
|
||||
verbose_name = "Section item"
|
||||
verbose_name_plural = "Section items (subsections)"
|
||||
fields = (
|
||||
"section",
|
||||
"icon",
|
||||
"badge",
|
||||
"title",
|
||||
"content_format",
|
||||
"content",
|
||||
"url",
|
||||
"image",
|
||||
"order",
|
||||
)
|
||||
ordering = ("section", "order")
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == "section":
|
||||
page_id = request.resolver_match.kwargs.get("object_id") if request.resolver_match else None
|
||||
if page_id:
|
||||
kwargs["queryset"] = CustomPageSection.objects.filter(page_id=page_id).order_by("order")
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
|
||||
@admin.register(CustomPageSection)
|
||||
class CustomPageSectionAdmin(admin.ModelAdmin):
|
||||
list_display = ("page", "section_type", "title", "order", "is_active")
|
||||
list_filter = ("section_type", "is_active", "page")
|
||||
@@ -175,6 +186,44 @@ class CustomPageSectionAdmin(admin.ModelAdmin):
|
||||
("Settings", {"fields": ("order", "is_active")}),
|
||||
)
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
instances = formset.save(commit=False)
|
||||
for instance in instances:
|
||||
if isinstance(instance, CustomPageSectionItem):
|
||||
instance.page = form.instance.page
|
||||
instance.save()
|
||||
for obj in formset.deleted_objects:
|
||||
obj.delete()
|
||||
formset.save_m2m()
|
||||
|
||||
|
||||
@admin.register(CustomPage)
|
||||
class CustomPageAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "slug", "show_in_nav", "menu_order", "is_published", "updated_at")
|
||||
list_filter = ("show_in_nav", "is_published")
|
||||
search_fields = ("title", "slug", "menu_label")
|
||||
list_editable = ("show_in_nav", "menu_order", "is_published")
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
inlines = [CustomPageSectionInline, CustomPageSectionItemOnPageInline]
|
||||
fieldsets = (
|
||||
(None, {"fields": ("title", "slug", "menu_label", "meta_description")}),
|
||||
("Navigation", {"fields": ("show_in_nav", "menu_order")}),
|
||||
("Publishing", {"fields": ("is_published",)}),
|
||||
)
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
instances = formset.save(commit=False)
|
||||
for instance in instances:
|
||||
if isinstance(instance, CustomPageSectionItem):
|
||||
instance.page = form.instance
|
||||
instance.save()
|
||||
for obj in formset.deleted_objects:
|
||||
obj.delete()
|
||||
formset.save_m2m()
|
||||
|
||||
|
||||
admin.site.register(CustomPageSection, CustomPageSectionAdmin)
|
||||
|
||||
|
||||
@admin.register(DownloadItem)
|
||||
class DownloadItemAdmin(admin.ModelAdmin):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2026-05-26 12:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0008_custom_pages'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='homepagesection',
|
||||
name='section_type',
|
||||
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters')], max_length=30),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.0.2 on 2026-05-26 12:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0009_alter_homepagesection_section_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='homepagesectionitem',
|
||||
name='url',
|
||||
field=models.CharField(blank=True, help_text='Optional link; makes this item clickable.', max_length=300),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='custompagesection',
|
||||
name='section_type',
|
||||
field=models.CharField(choices=[('hero', 'Hero'), ('intro', 'Intro Card'), ('grid', 'Grid Cards'), ('history', 'History Block'), ('custom', 'Custom Content'), ('features', 'Features Grid'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products Grid'), ('products_catalog', 'Products with Sub-products'), ('problems', 'Problems / Value Proposition'), ('supporters', 'Supporters'), ('about_strip', 'About Strip'), ('faq', 'FAQ Accordion')], default='custom', max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='custompagesectionitem',
|
||||
name='url',
|
||||
field=models.CharField(blank=True, help_text='Optional link; makes this item clickable.', max_length=300),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='homepagesection',
|
||||
name='section_type',
|
||||
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('products_catalog', 'Products with Sub-products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters')], max_length=30),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def set_custom_page_section_item_page(apps, schema_editor):
|
||||
CustomPageSectionItem = apps.get_model("pages", "CustomPageSectionItem")
|
||||
for item in CustomPageSectionItem.objects.select_related("section").iterator():
|
||||
item.page_id = item.section.page_id
|
||||
item.save(update_fields=["page_id"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pages", "0010_homepagesectionitem_url_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="aboutsectionitem",
|
||||
name="url",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Optional link; makes this item clickable.",
|
||||
max_length=300,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="custompagesectionitem",
|
||||
name="page",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="section_items",
|
||||
to="pages.custompage",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(set_custom_page_section_item_page, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name="custompagesectionitem",
|
||||
name="page",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="section_items",
|
||||
to="pages.custompage",
|
||||
),
|
||||
),
|
||||
]
|
||||
+18
-3
@@ -41,6 +41,7 @@ class HomepageSection(models.Model):
|
||||
TYPE_FEATURES = "features"
|
||||
TYPE_SCREENSHOTS = "screenshots"
|
||||
TYPE_PRODUCTS = "products"
|
||||
TYPE_PRODUCTS_CATALOG = "products_catalog"
|
||||
TYPE_PROBLEMS = "problems"
|
||||
TYPE_ABOUT_STRIP = "about_strip"
|
||||
TYPE_SUPPORTERS = "supporters"
|
||||
@@ -49,12 +50,13 @@ class HomepageSection(models.Model):
|
||||
(TYPE_FEATURES, "Features"),
|
||||
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
||||
(TYPE_PRODUCTS, "Products"),
|
||||
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
|
||||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||||
(TYPE_ABOUT_STRIP, "About Strip"),
|
||||
(TYPE_SUPPORTERS, "Supporters"),
|
||||
]
|
||||
|
||||
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, unique=True)
|
||||
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES)
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
@@ -77,6 +79,7 @@ class HomepageSectionItem(models.Model):
|
||||
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
content = models.TextField(blank=True)
|
||||
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||||
image = models.ImageField(upload_to="homepage/items/", blank=True, null=True, help_text="Logo or image (used for Supporters cards).")
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
|
||||
@@ -134,7 +137,7 @@ class AboutSectionItem(models.Model):
|
||||
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.URLField(blank=True, help_text="Used for History block 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_alt = models.CharField(max_length=200, blank=True)
|
||||
is_featured = models.BooleanField(default=False, help_text="Mark as featured item (e.g. large screenshot).")
|
||||
@@ -267,6 +270,7 @@ class CustomPageSection(models.Model):
|
||||
TYPE_FEATURES = "features"
|
||||
TYPE_SCREENSHOTS = "screenshots"
|
||||
TYPE_PRODUCTS = "products"
|
||||
TYPE_PRODUCTS_CATALOG = "products_catalog"
|
||||
TYPE_PROBLEMS = "problems"
|
||||
TYPE_SUPPORTERS = "supporters"
|
||||
TYPE_ABOUT_STRIP = "about_strip"
|
||||
@@ -281,6 +285,7 @@ class CustomPageSection(models.Model):
|
||||
(TYPE_FEATURES, "Features Grid"),
|
||||
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
||||
(TYPE_PRODUCTS, "Products Grid"),
|
||||
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
|
||||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||||
(TYPE_SUPPORTERS, "Supporters"),
|
||||
(TYPE_ABOUT_STRIP, "About Strip"),
|
||||
@@ -315,13 +320,18 @@ class CustomPageSection(models.Model):
|
||||
|
||||
|
||||
class CustomPageSectionItem(models.Model):
|
||||
page = models.ForeignKey(
|
||||
CustomPage,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="section_items",
|
||||
)
|
||||
section = models.ForeignKey(CustomPageSection, on_delete=models.CASCADE, related_name="items")
|
||||
icon = models.CharField(max_length=20, blank=True)
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
content = models.TextField(blank=True)
|
||||
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN)
|
||||
url = models.URLField(blank=True)
|
||||
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||||
image = models.ImageField(upload_to="pages/custom/", blank=True, null=True)
|
||||
image_alt = models.CharField(max_length=200, blank=True)
|
||||
is_featured = models.BooleanField(default=False)
|
||||
@@ -336,5 +346,10 @@ class CustomPageSectionItem(models.Model):
|
||||
def rendered_content(self):
|
||||
return render_content(self.content, self.content_format)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.section_id:
|
||||
self.page_id = self.section.page_id
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.section} › {self.title or self.icon or '(item)'}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.pages.models import CustomPage, CustomPageSection
|
||||
from apps.pages.models import CustomPage, CustomPageSection, CustomPageSectionItem
|
||||
|
||||
|
||||
class CustomPageViewTest(TestCase):
|
||||
@@ -40,6 +40,26 @@ class CustomPageViewTest(TestCase):
|
||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "missing"}))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_section_item_with_url_renders_as_link(self):
|
||||
section = CustomPageSection.objects.create(
|
||||
page=self.page,
|
||||
section_type=CustomPageSection.TYPE_FEATURES,
|
||||
title="Highlights",
|
||||
is_active=True,
|
||||
)
|
||||
CustomPageSectionItem.objects.create(
|
||||
page=self.page,
|
||||
section=section,
|
||||
title="Documentation",
|
||||
content="Read the docs.",
|
||||
url="/about/",
|
||||
order=1,
|
||||
)
|
||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "resources"}))
|
||||
self.assertContains(response, 'href="/about/"')
|
||||
self.assertContains(response, "section-item-link")
|
||||
self.assertContains(response, "Documentation")
|
||||
|
||||
|
||||
class CustomPageNavTest(TestCase):
|
||||
def test_nav_custom_pages_in_context(self):
|
||||
|
||||
+21
-7
@@ -1,11 +1,12 @@
|
||||
import random
|
||||
|
||||
from django.db.models import Prefetch
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, ListView, TemplateView
|
||||
|
||||
from apps.products.models import SubProduct
|
||||
from apps.products.models import MainProduct, SubProduct
|
||||
|
||||
from .forms import ContactForm
|
||||
from .models import (
|
||||
@@ -18,6 +19,19 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
def _homepage_products_catalog_queryset():
|
||||
return (
|
||||
MainProduct.objects.filter(is_active=True)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"sub_products",
|
||||
queryset=SubProduct.objects.filter(is_active=True).order_by("order", "name"),
|
||||
)
|
||||
)
|
||||
.order_by("order", "name")
|
||||
)
|
||||
|
||||
|
||||
class HomeView(TemplateView):
|
||||
template_name = "pages/home.html"
|
||||
|
||||
@@ -29,11 +43,11 @@ class HomeView(TemplateView):
|
||||
.prefetch_related("items")
|
||||
.order_by("order")
|
||||
)
|
||||
ctx["sub_products"] = (
|
||||
SubProduct.objects.filter(is_active=True, show_on_homepage=True)
|
||||
.select_related("main_product")
|
||||
ctx["homepage_products"] = (
|
||||
MainProduct.objects.filter(is_active=True, show_on_homepage=True)
|
||||
.order_by("homepage_order", "order")
|
||||
)
|
||||
ctx["homepage_products_catalog"] = _homepage_products_catalog_queryset()
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -115,9 +129,9 @@ class CustomPageView(DetailView):
|
||||
.prefetch_related("items")
|
||||
.order_by("order")
|
||||
)
|
||||
ctx["sub_products"] = (
|
||||
SubProduct.objects.filter(is_active=True, show_on_homepage=True)
|
||||
.select_related("main_product")
|
||||
ctx["homepage_products"] = (
|
||||
MainProduct.objects.filter(is_active=True, show_on_homepage=True)
|
||||
.order_by("homepage_order", "order")
|
||||
)
|
||||
ctx["homepage_products_catalog"] = _homepage_products_catalog_queryset()
|
||||
return ctx
|
||||
|
||||
Reference in New Issue
Block a user