创建项目

安装依赖包:

pip install Django
python -m django --version

创建项目:

django-admin startproject mysite

得到:

mysite/  --项目目录,名字可以随意起
    manage.py  --用于与项目交互的命令行脚本
    mysite/  --项目的Python包,后面需要引入的话需要写成类似 import mysite.urls
        __init__.py  --标记该目录为一个Python包
        settings.py  --项目配置文件
        urls.py  --项目url说明
        asgi.py  --兼容asgi web服务器的入口
        wsgi.py  --兼容wsgi web服务器的入口

运行网站:

cd mysite
python manage.py runserver

访问http://127.0.0.1:8000/能看到:
在这里插入图片描述
自定义端口:

python manage.py runserver 8080

自定义IP(0是0.0.0.0的简写):

python manage.py runserver 0:8000

现在用的是Django自带的测试环境服务器,不建议用于生产环境。

创建app

项目= app + 配置。一个项目能包含多个app,一个app可属于多个项目。

在当前项目下创建一个新的app:

python manage.py startapp polls

得到如下目录结构:

polls/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py

编辑views.py,添加:

from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello Django!")

新建urls.py,添加:

from django.urls import path
from . import views


urlpatterns = [
    path('', views.index, name='index')
]

编辑mysite\mysite\urls.py,添加:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('polls/', include('polls.urls')),  #引用其它URLconf文件
]

include能引用其它URLconf文件,当Django遇到这个include时,会将url中匹配该入口点的部分’polls/'截掉,剩下的部分发给polls.url进一步处理。

现在重新启动服务器,访问http://127.0.0.1:8000/polls/,即可看到:

Hello Django!

urls.py中的path函数很关键:

path('', views.index, name='index')
# views.index
def index(request):
    return HttpResponse("Hello Django!")

path()函数有4个参数:

  1. route,url 模式
  2. view,当找到匹配的route时,调用对应的view函数,返回的是一个HttpRequest对象
  3. kwargs (可选)
  4. name (可选)

修改settings.py

配置数据库

mysite\settings.py中有数据库配置选项:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

默认用python自带的sqlite3。

ENGINE可以替换为:
‘django.db.backends.sqlite3’,
‘django.db.backends.postgresql’,
‘django.db.backends.mysql’,
‘django.db.backends.oracle’

NAME为数据库名,默认在项目根目录(BASE_DIR)存储sqlite数据库文件:

在这里插入图片描述

配置语言和时区

setting.py配置语言和时区为中国:

# Chinese Shanghai
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
# db does not use utc time
USE_TZ = False

配置INSTALLED_APPS

Django的app是“可插拔的”,INSTALLED_APPS指定了项目中所有活动的app名称(app可被用于多个项目),默认有:

INSTALLED_APPS = [
    'django.contrib.admin',  # 管理页面
    'django.contrib.auth',  # 认证系统
    'django.contrib.contenttypes',  # 内容类型框架
    'django.contrib.sessions',  # session框架
    'django.contrib.messages',  # 消息框架
    'django.contrib.staticfiles',  # 静态文件管理框架
]

这些app可能会用到数据库中的一个或多个表,所以在使用这些app前要创建数据库:

python manage.py migrate

migrate指令会检查所有活动app,并创建它们所需的数据库表。上述指令创建出的表如下:
在这里插入图片描述
如果不需要某些app,可以在执行migrate命令前注释掉。

创建Model

在polls/models.py中添加:

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')  # human-readable name


class Choice(models.Model):
    questions = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

接着要在settings.py中添加poll app。

polls/app.py默认为:

from django.apps import AppConfig


class PollsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'polls'

只需把PollsConfig包含到INSTALLED_APPS即可:

INSTALLED_APPS = [
    'polls.apps.PollsConfig',  # 添加polls
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

现在可以执行:

 python manage.py makemigrations polls

显示:

Migrations for 'polls':
  polls/migrations/0001_initial.py
    - Create model Question
    - Create model Choice

makemigrations命令告诉Django models有变化,需要将变化存储为migration(migration是Django用来存储models变化的文件)。

然后运行命令:

python manage.py sqlmigrate polls 0001

能看到针对项目数据库类型自动生成的sql语句:

BEGIN;
--
-- Create model Question
--
CREATE TABLE "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL, "pub_date" datetime NOT NULL);
--
-- Create model Choice
--
CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL, "questions_id" bigint NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "polls_choice_questions_id_d356887e" ON "polls_choice" ("questions_id");
COMMIT;

注意:

  • 表名默认为 app名_模型名
  • 主键会自动生成(可以覆写)
  • 外键名为’关联表名_id’ (也可以覆写)
  • 外键关系会被 FOREIGN KEY 约束显式指出

sqlmigrate指令只是打印出建表语句,并没真正执行这些sql。

python manage.py check

现在可以再次执行migrate指令了:

python manage.py migrate

它会为上述models在database中建表。现在数据库中的表如下:
在这里插入图片描述

django_migrations表中记录了所有未应用的migration,migrate指令会执行这些migration。

至此,总结一下修改model的步骤:

python manage.py makemigrationspython manage.py migrate

专门记录migration,可以便于在版本控制系统中管理,也能让其它开发者使用。

玩转API

在python shell中可以使用Django提供的各种api。

打开python shell:

python manage.py shell
python

在shell中可以尝试database api:

>>> from polls.models import Choice, Question  # Import the model classes we just wrote.

# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>

# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# Save the object into the database. You have to call save() explicitly.
>>> q.save()

# Now it has an ID.
>>> q.id
1

# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>
__str__()
class Question(models.Model):
    # ...
    def __str__(self):
        return self.question_text

class Choice(models.Model):
    # ...
    def __str__(self):
        return self.choice_text

然后再在polls/models.py中添加一个方法:

import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

注意,这里分别引入了标准库的datetime和Django中的时间工具库:

import datetime
from django.utils import timezone
python manage.py shell
>>> from polls.models import Choice, Question

# Make sure our __str__() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>

# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.

# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>

# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. Django creates
# a set to hold the "other side" of a ForeignKey relation
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)

# Display any choices from the related object set -- none so far.
>>> q.choice_set.all()
<QuerySet []>

# Create three choices.
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)

# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>

# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3

# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()

Django Admin

创建超级用户:

python manage.py createsuperuser

用户名 (leave blank to use 'jiaqi'): admin
电子邮件地址: admin@126.com
Password: 
Password (again): 
python manage.py runserverdjango.contrib.auth
polls/admin.py
from django.contrib import admin

from .models import Question

admin.site.register(Question)

保存代码,刷新页面:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
太方便了。

创建更多的view

view是提供特定功能的一种网页,并有特定的模板。

polls/views.py
def detail(request, question_id):
    return HttpResponse("Question %s." % question_id)
    
def results(request, question_id):
    response = "Result of question %s."
    return HttpResponse(response % question_id)
    
def vote(request, question_id):
    response = "Voting question %s."
    return HttpResponse(response % question_id)
polls/urls.py
urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
Voting question 1.
settings.pyROOT_URLCONF = 'mysite.urls'mysite/mysite/urls.pyurlpatterns

使用模板

在polls目录下创建文件夹:templates。

settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,  # BACKEND是DjangoTemplates,且该选项为True。DjangoTemplates会自动查找所有INSTALLED_APP中的templates子目录
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

在templates文件夹下再建立一个polls文件夹,然后创建一个index.html:
在这里插入图片描述
这里涉及到了模板的命名空间问题。之所以不把index.html直接放到templates目录,是因为Django总会选择第一个名字匹配的模板,如果不同的app间有命名相同的模板,Djang就无法区别。所以最好的方法是为模板创建命名空间, 即在templates目录下再创建一层与app同名的目录。

index.htm中写入以下内容:

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

修改views.py,使用模板:

from django.template import loader

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))

保存代码,刷新网页:
在这里插入图片描述
上述代码可以简写为:

	from django.shortcuts import render


    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {
        'latest_question_list' : latest_question_list,
    }
    return render(request, 'polls/index.html', context)

再写一个detail.html模板:

{{ question }}

重写detail的view:

from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

如果找不到就触发404异常。

上述代码可简写为:

from django.shortcuts import get_object_or_404, render


def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})
get_list_or_404()

修改detail.html:

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

效果:
在这里插入图片描述
为了去除index.html中url的硬编码:

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

可以将其修改为:

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

上面的detail表示polls.urls.py中定义的url name为detail的path:

path('<int:question_id>/', views.detail, name='detail'),

这样,当polls.urls.py中的url定义改变时,只要name不变,template中就无需改变:

...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...

这就实现了前后端关于url名称的解耦。

为了区分不同app之间相同命名的path,需要指定app_name作为url名称的命名空间:

app_name = 'polls'  # 指定url命名空间
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

在模板中使用带命名空间的url:

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

使用表单

修改detail.html:

<form action="{% url 'polls:vote' question.id %}" method="post">
<!--    防止post方法可能产生的csrf攻击,所有指向内部url的post form都应该加上这句 -->
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
<!--  forloop.counter:当前循环计数,这里分别是 1、2  -->
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

legend 元素为 fieldset 元素定义标题,
效果:
在这里插入图片描述
生成的html代码:
在这里插入图片描述

它的action是’polls:vote’,所以接下来重写vote的view:

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

使用Form获取用户数据时,有以下惯用做法(不单是Django架构):

  1. 当会修改服务端数据时,用post方法;
  2. 处理完post请求后,返回一个HttpResponseRedirect,以防用户在刷新网页或点击返回按钮后,表单被重复提交;

处理完post请求,会跳转到results页面,下面添加results.html模板:

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

对应html:
在这里插入图片描述

投票完成后,效果如下:
在这里插入图片描述
注意这里的投票逻辑可能会存在竞争问题,即多个用户同时投票,结果可能会不正确。如何解决这一问题在这里暂不考虑。

generic views

上述view其实都做了类似的事:

  1. 根据url中的参数,从数据库取数据;
  2. 载入模板并用返回结果渲染;

这时可以用Django提供的generic views。

将views.py修改为:

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    ... # same as above, no changes needed.

然后将urls.py修改为:

from django.urls import path
from . import views

app_name = 'polls'  # url命名空间
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
views.DetailView.as_view()

自动测试

在大型项目中,代码的一些改动可能牵一发动全身。写自动化测试的好处:

  • 节省时间
  • 避免错误(而非确定错误)
  • 有利于团队合作(“Code without tests is broken by design.”——Jacob Kaplan-Moss, Django’s创造人之一)

TDD(Test-driven development)是指先写测试用例,再写代码的开发模式。

polls/tests.py
import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question


class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

在控制台执行测试:

python manage.py test polls

该指令进行了以下操作:

manage.py test pollsdjango.test.TestCasetest_was_published_recently_with_future_questionassertIs()was_published_recently()True

打印结果如下:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
was_published_recently() returns False for questions whose pub_date
----------------------------------------------------------------------
Traceback (most recent call last):
  File "E:\workspace\django_mysite\polls\tests.py", line 21, in test_was_published_recently_with_future_questi
on
    self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)
Destroying test database for alias 'default'...

polls/models.py
  def was_published_recently(self):
        return timezone.now() >= self.pub_date >= timezone.now() - datetime.timedelta(days=1)

再次运行test,打印:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Destroying test database for alias 'default'...

tests.py越写越多怎么办?无所谓,越多越好,有时产生冗余也不是坏事。

有一些准则来让测试用例不那么乱:

  1. 每个model或view都单独写一个TestClass
  2. 每个测试场景都写个单独的test方法
  3. test方法名称就是测试的具体描述

关于测试,详见:Testing in Django

静态文件

和模板类似,Django的STATICFILES_FINDERS也会自动扫描所有INSTALLED_APPS中叫static的子文件夹寻找静态文件,所以最好也用与app同名的命名空间区分:
在这里插入图片描述
新建polls/static/polls/style.css:

li a {
    color: green;
}
 {% load static %}
 <link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">
 

重启server,刷新网页(注意这里需要重启server才能生效),效果如下:
在这里插入图片描述
加入背景图片:
在这里插入图片描述

修改style.css如下:

li a {
    color: blue;
}

body {
   background-image: url("images/morty.jpg");
   background-size: cover;
   background-repeat: no-repeat;
   background-attachment: fixed;
}

效果如下:
在这里插入图片描述

重写admin模板

管理显示内容

修改polls/admin.py如下:

class QuestionAdmin(admin.ModelAdmin):
    fields = ['pub_date', 'question_text']

admin.site.register(Question, QuestionAdmin)
fieldsets
class QuestionAdmin(admin.ModelAdmin):
    # fields = ['pub_date', 'question_text']
    fieldsets = [
        ('Text',               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date']}),
    ]

在这里插入图片描述
将关联表choice整合到question界面:

class ChoiceInline(admin.StackedInline):
    model = Choice  # Choice objects are edited on the Question admin page
    extra = 3  # provide enough fields for 3 choices


class QuestionAdmin(admin.ModelAdmin):
    # fields = ['pub_date', 'question_text']
    fieldsets = [
        ('Text',               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date'],
        'classes' : ['collapse']}),  # 可折叠
    ]
    inlines = [ChoiceInline]

效果:
在这里插入图片描述
可以换成Tabular的样式嵌入:

class ChoiceInline(admin.TabularInline):
    model = Choice  # Choice objects are edited on the Question admin page
    extra = 3  # provide enough fields for 3 choices

在这里插入图片描述

在展示所有Question的列表页面,也可以增加显示字段:

    # fields = ['pub_date', 'question_text']
    list_display = ('question_text', 'pub_date', 'was_published_recently')  # 调用was_published_recently()作为新字段值
    fieldsets = [
        ('Text', {'fields': ['question_text']}),
        ('Date information', {
            'fields': ['pub_date'],
            'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline]

在这里插入图片描述
前两个字段是可以点击排序的,自定义字段则不支持。而且自定义字段的名称默认也是函数名。

为了让这个方法支持排序,且改下名字,可以在models.py里给它加个装饰器:

from django.contrib import admin

    @admin.display(
        boolean=True,
        ordering='pub_date',
        description='Published recently?',
    )
    def was_published_recently(self):
        return timezone.now() >= self.pub_date >= timezone.now() - datetime.timedelta(days=1)

效果如下:
在这里插入图片描述

接下来在QuestionAdmin里加一个过滤器:

list_filter = ['pub_date']

界面右侧多了个日期过滤器:
在这里插入图片描述
还可以增加搜索器:

search_fields = ['question_text']
LIKE

在这里插入图片描述
列表管理页面还默认提供了100条目/页的分区功能。

太方便了!

更改admin样式

运行下面指令找到Django源码目录:

python -c "import django; print(django.__path__)"

路径为:

/Users/xxx/miniconda3/envs/django_env/lib/python3.8/site-packages/django'
django/contrib/admin/templates/admin/base_site.htmlmysite/templates/admin/
{{ site_header|default:_('Django administration') }}
<h1 id="site-name"><a href="{% url 'admin:index' %}"> POLLS管理页 </a></h1>
settings.pyTEMPLATESDIRS
'DIRS': [BASE_DIR / 'templates'],
django.contrib.admin.AdminSite.site_header
app_list

至此,新手教程结束!