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.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
]
坑:{% url %} 与命名空间
使用带命名空间的写法(如 {% url 'fun:login_mysite' %})时,对应 app 必须指定 app_name,否则报错。
🧩 模板¶
遍历字典¶
{% 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:
修改后台标题¶
把 Django 后台左上角的「Django 管理」改为「公共服务」,需要覆盖默认 admin 模板:
- 在项目主目录(与
manage.py同级)创建templates文件夹。 - 在其中创建
admin文件夹,用于覆盖 Django admin 的默认模板。 - 在
admin下创建base_site.html。Django admin 会首先搜索这个文件,找到就用它替代默认模板。 -
在
base_site.html中继承admin/base.html,覆盖branding块: -
让项目知道新的
templates文件夹位置,在settings.py的TEMPLATES.DIRS中包含它:
🧹 自定义过滤器¶
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 |
🧅 中间件¶
- 在具体 app 里新建
middle文件夹。 - 创建中间件代码。
- 在
settings.py的MIDDLEWARE中注册。
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 |
是否为空 |
创建模型并注册管理:
# 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、django开头的表,并删除 app 下的全部migrations。
🔐 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 作缓存后端。
环境准备:
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。
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:
启动 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
定时任务¶
# 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(分组);delay 是 apply_async 的快捷封装,都返回 AsyncResult(ready() / get(timeout=) / traceback)。
⚡ 异步支持¶
uvicorn · async def · transaction.atomic。ASGI 启动与异步事务。
用 Uvicorn 启动 ASGI 组件:
# 异步视图
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)¶
# 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 展示,即白名单 |