FossilRepo

Standardize pagination: per-page selector (25/50/100) on all list views Moved pagination helpers to core/pagination.py (PER_PAGE_OPTIONS, get_per_page, manual_paginate) shared across all apps. Every list view now supports ?per_page= parameter with 25/50/100 selector matching the ticket list pattern. Both Django Paginator and manual pagination partials updated with consistent per-page links. Updated: timeline, wiki, forum, releases, technotes, branches, tags, unversioned files, webhooks, API tokens, branch protection, ticket fields, ticket reports, projects, groups, members, teams, pages, audit log. Also tracked P0 TODO items in .github/TODO.md.

lmata 2026-04-07 16:20 trunk
Commit 56b754c5c5d5bed07531dee3fd430f911364cdadc310e47e04ada6c404d8704f
--- a/.github/TODO.md
+++ b/.github/TODO.md
@@ -0,0 +1,13 @@
1
+# Open Items
2
+
3
+Tracked here until filed as proper tickets.
4
+
5
+## P0 — Must Fix
6
+
7
+- [ ] **Consistent pagination** — Ticket list has per-page selector (25/50/100) and proper count display. All other paginated lists use basic prev/next. Standardize all lists to match ticket_list pattern.
8
+- [ ] **Public repo unauthenticated view** — Public projects should be fully browsable without login. Currently some views may redirect to login. Audit all fossil views for anonymous access on public repos.
9
+- [ ] **Branch protection push enforcement** — Rules are currently advisory. Need Fossil hook integration to actually block pushes to protected branches.
10
+
11
+## P1 — Should Do
12
+
13
+- [ ] **Fossil sync to fossilrepo.io** — Automate git→fossil sync as part of deploy or CI pipeline
--- a/.github/TODO.md
+++ b/.github/TODO.md
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/TODO.md
+++ b/.github/TODO.md
@@ -0,0 +1,13 @@
1 # Open Items
2
3 Tracked here until filed as proper tickets.
4
5 ## P0 — Must Fix
6
7 - [ ] **Consistent pagination** — Ticket list has per-page selector (25/50/100) and proper count display. All other paginated lists use basic prev/next. Standardize all lists to match ticket_list pattern.
8 - [ ] **Public repo unauthenticated view** — Public projects should be fully browsable without login. Currently some views may redirect to login. Audit all fossil views for anonymous access on public repos.
9 - [ ] **Branch protection push enforcement** — Rules are currently advisory. Need Fossil hook integration to actually block pushes to protected branches.
10
11 ## P1 — Should Do
12
13 - [ ] **Fossil sync to fossilrepo.io** — Automate git→fossil sync as part of deploy or CI pipeline
--- a/core/pagination.py
+++ b/core/pagination.py
@@ -0,0 +1,43 @@
1
+"""Shared pagination helpers used across all list views."""
2
+
3
+import math
4
+
5
+PER_PAGE_OPTIONS = [25, 50, 100]
6
+
7
+
8
+def get_per_page(request, default=25):
9
+ """Get per_page from request, constrained to PER_PAGE_OPTIONS."""
10
+ try:
11
+ per_page = int(request.GET.get("per_page", default))
12
+ except (ValueError, TypeError):
13
+ per_page = default
14
+ return per_page if per_page in PER_PAGE_OPTIONS else default
15
+
16
+
17
+def manual_paginate(items, request, per_page=None):
18
+ """Paginate a plain list and return (sliced_items, pagination_dict).
19
+
20
+ The pagination dict has keys compatible with the _pagination_manual.html partial:
21
+ has_previous, has_next, previous_page_number, next_page_number, number, num_pages, count.
22
+ """
23
+ if per_page is None:
24
+ per_page = get_per_page(request)
25
+ total = len(items)
26
+ num_pages = max(1, math.ceil(total / per_page))
27
+ try:
28
+ page = int(request.GET.get("page", 1))
29
+ except (ValueError, TypeError):
30
+ page = 1
31
+ page = max(1, min(page, num_pages))
32
+ offset = (page - 1) * per_page
33
+ sliced = items[offset : offset + per_page]
34
+ pagination = {
35
+ "has_previous": page > 1,
36
+ "has_next": offset + per_page < total,
37
+ "previous_page_number": page - 1,
38
+ "next_page_number": page + 1,
39
+ "number": page,
40
+ "num_pages": num_pages,
41
+ "count": total,
42
+ }
43
+ return sliced, pagination
--- a/core/pagination.py
+++ b/core/pagination.py
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/core/pagination.py
+++ b/core/pagination.py
@@ -0,0 +1,43 @@
1 """Shared pagination helpers used across all list views."""
2
3 import math
4
5 PER_PAGE_OPTIONS = [25, 50, 100]
6
7
8 def get_per_page(request, default=25):
9 """Get per_page from request, constrained to PER_PAGE_OPTIONS."""
10 try:
11 per_page = int(request.GET.get("per_page", default))
12 except (ValueError, TypeError):
13 per_page = default
14 return per_page if per_page in PER_PAGE_OPTIONS else default
15
16
17 def manual_paginate(items, request, per_page=None):
18 """Paginate a plain list and return (sliced_items, pagination_dict).
19
20 The pagination dict has keys compatible with the _pagination_manual.html partial:
21 has_previous, has_next, previous_page_number, next_page_number, number, num_pages, count.
22 """
23 if per_page is None:
24 per_page = get_per_page(request)
25 total = len(items)
26 num_pages = max(1, math.ceil(total / per_page))
27 try:
28 page = int(request.GET.get("page", 1))
29 except (ValueError, TypeError):
30 page = 1
31 page = max(1, min(page, num_pages))
32 offset = (page - 1) * per_page
33 sliced = items[offset : offset + per_page]
34 pagination = {
35 "has_previous": page > 1,
36 "has_next": offset + per_page < total,
37 "previous_page_number": page - 1,
38 "next_page_number": page + 1,
39 "number": page,
40 "num_pages": num_pages,
41 "count": total,
42 }
43 return sliced, pagination
+68 -72
--- fossil/views.py
+++ fossil/views.py
@@ -9,43 +9,17 @@
99
from django.http import Http404, HttpResponse, JsonResponse
1010
from django.shortcuts import get_object_or_404, redirect, render
1111
from django.utils.safestring import mark_safe
1212
from django.views.decorators.csrf import csrf_exempt
1313
14
+from core.pagination import PER_PAGE_OPTIONS, get_per_page, manual_paginate
1415
from core.sanitize import sanitize_html
1516
from projects.models import Project
1617
1718
from .models import FossilRepository
1819
from .reader import FossilReader
1920
20
-
21
-def _manual_paginate(items, request, per_page=25):
22
- """Paginate a plain list (FossilReader results) and return (sliced_items, pagination_dict).
23
-
24
- The pagination dict has keys compatible with the _pagination_manual.html partial:
25
- has_previous, has_next, previous_page_number, next_page_number, number, num_pages, count.
26
- """
27
- total = len(items)
28
- num_pages = max(1, math.ceil(total / per_page))
29
- try:
30
- page = int(request.GET.get("page", 1))
31
- except (ValueError, TypeError):
32
- page = 1
33
- page = max(1, min(page, num_pages))
34
- offset = (page - 1) * per_page
35
- sliced = items[offset : offset + per_page]
36
- pagination = {
37
- "has_previous": page > 1,
38
- "has_next": offset + per_page < total,
39
- "previous_page_number": page - 1,
40
- "next_page_number": page + 1,
41
- "number": page,
42
- "num_pages": num_pages,
43
- "count": total,
44
- }
45
- return sliced, pagination
46
-
4721
4822
def _render_fossil_content(content: str, project_slug: str = "", base_path: str = "") -> str:
4923
"""Render content that may be Fossil wiki markup, HTML, or Markdown.
5024
5125
Fossil wiki pages can contain:
@@ -666,11 +640,11 @@
666640
def timeline(request, slug):
667641
project, fossil_repo, reader = _get_repo_and_reader(slug, request)
668642
669643
event_type = request.GET.get("type", "")
670644
page = int(request.GET.get("page", "1"))
671
- per_page = 50
645
+ per_page = get_per_page(request, default=50)
672646
offset = (page - 1) * per_page
673647
674648
with reader:
675649
entries = reader.get_timeline(limit=per_page, offset=offset, event_type=event_type or None)
676650
@@ -687,10 +661,12 @@
687661
"project": project,
688662
"fossil_repo": fossil_repo,
689663
"entries": graph_entries,
690664
"event_type": event_type,
691665
"page": page,
666
+ "per_page": per_page,
667
+ "per_page_options": PER_PAGE_OPTIONS,
692668
"active_tab": "timeline",
693669
},
694670
)
695671
696672
@@ -701,12 +677,11 @@
701677
project, fossil_repo, reader = _get_repo_and_reader(slug, request)
702678
703679
status_filter = request.GET.get("status", "")
704680
search = request.GET.get("search", "").strip()
705681
page = int(request.GET.get("page", "1"))
706
- per_page = int(request.GET.get("per_page", "50"))
707
- per_page = per_page if per_page in (25, 50, 100) else 50
682
+ per_page = get_per_page(request, default=50)
708683
709684
with reader:
710685
tickets = reader.get_tickets(status=status_filter or None, limit=1000)
711686
712687
if search:
@@ -731,11 +706,11 @@
731706
"tickets": tickets,
732707
"status_filter": status_filter,
733708
"search": search,
734709
"page": page,
735710
"per_page": per_page,
736
- "per_page_options": [25, 50, 100],
711
+ "per_page_options": PER_PAGE_OPTIONS,
737712
"has_next": has_next,
738713
"has_prev": has_prev,
739714
"total": total,
740715
"total_pages": total_pages,
741716
"active_tab": "tickets",
@@ -790,46 +765,34 @@
790765
791766
search = request.GET.get("search", "").strip()
792767
if search:
793768
pages = [p for p in pages if search.lower() in p.name.lower()]
794769
795
- pages, pagination = _manual_paginate(pages, request)
770
+ per_page = get_per_page(request)
771
+ pages, pagination = manual_paginate(pages, request, per_page=per_page)
796772
797773
home_content_html = ""
798774
if home_page:
799775
home_content_html = mark_safe(sanitize_html(_render_fossil_content(home_page.content, project_slug=slug)))
776
+
777
+ ctx = {
778
+ "project": project,
779
+ "fossil_repo": fossil_repo,
780
+ "pages": pages,
781
+ "home_page": home_page,
782
+ "home_content_html": home_content_html,
783
+ "search": search,
784
+ "pagination": pagination,
785
+ "per_page": per_page,
786
+ "per_page_options": PER_PAGE_OPTIONS,
787
+ "active_tab": "wiki",
788
+ }
800789
801790
if request.headers.get("HX-Request"):
802
- return render(
803
- request,
804
- "fossil/wiki_list.html",
805
- {
806
- "project": project,
807
- "fossil_repo": fossil_repo,
808
- "pages": pages,
809
- "home_page": home_page,
810
- "home_content_html": home_content_html,
811
- "search": search,
812
- "pagination": pagination,
813
- "active_tab": "wiki",
814
- },
815
- )
816
-
817
- return render(
818
- request,
819
- "fossil/wiki_list.html",
820
- {
821
- "project": project,
822
- "fossil_repo": fossil_repo,
823
- "pages": pages,
824
- "home_page": home_page,
825
- "home_content_html": home_content_html,
826
- "search": search,
827
- "pagination": pagination,
828
- "active_tab": "wiki",
829
- },
830
- )
791
+ return render(request, "fossil/wiki_list.html", ctx)
792
+
793
+ return render(request, "fossil/wiki_list.html", ctx)
831794
832795
833796
def wiki_page(request, slug, page_name):
834797
project, fossil_repo, reader = _get_repo_and_reader(slug, request)
835798
@@ -900,11 +863,12 @@
900863
search = request.GET.get("search", "").strip()
901864
if search:
902865
search_lower = search.lower()
903866
merged = [p for p in merged if search_lower in (p.get("title") or "").lower() or search_lower in (p.get("body") or "").lower()]
904867
905
- merged, pagination = _manual_paginate(merged, request)
868
+ per_page = get_per_page(request)
869
+ merged, pagination = manual_paginate(merged, request, per_page=per_page)
906870
907871
has_write = can_write_project(request.user, project)
908872
909873
return render(
910874
request,
@@ -914,10 +878,12 @@
914878
"fossil_repo": fossil_repo,
915879
"posts": merged,
916880
"has_write": has_write,
917881
"search": search,
918882
"pagination": pagination,
883
+ "per_page": per_page,
884
+ "per_page_options": PER_PAGE_OPTIONS,
919885
"active_tab": "forum",
920886
},
921887
)
922888
923889
@@ -1090,11 +1056,12 @@
10901056
10911057
search = request.GET.get("search", "").strip()
10921058
if search:
10931059
webhooks = webhooks.filter(url__icontains=search)
10941060
1095
- paginator = Paginator(webhooks, 25)
1061
+ per_page = get_per_page(request)
1062
+ paginator = Paginator(webhooks, per_page)
10961063
page_obj = paginator.get_page(request.GET.get("page", 1))
10971064
10981065
return render(
10991066
request,
11001067
"fossil/webhook_list.html",
@@ -1102,10 +1069,12 @@
11021069
"project": project,
11031070
"fossil_repo": fossil_repo,
11041071
"webhooks": page_obj,
11051072
"page_obj": page_obj,
11061073
"search": search,
1074
+ "per_page": per_page,
1075
+ "per_page_options": PER_PAGE_OPTIONS,
11071076
"active_tab": "settings",
11081077
},
11091078
)
11101079
11111080
@@ -1962,11 +1931,12 @@
19621931
search = request.GET.get("search", "").strip()
19631932
if search:
19641933
search_lower = search.lower()
19651934
notes = [n for n in notes if search_lower in (n.comment or "").lower()]
19661935
1967
- notes, pagination = _manual_paginate(notes, request)
1936
+ per_page = get_per_page(request)
1937
+ notes, pagination = manual_paginate(notes, request, per_page=per_page)
19681938
19691939
has_write = can_write_project(request.user, project)
19701940
19711941
return render(
19721942
request,
@@ -1975,10 +1945,12 @@
19751945
"project": project,
19761946
"notes": notes,
19771947
"has_write": has_write,
19781948
"search": search,
19791949
"pagination": pagination,
1950
+ "per_page": per_page,
1951
+ "per_page_options": PER_PAGE_OPTIONS,
19801952
"active_tab": "wiki",
19811953
},
19821954
)
19831955
19841956
@@ -2098,11 +2070,12 @@
20982070
search = request.GET.get("search", "").strip()
20992071
if search:
21002072
search_lower = search.lower()
21012073
files = [f for f in files if search_lower in f.name.lower()]
21022074
2103
- files, pagination = _manual_paginate(files, request)
2075
+ per_page = get_per_page(request)
2076
+ files, pagination = manual_paginate(files, request, per_page=per_page)
21042077
21052078
has_admin = can_admin_project(request.user, project)
21062079
21072080
return render(
21082081
request,
@@ -2111,10 +2084,12 @@
21112084
"project": project,
21122085
"files": files,
21132086
"has_admin": has_admin,
21142087
"search": search,
21152088
"pagination": pagination,
2089
+ "per_page": per_page,
2090
+ "per_page_options": PER_PAGE_OPTIONS,
21162091
"active_tab": "files",
21172092
},
21182093
)
21192094
21202095
@@ -2428,11 +2403,12 @@
24282403
search = request.GET.get("search", "").strip()
24292404
if search:
24302405
search_lower = search.lower()
24312406
branches = [b for b in branches if search_lower in b.name.lower()]
24322407
2433
- branches, pagination = _manual_paginate(branches, request)
2408
+ per_page = get_per_page(request)
2409
+ branches, pagination = manual_paginate(branches, request, per_page=per_page)
24342410
24352411
return render(
24362412
request,
24372413
"fossil/branch_list.html",
24382414
{
@@ -2439,10 +2415,12 @@
24392415
"project": project,
24402416
"fossil_repo": fossil_repo,
24412417
"branches": branches,
24422418
"search": search,
24432419
"pagination": pagination,
2420
+ "per_page": per_page,
2421
+ "per_page_options": PER_PAGE_OPTIONS,
24442422
"active_tab": "code",
24452423
},
24462424
)
24472425
24482426
@@ -2458,20 +2436,23 @@
24582436
search = request.GET.get("search", "").strip()
24592437
if search:
24602438
search_lower = search.lower()
24612439
tags = [t for t in tags if search_lower in t.name.lower()]
24622440
2463
- tags, pagination = _manual_paginate(tags, request)
2441
+ per_page = get_per_page(request)
2442
+ tags, pagination = manual_paginate(tags, request, per_page=per_page)
24642443
24652444
return render(
24662445
request,
24672446
"fossil/tag_list.html",
24682447
{
24692448
"project": project,
24702449
"tags": tags,
24712450
"search": search,
24722451
"pagination": pagination,
2452
+ "per_page": per_page,
2453
+ "per_page_options": PER_PAGE_OPTIONS,
24732454
"active_tab": "code",
24742455
},
24752456
)
24762457
24772458
@@ -2912,11 +2893,12 @@
29122893
search = request.GET.get("search", "").strip()
29132894
if search:
29142895
releases = releases.filter(tag_name__icontains=search) | releases.filter(name__icontains=search)
29152896
releases = releases.distinct()
29162897
2917
- paginator = Paginator(releases, 25)
2898
+ per_page = get_per_page(request)
2899
+ paginator = Paginator(releases, per_page)
29182900
page_obj = paginator.get_page(request.GET.get("page", 1))
29192901
29202902
return render(
29212903
request,
29222904
"fossil/release_list.html",
@@ -2925,10 +2907,12 @@
29252907
"fossil_repo": fossil_repo,
29262908
"releases": page_obj,
29272909
"page_obj": page_obj,
29282910
"has_write": has_write,
29292911
"search": search,
2912
+ "per_page": per_page,
2913
+ "per_page_options": PER_PAGE_OPTIONS,
29302914
"active_tab": "releases",
29312915
},
29322916
)
29332917
29342918
@@ -3325,11 +3309,12 @@
33253309
33263310
search = request.GET.get("search", "").strip()
33273311
if search:
33283312
tokens = tokens.filter(name__icontains=search)
33293313
3330
- paginator = Paginator(tokens, 25)
3314
+ per_page = get_per_page(request)
3315
+ paginator = Paginator(tokens, per_page)
33313316
page_obj = paginator.get_page(request.GET.get("page", 1))
33323317
33333318
return render(
33343319
request,
33353320
"fossil/api_token_list.html",
@@ -3337,10 +3322,12 @@
33373322
"project": project,
33383323
"fossil_repo": fossil_repo,
33393324
"tokens": page_obj,
33403325
"page_obj": page_obj,
33413326
"search": search,
3327
+ "per_page": per_page,
3328
+ "per_page_options": PER_PAGE_OPTIONS,
33423329
"active_tab": "settings",
33433330
},
33443331
)
33453332
33463333
@@ -3421,11 +3408,12 @@
34213408
34223409
search = request.GET.get("search", "").strip()
34233410
if search:
34243411
rules = rules.filter(branch_pattern__icontains=search)
34253412
3426
- paginator = Paginator(rules, 25)
3413
+ per_page = get_per_page(request)
3414
+ paginator = Paginator(rules, per_page)
34273415
page_obj = paginator.get_page(request.GET.get("page", 1))
34283416
34293417
return render(
34303418
request,
34313419
"fossil/branch_protection_list.html",
@@ -3433,10 +3421,12 @@
34333421
"project": project,
34343422
"fossil_repo": fossil_repo,
34353423
"rules": page_obj,
34363424
"page_obj": page_obj,
34373425
"search": search,
3426
+ "per_page": per_page,
3427
+ "per_page_options": PER_PAGE_OPTIONS,
34383428
"active_tab": "settings",
34393429
},
34403430
)
34413431
34423432
@@ -3568,11 +3558,12 @@
35683558
search = request.GET.get("search", "").strip()
35693559
if search:
35703560
fields = fields.filter(label__icontains=search) | fields.filter(name__icontains=search)
35713561
fields = fields.distinct()
35723562
3573
- paginator = Paginator(fields, 25)
3563
+ per_page = get_per_page(request)
3564
+ paginator = Paginator(fields, per_page)
35743565
page_obj = paginator.get_page(request.GET.get("page", 1))
35753566
35763567
return render(
35773568
request,
35783569
"fossil/ticket_fields_list.html",
@@ -3580,10 +3571,12 @@
35803571
"project": project,
35813572
"fossil_repo": fossil_repo,
35823573
"fields": page_obj,
35833574
"page_obj": page_obj,
35843575
"search": search,
3576
+ "per_page": per_page,
3577
+ "per_page_options": PER_PAGE_OPTIONS,
35853578
"active_tab": "settings",
35863579
},
35873580
)
35883581
35893582
@@ -3724,11 +3717,12 @@
37243717
search = request.GET.get("search", "").strip()
37253718
if search:
37263719
reports = reports.filter(title__icontains=search) | reports.filter(description__icontains=search)
37273720
reports = reports.distinct()
37283721
3729
- paginator = Paginator(reports, 25)
3722
+ per_page = get_per_page(request)
3723
+ paginator = Paginator(reports, per_page)
37303724
page_obj = paginator.get_page(request.GET.get("page", 1))
37313725
37323726
return render(
37333727
request,
37343728
"fossil/ticket_reports_list.html",
@@ -3737,10 +3731,12 @@
37373731
"fossil_repo": fossil_repo,
37383732
"reports": page_obj,
37393733
"page_obj": page_obj,
37403734
"can_admin": is_admin,
37413735
"search": search,
3736
+ "per_page": per_page,
3737
+ "per_page_options": PER_PAGE_OPTIONS,
37423738
"active_tab": "tickets",
37433739
},
37443740
)
37453741
37463742
37473743
--- fossil/views.py
+++ fossil/views.py
@@ -9,43 +9,17 @@
9 from django.http import Http404, HttpResponse, JsonResponse
10 from django.shortcuts import get_object_or_404, redirect, render
11 from django.utils.safestring import mark_safe
12 from django.views.decorators.csrf import csrf_exempt
13
 
14 from core.sanitize import sanitize_html
15 from projects.models import Project
16
17 from .models import FossilRepository
18 from .reader import FossilReader
19
20
21 def _manual_paginate(items, request, per_page=25):
22 """Paginate a plain list (FossilReader results) and return (sliced_items, pagination_dict).
23
24 The pagination dict has keys compatible with the _pagination_manual.html partial:
25 has_previous, has_next, previous_page_number, next_page_number, number, num_pages, count.
26 """
27 total = len(items)
28 num_pages = max(1, math.ceil(total / per_page))
29 try:
30 page = int(request.GET.get("page", 1))
31 except (ValueError, TypeError):
32 page = 1
33 page = max(1, min(page, num_pages))
34 offset = (page - 1) * per_page
35 sliced = items[offset : offset + per_page]
36 pagination = {
37 "has_previous": page > 1,
38 "has_next": offset + per_page < total,
39 "previous_page_number": page - 1,
40 "next_page_number": page + 1,
41 "number": page,
42 "num_pages": num_pages,
43 "count": total,
44 }
45 return sliced, pagination
46
47
48 def _render_fossil_content(content: str, project_slug: str = "", base_path: str = "") -> str:
49 """Render content that may be Fossil wiki markup, HTML, or Markdown.
50
51 Fossil wiki pages can contain:
@@ -666,11 +640,11 @@
666 def timeline(request, slug):
667 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
668
669 event_type = request.GET.get("type", "")
670 page = int(request.GET.get("page", "1"))
671 per_page = 50
672 offset = (page - 1) * per_page
673
674 with reader:
675 entries = reader.get_timeline(limit=per_page, offset=offset, event_type=event_type or None)
676
@@ -687,10 +661,12 @@
687 "project": project,
688 "fossil_repo": fossil_repo,
689 "entries": graph_entries,
690 "event_type": event_type,
691 "page": page,
 
 
692 "active_tab": "timeline",
693 },
694 )
695
696
@@ -701,12 +677,11 @@
701 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
702
703 status_filter = request.GET.get("status", "")
704 search = request.GET.get("search", "").strip()
705 page = int(request.GET.get("page", "1"))
706 per_page = int(request.GET.get("per_page", "50"))
707 per_page = per_page if per_page in (25, 50, 100) else 50
708
709 with reader:
710 tickets = reader.get_tickets(status=status_filter or None, limit=1000)
711
712 if search:
@@ -731,11 +706,11 @@
731 "tickets": tickets,
732 "status_filter": status_filter,
733 "search": search,
734 "page": page,
735 "per_page": per_page,
736 "per_page_options": [25, 50, 100],
737 "has_next": has_next,
738 "has_prev": has_prev,
739 "total": total,
740 "total_pages": total_pages,
741 "active_tab": "tickets",
@@ -790,46 +765,34 @@
790
791 search = request.GET.get("search", "").strip()
792 if search:
793 pages = [p for p in pages if search.lower() in p.name.lower()]
794
795 pages, pagination = _manual_paginate(pages, request)
 
796
797 home_content_html = ""
798 if home_page:
799 home_content_html = mark_safe(sanitize_html(_render_fossil_content(home_page.content, project_slug=slug)))
 
 
 
 
 
 
 
 
 
 
 
 
 
800
801 if request.headers.get("HX-Request"):
802 return render(
803 request,
804 "fossil/wiki_list.html",
805 {
806 "project": project,
807 "fossil_repo": fossil_repo,
808 "pages": pages,
809 "home_page": home_page,
810 "home_content_html": home_content_html,
811 "search": search,
812 "pagination": pagination,
813 "active_tab": "wiki",
814 },
815 )
816
817 return render(
818 request,
819 "fossil/wiki_list.html",
820 {
821 "project": project,
822 "fossil_repo": fossil_repo,
823 "pages": pages,
824 "home_page": home_page,
825 "home_content_html": home_content_html,
826 "search": search,
827 "pagination": pagination,
828 "active_tab": "wiki",
829 },
830 )
831
832
833 def wiki_page(request, slug, page_name):
834 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
835
@@ -900,11 +863,12 @@
900 search = request.GET.get("search", "").strip()
901 if search:
902 search_lower = search.lower()
903 merged = [p for p in merged if search_lower in (p.get("title") or "").lower() or search_lower in (p.get("body") or "").lower()]
904
905 merged, pagination = _manual_paginate(merged, request)
 
906
907 has_write = can_write_project(request.user, project)
908
909 return render(
910 request,
@@ -914,10 +878,12 @@
914 "fossil_repo": fossil_repo,
915 "posts": merged,
916 "has_write": has_write,
917 "search": search,
918 "pagination": pagination,
 
 
919 "active_tab": "forum",
920 },
921 )
922
923
@@ -1090,11 +1056,12 @@
1090
1091 search = request.GET.get("search", "").strip()
1092 if search:
1093 webhooks = webhooks.filter(url__icontains=search)
1094
1095 paginator = Paginator(webhooks, 25)
 
1096 page_obj = paginator.get_page(request.GET.get("page", 1))
1097
1098 return render(
1099 request,
1100 "fossil/webhook_list.html",
@@ -1102,10 +1069,12 @@
1102 "project": project,
1103 "fossil_repo": fossil_repo,
1104 "webhooks": page_obj,
1105 "page_obj": page_obj,
1106 "search": search,
 
 
1107 "active_tab": "settings",
1108 },
1109 )
1110
1111
@@ -1962,11 +1931,12 @@
1962 search = request.GET.get("search", "").strip()
1963 if search:
1964 search_lower = search.lower()
1965 notes = [n for n in notes if search_lower in (n.comment or "").lower()]
1966
1967 notes, pagination = _manual_paginate(notes, request)
 
1968
1969 has_write = can_write_project(request.user, project)
1970
1971 return render(
1972 request,
@@ -1975,10 +1945,12 @@
1975 "project": project,
1976 "notes": notes,
1977 "has_write": has_write,
1978 "search": search,
1979 "pagination": pagination,
 
 
1980 "active_tab": "wiki",
1981 },
1982 )
1983
1984
@@ -2098,11 +2070,12 @@
2098 search = request.GET.get("search", "").strip()
2099 if search:
2100 search_lower = search.lower()
2101 files = [f for f in files if search_lower in f.name.lower()]
2102
2103 files, pagination = _manual_paginate(files, request)
 
2104
2105 has_admin = can_admin_project(request.user, project)
2106
2107 return render(
2108 request,
@@ -2111,10 +2084,12 @@
2111 "project": project,
2112 "files": files,
2113 "has_admin": has_admin,
2114 "search": search,
2115 "pagination": pagination,
 
 
2116 "active_tab": "files",
2117 },
2118 )
2119
2120
@@ -2428,11 +2403,12 @@
2428 search = request.GET.get("search", "").strip()
2429 if search:
2430 search_lower = search.lower()
2431 branches = [b for b in branches if search_lower in b.name.lower()]
2432
2433 branches, pagination = _manual_paginate(branches, request)
 
2434
2435 return render(
2436 request,
2437 "fossil/branch_list.html",
2438 {
@@ -2439,10 +2415,12 @@
2439 "project": project,
2440 "fossil_repo": fossil_repo,
2441 "branches": branches,
2442 "search": search,
2443 "pagination": pagination,
 
 
2444 "active_tab": "code",
2445 },
2446 )
2447
2448
@@ -2458,20 +2436,23 @@
2458 search = request.GET.get("search", "").strip()
2459 if search:
2460 search_lower = search.lower()
2461 tags = [t for t in tags if search_lower in t.name.lower()]
2462
2463 tags, pagination = _manual_paginate(tags, request)
 
2464
2465 return render(
2466 request,
2467 "fossil/tag_list.html",
2468 {
2469 "project": project,
2470 "tags": tags,
2471 "search": search,
2472 "pagination": pagination,
 
 
2473 "active_tab": "code",
2474 },
2475 )
2476
2477
@@ -2912,11 +2893,12 @@
2912 search = request.GET.get("search", "").strip()
2913 if search:
2914 releases = releases.filter(tag_name__icontains=search) | releases.filter(name__icontains=search)
2915 releases = releases.distinct()
2916
2917 paginator = Paginator(releases, 25)
 
2918 page_obj = paginator.get_page(request.GET.get("page", 1))
2919
2920 return render(
2921 request,
2922 "fossil/release_list.html",
@@ -2925,10 +2907,12 @@
2925 "fossil_repo": fossil_repo,
2926 "releases": page_obj,
2927 "page_obj": page_obj,
2928 "has_write": has_write,
2929 "search": search,
 
 
2930 "active_tab": "releases",
2931 },
2932 )
2933
2934
@@ -3325,11 +3309,12 @@
3325
3326 search = request.GET.get("search", "").strip()
3327 if search:
3328 tokens = tokens.filter(name__icontains=search)
3329
3330 paginator = Paginator(tokens, 25)
 
3331 page_obj = paginator.get_page(request.GET.get("page", 1))
3332
3333 return render(
3334 request,
3335 "fossil/api_token_list.html",
@@ -3337,10 +3322,12 @@
3337 "project": project,
3338 "fossil_repo": fossil_repo,
3339 "tokens": page_obj,
3340 "page_obj": page_obj,
3341 "search": search,
 
 
3342 "active_tab": "settings",
3343 },
3344 )
3345
3346
@@ -3421,11 +3408,12 @@
3421
3422 search = request.GET.get("search", "").strip()
3423 if search:
3424 rules = rules.filter(branch_pattern__icontains=search)
3425
3426 paginator = Paginator(rules, 25)
 
3427 page_obj = paginator.get_page(request.GET.get("page", 1))
3428
3429 return render(
3430 request,
3431 "fossil/branch_protection_list.html",
@@ -3433,10 +3421,12 @@
3433 "project": project,
3434 "fossil_repo": fossil_repo,
3435 "rules": page_obj,
3436 "page_obj": page_obj,
3437 "search": search,
 
 
3438 "active_tab": "settings",
3439 },
3440 )
3441
3442
@@ -3568,11 +3558,12 @@
3568 search = request.GET.get("search", "").strip()
3569 if search:
3570 fields = fields.filter(label__icontains=search) | fields.filter(name__icontains=search)
3571 fields = fields.distinct()
3572
3573 paginator = Paginator(fields, 25)
 
3574 page_obj = paginator.get_page(request.GET.get("page", 1))
3575
3576 return render(
3577 request,
3578 "fossil/ticket_fields_list.html",
@@ -3580,10 +3571,12 @@
3580 "project": project,
3581 "fossil_repo": fossil_repo,
3582 "fields": page_obj,
3583 "page_obj": page_obj,
3584 "search": search,
 
 
3585 "active_tab": "settings",
3586 },
3587 )
3588
3589
@@ -3724,11 +3717,12 @@
3724 search = request.GET.get("search", "").strip()
3725 if search:
3726 reports = reports.filter(title__icontains=search) | reports.filter(description__icontains=search)
3727 reports = reports.distinct()
3728
3729 paginator = Paginator(reports, 25)
 
