创作后台开启,请开始你的表演

2019-08-1427784 阅读39 评论

在此之前我们完成了 django 博客首页视图的编写,我们希望首页展示发布的博客文章列表,但是它却抱怨:暂时还没有发布的文章!如它所言,我们确实还没有发布任何文章,本节我们将使用 django 自带的 admin 后台来发布我们的博客文章。

创建 admin 后台管理员账户

要想进入django admin 后台,首先需要创建一个超级管理员账户。我们在 Django 迁移、操作数据库 中已经创建了一个后台账户,但如果你没有按照前面的步骤创建账户的话,可以进入项目根目录,运行 pipenv run python manage.py createsuperuser 命令新建一个:

> pipenv run python manage.py createsuperuser

用户名 (leave blank to use 'yangxg'): admin
电子邮件地址: admin@example.com
Password:
Password (again):
Superuser created successfully.

注意:

在命令行输入密码时可能不会显示输入的字符,不要以为键盘坏了,照正常的方式输入密码即可。

在 admin 后台注册模型

要在后台注册我们自己创建的几个模型,这样 django admin 才能知道它们的存在,注册非常简单,只需要在 blog\admin.py 中加入下面的代码:

blog/admin.py

from django.contrib import admin
from .models import Post, Category, Tag

admin.site.register(Post)
admin.site.register(Category)
admin.site.register(Tag)

运行开发服务器,访问 http://127.0.0.1:8000/admin/ ,就进入了到了django admin 后台登录页面,输入刚才创建的管理员账户密码就可以登录到后台了。

django admin 后台

可以看到我们刚才注册的三个模型了,点击 Posts 后面的增加按钮,将进入添加 Post 的页面,也就是新增博客文章。然后在相关的地方输入一些测试用的内容,增加完后点击保存,这样文章就添加完毕了,你也可以多添加几篇看看效果。注意每篇文章必须有一个分类,在添加文章时你可以选择已有分类。如果数据库中还没有分类,在选择分类时点击 Category 后面的 + 按钮新增一个分类即可。

django admin 后台新增文章

你可能想往文章内容中添加图片,但目前来说还做不到。在支持 Markdown 语法部分中将介绍如何在文章中插入图片的方法。

访问 http://127.0.0.1:8000/ 首页,你就可以看到你添加的文章列表了,下面是我所在环境的效果图:

博客首页显示的文章列表

定制 admin 后台

使用 admin 后台的时候,我们发现了下面的一些体验相关的问题:

  • admin 后台本身的页面元素是已经汉化了的,但是我们自己的 blog 应用,以及 Post、Category、Tag 在页面中显示却是英文的,以及发布文章的时候,表单各字段的 label 也是英文的。
  • 在 admin 后台的 post 列表页面,我们只看到了文章的标题,但是我们希望它显示更加详细的信息,例如作者、发布时间、修改时间等。
  • 新增文章时,所有数据都要自己手动填写。但是,有些数据应该是自动生成。例如文章发布时间 Created time 和修改时间 Modified time,应该在创建或者修改文章时自动生成,而不是手动控制。同时我们的博客是单人博客系统,发布者肯定是文章作者,这个也应该自动设定为 admin 后台的登录账户。

虽然 django 的 admin 应用开箱即用,但也提供了丰富的定制功能,这正是 django 吸引人的地方,下面我们根据需求来一个个定制。

汉化 blog 应用

首先来看一下需要汉化的地方,admin 首页每个版块代表一个 app,比如 BLOG 版块表示 blog 应用,版块标题默认显示的就是应用名。应用版块下包含了该应用全部已经注册到 admin 后台的 model,之前我们注册了 Post、Category 和 Tag,所以显示的是这三个 model,显示的名字就是 model 的名字。如下图所示:

BLOG 应用版块

其次是新增 post 页面的表单,各个字段的 label 由定义在 Post 类的 Field 名转换而来,比如 Post 模型中定义了 title 字段,则对应表单的 label 就是 Title。

首先是 BLOG 版块的标题 BLOG,一个版块代表一个应用,显然这个标题使用应用名转换而来,在 blog 应用下有一个 app.py 模块,其代码如下:

from django.apps import AppConfig


class BlogConfig(AppConfig):
    name = 'blog'

这些是我们在运行 startapp 创建 blog 应用时自动生成的代码,可以看到有一个 BlogConfig 类,其继承自 AppConfig 类,看名字就知道这是和应用配置有关的类。我们可以通过设置这个类中的一些属性的值来配置这个应用的一些特性的。比如这里的 name 是用来定义 app 的名字,需要和应用名保持一致,不要改。要修改 app 在 admin 后台的显示名字,添加 verbose_name 属性。

class BlogConfig(AppConfig):
    name = 'blog'
    verbose_name = '博客'

同时,我们此前在 settings 中注册应用时,是直接注册的 app 名字 blog,现在在 BlogConfig 类中对 app 做了一些配置,所以应该将这个类注册进去:

INSTALLED_APPS = [
    'django.contrib.admin',
    ...

    'blog.apps.BlogConfig',  # 注册 blog 应用
]

再次登录后台,就可以看到 BLOG 版块的标题已经显示为博客了。

接下来是让应用下注册的 model 显示为中文,既然应用是在 apps.py 中配置,那么和 model 有关的配置应该去找相对应的 model 。配置 model 的一些特性是通过 model 的内部类 Meta 中来定义。比如对于 Post 模型,要让他在 admin 后台显示为中文,如下:

class Post(models.Model):
    ...
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    class Meta:
        verbose_name = '文章'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.title

同样地,这里通过 verbose_name 来指定对应的 model 在 admin 后台的显示名称,这里 verbose_name_plural 用来表示多篇文章时的复数显示形式。英语中,如果有多篇文章,就会显示为 Posts,表示复数,中文没有复数表现形式,所以定义为和 verbose_name一样。

同样的可以把 Tag 和 Category 也设置一下:

class Category(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        verbose_name = '分类'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class Tag(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        verbose_name = '标签'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name

在 admin 就可以看到汉化后的效果了。

然后就是修改 post 的表单的 label,label 由定义在 model 中的 Field 名转换二来,所以在 Field 中修改。

class Post(models.Model):
    title = models.CharField('标题', max_length=70)
    body = models.TextField('正文')
    created_time = models.DateTimeField('创建时间')
    modified_time = models.DateTimeField('修改时间')
    excerpt = models.CharField('摘要', max_length=200, blank=True)
    category = models.ForeignKey(Category, verbose_name='分类', on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag, verbose_name='标签', blank=True)
    author = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)

可以看到我们给每个 Field 都传入了一个位置参数,参数值即为 field 应该显示的名字(如果不传,django 自动根据 field 名生成)。这个参数的名字也叫 verbose_name,绝大部分 field 这个参数都位于第一个位置,但由于 ForeignKeyManyToManyField 第一个参数必须传入其关联的 Model,所以 category、tags 这些字段我们使用了关键字参数 verbose_name

文章列表显示更加详细的信息

在 admin 后台的文章列表页面,我们只看到了文章的标题,但是我们希望它显示更加详细的信息,这需要我们来定制 admin 了,在 admin.py 添加如下代码:

blog/admin.py

from django.contrib import admin
from .models import Post, Category, Tag

class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'created_time', 'modified_time', 'category', 'author']

# 把新增的 Postadmin 也注册进来
admin.site.register(Post, PostAdmin)
admin.site.register(Category)
admin.site.register(Tag)

刷新 admin Post 列表页面,可以看到显示的效果好多了。

定制后的 admin 文章列表页

简化新增文章的表单

接下来优化新增文章时,填写表单数据的不合理的地方。文章的创建时间和修改时间应该根据当前时间自动生成,而现在是由人工填写,还有就是文章的作者应该自动填充为后台管理员用户,那么这些自动填充数据的字段就不需要在新增文章的表单中出现了。

此前我们在 blog/admin.py 中定义了一个 PostAdmin 来配置 Post 在 admin 后台的一些展现形式。list_display 属性控制 Post 列表页展示的字段。此外还有一个 fields 属性,则用来控制表单展现的字段,正好符合我们的需求:

class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'created_time', 'modified_time', 'category', 'author']
    fields = ['title', 'body', 'excerpt', 'category', 'tags']

这里 fields 中定义的字段就是表单中展现的字段。

接下来是填充创建时间,修改时间和文章作者的值。之前提到,文章作者应该自动设定为登录后台发布此文章的管理员用户。发布文章的过程实际上是一个 HTTP 请求过程,此前提到,django 将 HTTP 请求封装在 HttpRequest 对象中,然后将其作为第一个参数传给视图函数(这里我们没有看到新增文章的视图,因为 django admin 已经自动帮我们生成了),而如果用户登录了我们的站点,那么 django 就会将这个用户实例绑定到 request.user 属性上,我们可以通过 request.user 取到当前请求用户,然后将其关联到新创建的文章即可。

Postadmin 继承自 ModelAdmin,它有一个 save_model 方法,这个方法只有一行代码:obj.save()。它的作用就是将此 Modeladmin 关联注册的 model 实例(这里 Modeladmin 关联注册的是 Post)保存到数据库。这个方法接收四个参数,其中前两个,一个是 request,即此次的 HTTP 请求对象,第二个是 obj,即此次创建的关联对象的实例,于是通过复写此方法,就可以将 request.user 关联到创建的 Post 实例上,然后将 Post 数据再保存到数据库:

class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'created_time', 'modified_time', 'category', 'author']
    fields = ['title', 'body', 'excerpt', 'category', 'tags']

    def save_model(self, request, obj, form, change):
        obj.author = request.user
        super().save_model(request, obj, form, change)

最后还剩下文章的创建时间和修改时间需要填充,一个想法我们可以沿用上面的思路,复写 save_model 方法,将创建的 post 对象关联当前时间,但是这存在一个问题,就是这样做的话只有通过 admin 后台创建的文章才能自动关联这些时间,但创建文章不一定是在 Admin,也可能通过命令行。这时候我们可以通过对 Post 模型的定制来达到目的。

首先,Model 中定义的每个 Field 都接收一个 default 关键字参数,这个参数的含义是,如果将 model 的实例保存到数据库时,对应的 Field 没有设置值,那么 django 会取这个 default 指定的默认值,将其保存到数据库。因此,对于文章创建时间这个字段,初始没有指定值时,默认应该指定为当前时间,所以刚好可以通过 default 关键字参数指定:

from django.utils import timezone

class Post(models.Model):
    ...
    created_time = models.DateTimeField('创建时间', default=timezone.now)
    ...

这里 default 既可以指定为一个常量值,也可以指定为一个可调用(callable)对象,我们指定 timezone.now 函数,这样如果没有指定 created_time 的值,django 就会将其指定为 timezone.now 函数调用后的值。timezone.now 是 django 提供的工具函数,返回当前时间。因为 timezone 模块中的函数会自动帮我们处理时区,所以我们使用的是 django 为我们提供的 timezone 模块,而不是 Python 提供的 datetime 模块来处理时间。

那么修改时间 modified_time 可以用 default 吗?答案是不能,因为虽然第一次保存数据时,会根据默认值指定为当前时间,但是当模型数据第二次修改时,由于 modified_time 已经有值,即第一次的默认值,那么第二次保存时默认值就不会起作用了,如果我们不修改 modified_time 的值的话,其值永远是第一次保存数据库时的默认值。

所以这里问题的关键是每次保存模型时,都应该修改 modified_time 的值。每一个 Model 都有一个 save 方法,这个方法包含了将 model 数据保存到数据库中的逻辑。通过覆写这个方法,在 model 被 save 到数据库前指定 modified_time 的值为当前时间不就可以了?代码如下:

from django.utils import timezone

class Post(models.Model):
    ...

    def save(self, *args, **kwargs):
        self.modified_time = timezone.now()
        super().save(*args, **kwargs)

要注意在指定完 modified_time 的值后,别忘了调用父类的 save 以执行数据保存回数据库的逻辑。

-- EOF --

39 评论
登录后回复
Feko
2022-07-26 18:02:01

通过+1

回复
yuemeng404
2021-07-25 20:11:11

您好,请问这里的重写的save_model()save()有什么区别吗?分别在什么情境下自动调用?把文章修改时间也在save_model里进行赋值是否可以?谢谢!

回复
追着乌龟走
2021-07-14 15:46:50

求问博主,按照教程在admin后台点击增加文章的时候报错,看错误是url的问题。

AttributeError at /admin/blog/post/add/
'NoneType' object has no attribute 'attname'
Request Method: GET
Request URL: http://127.0.0.1:8000/admin/blog/post/add/
Django Version: 3.2.3
Exception Type: AttributeError
Exception Value:
'NoneType' object has no attribute 'attname'
Exception Location: F:\software\python\lib\site-packages\django\db\models\query_utils.py, line 158, in _check_parent_chain
Python Executable: F:\software\python\python.exe
Python Version: 3.9.1
Python Path:
['E:\hellodjango-blog-tutorial\blogproject',
'F:\software\python\python39.zip',
'F:\software\python\DLLs',
'F:\software\python\lib',
'F:\software\python',
'F:\software\python\lib\site-packages',
'F:\software\python\lib\site-packages\pip-21.0-py3.9.egg',
'F:\software\python\lib\site-packages\win32',
'F:\software\python\lib\site-packages\win32\lib',
'F:\software\python\lib\site-packages\Pythonwin']
Server time: Wed, 14 Jul 2021 15:46:26 +0800

回复
追着乌龟走 追着乌龟走
2021-07-14 16:17:06

已解决,模型文件,方法名的问题。

回复
net探索者
2020-07-30 14:10:52

我认为下列代码太繁琐,多此一举。
def save(self, args, **kwargs):
self.modified_time = timezone.now()
super().save(
args, **kwargs)
因为在之前的代码中,加上一句就可以了,没有必要再多写这么多。
def save_model(self, request, obj, form, change):
obj.author = request.user
obj.modified_time = timezone.now()
super().save_model(request, obj, form, change)

回复
白露海风
2020-07-24 18:00:42

博主好,请问覆写save()方法,函数变量为啥要写成args和*kwargs。我知道这两个是可变参数,一个可以将参数打包成元组传递,一个打包成dict传递。难道是save方法保存Post实例时,需要用到dict和元组吗?小白不是很懂。。。

回复
追梦人物 白露海风
2020-07-25 20:20:11

args 和 kwargs 只是约定俗称的命名,父类 save 方法会接收额外参数,因此子类重新定义 save 方法时使用 args 和 kwargs 接收任意参数已获得最大的兼容性。因为 python 是没有函数重载的。

回复
白露海风 追梦人物
2020-07-27 09:31:22

谢谢博主解答

回复
Ifr4me
2020-07-09 18:55:16

    modified_time = models.DateTimeField('修改时间', auto_now=True)

我发现使用auto_now=True,也可以实现每次修改文字,自动改变修改时间。

回复
追梦人物 Ifr4me
2020-07-19 20:32:29

不推荐这样用,因为设置 auto_now=True 的字段很难去重置这个字段的值了。

回复
_北国烟雨
2020-06-11 00:10:51

您好,请问一下为什么后台添加文章后前台无法查看到,依旧显示暂无文章

回复
ning2510 _北国烟雨
2020-08-14 21:04:58

请问问题解决了吗,我也是跟着操作添加了一个新文章后在主页还是显示暂无文章

回复
ning2510 ning2510
2020-08-14 21:35:19

已解决,解决方案如下:

blog/view.py

def index(request):
    post_list = Post.objects.all().order_by('-created_time')
    return render(request, 'blog/index.html', context={'post_list': post_list})

回复
mmmhxy
2020-04-26 13:09:42

博主好,请问你设置auther自动填写也是在admin里面,这就不存在命令行无法自动获取的问题了吗?

回复
追梦人物 mmmhxy
2020-05-05 11:06:43

嗯,其他地方需要手动设置。

回复
Ipfirn
2020-03-09 17:17:33

怎么看文章?

回复
Ipfirn
2020-03-09 17:14:31

django.core.exceptions.ImproperlyConfigured: Application labels aren't unique, duplicates: blog

回复
Ipfirn Ipfirn
2020-03-09 17:16:15

对不起,我成功了,把'blog'删掉就可以了

回复
下_地_干_活 Ipfirn
2020-06-28 08:55:22

谢谢

回复
Zuwang Dai
2020-01-25 21:49:22

issue

博主你好,很感谢你的分享。本篇示例代码

from django.utils import timezone

class Post(models.Model):
    ...
    created_time = models.DateTimeField('创建时间', default=timezone.now)
    ...

timezone.now() 是不是被误写了

回复
追梦人物 Zuwang Dai
2020-01-31 11:42:23

不是的,default 必须是一个 callable,否则 timezone.now 会在 Post 导入时直接调用,default 的值就不是预期的值了。

回复
Ba-Gai
2020-01-06 16:51:49

博主,我的文章为啥显示不出来,模板也没错啊,还是那个模板的for循环需要注意缩进

<!DOCTYPE html>
{% load static %}

{% load static %}也只能写在<!DOCTYPE html>下面,写上面会报错。

回复
Zuwang Dai Ba-Gai
2020-01-25 19:08:22

请将报错代码贴出...

回复
_北国烟雨 Ba-Gai
2020-06-11 00:21:51

请问解决了吗

回复
lllwqqq
2019-11-13 16:49:43

这一步做完后,后台按住ctrl 选择tag后,没法添加tag,请问是哪里的问题呢?

回复
追梦人物 lllwqqq
2020-02-25 11:52:23

django 的 admin 就是这样的 api 设计

回复
lllwqqq
2019-11-12 15:52:37

admin.site.register(Post, PostAdmin)
请问为什么要把Post和PostAdmin 两个类注册到一起

回复
zzz_github lllwqqq
2019-11-14 21:45:36

不注册怎么调用,一个class单独放那是不生效的

回复
BigYoungs
2019-11-11 20:49:37

博主,按您说教程,配置了INSTALLED_APPS中的blog.apps.BlogConfig,导致后边标签模板提示没有注册,只有改回blog时,才能正常使用,环境一致,不知道是为什么?

回复
追梦人物 BigYoungs
2020-02-25 11:52:52

你看一下 blog.apps.BlogConfig 这个类存不存在?

回复
BigYoungs BigYoungs
2020-05-09 15:14:10

后台设置App(应用)中文名,建议通过应用下的__init__.py文件配置,详细内容,参考:https://www.bigyoung.cn/posts/68/

回复
zizxzy
2019-10-25 00:43:24

博主你好,在这一章的配置中一直出现这样的报错,
E:\BlogDajngo
λ pipenv run python manage.py runserver
Watching for file changes with StatReloader
Exception in thread django-main-thread:
Traceback (most recent call last):
File "e:\python37\Lib\threading.py", line 917, in bootstrap_inner
self.run()
File "e:\python37\Lib\threading.py", line 865, in run
self._target(self._args, **self._kwargs)
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\site-packages\django\utils\autoreload.py", line 54, in wrapper
fn(
args, kwargs)
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\site-packages\django\core\management\commands\runserver.py", line 109, in inner_run
autoreload.raise_last_exception()
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\site-packages\django\utils\autoreload.py", line 77, in raise_last_exception
raise exception[1]
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\site-packages\django\core\management_init
.py", line 337, in execute
autoreload.check_errors(django.setup)()
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\site-packages\django\utils\autoreload.py", line 54, in wrapper
fn(
args, *
kwargs)
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\site-packages\django_init
.py", line 24, in setup
apps.populate(settings.INSTALLED_APPS)
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\site-packages\django\apps\registry.py", line 91, in populate
app_config = AppConfig.create(entry)
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\site-packages\django\apps\config.py", line 156, in create
app_module = import_module(app_name)
File "E:\ENVS\BlogDajngo-DvD5pAXC\lib\importlib_init_.py", line 118, in import_module
if name.startswith('.'):
AttributeError: 'tuple' object has no attribute 'startswith'
有什么办法可以解决吗?
这是报错的代码的内容

回复
追梦人物 zizxzy
2019-10-25 10:41:17

检查你的 installed app 配置,极有可能写错了。

回复
用户6122873947 zizxzy
2019-11-04 14:43:02

你好,这个问题你解决了吗
我也遇到了同样的报错

回复
用户6122873947 追梦人物
2019-11-04 14:43:59

你好,这个配置在哪检查呀

回复
Ralph LU
2019-09-29 16:55:14

def str(self):
return self.title
这段是做什么的呢?
pythonCharm里title被加了灰底,好像没有正确引用。

回复
追梦人物 Ralph LU
2019-09-29 18:09:25

参考:Django 迁移、操作数据库取数据部分,作用是让 Model 打印显示时更加人性化。

回复
719923505
2019-09-04 11:43:31
  1. 不在后台注册我们自己创建的几个模型,这些数据会怎么样?
  2. project 里面的path('admin/', admin.site.urls), 这个写法跟include有啥不一样,还有就是这些url在哪里配置的?
  3. save,save_model 这两个方法有啥区别?
回复
Zuwang Dai 719923505
2020-01-25 19:15:06
  1. 动手试一试?
  2. 这是django1.x 和2.x 版本区别之一,除了引用的包没啥不一样
  3. save 是model.Model 的方法, 本篇中用于复写绑定 httprequest 对象的 user 属性于 创建的关系对象实例 obj; save_model 是admin的方法, 用于修改数据并将逻辑存回数据库
回复