本文描述了 Django 模板系统,它是如何工作的以及如何扩展它。 并且实践了其应用设计。
Django 的模板语言旨在在功能和易用性之间取得平衡。 它旨在让习惯使用 HTML 的人感到舒适。 如果读者接触过其他基于文本的模板语言,例如Jinja2,那么您应该对 Django 的模板感到宾至如归。
若开发出易于维护的程序,关键在于编写形式简洁且结构良好的代码。 Django
视图函数的作用,即处理请求的响应。 Django
把请求分发到处理请求的视图函数或继承View
视图类的HTTP
方法上。如果其需要访问数据库,然后生成响应回送浏览器。这里有两个过程,分别称为业务逻辑
和表现逻辑
。把业务逻辑
和表现逻辑
混在一起会导致代码难以理解和维护。把表现逻辑
移到模板中能够提升程序的可维护性。
模板是一个包含响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文中才能知道。使用真实值替换变量,再返回最终得到的响应字符串,将这一过程称为渲染。为了渲染模板,Django 使用了强大的模板引擎。
以之前的城市十大景点案例为本次的项目实践,在其基础上,创建cityspot
App,创建templates
文件夹与static
文件夹。并且在static
文件夹内,创建css
、img
以及js
子文件夹,用来存放所需要的样式CSS文件
、图片文件
和JavaScript文件
。到现在为止,创建的文件夹结构如下:
scenic_spot/
| -- cityspot/
| | -- migrations/
| | + -- __init__.py
| | -- __init__.py
| | -- apps.py
| | -- models.py
| | -- forms.py
| | -- views.py
| + -- urls.py
| -- static/
| | -- css/
| | -- img/
| | -- js/
| -- templates/
| -- scenic_spot.py
+ -- urls.py
在scenic_spot.py
文件中,加入下面的基本设置,同时把新建的cityspot
应用App加到INSTALLED_APPS
设置里,需要用到的内置应用App包含:django.contrib.auth
、django.contrib.contenttypes
、django.contrib.staticfiles
以及widget_tweaks
。清理不必要的信息项:
...
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
settings.configure(
...
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.staticfiles',
'widget_tweaks',
'cityspot',
],
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
MIDDLEWARE=(
'django.middleware.common.CommonMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
),
ROOT_URLCONF = 'urls',
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
}
},
STATIC_URL = '/static/',
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
],
)
找到的 TEMPLATES
变量中,并将 DIRS
键设置为 os.path.join(BASE_DIR, 'templates')
, 而BASE_DIR
表达为当前项目所在的基路径。基本上这项配置所做的是找到项目目录的完整路径并将“/templates”
附加到它。
一个 Django 项目可以配置一个或多个模板引擎。 Django 为其自己的模板系统(创造性地称为 Django 模板语言 (DTL))。通过设置BACKEND
使用内建的模板引擎"django.template.backends.django.DjangoTemplates"
,内建的模板引擎还有一个jinja2
引擎,全路径名为:"django.template.backends.jinja2.Jinja2"
。也可以使用非Django自带的引擎。
配置中的STATIC_URL
定义静态文件的相对路径。而STATICFILES_DIRS
则定义静态文件应用程序将遍历的其他位置。
其中的widget_tweaks
内置应用App,需要独立安装模块django_widget_tweaks
。安装命令如下:
pip install django_widget_tweaks
通常一个网站是从局部基础模板搭建起来的,其包含一些通用布局,在整个项目中都将被用到,布局设计为每个页面创建基础结构,对于城市十大景点的应用设计,考虑创建的基础模板为base.html
,将它放于当前项目的templates
的目录下,布局如下:
{% load static %}<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="icon" href="{% static 'img/favicon.ico' %}">
<title>{% block title %} 爱校码 {% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
{% block stylesheet %}{% endblock %}
</head>
<body>
{% block body %}
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'cityspot:home' %}">
<img src="{% static 'img/logo.png' %}" alt="爱校码" style="width:40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainMenu">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'cityspot:home' %}">城市列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'cityspot:newcity' %}">新增城市</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<ol class="breadcrumb my-4">
{% block breadcrumb %}
{% endblock %}
</ol>
{% block content %}
{% endblock %}
</div>
{% endblock body %}
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
{% block javascript %}{% endblock %}
</body>
</html>
以上基础模板需要一些静态文件标记加入其中,主要有CSS
、img图片
以及相关js文件
。在基础模板中主要 包含Bootstrap
布局设计,包含的静态文件有:bootstrap.min.css
、bootstrap.min.js
以及需要的jquery
相关文件,还有图片的favicon.ico
与logo.png
文件。在基模板中,使用静态模板标签使用配置中STATIC_URL
与STATICFILES_DIRS
为给定的相对路径构建 URL。
{% load static %}
<link rel="icon" href="{% static 'img/favicon.ico' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<img src="{% static 'img/logo.png' %}" alt="爱校码" style="width:40px;">
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
基础模板作为一个 母板页,其他模板添加它所独特的部分。每个要创建的模板都 extend
(继承) 这个特殊的模板。其中{% block 标识名 %}
与{% endblock %}
标签,它用于在模板中保留一个空间,“子”模板
(扩展母版页)可以在该空间中插入代码和 HTML。在{% block title %}
中设置了一个默认值"爱校码"
,如果在子模板中未设置 {% block title %}
的值,它就会被使用,也可以被子模板继承。在本例基础模板中,定义了标识名为 title
、stylesheet
、body
、breadcrumb
、content
和javascript
的块(block)。
{% block title %} ... {% endblock %}
{% block stylesheet %}{% endblock %}
{% block body %}
...
{% block breadcrumb %}
{% endblock %}
...
{% block content %}
{% endblock %}
...
{% endblock body %}
...
{% block javascript %}{% endblock %}
现在来重构的几个扩展模板为:home.html
、 city_spots.html
、 new_city.html
、new_spot.html
。
templates/home.html
{% extends 'base.html' %}
{% block breadcrumb %}
<li class="breadcrumb-item active">城市旅游景点</li>
{% endblock %}
{% block content %}
<table class="table">
<thead class="thead-inverse">
<tr>
<th>城市名</th>
<th>描述</th>
</tr>
</thead>
<tbody>
{% for city in citys %}
<tr>
<td>
<a href="{% url 'cityspot:city_spots' city.pk %}">{{ city.name }}</a>
</td>
<td class="align-middle">
{{ city.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
home.html
模板中的第一行是{% extends 'base.html' %}
。 这个标签告诉 Django 使用 base.html
模板作为母版页。 之后,使用{% block %}
标签来放置页面的独特内容。独特内容块的标识名为:breadcrumb
、content
。没有涉及的块则沿用基础模板的块内容不变。
在本例扩展模板的content
块内,使用了{% for %}
标签的循环控制结构:
{% for city in citys %}
...
{% endfor %}
其需求是在模板中渲染一组元素,展示了使用 for 循环实现这一需求。而在其中,{{ city.name }}
与{{ city.description }}
表示一种双大括号表达式结构,它是一种特殊的占位符,告诉模板引擎这个位置的值从渲染模板时使用的变量数据中获取。
另外,在本案例扩展模板使用了{% url %}
标签消除对 url 模式
配置中定义的特定 URL 路径的依赖:
{% url 'cityspot:city_spots' city.pk %}
其工作方式是查找在 cityspot.urls 文件中指定的 URL模式定义。cityspot:city_spots
表示cityspot应用命名空间下定义的city_spots模式名;city.pk
代表提供的id参数。在后续的cityspot.urls 的定义中会体现出来。
templates/city_spots.html
{% extends 'base.html' %}
{% block title %} {{ city.name }} - {{ block.super }} {% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'cityspot:home' %}">城市列表</a></li>
<li class="breadcrumb-item active">{{ city.name }}</li>
{% endblock %}
{% block content %}
<div class="mb-4">
<a href="{% url 'cityspot:newspot' city.pk %}" class="btn btn-primary" role="button">新添景点</a>
</div>
{% for spot in spots %}
<div class="card mb-2 ">
{% if forloop.first %}
<div class="card-header text-white bg-dark py-2 px-3">页 {{ page }}</div>
{% endif %}
<div class="card-body p-3">
<div class="row">
<div class="col-12">
<div class="row mb-3">
<div class="col-6">
<strong class="text-muted">{{ spot.name }}</strong>
</div>
<div class="col-6 text-right">
<small class="text-muted">{{ spot.description}}</small>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
{% if spots.has_other_pages %}
<nav aria-label="景点分页" class="mb-4">
<ul class="pagination">
{% if spots.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ spots.previous_page_number }}">前页</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">前页</span>
</li>
{% endif %}
{% for page_num in spots.paginator.page_range %}
{% if topics.number == page_num %}
<li class="page-item active">
<span class="page-link">
{{ page_num }}
<span class="sr-only">(current)</span>
</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% endif %}
{% endfor %}
{% if spots.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ spots.next_page_number }}">后页</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">后页</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}
同样,本案例模板第一行标签表示继承base.html
模板作为母版页。独特内容块的标识名为:title
、breadcrumb
、content
。
在title
的标识块中,除了加上本模板的标题外,还使用了{{ block.super }}
来继承父模板的标题部分。
在本例扩展模板的content
标识块内,除了使用{% for %}
标签的循环控制结构,还使用了{% if %}
条件控制结构:
{% if ... %}
...
{% else %}
...
{% endif %}
在本模板的底部,使用了 Bootstrap 4 分页组件正确呈现页面,实现了Django分页器类
来帮助管理分页数据 ——也就是说,数据被分割在多个页面上,并带有 “前页/后页” 的链接。
templates/new_city.html
{% extends 'base.html' %}
{% block title %}开启一个新城市 - {{ block.super }} {% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'cityspot:home' %}">城市列表</a></li>
<li class="breadcrumb-item active">新建城市</li>
{% endblock %}
{% block content %}
<form method="post" novalidate>
{{ form.as_p }}
<button type="submit" class="btn btn-success">发布</button>
<a href="{% url 'cityspot:home' %}" class="btn btn-outline-secondary" role="button">取消</a>
</form>
{% endblock %}
templates/new_spot.html
{% extends 'base.html' %}
{% block title %} 开启一个新景点 - {{ block.super }} {% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'cityspot:home' %}">城市列表</a></li>
<li class="breadcrumb-item"><a href="{% url 'cityspot:city_spots' city.pk %}">{{ city.name }}</a></li>
<li class="breadcrumb-item active">新建景点</li>
{% endblock %}
{% block content %}
<form method="post" novalidate>
{{ form.as_p }}
<button type="submit" class="btn btn-success">发布</button>
<a href="{% url 'cityspot:city_spots' city.pk %}" class="btn btn-outline-secondary" role="button">取消</a>
</form>
{% endblock %}
在以上两个模板中,除了继承基模板之外,在title
的标识块内,也使用了{{ block.super }}
继承父模板标题。在breadcrumb
标识块内,使用了{% url %}
标签指令实现导航链接。在content
标识块内,使用了{{ form.as_p }}
来渲染表单实例form
到模板中,即只需将表单实例放到模板的上下文中即可。如果表单在上下文中叫 form
,那么 {{ form }}
将渲染它相应的 <label>
和 <input>
元素。在表单渲染时,表单的输出不包含外层 <form>
标签以及 submit
控件,这些必须由本模板自己提供。
另外,{{ form.as_p }}
还有其他选项,含义如下:
<p>
标记中;<tr>
标记中的表格单元格中;<li>
标记中;scenic_spot/urls:
from django.urls import path, include
urlpatterns = [
path('cityspot/',include(('cityspot.urls','cityspot'),namespace='cityspot')),
]
scenic_spot/cityspot/urls
from django.urls import re_path
from . import views
urlpatterns = [
re_path(r'^$', views.CitysView.as_view(),name='home'),
re_path(r'^newcity$', views.CityView.as_view(),name='newcity'),
re_path(r'^addcity$', views.CityView.as_view(),name='addcity'),
re_path(r'^citys/(?P<id>\d+)$', views.SpotsView.as_view(), name='city_spots'),
re_path(r'^citys/(?P<id>\d+)/newspot$', views.SpotView.as_view(), name='newspot'),
re_path(r'^addspot$', views.SpotView.as_view(),name='addspot'),
]
from django.db import models
class City(models.Model):
name = models.CharField(max_length=30, unique=True)
description = models.CharField(max_length=100)
def __str__(self):
return self.name
def to_json(self):
json_city = {
'id': self.id,
'name': self.name,
'description': self.description
}
return json_city
class Spot(models.Model):
name = models.CharField(max_length=30, unique=True)
description = models.CharField(max_length=100)
city = models.ForeignKey(City, related_name='spots',on_delete=models.CASCADE)
def __str__(self):
return self.name
def to_json(self):
json_spot = {
'id': self.id,
'name': self.name,
'description': self.description,
'city': self.city.to_json()
}
return json_spot
数据迁移
python scenic_spot.py makemigrations
python scenic_spot.py migrate
这里有两个表单类CityForm
、SpotForm
。它们在forms.py
文件内完成。
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import City, Spot
class CityForm(forms.ModelForm):
name = forms.CharField(
label=_('名称'),
required=True,
error_messages={'required':'这是必填栏。'},
widget=forms.TextInput(attrs={'class': 'form-control'}))
description = forms.CharField(
label=_('说明'),
required=True,
error_messages={'required':'这是必填栏。'},
widget=forms.TextInput(attrs={'class': 'form-control'}))
class Meta:
model = City
fields = ['name', 'description']
class SpotForm(forms.ModelForm):
name = forms.CharField(
label=_('名称'),
required=True,
error_messages={'required':'这是必填栏。'},
widget=forms.TextInput(attrs={'class': 'form-control'}))
description = forms.CharField(
label=_('说明'),
required=False,
widget=forms.TextInput(attrs={'class': 'form-control'}))
city = forms.ModelChoiceField(
label=_('城市'),
queryset=City.objects.all().order_by('id'),
required=True,
error_messages={'required':'这是必填栏。'})
class Meta:
model = Spot
fields = ['name', 'description', 'city']
这里涉及CitysView
、CityView
、SpotsView
、SpotView
的设计,这些类的设计在views.py
文件内完成。
from django.shortcuts import render, get_object_or_404, redirect
from django.views import View
from django.core.paginator import Paginator,PageNotAnInteger,EmptyPage
from .models import City, Spot
from .forms import CityForm, SpotForm
class CitysView(View):
def get(self, request):
citys = City.objects.all()
return render(request, 'home.html', {'citys': citys })
class SpotsView(View):
def get(self,request,id):
city = get_object_or_404(City, pk=id)
queryset = city.spots.order_by('pk')
page = request.GET.get('page', 1)
paginator = Paginator(queryset, 5)
try:
spots = paginator.page(page)
except PageNotAnInteger:
# 回退到第一页
spots = paginator.page(1)
except EmptyPage:
# 添加页码不存在,回退到最后一页
spots = paginator.page(paginator.num_pages)
return render(request, 'city_spots.html', {'spots':spots,'city':city, 'page':page })
class CityView(View):
def get(self, request):
form = CityForm()
return render(request, 'new_city.html', {'form': form })
def post(self, request):
form = CityForm(request.POST)
if form.is_valid():
city = form.save(commit=False)
city.save()
return redirect('cityspot:home')
else:
return render(request, 'new_city.html', {'form': form })
class SpotView(View):
def get(self, request, id):
city = get_object_or_404(City, pk=id)
data = {'name': '', 'description': '', 'city': city}
form = SpotForm(data)
return render(request, 'new_spot.html', {'form': form, 'city': city })
def post(self, request,id):
form = SpotForm(request.POST)
city = get_object_or_404(City, pk=id)
if form.is_valid():
spot = form.save(commit=False)
spot.save()
return redirect('cityspot:city_spots',id)
else:
return render(request, 'new_spot.html', {'form': form, 'city': city })
博文最后更新时间: