Django 开发过程中的问题小结

Django 开发过程中的问题小结


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)

可以看出,codehilitecss_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一样解决就行。

但是如果我们还想修改ulli标签的属性呢?

实际上,这是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>]}
'''

注意:

  1. 有一个必须要注意的坑:返回的字典中不包含被标记为不可编辑(editable为False)的字段。
  2. 对于一对一、多对一外键,只返回外键模型的主键值;对于多对多外键,返回包含所有模型对象的列表。
  3. 该方法不是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]}}]
'''

注意:

  1. serializers.serialize只接受QuerySet,不接受单个模型对象;导出的对象也不是字典,而是包含QuerySet中所有模型对象序列化结果的列表。
  2. serializers.serialize导出的fields中的外键只有外键字段的主键值,但是你也可以对其进行自定义,可以参考刘江的django教程
  3. 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

扩展阅读:自定义查询器 | Django 文档 | Django