Python 现代开发学习笔记

(十八)筛选、分页与同一份 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 }}&amp;finished={{ finished }}&amp;page={{ page.previous_page_number }}">上一页</a>{% endif %}
  <span>第 {{ page.number }} 页,共 {{ page.paginator.num_pages }} 页</span>
  {% if page.has_next %}<a href="?q={{ q|urlencode }}&amp;finished={{ finished }}&amp;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,已登录用户得到自己的公开字段。这里没有另建一套存储或权限规则。

四本未读先统计总数,再按主键排序切为三条加一条,模板生成保留原筛选条件的下一页链接,JSON 仍读取相同用户记录

笔记正文与全部题解 · Ctrl / ⌘ K