From af62eeb3033c84a50008cf1b26792423c23742f6 Mon Sep 17 00:00:00 2001 From: Zorex Salvo Date: Wed, 10 Dec 2025 21:43:02 +0800 Subject: [PATCH 1/2] feat: Add configurable NavigationItem --- .../templates/home/components/navbar.html | 28 ++++++++ app/pages/admin.py | 21 +++++- app/pages/context_processors.py | 11 ++++ app/pages/migrations/0003_navigationitem.py | 33 ++++++++++ app/pages/models.py | 64 +++++++++++++++++++ config/settings.py | 1 + 6 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 app/pages/context_processors.py create mode 100644 app/pages/migrations/0003_navigationitem.py diff --git a/app/home/templates/home/components/navbar.html b/app/home/templates/home/components/navbar.html index 889bcb1..13a053b 100644 --- a/app/home/templates/home/components/navbar.html +++ b/app/home/templates/home/components/navbar.html @@ -252,6 +252,34 @@ + {% for item in navigation_items %} +
  • + + + +
  • + {% endfor %} + diff --git a/app/pages/admin.py b/app/pages/admin.py index 238fe2b..fe4efda 100644 --- a/app/pages/admin.py +++ b/app/pages/admin.py @@ -1,6 +1,23 @@ from django.contrib import admin -from .models import Page +from .models import NavigationItem, Page + + +class NavigationItemAdmin(admin.ModelAdmin): + list_display = ["title", "parent", "page", "external_url", "order", "is_active"] + list_filter = ["is_active", "parent"] + search_fields = ["title", "page__title"] + list_editable = ["order", "is_active"] + ordering = ["parent__order", "parent__title", "order", "title"] + + fieldsets = ( + (None, {"fields": ("title", "parent", "order", "is_active")}), + ( + "Link To", + {"fields": ("page", "external_url"), "description": "Choose either a page or external URL (not both)"}, + ), + ) + -# Register your models here. admin.site.register(Page) +admin.site.register(NavigationItem, NavigationItemAdmin) diff --git a/app/pages/context_processors.py b/app/pages/context_processors.py new file mode 100644 index 0000000..3a62575 --- /dev/null +++ b/app/pages/context_processors.py @@ -0,0 +1,11 @@ +from .models import NavigationItem + + +def navigation(request): + """ + Context processor to make navigation items available in all templates. + Returns only top-level active navigation items with their active children. + """ + nav_items = NavigationItem.objects.filter(is_active=True, parent=None).prefetch_related("children") + + return {"navigation_items": nav_items} diff --git a/app/pages/migrations/0003_navigationitem.py b/app/pages/migrations/0003_navigationitem.py new file mode 100644 index 0000000..cf2212a --- /dev/null +++ b/app/pages/migrations/0003_navigationitem.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2025-12-10 13:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pages', '0002_alter_page_slug'), + ] + + operations = [ + migrations.CreateModel( + name='NavigationItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100)), + ('external_url', models.URLField(blank=True, help_text='External URL (optional, used if no page is selected)', null=True)), + ('order', models.IntegerField(default=0, help_text='Lower numbers appear first')), + ('is_active', models.BooleanField(default=True, help_text='Show this item in navigation')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('page', models.ForeignKey(blank=True, help_text='Link to a page (optional if this is a dropdown parent)', null=True, on_delete=django.db.models.deletion.SET_NULL, to='pages.page')), + ('parent', models.ForeignKey(blank=True, help_text='Parent navigation item (leave empty for top-level items)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='pages.navigationitem')), + ], + options={ + 'verbose_name': 'Navigation Item', + 'verbose_name_plural': 'Navigation Items', + 'ordering': ['order', 'title'], + }, + ), + ] diff --git a/app/pages/models.py b/app/pages/models.py index b5b6abd..300738e 100644 --- a/app/pages/models.py +++ b/app/pages/models.py @@ -20,7 +20,71 @@ def save(self, *args, **kwargs): def __str__(self): return self.title + def get_absolute_url(self): + from django.urls import reverse + + return reverse("pages:detail", kwargs={"slug": self.slug}) + class Meta: ordering = ["-created_at"] verbose_name = "Page" verbose_name_plural = "Pages" + + +class NavigationItem(models.Model): + """Navigation item that can be top-level or a child (supports 1 level nesting only)""" + + title = models.CharField(max_length=100) + parent = models.ForeignKey( + "self", + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="children", + help_text="Parent navigation item (leave empty for top-level items)", + ) + page = models.ForeignKey( + Page, + on_delete=models.SET_NULL, + null=True, + blank=True, + help_text="Link to a page (optional if this is a dropdown parent)", + ) + external_url = models.URLField( + blank=True, null=True, help_text="External URL (optional, used if no page is selected)" + ) + order = models.IntegerField(default=0, help_text="Lower numbers appear first") + is_active = models.BooleanField(default=True, help_text="Show this item in navigation") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def save(self, *args, **kwargs): + if self.parent and self.parent.parent: + raise ValueError("Navigation items can only be nested 1 level deep") + super().save(*args, **kwargs) + + def get_url(self): + """Returns the URL for this nav item""" + if self.page: + return self.page.get_absolute_url() + if self.external_url: + return self.external_url + return "#" + + def has_children(self): + """Check if this nav item has any active children""" + return self.children.filter(is_active=True).exists() + + def is_top_level(self): + """Check if this is a top-level navigation item""" + return self.parent is None + + def __str__(self): + if self.parent: + return f"{self.parent.title} > {self.title}" + return self.title + + class Meta: + ordering = ["order", "title"] + verbose_name = "Navigation Item" + verbose_name_plural = "Navigation Items" diff --git a/config/settings.py b/config/settings.py index 9aaadec..3e4e791 100644 --- a/config/settings.py +++ b/config/settings.py @@ -113,6 +113,7 @@ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "app.pages.context_processors.navigation", ], }, }, From 9a688e49433c7654b3fd70c64a412d64d65637c0 Mon Sep 17 00:00:00 2001 From: Zorex Salvo Date: Wed, 10 Dec 2025 21:46:16 +0800 Subject: [PATCH 2/2] fix: Remove unnecessary white-bg --- app/pages/templates/pages/detail.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/pages/templates/pages/detail.html b/app/pages/templates/pages/detail.html index 9ef9124..33f3da7 100644 --- a/app/pages/templates/pages/detail.html +++ b/app/pages/templates/pages/detail.html @@ -3,11 +3,11 @@ {% block content %}
    - + {% include "home/components/navbar.html" %} - +
    -
    +

    {{ page.title }}

    {{ page.content|safe }}