3730 page_obj = paginator.get_page(request.GET.get("page", 1))
3731
3732 return render(
3733 request,
3734 "fossil/ticket_reports_list.html",
@@ -3737,10 +3731,12 @@
3737 "fossil_repo": fossil_repo,
3738 "reports": page_obj,
3739 "page_obj": page_obj,
3740 "can_admin": is_admin,
3741 "search": search,
 
 
3742 "active_tab": "tickets",
3743 },
3744 )
3745
3746
3747
--- fossil/views.py
+++ fossil/views.py
@@ -9,43 +9,17 @@
9 from django.http import Http404, HttpResponse, JsonResponse
10 from django.shortcuts import get_object_or_404, redirect, render
11 from django.utils.safestring import mark_safe
12 from django.views.decorators.csrf import csrf_exempt
13
14 from core.pagination import PER_PAGE_OPTIONS, get_per_page, manual_paginate
15 from core.sanitize import sanitize_html
16 from projects.models import Project
17
18 from .models import FossilRepository
19 from .reader import FossilReader
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
22 def _render_fossil_content(content: str, project_slug: str = "", base_path: str = "") -> str:
23 """Render content that may be Fossil wiki markup, HTML, or Markdown.
24
25 Fossil wiki pages can contain:
@@ -666,11 +640,11 @@
640 def timeline(request, slug):
641 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
642
643 event_type = request.GET.get("type", "")
644 page = int(request.GET.get("page", "1"))
645 per_page = get_per_page(request, default=50)
646 offset = (page - 1) * per_page
647
648 with reader:
649 entries = reader.get_timeline(limit=per_page, offset=offset, event_type=event_type or None)
650
@@ -687,10 +661,12 @@
661 "project": project,
662 "fossil_repo": fossil_repo,
663 "entries": graph_entries,
664 "event_type": event_type,
665 "page": page,
666 "per_page": per_page,
667 "per_page_options": PER_PAGE_OPTIONS,
668 "active_tab": "timeline",
669 },
670 )
671
672
@@ -701,12 +677,11 @@
677 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
678
679 status_filter = request.GET.get("status", "")
680 search = request.GET.get("search", "").strip()
681 page = int(request.GET.get("page", "1"))
682 per_page = get_per_page(request, default=50)
 
683
684 with reader:
685 tickets = reader.get_tickets(status=status_filter or None, limit=1000)
686
687 if search:
@@ -731,11 +706,11 @@
706 "tickets": tickets,
707 "status_filter": status_filter,
708 "search": search,
709 "page": page,
710 "per_page": per_page,
711 "per_page_options": PER_PAGE_OPTIONS,
712 "has_next": has_next,
713 "has_prev": has_prev,
714 "total": total,
715 "total_pages": total_pages,
716 "active_tab": "tickets",
@@ -790,46 +765,34 @@
765
766 search = request.GET.get("search", "").strip()
767 if search:
768 pages = [p for p in pages if search.lower() in p.name.lower()]
769
770 per_page = get_per_page(request)
771 pages, pagination = manual_paginate(pages, request, per_page=per_page)
772
773 home_content_html = ""
774 if home_page:
775 home_content_html = mark_safe(sanitize_html(_render_fossil_content(home_page.content, project_slug=slug)))
776
777 ctx = {
778 "project": project,
779 "fossil_repo": fossil_repo,
780 "pages": pages,
781 "home_page": home_page,
782 "home_content_html": home_content_html,
783 "search": search,
784 "pagination": pagination,
785 "per_page": per_page,
786 "per_page_options": PER_PAGE_OPTIONS,
787 "active_tab": "wiki",
788 }
789
790 if request.headers.get("HX-Request"):
791 return render(request, "fossil/wiki_list.html", ctx)
792
793 return render(request, "fossil/wiki_list.html", ctx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794
795
796 def wiki_page(request, slug, page_name):
797 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
798
@@ -900,11 +863,12 @@
863 search = request.GET.get("search", "").strip()
864 if search:
865 search_lower = search.lower()
866 merged = [p for p in merged if search_lower in (p.get("title") or "").lower() or search_lower in (p.get("body") or "").lower()]
867
868 per_page = get_per_page(request)
869 merged, pagination = manual_paginate(merged, request, per_page=per_page)
870
871 has_write = can_write_project(request.user, project)
872
873 return render(
874 request,
@@ -914,10 +878,12 @@
878 "fossil_repo": fossil_repo,
879 "posts": merged,
880 "has_write": has_write,
881 "search": search,
882 "pagination": pagination,
883 "per_page": per_page,
884 "per_page_options": PER_PAGE_OPTIONS,
885 "active_tab": "forum",
886 },
887 )
888
889
@@ -1090,11 +1056,12 @@
1056
1057 search = request.GET.get("search", "").strip()
1058 if search:
1059 webhooks = webhooks.filter(url__icontains=search)
1060
1061 per_page = get_per_page(request)
1062 paginator = Paginator(webhooks, per_page)
1063 page_obj = paginator.get_page(request.GET.get("page", 1))
1064
1065 return render(
1066 request,
1067 "fossil/webhook_list.html",
@@ -1102,10 +1069,12 @@
1069 "project": project,
1070 "fossil_repo": fossil_repo,
1071 "webhooks": page_obj,
1072 "page_obj": page_obj,
1073 "search": search,
1074 "per_page": per_page,
1075 "per_page_options": PER_PAGE_OPTIONS,
1076 "active_tab": "settings",
1077 },
1078 )
1079
1080
@@ -1962,11 +1931,12 @@
1931 search = request.GET.get("search", "").strip()
1932 if search:
1933 search_lower = search.lower()
1934 notes = [n for n in notes if search_lower in (n.comment or "").lower()]
1935
1936 per_page = get_per_page(request)
1937 notes, pagination = manual_paginate(notes, request, per_page=per_page)
1938
1939 has_write = can_write_project(request.user, project)
1940
1941 return render(
1942 request,
@@ -1975,10 +1945,12 @@
1945 "project": project,
1946 "notes": notes,
1947 "has_write": has_write,
1948 "search": search,
1949 "pagination": pagination,
1950 "per_page": per_page,
1951 "per_page_options": PER_PAGE_OPTIONS,
1952 "active_tab": "wiki",
1953 },
1954 )
1955
1956
@@ -2098,11 +2070,12 @@
2070 search = request.GET.get("search", "").strip()
2071 if search:
2072 search_lower = search.lower()
2073 files = [f for f in files if search_lower in f.name.lower()]
2074
2075 per_page = get_per_page(request)
2076 files, pagination = manual_paginate(files, request, per_page=per_page)
2077
2078 has_admin = can_admin_project(request.user, project)
2079
2080 return render(
2081 request,
@@ -2111,10 +2084,12 @@
2084 "project": project,
2085 "files": files,
2086 "has_admin": has_admin,
2087 "search": search,
2088 "pagination": pagination,
2089 "per_page": per_page,
2090 "per_page_options": PER_PAGE_OPTIONS,
2091 "active_tab": "files",
2092 },
2093 )
2094
2095
@@ -2428,11 +2403,12 @@
2403 search = request.GET.get("search", "").strip()
2404 if search:
2405 search_lower = search.lower()
2406 branches = [b for b in branches if search_lower in b.name.lower()]
2407
2408 per_page = get_per_page(request)
2409 branches, pagination = manual_paginate(branches, request, per_page=per_page)
2410
2411 return render(
2412 request,
2413 "fossil/branch_list.html",
2414 {
@@ -2439,10 +2415,12 @@
2415 "project": project,
2416 "fossil_repo": fossil_repo,
2417 "branches": branches,
2418 "search": search,
2419 "pagination": pagination,
2420 "per_page": per_page,
2421 "per_page_options": PER_PAGE_OPTIONS,
2422 "active_tab": "code",
2423 },
2424 )
2425
2426
@@ -2458,20 +2436,23 @@
2436 search = request.GET.get("search", "").strip()
2437 if search:
2438 search_lower = search.lower()
2439 tags = [t for t in tags if search_lower in t.name.lower()]
2440
2441 per_page = get_per_page(request)
2442 tags, pagination = manual_paginate(tags, request, per_page=per_page)
2443
2444 return render(
2445 request,
2446 "fossil/tag_list.html",
2447 {
2448 "project": project,
2449 "tags": tags,
2450 "search": search,
2451 "pagination": pagination,
2452 "per_page": per_page,
2453 "per_page_options": PER_PAGE_OPTIONS,
2454 "active_tab": "code",
2455 },
2456 )
2457
2458
@@ -2912,11 +2893,12 @@
2893 search = request.GET.get("search", "").strip()
2894 if search:
2895 releases = releases.filter(tag_name__icontains=search) | releases.filter(name__icontains=search)
2896 releases = releases.distinct()
2897
2898 per_page = get_per_page(request)
2899 paginator = Paginator(releases, per_page)
2900 page_obj = paginator.get_page(request.GET.get("page", 1))
2901
2902 return render(
2903 request,
2904 "fossil/release_list.html",
@@ -2925,10 +2907,12 @@
2907 "fossil_repo": fossil_repo,
2908 "releases": page_obj,
2909 "page_obj": page_obj,
2910 "has_write": has_write,
2911 "search": search,
2912 "per_page": per_page,
2913 "per_page_options": PER_PAGE_OPTIONS,
2914 "active_tab": "releases",
2915 },
2916 )
2917
2918
@@ -3325,11 +3309,12 @@
3309
3310 search = request.GET.get("search", "").strip()
3311 if search:
3312 tokens = tokens.filter(name__icontains=search)
3313
3314 per_page = get_per_page(request)
3315 paginator = Paginator(tokens, per_page)
3316 page_obj = paginator.get_page(request.GET.get("page", 1))
3317
3318 return render(
3319 request,
3320 "fossil/api_token_list.html",
@@ -3337,10 +3322,12 @@
3322 "project": project,
3323 "fossil_repo": fossil_repo,
3324 "tokens": page_obj,
3325 "page_obj": page_obj,
3326 "search": search,
3327 "per_page": per_page,
3328 "per_page_options": PER_PAGE_OPTIONS,
3329 "active_tab": "settings",
3330 },
3331 )
3332
3333
@@ -3421,11 +3408,12 @@
3408
3409 search = request.GET.get("search", "").strip()
3410 if search:
3411 rules = rules.filter(branch_pattern__icontains=search)
3412
3413 per_page = get_per_page(request)
3414 paginator = Paginator(rules, per_page)
3415 page_obj = paginator.get_page(request.GET.get("page", 1))
3416
3417 return render(
3418 request,
3419 "fossil/branch_protection_list.html",
@@ -3433,10 +3421,12 @@
3421 "project": project,
3422 "fossil_repo": fossil_repo,
3423 "rules": page_obj,
3424 "page_obj": page_obj,
3425 "search": search,
3426 "per_page": per_page,
3427 "per_page_options": PER_PAGE_OPTIONS,
3428 "active_tab": "settings",
3429 },
3430 )
3431
3432
@@ -3568,11 +3558,12 @@
3558 search = request.GET.get("search", "").strip()
3559 if search:
3560 fields = fields.filter(label__icontains=search) | fields.filter(name__icontains=search)
3561 fields = fields.distinct()
3562
3563 per_page = get_per_page(request)
3564 paginator = Paginator(fields, per_page)
3565 page_obj = paginator.get_page(request.GET.get("page", 1))
3566
3567 return render(
3568 request,
3569 "fossil/ticket_fields_list.html",
@@ -3580,10 +3571,12 @@
3571 "project": project,
3572 "fossil_repo": fossil_repo,
3573 "fields": page_obj,
3574 "page_obj": page_obj,
3575 "search": search,
3576 "per_page": per_page,
3577 "per_page_options": PER_PAGE_OPTIONS,
3578 "active_tab": "settings",
3579 },
3580 )
3581
3582
@@ -3724,11 +3717,12 @@
3717 search = request.GET.get("search", "").strip()
3718 if search:
3719 reports = reports.filter(title__icontains=search) | reports.filter(description__icontains=search)
3720 reports = reports.distinct()
3721
3722 per_page = get_per_page(request)
3723 paginator = Paginator(reports, per_page)
3724 page_obj = paginator.get_page(request.GET.get("page", 1))
3725
3726 return render(
3727 request,
3728 "fossil/ticket_reports_list.html",
@@ -3737,10 +3731,12 @@
3731 "fossil_repo": fossil_repo,
3732 "reports": page_obj,
3733 "page_obj": page_obj,
3734 "can_admin": is_admin,
3735 "search": search,
3736 "per_page": per_page,
3737 "per_page_options": PER_PAGE_OPTIONS,
3738 "active_tab": "tickets",
3739 },
3740 )
3741
3742
3743
--- organization/views.py
+++ organization/views.py
@@ -4,10 +4,11 @@
44
from django.core.paginator import Paginator
55
from django.db import models
66
from django.http import HttpResponse
77
from django.shortcuts import get_object_or_404, redirect, render
88
9
+from core.pagination import PER_PAGE_OPTIONS, get_per_page
910
from core.permissions import P
1011
1112
from .forms import (
1213
MemberAddForm,
1314
OrganizationSettingsForm,
@@ -65,19 +66,27 @@
6566
6667
search = request.GET.get("search", "").strip()
6768
if search:
6869
members = members.filter(member__username__icontains=search)
6970
70
- paginator = Paginator(members, 25)
71
+ per_page = get_per_page(request)
72
+ paginator = Paginator(members, per_page)
7173
page_obj = paginator.get_page(request.GET.get("page", 1))
74
+
75
+ ctx = {
76
+ "members": page_obj,
77
+ "page_obj": page_obj,
78
+ "org": org,
79
+ "search": search,
80
+ "per_page": per_page,
81
+ "per_page_options": PER_PAGE_OPTIONS,
82
+ }
7283
7384
if request.headers.get("HX-Request"):
74
- return render(
75
- request, "organization/partials/member_table.html", {"members": page_obj, "page_obj": page_obj, "org": org, "search": search}
76
- )
85
+ return render(request, "organization/partials/member_table.html", ctx)
7786
78
- return render(request, "organization/member_list.html", {"members": page_obj, "page_obj": page_obj, "org": org, "search": search})
87
+ return render(request, "organization/member_list.html", ctx)
7988
8089
8190
@login_required
8291
def member_add(request):
8392
P.ORGANIZATION_MEMBER_ADD.check(request.user)
@@ -125,17 +134,20 @@
125134
126135
search = request.GET.get("search", "").strip()
127136
if search:
128137
teams = teams.filter(name__icontains=search)
129138
130
- paginator = Paginator(teams, 25)
139
+ per_page = get_per_page(request)
140
+ paginator = Paginator(teams, per_page)
131141
page_obj = paginator.get_page(request.GET.get("page", 1))
132142
143
+ ctx = {"teams": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
144
+
133145
if request.headers.get("HX-Request"):
134
- return render(request, "organization/partials/team_table.html", {"teams": page_obj, "page_obj": page_obj, "search": search})
146
+ return render(request, "organization/partials/team_table.html", ctx)
135147
136
- return render(request, "organization/team_list.html", {"teams": page_obj, "page_obj": page_obj, "search": search})
148
+ return render(request, "organization/team_list.html", ctx)
137149
138150
139151
@login_required
140152
def team_create(request):
141153
P.TEAM_ADD.check(request.user)
@@ -467,11 +479,11 @@
467479
468480
469481
@login_required
470482
def audit_log(request):
471483
"""Unified audit log across all tracked models. Requires superuser or org admin."""
472
- import math
484
+ from core.pagination import manual_paginate
473485
474486
if not request.user.is_superuser:
475487
P.ORGANIZATION_CHANGE.check(request.user)
476488
477489
from fossil.models import FossilRepository
@@ -504,30 +516,12 @@
504516
}
505517
)
506518
507519
entries.sort(key=lambda x: x["date"], reverse=True)
508520
509
- # Manual pagination over the merged, sorted list
510
- per_page = 25
511
- total = len(entries)
512
- num_pages = max(1, math.ceil(total / per_page))
513
- try:
514
- page = int(request.GET.get("page", 1))
515
- except (ValueError, TypeError):
516
- page = 1
517
- page = max(1, min(page, num_pages))
518
- offset = (page - 1) * per_page
519
- entries = entries[offset : offset + per_page]
520
- pagination = {
521
- "has_previous": page > 1,
522
- "has_next": offset + per_page < total,
523
- "previous_page_number": page - 1,
524
- "next_page_number": page + 1,
525
- "number": page,
526
- "num_pages": num_pages,
527
- "count": total,
528
- }
521
+ per_page = get_per_page(request)
522
+ entries, pagination = manual_paginate(entries, request, per_page=per_page)
529523
530524
available_models = [label for label, _ in trackable_models]
531525
532526
return render(
533527
request,
@@ -535,10 +529,12 @@
535529
{
536530
"entries": entries,
537531
"model_filter": model_filter,
538532
"available_models": available_models,
539533
"pagination": pagination,
534
+ "per_page": per_page,
535
+ "per_page_options": PER_PAGE_OPTIONS,
540536
},
541537
)
542538
543539
544540
@login_required
545541
--- organization/views.py
+++ organization/views.py
@@ -4,10 +4,11 @@
4 from django.core.paginator import Paginator
5 from django.db import models
6 from django.http import HttpResponse
7 from django.shortcuts import get_object_or_404, redirect, render
8
 
9 from core.permissions import P
10
11 from .forms import (
12 MemberAddForm,
13 OrganizationSettingsForm,
@@ -65,19 +66,27 @@
65
66 search = request.GET.get("search", "").strip()
67 if search:
68 members = members.filter(member__username__icontains=search)
69
70 paginator = Paginator(members, 25)
 
71 page_obj = paginator.get_page(request.GET.get("page", 1))
 
 
 
 
 
 
 
 
 
72
73 if request.headers.get("HX-Request"):
74 return render(
75 request, "organization/partials/member_table.html", {"members": page_obj, "page_obj": page_obj, "org": org, "search": search}
76 )
77
78 return render(request, "organization/member_list.html", {"members": page_obj, "page_obj": page_obj, "org": org, "search": search})
79
80
81 @login_required
82 def member_add(request):
83 P.ORGANIZATION_MEMBER_ADD.check(request.user)
@@ -125,17 +134,20 @@
125
126 search = request.GET.get("search", "").strip()
127 if search:
128 teams = teams.filter(name__icontains=search)
129
130 paginator = Paginator(teams, 25)
 
131 page_obj = paginator.get_page(request.GET.get("page", 1))
132
 
 
133 if request.headers.get("HX-Request"):
134 return render(request, "organization/partials/team_table.html", {"teams": page_obj, "page_obj": page_obj, "search": search})
135
136 return render(request, "organization/team_list.html", {"teams": page_obj, "page_obj": page_obj, "search": search})
137
138
139 @login_required
140 def team_create(request):
141 P.TEAM_ADD.check(request.user)
@@ -467,11 +479,11 @@
467
468
469 @login_required
470 def audit_log(request):
471 """Unified audit log across all tracked models. Requires superuser or org admin."""
472 import math
473
474 if not request.user.is_superuser:
475 P.ORGANIZATION_CHANGE.check(request.user)
476
477 from fossil.models import FossilRepository
@@ -504,30 +516,12 @@
504 }
505 )
506
507 entries.sort(key=lambda x: x["date"], reverse=True)
508
509 # Manual pagination over the merged, sorted list
510 per_page = 25
511 total = len(entries)
512 num_pages = max(1, math.ceil(total / per_page))
513 try:
514 page = int(request.GET.get("page", 1))
515 except (ValueError, TypeError):
516 page = 1
517 page = max(1, min(page, num_pages))
518 offset = (page - 1) * per_page
519 entries = entries[offset : offset + per_page]
520 pagination = {
521 "has_previous": page > 1,
522 "has_next": offset + per_page < total,
523 "previous_page_number": page - 1,
524 "next_page_number": page + 1,
525 "number": page,
526 "num_pages": num_pages,
527 "count": total,
528 }
529
530 available_models = [label for label, _ in trackable_models]
531
532 return render(
533 request,
@@ -535,10 +529,12 @@
535 {
536 "entries": entries,
537 "model_filter": model_filter,
538 "available_models": available_models,
539 "pagination": pagination,
 
 
540 },
541 )
542
543
544 @login_required
545
--- organization/views.py
+++ organization/views.py
@@ -4,10 +4,11 @@
4 from django.core.paginator import Paginator
5 from django.db import models
6 from django.http import HttpResponse
7 from django.shortcuts import get_object_or_404, redirect, render
8
9 from core.pagination import PER_PAGE_OPTIONS, get_per_page
10 from core.permissions import P
11
12 from .forms import (
13 MemberAddForm,
14 OrganizationSettingsForm,
@@ -65,19 +66,27 @@
66
67 search = request.GET.get("search", "").strip()
68 if search:
69 members = members.filter(member__username__icontains=search)
70
71 per_page = get_per_page(request)
72 paginator = Paginator(members, per_page)
73 page_obj = paginator.get_page(request.GET.get("page", 1))
74
75 ctx = {
76 "members": page_obj,
77 "page_obj": page_obj,
78 "org": org,
79 "search": search,
80 "per_page": per_page,
81 "per_page_options": PER_PAGE_OPTIONS,
82 }
83
84 if request.headers.get("HX-Request"):
85 return render(request, "organization/partials/member_table.html", ctx)
 
 
86
87 return render(request, "organization/member_list.html", ctx)
88
89
90 @login_required
91 def member_add(request):
92 P.ORGANIZATION_MEMBER_ADD.check(request.user)
@@ -125,17 +134,20 @@
134
135 search = request.GET.get("search", "").strip()
136 if search:
137 teams = teams.filter(name__icontains=search)
138
139 per_page = get_per_page(request)
140 paginator = Paginator(teams, per_page)
141 page_obj = paginator.get_page(request.GET.get("page", 1))
142
143 ctx = {"teams": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
144
145 if request.headers.get("HX-Request"):
146 return render(request, "organization/partials/team_table.html", ctx)
147
148 return render(request, "organization/team_list.html", ctx)
149
150
151 @login_required
152 def team_create(request):
153 P.TEAM_ADD.check(request.user)
@@ -467,11 +479,11 @@
479
480
481 @login_required
482 def audit_log(request):
483 """Unified audit log across all tracked models. Requires superuser or org admin."""
484 from core.pagination import manual_paginate
485
486 if not request.user.is_superuser:
487 P.ORGANIZATION_CHANGE.check(request.user)
488
489 from fossil.models import FossilRepository
@@ -504,30 +516,12 @@
516 }
517 )
518
519 entries.sort(key=lambda x: x["date"], reverse=True)
520
521 per_page = get_per_page(request)
522 entries, pagination = manual_paginate(entries, request, per_page=per_page)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
524 available_models = [label for label, _ in trackable_models]
525
526 return render(
527 request,
@@ -535,10 +529,12 @@
529 {
530 "entries": entries,
531 "model_filter": model_filter,
532 "available_models": available_models,
533 "pagination": pagination,
534 "per_page": per_page,
535 "per_page_options": PER_PAGE_OPTIONS,
536 },
537 )
538
539
540 @login_required
541
+7 -3
--- pages/views.py
+++ pages/views.py
@@ -4,10 +4,11 @@
44
from django.core.paginator import Paginator
55
from django.http import HttpResponse
66
from django.shortcuts import get_object_or_404, redirect, render
77
from django.utils.safestring import mark_safe
88
9
+from core.pagination import PER_PAGE_OPTIONS, get_per_page
910
from core.permissions import P
1011
from core.sanitize import sanitize_html
1112
from organization.views import get_org
1213
1314
from .forms import PageForm
@@ -24,17 +25,20 @@
2425
2526
search = request.GET.get("search", "").strip()
2627
if search:
2728
pages = pages.filter(name__icontains=search)
2829
29
- paginator = Paginator(pages, 25)
30
+ per_page = get_per_page(request)
31
+ paginator = Paginator(pages, per_page)
3032
page_obj = paginator.get_page(request.GET.get("page", 1))
3133
34
+ ctx = {"pages": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
35
+
3236
if request.headers.get("HX-Request"):
33
- return render(request, "pages/partials/page_table.html", {"pages": page_obj, "page_obj": page_obj, "search": search})
37
+ return render(request, "pages/partials/page_table.html", ctx)
3438
35
- return render(request, "pages/page_list.html", {"pages": page_obj, "page_obj": page_obj, "search": search})
39
+ return render(request, "pages/page_list.html", ctx)
3640
3741
3842
@login_required
3943
def page_create(request):
4044
P.PAGE_ADD.check(request.user)
4145
--- pages/views.py
+++ pages/views.py
@@ -4,10 +4,11 @@
4 from django.core.paginator import Paginator
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, redirect, render
7 from django.utils.safestring import mark_safe
8
 
9 from core.permissions import P
10 from core.sanitize import sanitize_html
11 from organization.views import get_org
12
13 from .forms import PageForm
@@ -24,17 +25,20 @@
24
25 search = request.GET.get("search", "").strip()
26 if search:
27 pages = pages.filter(name__icontains=search)
28
29 paginator = Paginator(pages, 25)
 
30 page_obj = paginator.get_page(request.GET.get("page", 1))
31
 
 
32 if request.headers.get("HX-Request"):
33 return render(request, "pages/partials/page_table.html", {"pages": page_obj, "page_obj": page_obj, "search": search})
34
35 return render(request, "pages/page_list.html", {"pages": page_obj, "page_obj": page_obj, "search": search})
36
37
38 @login_required
39 def page_create(request):
40 P.PAGE_ADD.check(request.user)
41
--- pages/views.py
+++ pages/views.py
@@ -4,10 +4,11 @@
4 from django.core.paginator import Paginator
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, redirect, render
7 from django.utils.safestring import mark_safe
8
9 from core.pagination import PER_PAGE_OPTIONS, get_per_page
10 from core.permissions import P
11 from core.sanitize import sanitize_html
12 from organization.views import get_org
13
14 from .forms import PageForm
@@ -24,17 +25,20 @@
25
26 search = request.GET.get("search", "").strip()
27 if search:
28 pages = pages.filter(name__icontains=search)
29
30 per_page = get_per_page(request)
31 paginator = Paginator(pages, per_page)
32 page_obj = paginator.get_page(request.GET.get("page", 1))
33
34 ctx = {"pages": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
35
36 if request.headers.get("HX-Request"):
37 return render(request, "pages/partials/page_table.html", ctx)
38
39 return render(request, "pages/page_list.html", ctx)
40
41
42 @login_required
43 def page_create(request):
44 P.PAGE_ADD.check(request.user)
45
--- projects/views.py
+++ projects/views.py
@@ -3,10 +3,11 @@
33
from django.core.paginator import Paginator
44
from django.db.models import Count
55
from django.http import HttpResponse
66
from django.shortcuts import get_object_or_404, redirect, render
77
8
+from core.pagination import PER_PAGE_OPTIONS, get_per_page
89
from core.permissions import P
910
from organization.models import Team
1011
from organization.views import get_org
1112
1213
from .forms import ProjectForm, ProjectGroupForm, ProjectTeamAddForm, ProjectTeamEditForm
@@ -20,17 +21,20 @@
2021
2122
search = request.GET.get("search", "").strip()
2223
if search:
2324
projects = projects.filter(name__icontains=search)
2425
25
- paginator = Paginator(projects, 25)
26
+ per_page = get_per_page(request)
27
+ paginator = Paginator(projects, per_page)
2628
page_obj = paginator.get_page(request.GET.get("page", 1))
2729
30
+ ctx = {"projects": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
31
+
2832
if request.headers.get("HX-Request"):
29
- return render(request, "projects/partials/project_table.html", {"projects": page_obj, "page_obj": page_obj, "search": search})
33
+ return render(request, "projects/partials/project_table.html", ctx)
3034
31
- return render(request, "projects/project_list.html", {"projects": page_obj, "page_obj": page_obj, "search": search})
35
+ return render(request, "projects/project_list.html", ctx)
3236
3337
3438
@login_required
3539
def project_create(request):
3640
P.PROJECT_ADD.check(request.user)
@@ -258,17 +262,20 @@
258262
259263
search = request.GET.get("search", "").strip()
260264
if search:
261265
groups = groups.filter(name__icontains=search)
262266
263
- paginator = Paginator(groups, 25)
267
+ per_page = get_per_page(request)
268
+ paginator = Paginator(groups, per_page)
264269
page_obj = paginator.get_page(request.GET.get("page", 1))
265270
271
+ ctx = {"groups": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
272
+
266273
if request.headers.get("HX-Request"):
267
- return render(request, "projects/partials/group_table.html", {"groups": page_obj, "page_obj": page_obj, "search": search})
274
+ return render(request, "projects/partials/group_table.html", ctx)
268275
269
- return render(request, "projects/group_list.html", {"groups": page_obj, "page_obj": page_obj, "search": search})
276
+ return render(request, "projects/group_list.html", ctx)
270277
271278
272279
@login_required
273280
def group_create(request):
274281
P.PROJECT_GROUP_ADD.check(request.user)
275282
--- projects/views.py
+++ projects/views.py
@@ -3,10 +3,11 @@
3 from django.core.paginator import Paginator
4 from django.db.models import Count
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, redirect, render
7
 
8 from core.permissions import P
9 from organization.models import Team
10 from organization.views import get_org
11
12 from .forms import ProjectForm, ProjectGroupForm, ProjectTeamAddForm, ProjectTeamEditForm
@@ -20,17 +21,20 @@
20
21 search = request.GET.get("search", "").strip()
22 if search:
23 projects = projects.filter(name__icontains=search)
24
25 paginator = Paginator(projects, 25)
 
26 page_obj = paginator.get_page(request.GET.get("page", 1))
27
 
 
28 if request.headers.get("HX-Request"):
29 return render(request, "projects/partials/project_table.html", {"projects": page_obj, "page_obj": page_obj, "search": search})
30
31 return render(request, "projects/project_list.html", {"projects": page_obj, "page_obj": page_obj, "search": search})
32
33
34 @login_required
35 def project_create(request):
36 P.PROJECT_ADD.check(request.user)
@@ -258,17 +262,20 @@
258
259 search = request.GET.get("search", "").strip()
260 if search:
261 groups = groups.filter(name__icontains=search)
262
263 paginator = Paginator(groups, 25)
 
264 page_obj = paginator.get_page(request.GET.get("page", 1))
265
 
 
266 if request.headers.get("HX-Request"):
267 return render(request, "projects/partials/group_table.html", {"groups": page_obj, "page_obj": page_obj, "search": search})
268
269 return render(request, "projects/group_list.html", {"groups": page_obj, "page_obj": page_obj, "search": search})
270
271
272 @login_required
273 def group_create(request):
274 P.PROJECT_GROUP_ADD.check(request.user)
275
--- projects/views.py
+++ projects/views.py
@@ -3,10 +3,11 @@
3 from django.core.paginator import Paginator
4 from django.db.models import Count
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, redirect, render
7
8 from core.pagination import PER_PAGE_OPTIONS, get_per_page
9 from core.permissions import P
10 from organization.models import Team
11 from organization.views import get_org
12
13 from .forms import ProjectForm, ProjectGroupForm, ProjectTeamAddForm, ProjectTeamEditForm
@@ -20,17 +21,20 @@
21
22 search = request.GET.get("search", "").strip()
23 if search:
24 projects = projects.filter(name__icontains=search)
25
26 per_page = get_per_page(request)
27 paginator = Paginator(projects, per_page)
28 page_obj = paginator.get_page(request.GET.get("page", 1))
29
30 ctx = {"projects": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
31
32 if request.headers.get("HX-Request"):
33 return render(request, "projects/partials/project_table.html", ctx)
34
35 return render(request, "projects/project_list.html", ctx)
36
37
38 @login_required
39 def project_create(request):
40 P.PROJECT_ADD.check(request.user)
@@ -258,17 +262,20 @@
262
263 search = request.GET.get("search", "").strip()
264 if search:
265 groups = groups.filter(name__icontains=search)
266
267 per_page = get_per_page(request)
268 paginator = Paginator(groups, per_page)
269 page_obj = paginator.get_page(request.GET.get("page", 1))
270
271 ctx = {"groups": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
272
273 if request.headers.get("HX-Request"):
274 return render(request, "projects/partials/group_table.html", ctx)
275
276 return render(request, "projects/group_list.html", ctx)
277
278
279 @login_required
280 def group_create(request):
281 P.PROJECT_GROUP_ADD.check(request.user)
282
--- templates/includes/_pagination.html
+++ templates/includes/_pagination.html
@@ -1,18 +1,29 @@
1
-{% if page_obj.has_other_pages %}
1
+{% if page_obj.has_other_pages or per_page_options %}
22
<nav class="flex items-center justify-between border-t border-gray-700 px-2 py-3 mt-4">
3
- <div class="text-xs text-gray-500">
4
- Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
5
- ({{ page_obj.paginator.count }} total)
3
+ <div class="flex items-center gap-4">
4
+ <span class="text-xs text-gray-500">
5
+ Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
6
+ ({{ page_obj.paginator.count }} total)
7
+ </span>
8
+ {% if per_page_options %}
9
+ <div class="flex items-center gap-1 text-xs text-gray-500">
10
+ <span>Show</span>
11
+ {% for opt in per_page_options %}
12
+ <a href="?per_page={{ opt }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
13
+ class="px-2 py-0.5 rounded {% if per_page == opt %}bg-gray-700 text-gray-200{% else %}text-gray-500 hover:text-gray-300{% endif %}">{{ opt }}</a>
14
+ {% endfor %}
15
+ </div>
16
+ {% endif %}
617
</div>
718
<div class="flex gap-1">
819
{% if page_obj.has_previous %}
9
- <a href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
20
+ <a href="?page={{ page_obj.previous_page_number }}{% if per_page %}&per_page={{ per_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
1021
class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Previous</a>
1122
{% endif %}
1223
{% if page_obj.has_next %}
13
- <a href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
24
+ <a href="?page={{ page_obj.next_page_number }}{% if per_page %}&per_page={{ per_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
1425
class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Next</a>
1526
{% endif %}
1627
</div>
1728
</nav>
1829
{% endif %}
1930
--- templates/includes/_pagination.html
+++ templates/includes/_pagination.html
@@ -1,18 +1,29 @@
1 {% if page_obj.has_other_pages %}
2 <nav class="flex items-center justify-between border-t border-gray-700 px-2 py-3 mt-4">
3 <div class="text-xs text-gray-500">
4 Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
5 ({{ page_obj.paginator.count }} total)
 
 
 
 
 
 
 
 
 
 
 
6 </div>
7 <div class="flex gap-1">
8 {% if page_obj.has_previous %}
9 <a href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
10 class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Previous</a>
11 {% endif %}
12 {% if page_obj.has_next %}
13 <a href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
14 class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Next</a>
15 {% endif %}
16 </div>
17 </nav>
18 {% endif %}
19
--- templates/includes/_pagination.html
+++ templates/includes/_pagination.html
@@ -1,18 +1,29 @@
1 {% if page_obj.has_other_pages or per_page_options %}
2 <nav class="flex items-center justify-between border-t border-gray-700 px-2 py-3 mt-4">
3 <div class="flex items-center gap-4">
4 <span class="text-xs text-gray-500">
5 Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
6 ({{ page_obj.paginator.count }} total)
7 </span>
8 {% if per_page_options %}
9 <div class="flex items-center gap-1 text-xs text-gray-500">
10 <span>Show</span>
11 {% for opt in per_page_options %}
12 <a href="?per_page={{ opt }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
13 class="px-2 py-0.5 rounded {% if per_page == opt %}bg-gray-700 text-gray-200{% else %}text-gray-500 hover:text-gray-300{% endif %}">{{ opt }}</a>
14 {% endfor %}
15 </div>
16 {% endif %}
17 </div>
18 <div class="flex gap-1">
19 {% if page_obj.has_previous %}
20 <a href="?page={{ page_obj.previous_page_number }}{% if per_page %}&per_page={{ per_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
21 class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Previous</a>
22 {% endif %}
23 {% if page_obj.has_next %}
24 <a href="?page={{ page_obj.next_page_number }}{% if per_page %}&per_page={{ per_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
25 class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Next</a>
26 {% endif %}
27 </div>
28 </nav>
29 {% endif %}
30
--- templates/includes/_pagination_manual.html
+++ templates/includes/_pagination_manual.html
@@ -1,18 +1,29 @@
1
-{% if pagination.num_pages > 1 %}
1
+{% if pagination.num_pages > 1 or per_page_options %}
22
<nav class="flex items-center justify-between border-t border-gray-700 px-2 py-3 mt-4">
3
- <div class="text-xs text-gray-500">
4
- Page {{ pagination.number }} of {{ pagination.num_pages }}
5
- ({{ pagination.count }} total)
3
+ <div class="flex items-center gap-4">
4
+ <span class="text-xs text-gray-500">
5
+ Page {{ pagination.number }} of {{ pagination.num_pages }}
6
+ ({{ pagination.count }} total)
7
+ </span>
8
+ {% if per_page_options %}
9
+ <div class="flex items-center gap-1 text-xs text-gray-500">
10
+ <span>Show</span>
11
+ {% for opt in per_page_options %}
12
+ <a href="?per_page={{ opt }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
13
+ class="px-2 py-0.5 rounded {% if per_page == opt %}bg-gray-700 text-gray-200{% else %}text-gray-500 hover:text-gray-300{% endif %}">{{ opt }}</a>
14
+ {% endfor %}
15
+ </div>
16
+ {% endif %}
617
</div>
718
<div class="flex gap-1">
819
{% if pagination.has_previous %}
9
- <a href="?page={{ pagination.previous_page_number }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
20
+ <a href="?page={{ pagination.previous_page_number }}{% if per_page %}&per_page={{ per_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
1021
class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Previous</a>
1122
{% endif %}
1223
{% if pagination.has_next %}
13
- <a href="?page={{ pagination.next_page_number }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
24
+ <a href="?page={{ pagination.next_page_number }}{% if per_page %}&per_page={{ per_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
1425
class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Next</a>
1526
{% endif %}
1627
</div>
1728
</nav>
1829
{% endif %}
1930
--- templates/includes/_pagination_manual.html
+++ templates/includes/_pagination_manual.html
@@ -1,18 +1,29 @@
1 {% if pagination.num_pages > 1 %}
2 <nav class="flex items-center justify-between border-t border-gray-700 px-2 py-3 mt-4">
3 <div class="text-xs text-gray-500">
4 Page {{ pagination.number }} of {{ pagination.num_pages }}
5 ({{ pagination.count }} total)
 
 
 
 
 
 
 
 
 
 
 
6 </div>
7 <div class="flex gap-1">
8 {% if pagination.has_previous %}
9 <a href="?page={{ pagination.previous_page_number }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
10 class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Previous</a>
11 {% endif %}
12 {% if pagination.has_next %}
13 <a href="?page={{ pagination.next_page_number }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
14 class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Next</a>
15 {% endif %}
16 </div>
17 </nav>
18 {% endif %}
19
--- templates/includes/_pagination_manual.html
+++ templates/includes/_pagination_manual.html
@@ -1,18 +1,29 @@
1 {% if pagination.num_pages > 1 or per_page_options %}
2 <nav class="flex items-center justify-between border-t border-gray-700 px-2 py-3 mt-4">
3 <div class="flex items-center gap-4">
4 <span class="text-xs text-gray-500">
5 Page {{ pagination.number }} of {{ pagination.num_pages }}
6 ({{ pagination.count }} total)
7 </span>
8 {% if per_page_options %}
9 <div class="flex items-center gap-1 text-xs text-gray-500">
10 <span>Show</span>
11 {% for opt in per_page_options %}
12 <a href="?per_page={{ opt }}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
13 class="px-2 py-0.5 rounded {% if per_page == opt %}bg-gray-700 text-gray-200{% else %}text-gray-500 hover:text-gray-300{% endif %}">{{ opt }}</a>
14 {% endfor %}
15 </div>
16 {% endif %}
17 </div>
18 <div class="flex gap-1">
19 {% if pagination.has_previous %}
20 <a href="?page={{ pagination.previous_page_number }}{% if per_page %}&per_page={{ per_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
21 class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Previous</a>
22 {% endif %}
23 {% if pagination.has_next %}
24 <a href="?page={{ pagination.next_page_number }}{% if per_page %}&per_page={{ per_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
25 class="px-3 py-1 text-xs rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700">Next</a>
26 {% endif %}
27 </div>
28 </nav>
29 {% endif %}
30

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button