(十八)筛选、分页与同一份 JSON
# 续写同一文件:reading_project/src/library/views.py
@login_required
def index(request):
# 归属条件先加入查询,再在这份结果上筛选并分页。
q = request.GET.get("q", "").strip()
finished = request.GET.get("finished", "all")
if len(q) > 100 or finished not in {"all", "0", "1"}:
raise BadRequest("请检查筛选字段")
books = Book.objects.filter(owner=request.user).prefetch_related("tags")
if q:
books = books.filter(title__contains=q)
if finished != "all":
books = books.filter(finished=finished == "1")
page = Paginator(books, 3).get_page(request.GET.get("page"))
return render(
request, "library/index.html", {"page": page, "q": q, "finished": finished}
)
@login_required
def detail(request, pk):
return render(
request, "library/detail.html", {"book": owned_book(request.user, pk)}
)
def api_books(request):
if not request.user.is_authenticated:
return JsonResponse({"error": "请先登录"}, status=401)
books = Book.objects.filter(owner=request.user).prefetch_related("tags")
return JsonResponse({"books": [public_book(book) for book in books]})title__contains 是 Django 的字段查询写法,框架把输入作为 SQL 参数,并处理对应 LIKE 含义;多个 filter 继续累加条件。归属条件在整个查询中保留。Paginator(books, 3) 每页三本,get_page 取得当前页对象。
alice 的六本书中四本未读,筛选后编号依次为 1、3、4、6。第一页读前三条,第二页读编号 6,总数仍为四。page 不是整个结果列表,page.paginator.count 是筛选总数。
# 项目文件:demos/pagination_steps.py
# 先筛出未读记录并固定排序,再按每页三本切片,逐页观察内容和页码。
from lab import lab
with lab():
from django.contrib.auth import get_user_model
from django.core.paginator import Paginator
from library.models import Book
alice = get_user_model().objects.get(username="alice")
unread = Book.objects.filter(owner=alice, finished=False)
pages = Paginator(unread, 3)
print("未读总数", pages.count, "页数", pages.num_pages)
for number in [1, 2]:
page = pages.get_page(number)
print("第", number, "页编号", [book.pk for book in page])<!-- 项目文件:reading_project/src/library/templates/library/index.html -->
{% extends 'base.html' %}
{% block title %}我的书架{% endblock %}
{% block content %}
<div class="section-heading"><div><p class="eyebrow">MY READING RECORDS</p><h1>我的书架</h1></div>
<a class="button" href="{% url 'library:create' %}">添加书籍</a></div>
<div class="metrics"><span>筛选结果 <strong>{{ page.paginator.count }}</strong></span>
<span>当前页 <strong>{{ page.number }} / {{ page.paginator.num_pages }}</strong></span>
{% if perms.library.export_book %}<a class="button secondary" href="{% url 'library:export' %}">导出我的书架</a>{% endif %}</div>
<form class="filters panel" method="get">
<div><label for="q">按书名查找</label><input id="q" name="q" value="{{ q }}" maxlength="100"></div>
<div><label for="finished">阅读状态</label><select id="finished" name="finished">
<option value="all" {% if finished == 'all' %}selected{% endif %}>全部</option>
<option value="0" {% if finished == '0' %}selected{% endif %}>未读</option>
<option value="1" {% if finished == '1' %}selected{% endif %}>已读</option>
</select></div><button type="submit">筛选</button><a href="{% url 'library:index' %}">重置</a>
</form>
<section class="book-grid" aria-label="书籍列表">
{% for book in page %}<article class="panel book" data-book-id="{{ book.pk }}">
<p class="status">{{ book.finished|yesno:'已读,未读' }}</p>
<h2><a href="{% url 'library:detail' book.pk %}">{{ book.title }}</a></h2>
<p class="muted">{% for tag in book.tags.all %}{{ tag.name }}{% if not forloop.last %} · {% endif %}{% empty %}未设置标签{% endfor %}</p>
<a href="{% url 'library:edit' book.pk %}">修改阅读记录</a>
</article>{% empty %}<div class="panel"><h2>这里还没有书</h2><p>可以调整筛选,或添加一本书。</p></div>{% endfor %}
</section>
<nav class="pagination" aria-label="分页">
{% if page.has_previous %}<a href="?q={{ q|urlencode }}&finished={{ finished }}&page={{ page.previous_page_number }}">上一页</a>{% endif %}
<span>第 {{ page.number }} 页,共 {{ page.paginator.num_pages }} 页</span>
{% if page.has_next %}<a href="?q={{ q|urlencode }}&finished={{ finished }}&page={{ page.next_page_number }}">下一页</a>{% endif %}
</nav><p class="muted"><a href="{% url 'library:api' %}">查看同一书架的 JSON</a></p>
{% endblock %}分页链接保留关键词和状态,只改页码;urlencode 处理关键词中的空格等字符。get_page 对非数字页码回到第一页,对超出结果的页码回到最后一页,空书架也有可显示的空结果页。
api_books 复用相同归属与预取关系,匿名调用返回 JSON 401,已登录用户得到自己的公开字段。这里没有另建一套存储或权限规则。