本文描述了 Django 模板系统,它是如何工作的以及如何扩展它。 并且实践了其应用设计。
                 爱校码
Django 的模板语言旨在在功能和易用性之间取得平衡。 它旨在让习惯使用 HTML 的人感到舒适。 如果读者接触过其他基于文本的模板语言,例如Jinja2,那么您应该对 Django 的模板感到宾至如归。
若开发出易于维护的程序,关键在于编写形式简洁且结构良好的代码。 Django视图函数的作用,即处理请求的响应。 Django 把请求分发到处理请求的视图函数或继承View 视图类的HTTP方法上。如果其需要访问数据库,然后生成响应回送浏览器。这里有两个过程,分别称为业务逻辑和表现逻辑。把业务逻辑和表现逻辑混在一起会导致代码难以理解和维护。把表现逻辑移到模板中能够提升程序的可维护性。
模板是一个包含响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文中才能知道。使用真实值替换变量,再返回最终得到的响应字符串,将这一过程称为渲染。为了渲染模板,Django 使用了强大的模板引擎。
以之前的城市十大景点案例为本次的项目实践,在其基础上,创建cityspotApp,创建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 })
           
       
   博文最后更新时间: