Django 开发过程中的问题小结
- 1. 一对多、多对多时如何筛选?
- 2. 如何自定义Markdown模块输出的代码块样式?
- 3. 如何自定义Markdown模块输出的目录样式?
- 4. 如何在后端构造url?
- 5. 如何在后端渲染模板后获取文本?
- 6. 如何优雅地将模型对象转换为字典?
- 7. 如何优雅地将后端数据传递给前端JavaScript?
- 8. 如何自定义一个查询方法?
1. 一对多、多对多时如何筛选? #
举个简单的例子:
# models.py
from django.db import models
# 标签
class Tag(models.Model):
name = models.CharField(max_length=100)
# 文章
class Entry(models.Model):
date = models.DateTimeField("date published")
last_modified_date = models.DateTimeField("date published")
title = models.CharField(max_length=50)
text = models.TextField(max_length=1000000)
# 一篇文章可以有多个标签 一个标签下也可以有多篇文章 (多对多)
tags = models.ManyToManyField(Tag, blank=True)
hidden = models.BooleanField(default=False)
views = models.PositiveIntegerField(default=0)
绝大部分情况下,肯定会需要编写“显示拥有该标签的所有文章”的功能代码。
Entry.objects.filter("???")
那么filter
方法里的参数应该如何填写呢?其实没有那么复杂
# 用"字段名__属性名"取属性即可
Entry.objects.filter(tags__name="标签名")
# 也可以直接用"字段名=一个模型对象"
# 不过我更倾向于上面这个方法 因为更直观 也不需要特地获取模型对象
Entry.objects.filter(tags=tag)
2. 如何自定义Markdown模块输出的代码块样式? #
Markdown模块转换输出的html代码中,代码块部分会被这样转换:
<div class="codehilite">
<pre>
<code>
balabala...
</code>
</pre>
</div>
有时我们需要修改最外层的div
块的class
属性:
<div class="codehilite card">
balabala...
</div>
该如何操作呢?
对转换输出的html代码字符串用replace()
方法替换?虽然确实可以解决问题,但是不够优雅不够清真。
代码块的html样式默认是codehilite
,所以我们先尝试在Markdown模块的目录下搜索关键词codehilite
:
# ./Lib/site-packages/markdown/extensions/codehilite.py
...
class CodeHiliteExtension(Extension):
""" Add source code hilighting to markdown codeblocks. """
def __init__(self, **kwargs):
# define default configs
self.config = {
'linenums': [None,
"Use lines numbers. True=yes, False=no, None=auto"],
'guess_lang': [True,
"Automatic language detection - Default: True"],
'css_class': ["codehilite",
"Set class name for wrapper <div> - "
"Default: codehilite"],
'pygments_style': ['default',
'Pygments HTML Formatter Style '
'(Colorscheme) - Default: default'],
'noclasses': [False,
'Use inline styles instead of CSS classes - '
'Default false'],
'use_pygments': [True,
'Use Pygments to Highlight code blocks. '
'Disable if using a JavaScript library. '
'Default: True']
}
super().__init__(**kwargs)
...
def makeExtension(**kwargs): # pragma: no cover
return CodeHiliteExtension(**kwargs)
可以看出,codehilite
是css_class
的默认值,理论上该属性是可以进行配置的。
阅读Markdown模块的官方文档,官方给出的示例代码如下:
markdown.markdown(
txt,
extensions=['myextension'],
extension_configs = {
'myextension': {'ins_del': True}
}
)
那我们就可以照葫芦画瓢了:
md = markdown.Markdown(
extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
],
extension_configs={
'markdown.extensions.codehilite': {
'css_class': 'codehilite card',
},
}
)
3. 如何自定义Markdown模块输出的目录样式? #
Markdown模块转换输出的html代码中,目录块的格式是这样的:
<div class="toc">
<ul>
<li>...</li>
<li>...</li>
...
</ul>
</div>
如果要修改最外层的div
块的class
属性,那么像问题2一样解决就行。
但是如果我们还想修改ul
和li
标签的属性呢?
实际上,这是markdown对象的toc
属性输出的html代码。
我们可以通过markdown对象的toc_tokens
属性,获取目录的元数据。
md = markdown.Markdown(
extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
]
)
markdown_text = '''
# 小标题1
## 小标题1.1
# 小标题2
# 小标题3
'''
md.convert(markdown_text)
print(md.toc_tokens)
输出:
[
{'level': 1, 'id': '1', 'name': '小标题1', 'children':
[
{'level': 2, 'id': '11', 'name': '小标题1.1', 'children': []}
]
},
{'level': 1, 'id': '2', 'name': '小标题2', 'children': []},
{'level': 1, 'id': '3', 'name': '小标题3', 'children': []}
]
既然有了toc_tokens
,那剩下的就交给模板来处理吧。
4. 如何在后端构造url? #
在模板中构造url使用的是”url”标签:
<a href="{% url 'entry:detail' id_ %}">{{ title }}</a>
“url”之后的第一个参数是视图函数的名字,然后是要传递给该视图函数的参数。
这个无需过多介绍,网上几乎任何一篇Django教程都会讲这一点。
但是如果你想要在后端构造url,那该怎么做呢?
答案是使用django.urls.reverse
方法:
from django.urls import reverse
reverse("entry:detail", args=[id_, ])
用起来和模板中的url方法几乎无异,具体可参考此方法的帮助文档。
5. 如何在后端渲染模板后获取文本? #
笨方法:
from django.shortcuts import render
html_render = render(
request,
"path/to/template/file.html",
{"key": "value"},
)
# 对响应内容进行解码
html_text = html_render.content.decode(html_render.charset)
简单方法:
from django.template.loader import render_to_string
html_text = render_to_string(
"path/to/template/file.html",
{"key": "value"},
# 有需要的话可以传入request参数, 也可以不传
# request=request,
)
6. 如何优雅地将模型对象转换为字典? #
注:在尝试之前,你需要先搞清楚为什么要把模型对象转换为字典。如果你是要将模型转为方便前端使用的json格式的话,那首先应该考虑使用Django的json序列化器。
方法1:使用model_to_dict #
from django.forms.models import model_to_dict
entry_obj = Entry.objects.first()
print(entry_obj.__dict__)
'''
{'_state': <django.db.models.base.ModelState at 0x282ee738f48>,
'id': 1,
'date': datetime.datetime(1970, 01, 01, 00, 00),
'last_modified_date': datetime.datetime(1970, 01, 01, 00, 59),
'title': 'Test title',
'text': 'Test text',
'is_markdown': True,
'hidden': False,
'views': 19}
'''
entry_dic = model_to_dict(entry_obj)
print(entry_dic)
'''
{'id': 9,
'date': datetime.datetime(1970, 01, 01, 00, 00),
'last_modified_date': datetime.datetime(1970, 01, 01, 00, 59),
'title': 'Test title',
'text': 'Test text',
'is_markdown': True,
'hidden': False,
'views': 19,
'tags': [<Tag: Python>]}
'''
注意:
- 有一个必须要注意的坑:返回的字典中不包含被标记为不可编辑(editable为False)的字段。
- 对于一对一、多对一外键,只返回外键模型的主键值;对于多对多外键,返回包含所有模型对象的列表。
- 该方法不是Django文档中的公开API,换句话说,Django官方不建议开发者们在项目中使用此方法。
方法2:使用django的序列化功能 #
from django.core import serializers
entry_obj = Entry.objects.first()
print(entry_obj.__dict__)
'''
{'_state': <django.db.models.base.ModelState at 0x282ee738f48>,
'id': 1,
'date': datetime.datetime(1970, 01, 01, 00, 00),
'last_modified_date': datetime.datetime(1970, 01, 01, 00, 59),
'title': 'Test title',
'text': 'Test text',
'is_markdown': True,
'hidden': False,
'views': 19}
'''
entry_dic_list = serializers.serialize("python", Entry.objects.filter(pk=1))
print(entry_dic_list)
'''
[{'model': 'foo.Entry',
'pk': 1,
'fields': {'date': datetime.datetime(1970, 01, 01, 00, 00),
'last_modified_date': datetime.datetime(1970, 01, 01, 00, 59),
'title': 'Test title',
'text': 'Test text',
'is_markdown': True,
'hidden': False,
'views': 19,
'tags': [1]}}]
'''
注意:
serializers.serialize
只接受QuerySet,不接受单个模型对象;导出的对象也不是字典,而是包含QuerySet中所有模型对象序列化结果的列表。serializers.serialize
导出的fields
中的外键只有外键字段的主键值,但是你也可以对其进行自定义,可以参考刘江的django教程。fields
字典中不包含模型的主键。
方法3:DIY #
方法1和方法2都有不完美的地方,那么。。。
from django.db.models import ForeignKey
def model_to_dict_(instance):
""" 此方法修改自django.forms.models.model_to_dict方法 """
opts = instance._meta
data = {}
for f in chain(opts.concrete_fields, opts.private_fields, opts.many_to_many):
# 对于一对一和多对一外键, 返回外键模型对象
if isinstance(f, ForeignKey):
data[f.name] = getattr(instance, f.name, None)
else:
data[f.name] = f.value_from_object(instance)
return data
7. 如何优雅地将后端数据传递给前端JavaScript? #
或许你已经想到了:在后端将数据转为json格式,然后传递给模板,再在模板中用JavaScript解析json数据。
也可以用Django内置的json_script
过滤器来简化这个过程:
# views.py
def foo(request):
return render(
request,
"path/to/template/file.html",
{"numbers": list(range(5))}
)
<!-- path/to/template/file.html -->
{{ numbers | json_script:"number-data" }}
<!-- 这将被渲染为: -->
<script id="number-data" type="application/json">[0, 1, 2, 3, 4]</script>
在JavaScript中获取数据的话,可以这样:
// 原生JavaScript
JSON.parse(document.getElementById('number-data').textContent)
// JQuery
JSON.parse($('#number-data').text())
json_script
过滤器默认会对特殊字符进行转义,避免XSS攻击。
简单,安全,优雅。
8. 如何自定义一个查询方法? #
假设我们定义了一个客户(Customer)模型,该模型中有一个积分(score)字段:
# models.py
from django.db import models
class Customer(models.Model):
score = models.PositiveIntegerField("积分", default=0)
...
举个有点不恰当的例子,我们假设如果客户的积分大于0,则认为该客户是会员客户。
那么,每次要查询并筛选出会员客户时,都需要写Customer.objects.filter(score__gt=0)
,特别麻烦。
而且,假设以后因为业务需要,只有积分大于10的客户才算是会员客户,那就又得重构代码。
可不可以自定义一个查询方法呢?当然可以!
# models.py
from django.db import models
class Customer(models.Model):
score = models.PositiveIntegerField("积分", default=0)
...
class _CustomerManager(models.Manager):
""" 新增一个自定义的查询方法 """
def filter_is_vip(self):
""" 选择积分大于0的客户(会员客户) """
return self.filter(score__gt=0)
# objects是一个django.db.models.Manager实例, 本质上是Customer模型的类属性
objects = _CustomerManager()
# 用法: Customer.objects.filter_is_vip(), 返回包含所有积分大于0的客户(会员客户)的QuerySet
注意:以上代码有个缺陷:QuerySet对象不能调用
filter_is_vip
方法
即:Customer.objects.all().filter_is_vip()
会抛出异常
(因为Customer.objects.all()
返回了一个QuerySet,我们并没有为QuerySet对象实现filter_is_vip
方法)
因此,这样写代码还是过于复杂了,以下是更简单的方法:
# models.py
from django.db import models
class Customer(models.Model):
score = models.PositiveIntegerField("积分", default=0)
...
@classmethod
def queryset_is_vip(cls):
""" 选择所有积分大于0的客户(会员客户) """
return cls.objects.filter(score__gt=0)
# 用法: Customer.queryset_is_vip(), 返回包含所有积分大于0的客户(会员客户)的QuerySet