fix: show product in replacement of sub products in homepage

This commit is contained in:
mohamad
2026-05-26 17:38:53 +03:30
parent cacf1ba77b
commit 52516d5a5a
17 changed files with 402 additions and 205 deletions
+64 -15
View File
@@ -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
View File
@@ -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)'}"
+21 -1
View File
@@ -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
View File
@@ -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