2-2 Project
3 Web 应用程序
使用 Django 创建 简单的 Web 应用程序
3.1 Init
- 创建并激活虚拟环境 ```shell # 创建名为 ll_venv 的虚拟环境
约 1980 个字 739 行代码 预计阅读时间 16 分钟
python3 -m venv [name]
# 激活虚拟环境
source ll_venv/bin/activate
# 退出虚拟环境
deactivate
```
-
安装并创建 Djangle 项目(虚拟环境中)
# 安装 Djangle pip3 install django # 创建名为 learning_logd 的项目(不要忘记最后的英文句号) django-admin startproject learning_log .
项目结构如下:
-
创建数据库
Djangle 将大部分与项目相关的信息存储在数据库中
-
启动项目
-
创建应用程序
每次对单个应用进行修改都应该遵循以下顺序:
修改
models.py
-> 调用makemigrations
-> 使用migrate
进行迁移Django 项目由一系列应用程序构成
-
定义模型
模型用于告诉 Django 如何处理应用程序中的数据(类似于 Schema)
在代码层面,模型就是一个 Class,包含属性和方法
# @ models.py from django.db import models # 用于存储学习主题的模型 (继承自 Model) class Topic(models.Model): # 其他字段类型见 Django Model File Reference text = models.CharField(max_length=20) # 自动设置当前日期时间作为 date_added date_added = model.DateTimeField(auto_now_add=True) # 模型的简单表示(类似于 print 的默认结果?) def __str__(self): # 此处直接返回 text 表示该模型实例 return self.text
-
激活模型
-
只有在
settings.py
中被包含的模型才可以正常使用自定义应用排在默认应用前起到 覆盖默认行为 的作用
-
让 Django 修改数据库,使之能够存储与 Topic 相关的信息
-
-
3.2 Django Admin Site
本步骤用于建立后台管理页面,并使用 Topic 模型来添加一些主题
- Django 提供的 Admin Site 让你能够轻松的处理 models(只允许管理员而非普通用户使用)
-
默认通过
http://127.0.0.1:8000/admin
进行访问(需要登陆捏) -
创建超级用户
-
向管理网站注册模型
Django 自动在后台网页中添加了一些模型(如 User / Group),但我们必须手动注册自定义模型
-
添加 Topic
单击 Topics 进入主题页面,进一步单击 Add,输入文本后点击 Save 即可
-
定义 Entry 模型
多个 Entry 可以对应同一 Topic
# @ models.py # 用于记录 Topic 下的具体条目 class Entry(models.Model): # 当 topic 删除时进行级联删除 topic = models.ForeinKey(Topic, on_delete=models.CASCADE) # 长度不受限制 text = models.TextField() date_added = models.DateTimeField(auto_now_add=True) # 有必要时,使用 Entries(而非 Entrys)表示多个条目信息 class Meta: verbose_name_plural = 'entries' def __str__(self): # 返回前 50 个字符 return f"{self.text[:50]}..."
-
迁移并注册
使用 Django Shell 查看数据:
from app_name.models import Topic
# 获取所有的 Topic 实例,返回一个 List
topics = Topic.objects.all()
# 遍历列表
for topic in topics:
print(topic.id, topic)
# 通过 id 获取指定对象并查看属性
t = Topic.objects.get(id=1)
# 访问具体属性
t.text
t.date_added
# 获取以其作为外间的所有 entry
t.entry_set.all()
通过外键关系获取数据
基本格式: 被引用实例.引用类_set.all()
3.3 创建主页
3.3.1 映射 URL
描述了如何匹配浏览器请求与返回的页面
此处,我们将默认路径 http://localhost:8000/
映射到“学习笔记”主页
-
打开
/learn_log/urls.py
,做如下修改: -
在
/learnings
下新建该模块的urls.py
文件 -
path("匹配路径", 需要调用的 views.py 中的函数, "本模式名称")
「模式名称」使得我们可以在代码的其他位置使用 name 而非 URL 引用这个映射关系
3.3.2 编写视图
「视图函数」接受请求中的信息,准备生成页面所需的数据,并将其返回给浏览器
# @ /learnings/views.py
from django.shortcuts inport render
# 创建视图
def index(request):
return render(request, 'learnings/index.html')
3.3.3 编写模版
模版可以访问视图中的任何数据
-
新建路径
learnings/templates/learnings
什么迷幻套娃
-
在新建路径中创建
index.html
3.4 创建更多页面
3.4.1 模版继承
一些通用元素
-
定义「父模版」
- 「模版标签」
{%%}
-
url namespace:name
用于生成一个 URL(匹配对应命名空间下的 name 映射模式)此处的命名空间由上文中的
app_name
决定 -
子模版不需要定义父模版中预留的 所有插槽 一对
{%block name%}{%endblock name%}
定义的是一个「具名插槽」,在子模版中按照对应 name 声明内容即可嵌入至指定位置
- 「模版标签」
-
定义「子模版」
- 继承对象必须在 首行 用
extends
指出
- 继承对象必须在 首行 用
3.4.2 用于显示所有 Topic 的页面
-
在
learnings/urls.py
中添加新的 URL -
创建对应的视图函数
-
编写模版
# @ learnings/templates/learnings/topics/html # 继承 {% extends 'learnings/base.html' %} # 插槽 content 内容 {% block content %} <h2>TOPICS are as follows:</h2> <ul> # 访问的是 context 字典中的 topics 分量 {% for t in topics %} <li> # topic 的详情界面 <a href="{% url 'learnings:topic' t.id %}"> {{ t }} </a> </li> # 显示 __str__ 的返回值 {% empty %} # topics 数组为空的情景 <li>Sorry, 尚未添加任何 Topic</li> {% endfor %} </ul> {% endblock content %}
3.4.3 用于显示单个 Topic 下所有 Entry 的页面
-
新增 URL
-
新增视图
-
新增模版
|
表示「模版过滤器」=> 实际输出的是竖线后的内容date:'M d, Y H:i'
用于显示形如Januart 1,2018 23:00
的时间戳linebreaks
用于显示完整内容(而不是__str__
返回的前 50 字)
3.4 用户管理
3.4.1 支持添加 Topic
需要导入包含表单的模块
forms.py
-
用于新建 Topic 的表单
-
新增 URL
-
创建视图函数
需要处理两种情况:刚进入页面(空表单)/ 提交表单(重定向至 topics 页面)
前者发送 GET 请求,而后者发送 POST 请求,可以通过
request.method
进行区分is_valid()
用于验证用户填写了所有 必填字段,且输入与要求的字段类型一致save()
用于将指定的表单内容保存至数据库
from django,shortcuts import redirect from .forms import TopicForm def new_topic(request): if request.method != 'POST': # 未提交,新建空表单 form = TopicForm() else: # 处理提交的数据 form = TopicForm(data=request.POST) if form.is_valid(): form.save() return redirect('learnings:topics') # 显示空表单 / 指出数据无效 context = {'form': form} return render(request, 'learnings/new_topic.html', context)
-
创建新的模版
- action 指出该表单将发送给 view 中的
new_topic()
{% csrf token %}
用于防范跨站点请求伪造攻击-
{{ form.as_p }}
用于自动创建表单所需的全部字段as_p
表示以段落格式渲染所有的表单元素但 Django 不会自动创建提交按钮
- action 指出该表单将发送给 view 中的
-
将 topics 界面连接到 new_topic 界面
3.4.2 支持添加新的 Entry
-
创建表单 EntryForm
-
添加 URL
new_entry
-
添加视图函数
def new_entry(request, topic_id): topic = Topic.objects.get(id=topic_id) if request.method != 'POST': form = EntryForm() else: form = EntryForm(data=request.POST) if form.is_valid(): # 阻止提交(还没指定 Topic) new_entry = form.save(commit=False) # 手动指定 Topic new_entry.topic = topic new_entry.save() return redirect('learnings:topic', topic_id=topic_id) context = { 'topic': topic, 'form': form } return render(request, 'learnings/new_entry.html', context)
-
编写模版
-
连接到 topic 详情页面
3.4.3 支持编辑 Entry
-
新增 URL
-
视图函数
def edit_entry(request, entry_id): entry = Entry.objects.get(id=entry_id) topic = entry.topic if request.method != 'POST': # 读取旧实例信息 form = EntryForm(instance=entry) else: # 用同一个实例存储新的信息 form = EntryForm(instance=entry, data=request.POST) if form.is_valid(): form.save() return redirect('learnings:topic', topic_id=topic.id) context = { 'entry': entry, 'topic': topic, 'form': form } return render(request, 'learnings/edit_entry.html', context)
-
模版
-
链接到 topic 页面
3.4.4 创建普通账号
使用 Django 自带的用户身份验证系统完成,修改 Topic 使其归属于创建者账户
- 创建新的 app
users
- 将新 app 包含至项目
- 添加新的 URL
3.4.4.1 登录页面
-
定义 URL
- 默认的身份认证 URL 包含了
login
,logout
,对应的请求将直接发送给默认视图函数(不用写哩)
- 默认的身份认证 URL 包含了
-
编写模版
- 注意三级路径是
registraion
(不是同名的users
)
# @ users/templates/registration/login.html # 继承 learnings 中定义的 base 模版 {% extends 'learnings/base.html' %} # 插槽内容 {% block content %} {% if form.errors %} <p>用户名或密码错误,请重试</p> {% endif %} <form method='post' action="{% url 'users:login' %}"> {% csrf_token %} {{ form.as_p }} <button name='submit'>登录</button> # 隐藏元素用于告知 Django 登录成功后的重定向地址(返回 index) <input type='hidden' name='next' value='{% url "learnings:index" %}'/> </form> {% endblock content %}
- 注意三级路径是
-
链接到 base 模版
- 已登陆情况下不再显示登录入口 —— 用 IF 包裹
在 Django 的用户认证系统中,所有页面 都可以 访问
user
变量。其中is_authenticated
是一个 Boolean 类型的属性,用于判断用户是否已经登录
3.4.4.2 注销页面
-
在 base 模版中添加注销入口
只是在“已登陆”的情况下添加一个超链接
-
编写模版(注意名字)
3.4.4.3 注册页面
- 新建 URL
- 新建视图函数
from django.contrib.auth import login # 表单用的是自带的 from django.contrib.auth.forms import UserCreationForm def register(request): if request.method != 'POST': form = UserCreationForm() else: form = UserCreationForm(data=request.POST) if form.is_valid(): new_user = form.save() # 自动登录 login(request, new_user) # 重定向至首页 redirect('learnings:index') # 显示空表单 / 表单无效 context = { 'form': form } return render(request, 'registration/register.html', context)
- 创建模版
# @ users/templates/registration/register.html # 继承 base {% block content %} <h2>这是注册页面</h2> <form method='post' action="{% url 'users:register' %}"> {% csrf_token %} {{ form.as_p }} <button name='submit'>注册</button> <input type='hidden' name='next' value='{% url "learnings:index" %}'/> </form> {% endblock content %}
-
联系到 base 模版
完善未登录分支的逻辑
好像没办法正确重定向到 index,淦!
3.4.4 访问限制
-
只允许登录用户访问 Topics 页面
使用装饰器
@login_required
=> 不登录直接访问会报 404 -
将未登录访问操作重定向至登录页面
理论上除了 登录/注册/主页 以外的所有页面都应该加访问限制的装饰器
3.4.5 关联用户与数据
-
修改 Topic 模型
-
迁移数据库
把现有主题全都关联到 admin(不知道亲妈,开始摆烂)
-
仅允许用户访问自己拥有的 Topic
-
保护单个主题页面
目前只做了登录限制,但是记住 topic_id 仍可以越权访问其他用户创建的 topic
-
保护 edit_entry
同理,在
edit_entry
里判断一下就行 -
将新建 Topic 关联至当前用户
3.5 样式设计
使用
django-bootstrap4
创建响应式页面
- 安装
django-bootstrap4
- 引入
3.5.1 修改 base
直接推倒重来哩
-
定一个标题,完善 bootstrap 所需信息
-
定义 navBar
为了移动端适配,这里分三块定义 navBar
<body> <nav class='navbar navbar-expend-md navbar-light bg-light mb-4 border'> # 1. 当平时的 Logo 主页链接用的 <a class='navbar-brand' href="{% url 'learnings:index'%}"> 学习笔记 </a> # 2. 一个折叠菜单 <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expend="false" aria-label="Toggle navigation" > <span class="navbar-toggler-icon"></span> </button> # 3. 菜单选项(们) <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a class="nav-link" href="{% url 'learnings:topics' %}"> 全部主题 </a> </li> </ul> </div> # 4. 登录相关的组件 <ul class="navbar-nav ml-auto"> {% if user.is_authenticated %} <li class="nav-item"> <span class="navbar-text">Hello, {{ user.username }}.</span> </li> <li class="nav-item"> <a class="navbar-link" href="{% url 'users:logout' %}">登出</a> </li> {% else %} <li class="nav-item"> <a class="navbar-link" href="{% url 'users:register' %}">注册</a> </li> <li class="nav-item"> <a class="navbar-link" href="{% url 'users:login' %}">登录</a> </li> {% endif%} </ul> </nav> </body>
-
定义 content 部分的样式
3.5.2 修改 index
使用
jumbotron
元素修改主页样式(给一些操作提示)
# 继承
{% block page_header %}
<div class="jumbotron">
<h1 class="display-3">
记录你的学习过程
</h1>
<p class="lead">
Make your owen Learning Log, and keetp a list of the topics you're learning about. Whenever you learn something new about a topic, make an entry summarizing what you've learned.
</p>
<a class="btn btn-lg btn-primary" href="{% url 'users:register' %}">
霍霍,快来注册啊!»
</a>
</div>
{% endblock page_header %}
3.5.3 修改 login
# 继承
{% load bootstrap4 %}
{% block page_header %}
<h2>登录到你的账户</h2>
{% endblock page_header %}
{% block content %}
<form method='post' action="{% url 'users:login' %}" class="form">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button name='submit'>登录</button>
{% endbuttons%}
<input type='hidden' name='next'
value='{% url "learnings:index" %}'/>
</form>
{% endblock content %}
3.5.4 修改 topics
# 继承
{% block page_header %}
<h1>主题列表</h1>
{% endblock page_header %}
{% block content %}
<ul>
{% for t in topics %}
<li>
<h3>
<a href="{% url 'learnings:topic' t.id%}">{{ t }}</a>
</h3>
</li>
{% empty %}
<li>
<h3>
暂未添加任何主题捏
</h3>
</li>
{% endfor %}
</ul>
<h3>
<a href="{% url 'learnings:new_topic' %}">新增主题</a>
</h3>
{% endblock content %}
3.5.5 修改 topic
# 继承
{% block page_header %}
<h1>主题:{{ topic }}</h1>
{% endblock page_header %}
{% block content %}
<p>
<a href="{% url 'learnings:new_entry' topic.id%}">新建条目</a>
</p>
{% for e in entries %}
<div class="card mb-3">
<h4 class="card-header">
{{ e.date_added|date:'M d, Y H:i' }}
<small>
<a href="{% url 'learnings:edit_entry' e.id %}">
编辑
</a>
</small>
</h4>
<div class="card-body">
{{ e.text|linebreaks }}
</div>
</div>
{% empty %}
<p>抱歉,该主题下还没有条目捏</p>
{% endfor %}
</ul>
{% endblock content %}
3.6 项目部署
部署到
Heroku
,这是一个基于 Web 的平台
- 注册 Heroku 账号
- 安装 Heroku CLI
- 安装依赖
- 创建文件
requirements.txt
- 指定 Python 版本
- 修改
settings.py
-
创建 Procfile
告诉 Heroku 应该启动哪些进程
-
初始化 Git 仓库
-
将项目推送至 Heroku
-
在 heroku 上建立数据库
-
在 heroku 中创建 admin
-
创建友好的 URL
-
关闭调试信息
-
提交并推送修改
-
在 heroku 上设置环境变量(不然上面的限制就没有意义了)
-
从 heroku 删除项目
可以直接登录 heroku 进行删除,也可以使用 bash 命令
自定义错误页面
- 创建模版
- 修改
settings.py
用户试图用 id 访问不存在的 topic / entry 将导致 500 错误(因为没有足够的信息)
事实上,将其视为 404 错误会更加合理
=> 我们使用 get_object_or_404
来解决