feat: Pagination for hosts overrides

This commit is contained in:
Kumi 2025-10-25 20:05:05 +02:00
commit 4e5a5c4b1b
No known key found for this signature in database
GPG key ID: ECBCC9082395383F
6 changed files with 159 additions and 2 deletions

View file

@ -29,3 +29,27 @@ class RecordForm(forms.ModelForm):
class Meta:
model = Record
fields = ["name", "type", "data", "ttl", "priority"]
class HostsOverrideRecordFilterForm(forms.Form):
search = forms.CharField(
required=False,
label="",
widget=forms.TextInput(attrs={'placeholder': 'Name or Value'})
)
record_type = forms.ChoiceField(
choices=[('', '--Type--'), ("A", "A"), ("AAAA", "AAAA")],
required=False,
label=""
)
enabled = forms.ChoiceField(
choices=[('', '--Enabled--'), ('yes', '✓ Enabled'), ('no', '✗ Disabled')],
required=False,
label=""
)
import_source = forms.ModelChoiceField(
queryset=HostsFileImportSource.objects.all(),
required=False,
label="",
empty_label="--Import Source--"
)

View file

@ -1,7 +1,23 @@
{% extends "frontend/base.html" %}
{% load pagination_tags %}
{% block content %}
<h1>Host Override Records</h1>
<a class="button" href="{% url 'hosts_add' %}">Add Host Record</a>
<form method="get"
style="margin-bottom:1.5em;
display: flex;
gap: 0.7em;
flex-wrap: wrap;
align-items: flex-end">
<div>{{ filter_form.search }}</div>
<div>{{ filter_form.record_type }}</div>
<div>{{ filter_form.enabled }}</div>
<div>{{ filter_form.import_source }}</div>
<button type="submit" class="button button-secondary">Filter</button>
{% if request.GET %}
<a class="button" style="margin-left:6px;" href="{% url 'hosts_list' %}">Reset</a>
{% endif %}
</form>
<table>
<thead>
<tr>
@ -41,4 +57,57 @@
{% endfor %}
</tbody>
</table>
{% if is_paginated and paginator.num_pages > 1 %}
<div class="pagination"
style="margin:1.5em 0;
display:flex;
align-items:center;
gap:0.7em;
flex-wrap:wrap">
{% with page=page_obj.number num_pages=paginator.num_pages %}
<!-- Previous arrow -->
{% if page_obj.has_previous %}
<a class="button button-secondary"
href="?{% for k,v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v|urlencode }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">&#8592;</a>
{% endif %}
{% smart_pager page num_pages 2 as pager %}
{% for p in pager %}
{% if p %}
{% if p == page %}
<span style="font-weight:bold;
background:#c1d1fa;
padding:4px 12px;
border-radius:5px">{{ p }}</span>
{% else %}
<a class="button button-secondary"
href="?{% for k,v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v|urlencode }}&{% endif %}{% endfor %}page={{ p }}">{{ p }}</a>
{% endif %}
{% else %}
<span style="padding:0 5px;"></span>
{% endif %}
{% endfor %}
<!-- Next arrow -->
{% if page_obj.has_next %}
<a class="button button-secondary"
href="?{% for k,v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v|urlencode }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">&#8594;</a>
{% endif %}
<!-- Jump to page -->
<form method="get" style="display:inline; margin-left: 9px;">
{% for k, v in request.GET.items %}
{% if k != 'page' %}<input type="hidden" name="{{ k }}" value="{{ v|urlencode }}">{% endif %}
{% endfor %}
<input type="number"
min="1"
max="{{ num_pages }}"
name="page"
value="{{ page }}"
style="width:3em">
<button type="submit"
class="button button-secondary"
style="padding:2px 9px">Go</button>
</form>
<span style="margin-left:6px;">Page <b>{{ page }}</b> of <b>{{ num_pages }}</b></span>
{% endwith %}
</div>
{% endif %}
{% endblock content %}

View file

@ -0,0 +1,32 @@
from django import template
register = template.Library()
@register.simple_tag
def smart_pager(page, num_pages, window=2):
"""
Smart pagination: always show 1, N, and window around current page.
Returns list, e.g. [1, None, 4, 5, 6, None, 19], None means ''
"""
page = int(page)
num_pages = int(num_pages)
window = int(window)
result = []
left = max(1, page - window)
right = min(num_pages, page + window)
# Always show 1
result.append(1)
# Left gap
if left > 2:
result.append(None)
for p in range(max(2, left), min(right+1, num_pages)):
result.append(p)
# Right gap
if right < num_pages - 1:
result.append(None)
# Always show last
if num_pages > 1:
result.append(num_pages)
return result

View file

@ -11,6 +11,8 @@ from django.shortcuts import get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.db import models
from django.core.paginator import Paginator
from mallarddns.records.models import (
HostsOverrideRecord,
@ -19,7 +21,7 @@ from mallarddns.records.models import (
Record,
)
from mallarddns.records.hosts_import import import_hosts_from_url
from .forms import HostsOverrideForm, HostsImportForm, ZoneForm, RecordForm
from .forms import HostsOverrideForm, HostsImportForm, ZoneForm, RecordForm, HostsOverrideRecordFilterForm
# Dashboard
@ -36,6 +38,36 @@ class HostsOverrideListView(LoginRequiredMixin, ListView):
model = HostsOverrideRecord
context_object_name = "records"
ordering = ["-enabled", "name"]
paginate_by = 100 # Show 100 records per page
def get_filter_form(self):
data = self.request.GET if self.request.GET else None
return HostsOverrideRecordFilterForm(data)
def get_queryset(self):
qs = HostsOverrideRecord.objects.all()
form = self.get_filter_form()
if form.is_valid():
cd = form.cleaned_data
if cd.get("search"):
# Filter by name or value substring (case-insensitive)
qs = qs.filter(
models.Q(name__icontains=cd["search"]) | models.Q(value__icontains=cd["search"])
)
if cd.get("record_type"):
qs = qs.filter(record_type=cd["record_type"])
if cd.get("enabled") == "yes":
qs = qs.filter(enabled=True)
elif cd.get("enabled") == "no":
qs = qs.filter(enabled=False)
if cd.get("import_source"):
qs = qs.filter(import_source=cd["import_source"])
return qs.order_by(*self.ordering)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["filter_form"] = self.get_filter_form()
return context
class HostsOverrideCreateView(LoginRequiredMixin, CreateView):

View file

@ -94,7 +94,7 @@ class HostsFileImportSource(models.Model):
last_updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.url
return self.description if self.description else self.url
class RootServer(models.Model):