18-2 简短的条目 :当前, Django 在管理网站或 shell 中显示 Entry 实例时,模型Entry 的方法 __str__() 都在它的末尾加上省略号。请在方法 __str__() 中添加一条 if 语句,以便仅在条目长度超过 50 字符时才添加省略号。使用管理网站来添加一个长度少于 50 字符的条目,并核实显示它时没有省略号。
18-4 比萨店 :新建一个名为 pizzeria 的项目,并在其中添加一个名为 pizzas 的应用程序。定义一个名为 Pizza 的模型,它包含字段 name ,用于存储比萨名称,如 Hawaiian 和 Meat Lovers 。定义一个名为 Topping 的模型,它包含字段 pizza和 name ,其中字段 pizza 是一个关联到 Pizza 的外键,而字段 name 用于存储配料,如 pineapple 、 Canadian bacon 和 sausage 。
向管理网站注册这两个模型,并使用管理网站输入一些比萨名和配料。使用 shell 来查看你输入的数据。
18-6 比萨店主页 :在你为完成练习 18-4 而创建的项目 Pizzeria 中,添加一个主页。
18-8 比萨店页面 :在练习 18-6 中开发的项目 Pizzeria 中添加一个页面,它显示供应的比萨的名称。然后,将每个比萨名称都设置成一个链接,单击这种链接将显示一个页面,其中列出了相应比萨的配料。请务必使用模板继承来高效地创建页面。
19-1 博客 :新建一个 Django 项目,将其命名为 Blog 。在这个项目中,创建一个名为 blogs 的应用程序,并在其中创建一个名为 BlogPost 的模型。这个模型应包含 title 、 text 和 date_added 等字段。为这个项目创建一个超级用户,并使用管理网站创建几个简短的帖子。创建一个主页,在其中按时间顺序显示所有的帖子。
创建两个表单,其中一个用于发布新帖子,另一个用于编辑既有的帖子。
尝试填写这些表单,确认它们能够正确地工作。
19-2 博客账户 :在你为完成练习 19-1 而开发的项目 Blog 中,添加一个用户身份验证和注册系统。让已登录的用户在屏幕上看到其用户
名,并让未注册的用户看到一个到注册页面的链接。
19-3 重构 :在 views.py 中,我们在两个地方核实主题关联到的用户为当前登录的用户。请将执行这种检查的代码放在一个名为 check_topic_owner() 的函数中,并在恰当的地方调用这个函数。
19-4 保护页面 new_entry :一个用户可在另一个用户的学习笔记中添加条目,方法是输入这样的 URL ,即其中包含输入另一个用户的主题的 ID 。为防范这种攻击,请在保存新条目前,核实它所属的主题归当前用户所有。
19-5 受保护的博客 :在你创建的项目 Blog 中,确保每篇博文都与特定用户相关联。确保任何用户都可访问所有的博文,但只有已登录的用户能够发表博文以及编辑既有博文。在让用户能够编辑其博文的视图中,在处理表单前确认用户编辑的是他自己发表的博文。
合并为项目“pizzeria ”,实现过程(Windows系统+python3.8.0+django3.0.1):
from django.db import models
# Create your models here.
class Pizza(models.Model):
""" 披萨 """
# 由字符或文本组成的数据(限制长度100)
name = models.CharField(max_length=100)
# 记录日期和时间的数据(自动创建当前时间)
date_added = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Topping(models.Model):
""" 配料 """
# 外键实例
# django 升级到2.0之后,表与表之间关联的时候,必须要写on_delete参数
# 由于多对多(ManyToManyField)没有 on_delete 参数,所以以上只针对外键(ForeignKey)和一对一(OneToOneField)
pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)
name = models.TextField()
date_added = models.DateTimeField(auto_now_add=True)
def __str__(self):
""" 返回模型的字符串表示 """
# 长于50字符才截取和显示省略号
if len(self.name) > 50:
return self.name[:50] + "..."
else:
return self.name
from django.contrib import admin
from pizzas.models import Pizza, Topping
# Register your models here.
admin.site.register(Pizza)
admin.site.register(Topping)
# 导入项目和网站管理 URL 的函数和模块
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
# namespace 将 pizzas 的 URL 同项目中的其他 URL 区分开来
# 源码函数def include(arg, namespace=None)中“urlconf_module, app_name = arg”,所以arg需要传入一个两元元组
url(r'', include(('pizzas.urls', 'pizzas'), namespace='pizzas')),
]
""" 定义 pizzas 的 URL 模式 """
from django.conf.urls import url
from . import views
urlpatterns = [
# 主页
# r让Python将接下来的字符串视为原始字符串,而引号告诉 Python 正则表达式始于和终于何处。脱字符( ^ )让 Python 查看字符串的开头,
# 而美元符号让 Python 查看字符串的末尾。总体而言,这个正则表达式让 Python 查找开头和末尾之间没有任何东西的 URL 。
url(r'^$', views.index, name='index'),
# 显示所有的配料
url(r'^pizzas/$', views.pizzas, name='pizzas'),
# 特定主题的详细页面
# r 让 Django 将这个字符串视为原始字符串,并指出正则表达式包含在引号内。
# 这个表达式的第二部分( /(?P\d+)/ )与包含在两个斜杠内的整数匹配,并将这个整数存储在一个名为 pizza_id 的实参中。
# 这部分表达式两边的括号捕获 URL 中的值; ?P 将匹配的值存储到 pizza_id 中;
# 而表达式 \d+ 与包含在两个斜杆内的任何数字都匹配,不管这个数字为多少位。
url(r'^pizza/(?P\d+)/$', views.pizza, name='pizza'),
]
from django.shortcuts import render
from .models import Pizza
# Create your views here.
def index(request):
""" 披萨的主页 """
return render(request, 'pizzas/index.html')
def pizzas(request):
""" 显示所有的配料 """
pizzas = Pizza.objects.order_by('date_added')
context = {'pizzas': pizzas}
return render(request, 'pizzas/pizzas.html', context)
def pizza(request, pizza_id):
""" 显示单个披萨及其所有的配料 """
pizza = Pizza.objects.get(id=pizza_id)
# date_added 前面的减号指定按降序排列
toppings = pizza.topping_set.order_by('-date_added')
context = {'pizza': pizza, 'toppings': toppings}
return render(request, 'pizzas/pizza.html', context)
{% block content %}{% endblock content %}
{% extends "pizzas/base.html" %}
{% block content %}
Pizza, also known as Pizza, Pizza, Pizza, Pizza, is a food originated in Italy, is popular around the world.Pizza is usually made from fermented pita covered with tomato sauce, cheese and other ingredients and baked in an oven.
{% endblock content %}
{% extends "pizzas/base.html" %}
{% block content %}
Pizzas
{% for pizza in pizzas %}
-
{
{ pizza }}
{% empty %}
- No pizzas have been added yet.
{% endfor %}
{% endblock content %}
{% extends 'pizzas/base.html' %}
{% block content %}
Pizza: {
{ pizza }}
Toppings:
{% for topping in toppings %}
-
{
{ topping.date_added|date:'M d, Y H:i' }}
{
{ topping.name|linebreaks }}
{% empty %}
-
There are no toppings for this pizza yet.
{% endfor %}
{% endblock content %}
from django import forms
from .models import Pizza, Topping
class PizzaForm(forms.ModelForm):
class Meta:
model = Pizza
fields = ['name']
# 让 Django 不要为字段 name 生成标签
labels = {'name': ''}
class ToppingForm(forms.ModelForm):
class Meta:
model = Topping
fields = ['name']
labels = {'name': ''}
# 设置属性widgets,可覆盖Django选择的默认小部件
# 文本区域改成80列(非默认的40列)
widgets = {'name': forms.Textarea(attrs={'cols': 80})}
""" 定义 pizzas 的 URL 模式 """
from django.conf.urls import url
from . import views
urlpatterns = [
# 主页
# r让Python将接下来的字符串视为原始字符串,而引号告诉 Python 正则表达式始于和终于何处。脱字符( ^ )让 Python 查看字符串的开头,
# 而美元符号让 Python 查看字符串的末尾。总体而言,这个正则表达式让 Python 查找开头和末尾之间没有任何东西的 URL 。
url(r'^$', views.index, name='index'),
# 显示所有的配料
url(r'^pizzas/$', views.pizzas, name='pizzas'),
# 特定主题的详细页面
# r 让 Django 将这个字符串视为原始字符串,并指出正则表达式包含在引号内。
# 这个表达式的第二部分( /(?P\d+)/ )与包含在两个斜杠内的整数匹配,并将这个整数存储在一个名为 pizza_id 的实参中。
# 这部分表达式两边的括号捕获 URL 中的值; ?P 将匹配的值存储到 pizza_id 中;
# 而表达式 \d+ 与包含在两个斜杆内的任何数字都匹配,不管这个数字为多少位。
url(r'^pizza/(?P\d+)/$', views.pizza, name='pizza'),
# 用于添加新披萨的页面
url(r'^new_pizza/$', views.new_pizza, name='new_pizza'),
# 用于添加新配料的页面
url(r'^new_topping/(?P\d+)/$', views.new_topping, name='new_topping'),
# 用于编辑配料的页面
url(r'^edit_topping/(?P\d+)/$', views.edit_topping, name='edit_topping'),
]
from django.shortcuts import render
from django.http import HttpResponseRedirect
# django2.0后把原来的django.core.urlresolvers包更改为了django.urls包
from django.urls import reverse
from .models import Pizza, Topping
from .forms import PizzaForm, ToppingForm
# Create your views here.
def index(request):
""" 披萨的主页 """
return render(request, 'pizzas/index.html')
def pizzas(request):
""" 显示所有的配料 """
pizzas = Pizza.objects.order_by('date_added')
context = {'pizzas': pizzas}
return render(request, 'pizzas/pizzas.html', context)
def pizza(request, pizza_id):
""" 显示单个披萨及其所有的配料 """
pizza = Pizza.objects.get(id=pizza_id)
# date_added 前面的减号指定按降序排列
toppings = pizza.topping_set.order_by('-date_added')
context = {'pizza': pizza, 'toppings': toppings}
return render(request, 'pizzas/pizza.html', context)
def new_pizza(request):
""" 添加新披萨 """
if request.method != 'POST':
# 未提交数据:创建一个新表单
form = PizzaForm()
else:
# POST 提交的数据 , 对数据进行处理
form = PizzaForm(request.POST)
# is_valid校验填写了必填信息和符合数据类型及长度
if form.is_valid():
form.save()
# 将用户重定向到网页 pizzas
return HttpResponseRedirect(reverse('pizzas:pizzas'))
context = {'form': form}
return render(request, 'pizzas/new_pizza.html', context)
def new_topping(request, pizza_id):
""" 在特定的披萨中添加新配料 """
pizza = Pizza.objects.get(id=pizza_id)
if request.method != 'POST':
# 未提交数据 , 创建一个空表单
form = ToppingForm()
else:
# POST 提交的数据 , 对数据进行处理
form = ToppingForm(data=request.POST)
if form.is_valid():
# 传递了实参commit=False,让Django创建一个新的配料对象,并将其存储到new_topping中,但不将它保存到数据库中
new_topping = form.save(commit=False)
new_topping.pizza = pizza
# 把配料保存到数据库,并将其与正确的披萨相关联
new_topping.save()
return HttpResponseRedirect(reverse('pizzas:pizza', args=[pizza_id]))
context = {'pizza': pizza, 'form': form}
return render(request, 'pizzas/new_topping.html', context)
def edit_topping(request, topping_id):
""" 编辑既有配料 """
topping = Topping.objects.get(id=topping_id)
pizza = topping.pizza
if request.method != 'POST':
# 初次请求,使用当前条目填充表单(instance=topping)
form = ToppingForm(instance=topping)
else:
# POST 提交的数据,对数据进行处理
form = ToppingForm(instance=topping, data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('pizzas:pizza', args=[pizza.id]))
context = {'topping': topping, 'pizza': pizza, 'form': form}
return render(request, 'pizzas/edit_topping.html', context)
{% extends "pizzas/base.html" %}
{% block content %}
Add a new pizza:
{% endblock content %}
{% extends "pizzas/base.html" %}
{% block content %}
Add a new topping:
{% endblock content %}
{% extends "pizzas/base.html" %}
{% block content %}
Edit topping:
{% endblock content %}
{% extends "pizzas/base.html" %}
{% block content %}
Pizzas
{% for pizza in pizzas %}
-
{
{ pizza }}
{% empty %}
- No pizzas have been added yet.
{% endfor %}
Add a new pizza:
{% endblock content %}
{% extends 'pizzas/base.html' %}
{% block content %}
Pizza: {
{ pizza }}
Toppings:
{% for topping in toppings %}
-
{
{ topping.date_added|date:'M d, Y H:i' }}
{
{ topping.name|linebreaks }}
{% empty %}
-
There are no toppings for this pizza yet.
{% endfor %}
{% endblock content %}
# 导入项目和网站管理 URL 的函数和模块
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^users/', include(('users.urls', 'users'), namespace='users')),
# namespace 将 pizzas 的 URL 同项目中的其他 URL 区分开来
# 源码函数def include(arg, namespace=None)中“urlconf_module, app_name = arg”,所以arg需要传入一个两元元组
url(r'', include(('pizzas.urls', 'pizzas'), namespace='pizzas')),
]
""" 为应用程序 users 定义 URL 模式 """
from django.conf.urls import url
from django.contrib.auth.views import LoginView
from . import views
urlpatterns = [
#登录界面 LoginView.as_view后面要加上()
url(r'^login/$', LoginView.as_view(template_name='users/login.html'), name='login'),
# 注销
url(r'^logout/$', views.logout_view, name='logout'),
# 注册页面
url(r'^register/$', views.register, name='register'),
]
{% extends "pizzas/base.html" %}
{% block content %}
{% if form.errors %}
Your username and password didn't match. Please try again.
{% endif %}
{% endblock content %}
{% extends "pizzas/base.html" %}
{% block content %}
{% endblock content %}
pizzeria -
pizzas -
{% if user.is_authenticated %}
Hello, {
{ user.username }}.
log out
{% else %}
register -
log in
{% endif %}
{% block content %}{% endblock content %}
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.forms import UserCreationForm
def logout_view(request):
""" 注销用户 """
logout(request)
return HttpResponseRedirect(reverse('pizzas:index'))
def register(request):
""" 注册新用户 """
if request.method != 'POST':
# 显示空的注册表单
form = UserCreationForm()
else:
# 处理填写好的表单
form = UserCreationForm(data=request.POST)
if form.is_valid():
new_user = form.save()
# 让用户自动登录,再重定向到主页
authenticated_user = authenticate(username=new_user.username, password=request.POST['password1'])
login(request, authenticated_user)
return HttpResponseRedirect(reverse('pizzas:index'))
context = {'form': form}
return render(request, 'users/register.html', context)
from django.shortcuts import render
from django.http import HttpResponseRedirect, Http404
# django2.0后把原来的django.core.urlresolvers包更改为了django.urls包
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from .models import Pizza, Topping
from .forms import PizzaForm, ToppingForm
# Create your views here.
def index(request):
""" 披萨的主页 """
return render(request, 'pizzas/index.html')
# login_required() 的代码检查用户是否已登录,仅当用户已登录时, Django 才运行 topics() 的代码。如果用户未登录,就重定向到登录页面。
@login_required
def pizzas(request):
""" 显示所有的配料 """
pizzas = Pizza.objects.filter(owner=request.user).order_by('date_added')
context = {'pizzas': pizzas}
return render(request, 'pizzas/pizzas.html', context)
@login_required
def pizza(request, pizza_id):
""" 显示单个披萨及其所有的配料 """
pizza = Pizza.objects.get(id=pizza_id)
# 请求的披萨不归当前用户所有,我们就引发 Http404 异常,让 Django 返回一个 404 错误页面
check_topic_owner(pizza, request)
# date_added 前面的减号指定按降序排列
toppings = pizza.topping_set.order_by('-date_added')
context = {'pizza': pizza, 'toppings': toppings}
return render(request, 'pizzas/pizza.html', context)
@login_required
def new_pizza(request):
""" 添加新披萨 """
if request.method != 'POST':
# 未提交数据:创建一个新表单
form = PizzaForm()
else:
# POST 提交的数据 , 对数据进行处理
form = PizzaForm(request.POST)
# is_valid校验填写了必填信息和符合数据类型及长度
if form.is_valid():
new_pizza = form.save(commit=False)
new_pizza.owner = request.user
new_pizza.save()
# 将用户重定向到网页 pizzas
return HttpResponseRedirect(reverse('pizzas:pizzas'))
context = {'form': form}
return render(request, 'pizzas/new_pizza.html', context)
@login_required
def new_topping(request, pizza_id):
""" 在特定的披萨中添加新配料 """
pizza = Pizza.objects.get(id=pizza_id)
# 解决一个用户可在另一个用户的学习笔记中添加条目问题
check_topic_owner(pizza, request)
if request.method != 'POST':
# 未提交数据 , 创建一个空表单
form = ToppingForm()
else:
# POST 提交的数据 , 对数据进行处理
form = ToppingForm(data=request.POST)
if form.is_valid():
# 传递了实参commit=False,让Django创建一个新的配料对象,并将其存储到new_topping中,但不将它保存到数据库中
new_topping = form.save(commit=False)
new_topping.pizza = pizza
# 把配料保存到数据库,并将其与正确的披萨相关联
new_topping.save()
return HttpResponseRedirect(reverse('pizzas:pizza', args=[pizza_id]))
context = {'pizza': pizza, 'form': form}
return render(request, 'pizzas/new_topping.html', context)
@login_required
def edit_topping(request, topping_id):
""" 编辑既有配料 """
topping = Topping.objects.get(id=topping_id)
pizza = topping.pizza
check_topic_owner(pizza, request)
if request.method != 'POST':
# 初次请求,使用当前条目填充表单(instance=topping)
form = ToppingForm(instance=topping)
else:
# POST 提交的数据,对数据进行处理
form = ToppingForm(instance=topping, data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('pizzas:pizza', args=[pizza.id]))
context = {'topping': topping, 'pizza': pizza, 'form': form}
return render(request, 'pizzas/edit_topping.html', context)
def check_topic_owner(pizza, request):
"""校验关联到的用户是否为当前登录的用户"""
if pizza.owner != request.user:
raise Http404
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Pizza(models.Model):
""" 披萨 """
# 由字符或文本组成的数据(限制长度100)
name = models.CharField(max_length=100)
# 记录日期和时间的数据(自动创建当前时间)
date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.name
class Topping(models.Model):
""" 配料 """
# 外键实例
# django 升级到2.0之后,表与表之间关联的时候,必须要写on_delete参数
# 由于多对多(ManyToManyField)没有 on_delete 参数,所以以上只针对外键(ForeignKey)和一对一(OneToOneField)
pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)
name = models.TextField()
date_added = models.DateTimeField(auto_now_add=True)
def __str__(self):
""" 返回模型的字符串表示 """
# 长于50字符才截取和显示省略号
if len(self.name) > 50:
return self.name[:50] + "..."
else:
return self.name
总结,整个项目结构如下: