跳转至

Django

个人常用配置、踩坑与集成记录,通用教程类内容不再收录。官方文档:https://docs.djangoproject.com/zh-hans/5.0/contents/

  • 配置


    静态 / 模板 / 数据库 / 时区设置

  • URL 与模板


    路由、命名空间、模板标签与过滤器

  • 数据与任务


    ORM、中间件、Redis、Celery

  • 异步与实时


    ASGI、WebSocket channels、前后端交互

🗂️ 静态文件与常用配置

STATIC_ROOT · STATICFILES_DIRS · TEMPLATES · DATABASES · LANGUAGE_CODE。静态资源与 settings 常用项。

静态资源

static 目录找不到资源时,有两种补法:

静态资源处理

from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path('', include('usermodectl.urls')),
]
urlpatterns += staticfiles_urlpatterns()
from django.conf import settings
from django.contrib import admin
from django.urls import path, re_path, include
from django.views.static import serve

from usermodectl.views import *

STATIC_ROOT = os.path.join(BASE_DIR, "static")

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r"^static/(?P<path>.*)$", serve,
            {"document_root": settings.STATIC_ROOT}, name='static'),
    re_path('', include('usermodectl.urls')),
]

静态 / 模板常用配置

# settings.py
STATIC_ROOT = os.path.join(BASE_DIR, "static")

STATIC_URL = '/static/'
STATICFILES_DIRS = [
    BASE_DIR / 'static',
]

# 模板文件存放在 BASE_DIR/templates
TEMPLATES = [
    {
        ...
        'DIRS': [BASE_DIR / 'templates'],
        ...
    },
]

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

LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False

🔗 URL 与路由

re_path · path · include · app_name · {% url %}。路由写法与命名空间。

路由

urlpatterns = [
    re_path('^$', index_page),  # 一般首页的写法
    re_path('model_info', model_info),

    # 正则匹配;name 可在模板中用 {% url "queryproduct" per_model.1 %} 引用
    re_path("query/(?P<product_name>.*)", product_list, name="queryproduct"),

    path('contol/', admin.site.urls),  # 修改默认管理地址
    re_path("", include("cneos.urls")),  # 走到某个 app
]
from django.urls import re_path

from aboutpbc.views import *

app_name = 'aboutpbc'

urlpatterns = [
    re_path("", index, name="index"),
]

坑:{% url %} 与命名空间

使用带命名空间的写法(如 {% url 'fun:login_mysite' %})时,对应 app 必须指定 app_name,否则报错。

app_name = 'fun'

urlpatterns = [
    re_path('login_mysite', login_mysite, name='login_mysite'),
    re_path('', index),
]

🧩 模板

遍历字典

{% for key, value in dicts.items %}
  <tr class="{% cycle 'altrow' '' %}">
    <td>{{ forloop.counter }}</td>
    <td>{{ key }}</td>
    <td>{{ value }}</td>
  </tr>
{% endfor %}

也可单独循环 user_dict.keys / user_dict.values

{% for row in user_dict.keys %}
{% for row in user_dict.values %}

修改后台标题

把 Django 后台左上角的「Django 管理」改为「公共服务」,需要覆盖默认 admin 模板:

  1. 在项目主目录(与 manage.py 同级)创建 templates 文件夹。
  2. 在其中创建 admin 文件夹,用于覆盖 Django admin 的默认模板。
  3. admin 下创建 base_site.html。Django admin 会首先搜索这个文件,找到就用它替代默认模板。
  4. base_site.html 中继承 admin/base.html,覆盖 branding 块:

    {% extends "admin/base.html" %}
    {% load i18n %}
    
    {% block title %}{{ title }} | {% trans "公共服务" %}{% endblock %}
    
    {% block branding %}
    <h1 id="site-name"><a href="{% url 'admin:index' %}">{% trans "公共服务" %}</a></h1>
    {% endblock %}
    
    {% block nav-global %}{% endblock %}
    
  5. 让项目知道新的 templates 文件夹位置,在 settings.pyTEMPLATES.DIRS 中包含它:

    TEMPLATES = [
        {
            ...
            'DIRS': [os.path.join(BASE_DIR, 'templates')],
            ...
        },
    ]
    

🧹 自定义过滤器

templatetags · Library() · register.filter。自定义过滤器与常见内置过滤器。

在 app 目录下建包 templatetags,在包内建一个 py 文件(文件名随意):

from django.template import Library
import datetime

register = Library()

def show_time(v):
    # 首页返回底部时间的过滤器
    return datetime.datetime.now().date()

register.filter('show_time', show_time)

html 文件开头 {% load my_page %} 后使用。修改后需重启服务器!

常见内置过滤器

过滤器 描述 示例
upper 以大写方式输出 {{ user.name \| upper }}
add 给 value 加上一个数值 {{ user.age \| add:"5" }}
addslashes 单引号加上转义号
capfirst 第一个字母大写 {{ 'good' \| capfirst }} 返回 Good
center 输出指定长度的字符串,把变量居中 {{ "abcd" \| center:"50" }}
cut 删除指定字符串 {{ "You are not a Englishman" \| cut:"not" }}
date 格式化日期 {{ 时间 \| date:"Y-m-d" }}
default 如果值不存在,则使用默认值代替 {{ value \| default:"(N/A)" }}
default_if_none 如果值为 None,则使用默认值代替
dictsort 按某字段排序,变量必须是一个 dictionary {% for moment in moments \| dictsort:"id" %}
dictsortreversed 按某字段倒序排序,变量必须是 dictionary
divisibleby 判断是否可以被数字整除 {{ 224 \| divisibleby:2 }} 返回 True
escape 按 HTML 转义,比如将 ; 转换
filesizeformat 增加数字的可读性,转换结果为 13KB、89MB、3Bytes 等 {{ 1024 \| filesizeformat }} 返回 1.0KB
first 返回列表的第 1 个元素,变量必须是一个列表
floatformat 转换为指定精度的小数,默认保留 1 位 {{ 3.1415926 \| floatformat:3 }} 返回 3.142(四舍五入)
get_digit 从个位数开始截取指定位置的数字 {{ 123456 \| get_digit:'1' }}
join 用指定分隔符连接列表 {{ ['abc','45'] \| join:'*' }} 返回 abc*45
length 返回列表中元素的个数或字符串长度
length_is 检查列表、字符串长度是否符合指定的值 {{ 'hello' \| length_is:'3' }}
linebreaks <p><br> 标签包裹变量 {{ "Hi\n\nDavid" \| linebreaks }} 返回 <p>Hi</p><p>David</p>
linebreaksbr <br/> 标签代替换行符
linenumbers 为变量中的每一行加上行号
ljust 输出指定长度的字符串,变量左对齐 {{ 'ab' \| ljust:5 }} 返回 'ab '
lower 字符串变小写
make_list 将字符串转换为列表
pluralize 根据数字确定是否输出英文复数符号
random 返回列表的随机一项
removetags 删除字符串中指定的 HTML 标记 {{ value \| removetags:"h1 h2" }}
rjust 输出指定长度的字符串,变量右对齐
slice 切片操作,返回列表 {{ [3,9,1] \| slice:':2' }} 返回 [3,9]{{ 'asdikfjhihgie' \| slice:':5' }} 返回 'asdik'
slugify 在字符串中留下减号和下划线,其它符号删除,空格用减号替换 {{ '5-2=3and5 2=3' \| slugify }} 返回 5-23and5-23
stringformat 字符串格式化,语法同 python
time 返回日期的时间部分
timesince 以「到现在为止过了多长时间」显示时间变量 结果可能为 45 days, 3 hours
timeuntil 以「从现在开始到时间变量还有多长时间」显示时间变量
title 每个单词首字母大写
truncatewords 将字符串转换为省略表达方式 {{ 'This is a pen' \| truncatewords:2 }} 返回 This is ...
truncatewords_html 同上,但保留其中的 HTML 标签 {{ '<p>This is a pen</p>' \| truncatewords:2 }} 返回 <p>This is ...</p>
urlencode 将字符串中的特殊字符转换为 url 兼容表达方式 {{ 'http://www.aaa.com/foo?a=b&b=c' \| urlencode }}
urlize 将变量字符串中的 url 由纯文本变为链接
wordcount 返回变量字符串中的单词数
yesno 将布尔变量转换为字符串 yes、no 或 maybe {{ True \| yesno }}{{ False \| yesno }}{{ None \| yesno }} 返回 yes no maybe

🧅 中间件

  1. 在具体 app 里新建 middle 文件夹。
  2. 创建中间件代码。
  3. settings.pyMIDDLEWARE 中注册。
from django.utils.deprecation import MiddlewareMixin


class letdo(MiddlewareMixin):
    # 它应该返回 None 或 HttpResponse 对象。如果返回 None,Django 将继续处理这个请求
    ''' 顺序
    调用 self.process_request(request)(如果被定义过)。
    调用 self.get_response(request) 来从后续的中间件和视图得到响应。
    调用 self.process_response(request, response)(如果被定义过)。
    返回响应。
    '''
    def __call__(self, request):
        '''
        :param request: 是中间件的主要入口点。请求到达时 Django 调用中间件的 __call__ 方法,
            传递 request 对象作为参数。中间件可在此执行任何预处理,例如记录请求信息、
            检查用户身份、设置请求标志等。
        :return:
        '''
        ...

    def process_view(self, request, view_func, view_args, view_kwargs):
        ''' 只在 Django 调用视图前被调用;请求被路由到视图函数之前调用
        request 是一个 HttpRequest 对象。
        view_func 是一个 Django 将要使用的 Python 函数(真实的函数对象,不是函数名);
        view_args 是传递给视图的位置参数列表;
        view_kwargs 是传递给视图的关键字参数字典。
        view_args 和 view_kwargs 都不包含第一个视图参数 (request)。
        '''
        ...

    def process_exception(self, request, exception):
        '''
        当视图引发异常时,Django 会调用
        :param request:
        :param exception:
        :return:
        '''
        ...

    def process_template_response(self, request, response):
        '''
        在视图被完全执行后调用;它必须返回一个实现了 render 方法的响应对象。
        当视图函数返回一个模板响应时,这个方法会被调用。中间件可在这里处理模板响应,
        例如添加额外的上下文变量或修改响应内容。
        :param request:
        :param response:
        :return:
        '''
        ...

    def process_request(self, request):
        '''
        这个方法在 __call__ 方法之前调用,允许中间件在请求处理之前进行一些操作
        '''
        ...

    def process_response(self, request, response):
        # 这个方法在视图函数处理请求并生成响应后调用
        ...

🗄️ ORM 与数据

Q · values_list · update · AbstractUser · dumpdata/loaddata · exec。查询、迁移与自定义用户。

数据相关

语法 效果
~Q(jira_key__istartswith="ARC") 不以 ARC 开头的任务
__exact 精确等于 like '111'
__iexact 精确等于,忽略大小写 ilike '111'
__contains 包含 like '%111%'
__icontains 包含,忽略大小写 ilike '%111%'
__gt 大于
__gte 大于等于
__lt 小于
__lte 小于等于
__in 存在于一个 list 范围内
__startswith 以…开头
__istartswith 以…开头,忽略大小写
__endswith 以…结尾
__iendswith 以…结尾,忽略大小写
__range 在…范围内
__year 日期字段的年份
__month 日期字段的月份
__day 日期字段的日
__isnull=True/False 是否为空
# flat=True 返回扁平值列表,否则是元组列表
bug_sum.objects.values_list("teamname", flat=True).distinct()
# <QuerySet ['公共组件', '能力开发', '质效保障', '硬件开发', 'NSECOS系统']>

bug_sum.objects.values_list("teamname").distinct()
# <QuerySet [('公共组件',), ('能力开发',), ('质效保障',), ('硬件开发',), ('NSECOS系统',)]>
from myapp.models import MyModel

# 假设要更新 id 为 1 的实例
instance_id = 1
data = {
    'field1': 'New Value 1',
    'field2': 'New Value 2',
}

# 使用 update() 批量更新
MyModel.objects.filter(id=instance_id).update(**data)

创建模型并注册管理:

# models.py
from django.contrib.auth.models import AbstractUser

class CACA(AbstractUser):
    phone = models.IntegerField("手机号", null=True, blank=True)

    def __str__(self):
        return self.username

# admin.py
admin.site.register(CACA, myusers)

