message组件
# 应用场景
假设: 你正在做一个订单支付平台, 其中用到了删除/撤销订单问题. 想给予用户一些提示. 可以用到Django的message组件.
该组件通过第一次请求, 写入提示信息并返回重定向, 第二次请求, 呈现提示内容.
建议: 有跳转的时候才使用message, 更多时候通过ajax就可完成!
点击客户列表里某条记录的编辑按钮.
首先要知道 customer/edit/<int:pk>/ 路由地址对应的视图函数customer_edit 里能处理get以及post请求.
1> 先是向这个地址发送get请求 -- http://127.0.0.1:8000/customer/edit/1/
2> 在编辑页面点击保存按钮,向该地址发送post请求 -- http://127.0.0.1:8000/customer/edit/1/
因为该地址对应的视图函数customer_edit在处理post请求时,最后返回用到了redirect重定向!!
So,注意,该post请求的状态是302.
★ 将这个地址放到响应头的Location字段中,让浏览器再向这个重定向的地址发送请求!!!
<并不是>Django直接拿这个地址的内容给浏览器.
3> 浏览器再向该地址发送get请求 http://127.0.0.1:8000/customer/list/
http协议是无状态的短链接. 一次请求和一次响应后,断开连接.(一问一答)
当步骤2中出错了,是没办法在步骤3这个新请求里展示错误信息的!! "或者说, 删除/撤销 订单成功,在重定向的页面上展示,是没有办法的."
因为步骤2的请求和步骤3的请求是两个不同的请求!两者是没有关系的.
2
3
4
5
6
7
8
9
10
11
12
13
# 快速使用
message组件在Django中就是一个App
配置: 需要在settings文件中配置4个地方!!
# -- 第1个位置! app
INSTALLED_APPS = [
... ...
'django.contrib.messages',
]
# -- 第2个位置! 中间件
MIDDLEWARE = [
... ...
'django.contrib.messages.middleware.MessageMiddleware',
]
# -- 第3个位置! 让我们在html模版里操作比较方便.
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
# context_processors选项是一个可调用的列表,里面的项称为上下文处理器
# 它将一个请求对象作为参数,并返回一个要合并到上下文中的项目字典.
'context_processors': [
... ...
'django.contrib.messages.context_processors.messages', # !
],
},
},
]
# -- 第4个位置 写在settings配置文件的任意地方都可.
# 该配置表明把数据存储在哪里? cookie/session/
# FallbackStorage --> cookie和session中都保存.(默认) 在Django的原本的setting配置中写的.
# MESSAGE_STORAGE = 'django.contrib.messages.storage.fallback.FallbackStorage'
# MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
设置值: 在某个视图函数里设置值!!
def my_order_cancel(request, pk):
"""客户撤单"""
from django.contrib import messages
# add_message的参数: 当前请求对象、消息的级别、具体的消息
messages.add_message(request, messages.SUCCESS, "撤单成功1!")
messages.add_message(request, messages.SUCCESS, "撤单成功2!")
return redirect(reverse("home")) # -- 重定向到home页面
2
3
4
5
6
7
读取值: 有两个地方可以读取!!
任何视图都可以往里放数据, 任何请求都可以取, 只要该请求没结束, 就可以无限次取.. 该次请求结束, 其他请求想取,没有了..
可以在redirect重定向的路由对应的视图函数中读取.
def home(request):
# 读取之前页面设置的message
from django.contrib.messages.api import get_messages
messages = get_messages(request)
for msg in messages:
print(msg)
return render(request, "home.html")
2
3
4
5
6
7
也可以在重定向的路由对应的模版中读取.
def home(request):
return render(request, "home.html")
{% extends 'layout.html' %}
{% block content %}
<h3>欢迎登陆!</h3>
{% for message in messages %}
<li>{{ message.tags }} {{ message }}</li>
{% endfor %}
{% endblock %}
2
3
4
5
6
7
8
9
10
11
# 源码分析
message组件的源码是很简单的源码啦, 这都看不懂的话, 那就洗洗睡吧..
假比, 现在进行的是一个删除功能!! 用户点击删除按钮, 向/user/delete/1/
进行了url 跳转/发起请求..
# 在模版中的配置
是做了怎样的配置, 使得在模版中可以直接使用 message变量 ?? 至于为什么,此处不探究.
"""
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
... ...
'django.contrib.messages.context_processors.messages', # !
],
},
},
]
"""
1. context_processors选项是一个可调用的列表,里面的项称为上下文处理器
它将一个请求对象作为参数,并返回一个要合并到上下文中的项目字典.
为啥这样设置就可在模版中调用,具体的底层原理此处不探究!简单看了下Django文档.涉及到RequestContext对象.
2. command+点击 messages, 查看该上下文处理器的源码.
源码如下:
from django.contrib.messages.api import get_messages
from django.contrib.messages.constants import DEFAULT_LEVELS
def messages(request):
# -- 此处return的字典中的"键"可以在任一模版中的作为一个变量直接使用.
return {
'messages': get_messages(request), # 熟悉吗?这个get_messages函数就是我们读取值时调用的函数!!
'DEFAULT_MESSAGE_LEVELS': DEFAULT_LEVELS, # 表示级别的常量
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# step1 请求来
执行 message组件的中间件的process_request 方法, 得到一个 SessionStorage类的实例化对象!!
注意哦!! 不一定就是 SessionStorage类的实例化对象, 因为这里MESSAGE_STORAGE使用的是session!!!
简单来说, 请求一来
1> 执行中间件里的process_request方法
2> import_string
3> 实则就是调用SessionStorage类的父类BaseStorage的__init__
方法 进行了类的实例化, 封装了一些成员进去.
该实例化对象赋值给了 request.__message
一开始, request.__message 里有 request=request; _queued_messages = []; used = False; added_new = False
"""
中间件源码
"""
from django.conf import settings
from django.contrib.messages.storage import default_storage
from django.utils.deprecation import MiddlewareMixin
class MessageMiddleware(MiddlewareMixin):
def process_request(self, request):
request._messages = default_storage(request) # !看default_storage方法.
def process_response(self, request, response):
if hasattr(request, '_messages'):
unstored_messages = request._messages.update(response)
if unstored_messages and settings.DEBUG:
raise ValueError('Not all temporary messages could be stored.')
return response
"""
default_storage方法
"""
from django.conf import settings
from django.utils.module_loading import import_string
def default_storage(request):
# import_string本质就是 根据字符串的形式找到类
# from django.contrib.messages.storage.session import SessionStorage
# SessionStorage(request) 类的实例化,内部封装了几个值.
return import_string(settings.MESSAGE_STORAGE)(request) # -- 可扩展性的体现.
"""
SessionStorage(request) 类的实例化 执行初始化方法__init__
"""
class SessionStorage(BaseStorage):
session_key = '_messages'
def __init__(self, request, *args, **kwargs):
# 断言, assert expression [, arguments]
# 意味着, hasattr(request, 'session') 为False,就抛出异常信息, “The session-based...”
# hasattr(request, 'session') 为True, 才会执行 super().__init__(request, *args, **kwargs)
# 为何这样判断,因为在session中间件的源码是这样定义的. 没有执行session中间件,request对象里肯定没有session这个成员.
"""
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key)
"""
assert hasattr(request, 'session'), "The session-based temporary "\
"message storage requires session middleware to be installed, "\
"and come before the message middleware in the "\
"MIDDLEWARE list."
# 执行父类的初始化方法
super().__init__(request, *args, **kwargs)
"""
SessionStorage类的父类BaseStorage类的初始化方法
"""
class BaseStorage:
def __init__(self, request, *args, **kwargs):
self.request = request
self._queued_messages = []
self.used = False
self.added_new = False
super().__init__(*args, **kwargs) # BaseStorage的父类就是object类啦!
"""
在seetings配置文件中的配置
"""
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# step2 视图中
开始执行路由对应的视图函数
简单来说, 视图函数里每执行一句 messages.add_message()
, 就会创建一个 Message类的实例化对象.
并将这些消息的实例化对象 放到了SessionStorage类的实例化对象的成员 _queued_messages
这个列表里!!
Message类的实例化对象有成员: level = int(level) ; message = message ; extra_tags = extra_tags
"""
视图函数
"""
# path('user/delete/<int:pk>/', user.user_delete, name='user_delete'),
def user_delete(request, pk):
from django.contrib import messages
# add_message的参数: 当前请求对象、消息的级别、具体的消息
messages.add_message(request, messages.SUCCESS, "删除成功1!") # 查看add_message方法
messages.add_message(request, messages.SUCCESS, "删除成功2!")
return redirect(reverse('user_list'))
"""
add_message方法
"""
Q: command+B跳转过去,该方法的源码在 django.contrib.messages.api 这个py文件里.
那为啥 用messages.add_message()就可以调用呢?
A: 因为在 django.contrib.messages.__init__ 这个py文件里写了这两行代码.
from django.contrib.messages.api import * # NOQA
from django.contrib.messages.constants import * # NOQA
def add_message(request, level, message, extra_tags='', fail_silently=False):
"""参数:当前请求对象、消息的级别、具体的消息"""
try:
# 在中间件的process_request方法里就往当前请求对象request里放入了名为_messages的成员
# 而且通过分析得知,该成员是一个 SessionStorage(request) 类的实例化对象!!
messages = request._messages
# 这里抛错,只有可能是基于 request._messages 这行代码出发.
# 1> add_message传入的request不是当前请求对象
# 2> 没有执行message组件的中间件(中间件未注册)导致request对象里没有_messages这个成员.
# 若调用add_message方法时传入fail_silently=True,那么就不会报这个错啦..静默处理.
except AttributeError:
if not hasattr(request, 'META'):
raise TypeError(
"add_message() argument must be an HttpRequest object, not "
"'%s'." % request.__class__.__name__
)
if not fail_silently:
raise MessageFailure(
'You cannot add messages without installing '
'django.contrib.messages.middleware.MessageMiddleware'
)
else:
# messages = request._messages 这行代码未抛错,执行下面这行代码!
# 前面说了 request._messages是SessionStorage类的实例化对象, 赋值给了 messages变量.
# So,这里我们去 SessionStorage类 里找add方法, 没找到 就去SessionStorage类的父类BaseStorage中找..
return messages.add(level, message, extra_tags)
"""
add方法
"""
# 视图函数里调用时,是这样传参的 messages.add_message(request, messages.SUCCESS, "删除成功1!")
class BaseStorage:
def add(self, level, message, extra_tags=''):
if not message:
return
level = int(level)
# -- 这个无伤大雅,就不细说啦,消息级别是debug级别是不会写入到session中的..
# 看源码的话,也很容易理解. 注意两个点:
# 1> self.level 是用property将方法包装成的数据属性
# 2> messages.SUCCESS 可以直接用是因为 messages这个包里 有个__init__.py 文件,导入了一些成员.
if level < self.level:
return
self.added_new = True # 设计一个标记,表示有新增的啦!
# -- 将要添加的信息打包成一个Message对象!
message = Message(level, message, extra_tags=extra_tags)
# ★ 信息封装成了对象,并保存到了内存中的一个列表里!!!
self._queued_messages.append(message)
"""
Message类的初始化
"""
class Message:
def __init__(self, level, message, extra_tags=None):
self.level = int(level)
self.message = message
self.extra_tags = extra_tags
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# step3 请求走
路由对应的视图函数最后执行
return redirect(reverse('user_list'))
返回, 又开始走中间件!
"""
中间件源码 process_response
"""
from django.conf import settings
from django.contrib.messages.storage import default_storage
from django.utils.deprecation import MiddlewareMixin
class MessageMiddleware(MiddlewareMixin):
def process_request(self, request):
request._messages = default_storage(request)
def process_response(self, request, response):
# 先反射判断request对象里有没有_messages. 都走到这里了,肯定是有的.
if hasattr(request, '_messages'):
# 看update方法. 经分析,用的是BaseStorage类中的update方法.
unstored_messages = request._messages.update(response)
if unstored_messages and settings.DEBUG:
raise ValueError('Not all temporary messages could be stored.')
return response
"""
update方法
"""
class BaseStorage:
def update(self, response):
self._prepare_messages(self._queued_messages) # 没啥意义,暂且不看.
if self.used:
return self._store(self._queued_messages, response)
elif self.added_new:
# -- 把之前的和新增的拿过来进行拼接
messages = self._loaded_messages + self._queued_messages
# 调用的是SessionStorage类里的_store方法.
return self._store(messages, response)
"""
_loaded_messages方法
"""
@property
def _loaded_messages(self):
# 这里写了个hasattr反射,是为了 视图里多次取时, 不用反复去session中取.
if not hasattr(self, '_loaded_data'):
messages, all_retrieved = self._get()
self._loaded_data = messages or []
return self._loaded_data
"""
_get方法
"""
def _get(self, *args, **kwargs):
# 从session中取
return self.deserialize_messages(self.request.session.get(self.session_key)), True
"""
_store方法
"""
class SessionStorage(BaseStorage):
def _store(self, messages, response, *args, **kwargs):
if messages:
# -- 将信息序列化后存储到session中!!
self.request.session[self.session_key] = self.serialize_messages(messages)
else:
self.request.session.pop(self.session_key, None)
return []
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# ★ 总结
Ps: 图中是message组件关键源码..
看源码心得: 类实例化对象调用某个方法, 先在自己类里找, 没有就去父类里找, 调用父类里的某个self.方法() 时, 也是这个逻辑, 先自己类里找, 没有就父类里找.. 很简单的一个知识点, 但看了源码, 才有深刻的体验..
"""
就只有新增
"""
[step1]
request._messages = SessionStorage(request)
-- request._messages 成员 --
request=request
_queued_messages = []
used = False
added_new = False
[step2]
messages = request._messages
messages.add(level, message, extra_tags)
-- messages.add(level, message, extra_tags) 即--
message = Message(level, message, extra_tags=extra_tags)
-- 成员 --
level = int(level)
message = message
extra_tags = extra_tags
messages.added_new = True
messages._queued_messages.append(message)
[step3]
loaded_data = 反序列化(messages.request.session.get("__messages")) or []
messages.request.session["__messages"] = 序列化(loaded_data + messages._queued_messages)
"""
怎么取的,上面没有单独讲解..很简单,懒得补充了.详细阐述的只是新增过程涉及到的源码.
★ 这里阐述下,整体的一个逻辑!!
"""
★★★★★★ *任何视图都可以往里放数据,ABCD任何请求都可以取.
假设A取,只要A请求没结束,就可以无限次取..在视图中取模版中取! 当A请求结束了,BCD其他请求想取,里面没有东西了..*
▲ 视图:
增 --> 往_queued_messages列表里添加message对象 add_new = True
取 --> seesion里 + 新增的 一起迭代出来,<★并>将_queued_messages置为空列表 used = True
▲ 请求走了:
不管是连续多增、连续多取、先(多)增后(多)取再(多)增
if used:
只将最后新增的放到session中,session中"__messages"这个key中只有新增的
elif add_new:
session中原来的取出来 + 新增的 重新组合下, 再放到session中.
很巧妙的运用 add_new、used 这两个标志!!
不管是取的时候将 _queued_messages置为空列表, 还是在请求走时, 那个if..elif判断, 都是我应该学习的地方!!!
★ 而且,源码里updata的方法里的英文注释翻译过来:
可以很简单明了的表述上面的逻辑,只不过第一次看到时,不能很快的转过弯来罢了.
- ★★★★★★ 储存所有未读消息. 如果后端尚未迭代, 则再次存储以前存储的消息. 否则,只存储在最后一次迭代之后添加的消息.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 撤单
思考:
Q: 在前面新建订单时, 最后一步是 将下单的任务加到redis队列中的.. 那么此处撤单成功后, 需要将此订单从队列中移除吗?
A: No, 不用. 若要进行移除的话, 需要循环队列..较繁琐, 因为后续的worker从订单中拿到一个订单ID后, 会根据订单ID拿到订单的其他详细信息, 拿到后可以做个判断, 订单状态为待执行便执行; 订单状态为已撤单便不管啦!!
# 页面提示处理
1. 在撤单对应的视图函数里得判断该订单是否存在.
筛选的条件有四个: id=pk, active=1, status=1, customer=request.nb_user.id
2. 在撤单对应的视图函数里,例如message组件写入信息. 并在订单列表页面展示 写入的信息.. 比如:订单不存在、撤单成功.
注意,在订单页面展示信息时,不同类型的信息提示框的背景色应该是不一样的.
查询bootstrap官方文档,警告框支持的样式 包括 成功、消息、警告或危险 - success、info、warning、danger
在message组件的constans.py中 支持的包括 DEBUG、INFO、SUCCESS、WARNING、ERROR 是没有danger的!!
我们可以扩写:(在页面上是这样的)
{% for obj in messages %}
<div class="alert alert-{{ obj.level_tag }}">
{{ obj.message }}
</div>
{% endfor %}
主要是 obj.level_tag !! 看源码一探究竟,无外乎看 Message类的源码.
from django.conf import settings
from django.contrib.messages import constants
def get_level_tags():
# 这个有点东西,将多个字典组合成一个字典!!学费了.
# 本质上最后这个字典 {10:"debug",20:"info"}
return {
**constants.DEFAULT_TAGS,
# 意味着,我们可以在配置文件里扩展!!
# MESSAGE_DANDER_TAG = 50 MESSAGE_TAGS = {MESSAGE_DANDER_TAG: "danger"}
# 视图函数里 这样使用: messages.add_message(request, settings.MESSAGE_DANDER_TAG, "订单不存在")
**getattr(settings, 'MESSAGE_TAGS', {}),
}
LEVEL_TAGS = utils.get_level_tags()
@property
def level_tag(self):
# 注意: 在视图函数中 messages.add_message(request, messages.ERROR, "订单不存在")
# messages.ERROR 这玩意儿,到了Message类里就是self.level 本质就是一个数字罢了.
return LEVEL_TAGS.get(self.level, '')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 逻辑处理
同样的, 会基于事务基于锁
def my_order_cancel(request, pk):
"""客户撤单"""
# 0.撤销的订单是否存在
order_object = models.Order.objects.filter(id=pk, active=1, status=1, customer=request.nb_user.id).first()
if not order_object:
messages.add_message(request, settings.MESSAGE_DANDER_TAG, "订单不存在.")
return redirect(reverse("my_order_list"))
try:
with transaction.atomic():
models.Customer.objects.filter(id=request.nb_user.id).select_for_update().first() # 单纯的就是想加锁
# 1.订单状态变化为 (5, "已撤单"),
# 方式1
# order_object.status = 5
# order_object.save()
# 方式2
models.Order.objects.filter(id=pk, active=1, status=1, customer=request.nb_user.id).update(status=5)
# 2.归还扣款到余额中
models.Customer.objects.filter(id=request.nb_user.id).update(
balance=F("balance") + order_object.real_price
)
# 3.添加一个撤单的交易记录
models.TransactionRecord.objects.create(
charge_type=5,
customer_id=request.nb_user.id,
amount=order_object.real_price,
order_oid=order_object.oid
)
except Exception as e:
messages.add_message(request, settings.MESSAGE_DANDER_TAG, "撤单失败,{}".format(str(e)))
return redirect(reverse("my_order_list"))
# 撤单成功了
messages.add_message(request, messages.SUCCESS, "撤单成功")
return redirect(reverse("my_order_list"))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
再次强调:
页面跳转页面会刷新,无法展示错误信息,才用的message组件;
若使用ajax, 一点有错误了, 直接在页面展示即可.. 不存在页面跳转刷新问题.. So, 用ajax就不用message组件.