settings.py 中指定:

AUTH_USER_MODEL = "syslog.CACA"

注意:

  1. 尽可能在项目开始时就创建。
  2. 项目开发中途操作可能出现错误。
  3. 出错可以尝试删除 authdjango 开头的表,并删除 app 下的全部 migrations
# 应用导出
python manage.py dumpdata [appname] > appname_data.json
# 应用导出的某一张表
python manage.py dumpdata [appname.table] > appname_data.json

# 导入(可一次多个文件)
python manage.py loaddata blog_dump.json blog_dump2.json blog_dump3.json

把结果定义在命名空间字典 namespace 中,用 exec() 执行代码,再从字典取执行结果:

namespace = {}
exec(svnaddr, namespace)
logging.error(namespace.get("components_list_rootfs"))
logging.error(namespace.get("components_list"))

🔐 LDAP

settings.py 中需要添加的配置:

AUTHENTICATION_BACKENDS = [
    # 'django_auth_ldap.backend.LDAPBackend',
    'hoaofun.backend.SettingsBackend',
    'django.contrib.auth.backends.ModelBackend',
]
LDAP_CONFIG = {"HOST": "ldap://192.168.1.1:389"}

依赖 pip install python-ldap。参考资料:

🔴 Redis

django-redis · CACHES。用 Redis 作缓存后端。

环境准备:

apt install redis
pip install django-redis

settings.py 中添加:

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
    }
}

🌱 Celery

Celery 之前的版本需要一个单独的库才能与 Django 一起使用,自 3.1 之后不再需要,Django 现在开箱即用。前提是已有 Redis。

pip install Celery "celery[redis,auth,msgpack]"

settings.py 中添加配置:

CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/1'
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = "Asia/Shanghai"
CELERY_MAX_TASKS_PER_CHILD = 100
CELERY_LOG_FILE = os.path.join(BASE_DIR, 'celery.log')
CELERYBEAT_LOG_FILE = os.path.join(BASE_DIR, 'celery.log.beta')

app 下创建 celery.py

import os
from celery import Celery

# 为 'celery' 程序设置默认的 Django settings 模块
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Assetinventory.settings')  # 注意自己的 setting

app = Celery('tech')  # 注意替换 app

# 用字符串配置,worker 无需把配置对象序列化给子进程
# namespace='CELERY' 表示所有 celery 相关配置项都以 CELERY_ 开头
app.config_from_object('django.conf:settings', namespace='CELERY')

# 加载所有已注册 Django app 的任务模块
app.autodiscover_tasks()  # 自动遍历目录下的 tasks.py

@app.task(bind=True, ignore_result=True)
def debug_task(self):
    print(f'Request: {self.request!r}')

新建 tasks.py

from celery import shared_task
from .about_qiwei import wxsendmsg

@shared_task
def shared_task_demo(username, txt):
    print('shared_task_demo')
    wxsendmsg(username, txt)

正常代码中的调用方式 delay

shared_task_demo.delay(c.userinfo_obj.username,
                       txt=f"{request.user.first_name}申请使用你在用的设备")

启动 worker / flower / 指定 settings:

celery --app tasks worker --loglevel=info
celery -A tasks flower --broker=redis://@localhost:6379/0
DJANGO_SETTING_MODULE=setting.local celery --app tasks worker --loglevel=info

定时任务

@shared_task
def check_our_dev():
    check_devalive()
# settings.py
CELERY_BEAT_SCHEDULE = {
    'run-every-minute': {
        'task': 'tech.tasks.check_our_dev',
        'schedule': 60.0,  # 每分钟执行一次
    },
}

使用注意事项

export C_FORCE_ROOT='true'                 # root 启动 worker 时需设置
pip install "celery[redis]==4.3.0"         # Celery 4.x 起不再支持 Windows,需 3.x/4.3
# 生产环境不要用关系型数据库做 Broker;用 RabbitMQ 或 Redis
worker_prefetch_multiplier = 0    # 禁用预取,避免分配不均导致重复执行
worker_max_tasks_per_child = 50   # 每个 worker 执行 50 次即销毁,防卡死
# 不要用复杂对象做任务参数(会序列化失败/查询不到)
# Good
@app.task
def my_task(user_id):
    user = User.objects.get(id=user_id)

# Bad
@app.task
def my_task(user):
    print(user.name)

任务执行单元:group(并行)、chain(链式,上一个结果传给下一个)、chord(header 全完成后执行 body)、chunks(分组);delayapply_async 的快捷封装,都返回 AsyncResultready() / get(timeout=) / traceback)。

⚡ 异步支持

uvicorn · async def · transaction.atomicASGI 启动与异步事务。

用 Uvicorn 启动 ASGI 组件:

pip install uvicorn
uvicorn djangoProject.asgi:application
# 异步视图
async def index(request):
    return HttpResponse("Hello, Django!")

async def async_view(request):
    loop = asyncio.get_event_loop()
    loop.create_task(async_task())
    return HttpResponse("Hello, async Django!")

坑:异步任务无法保存数据库

事务是将一系列数据库操作作为原子操作执行的机制,要么全部成功提交,要么全部回滚。同步请求默认自动开启事务,并在请求结束时提交;异步任务默认不会自动开启事务,需要手动管理,否则数据不落库。django.db.transaction 模块提供了事务处理功能:

from django.db import transaction

# 使用上下文管理器
def your_task_function():
    with transaction.atomic():
        # 执行数据库操作
        # 如果发生异常,事务将回滚
        ...

# 或者装饰器
@transaction.atomic
def your_task_function():
    # 执行数据库操作
    # 如果发生异常,事务将回滚
    ...

使用事务处理后,重启 celery 服务,即可生效。

🔌 WebSocket(uvicorn + channels)

pip install 'uvicorn[standard]' channels channels_redis
# asgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from django.urls import re_path
from app import consumers

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Assetinventory.settings')
application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
        URLRouter([re_path("msg/chat/", consumers.MyConsumer.as_asgi())])
    ),
})
# consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer

class MyConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_group_name = "meet"
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.room_group_name, self.channel_name)

    async def chat_message(self, event):
        await self.send(text_data=json.dumps({'message': event['message']}))

外部向组发消息:在 connect 里把 channel 加入组后,调用:

from channels.layers import get_channel_layer

async def send_message_to_group(room_group_name, message):
    channel_layer = get_channel_layer()
    await channel_layer.group_send(
        room_group_name,
        {'type': 'chat_message', 'message': message}
    )

settings.py 使用 Redis 作为 channel layer:

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {"hosts": [("127.0.0.1", 6379)]},
    },
}

🔄 前后端交互

FormData · fetch · JsonResponse。阻止表单默认提交并提交 JSON。

前端阻止表单默认提交,收集为 JSON 后 fetch 提交:

function modifyx(data, url) {
    // 发送 POST 请求
    fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
    })
        .then(response => response.json())
        .then(responseData => {
            showmsg("修改成功");
        })
        .catch((error) => {
            showmsg("修改失败");
        });
}

document.getElementById('dynamic-form').addEventListener('submit', function (event) {
    event.preventDefault(); // 阻止表单默认提交行为

    // 获取表单数据
    const formData = new FormData(this);
    const url = this.action;
    const data = {};
    formData.forEach((value, key) => {
        data[key] = value;
    });
    modifyx(data, url);
});

//=====================================
function submit(showlog = true) {
    const form = document.getElementById('dynamic-form');
    const formData = new FormData(form);
    const url = form.action;
    const data = {
        "key": 'task',
    };
    formData.forEach((value, key) => {
        data[key] = value;
    });
    modifyx(data, url, showlog);
}

后端接收并返回 JsonResponse

import json
from django.http import JsonResponse

def saveaiconf(request):
    info = json.loads(request.body)
    remark = info.get("remark")
    return JsonResponse({"success": '更新成功'})

🧰 其他技巧

iframe 不允许渲染

X-Frame-Options 响应头用来给浏览器设置一个页面可否在 <frame><iframe> 中展现,有三个属性:

含义
deny 页面不允许在 iframe 中展现,相同域名嵌套也不允许
sameorigin 允许在相同域名嵌套展示
allow-from uri 允许指定源的 iframe 展示,即白名单

直接执行 Django 程序(Debug)

import os
import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tech.settings')
django.setup()