Webapp——用户账户

发布于 2022-04-11  430 次阅读


Web应用程序的核心是让任何用户都能够注册账号并能够使用它,不管用户身处何方。在本章中,我们将创建一些表单,让用户能够添加主题和条目,以及编辑既有的条目,以及编辑既有条目。还学习Django如何防范对基本表单的页面发起的常见攻击,从而无需确保应用程序安全的问题。
然后,实现一个用户身份验证系统。首先创建一个注册页面,供用户创建账户,并让有些页面只能供已登录的用户访问。接下来,修改一些视图函数,使得用户只能看见自己的数据。+如何确保用户数据安全。

让用户输入数据

建立用于创建用户账号的身份验证系统之前,先添加几个页面,让用户能够输入数据。让用户添加新主题。添加新条目以及编辑既有条目。
当前,只有超级用户能够通过管理网站输入数据。我们不想让用户与管理网站交互,因此使用Django的表单创建工具来创建让用户能够输入数据的页面。

添加新主题

首先让用户能够添加新主题,创建基于表单的页面的方法几乎与前面创建页面一样:定义URL,编写视图函数并编写一个模板,一个主要差别是,需要导入包含表单的模块froms.py.

1.用于添加主题的表单

让用户输入并提交信息的页面都是表单,哪怕看起来不像。用户输入信息时,需要进行验证,确认提供的信息都是正确的数据类型,而不是恶意的信息,如中断服务器的代码。然后,对这些有效信息进行处理,并将其保存到数据库的合适地方。这些工作很多都是由Django自动完成的。
在Django中,创建表单的最简单方式是使用ModelForm。创建一个名为forms.py的文件,并将其存储到models.py所在的目录。

from django import forms

from .models import Topic

class TopicForm(forms.ModelForm):
    class Meta:
        model = Topic
        fields = ['text']
        labels = {'text': ''}

首先导入模块forms以及要使用的模型Topic。定义一个TopicForm的类,它继承了forms.ModeForm。
最简单的ModelForm版本只包含一个内嵌的Meta类。他告诉Django根据那个模型创建表单以及在表单中包含哪些字段,根据模型Topic创建表单,其中只包含字段text,最后一行代码让Django不要为字段text生成标签。

2.URL模式new_topic

新页面的URL应具有描述性,因此当用户要添加新主题时,切换到http://loclhost:8000/new_topic/
下面是页面new_topic的URL模式。添加到learning_logs/urls.py中

from django.shortcuts import render
from .models import Topic

# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)

这个URL模式将请求交给函数模板new_topic()

3.视图函数new_topic()

函数new_topic()需要处理两种情形,一是刚进入new_topic页面(这种情况下应显示空表单);二是对提交的表单数据进行处理,并将用户重定向到页面topics:

from django.shortcuts import render,redirect
from .models import Topic
from .forms import TopicForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_longs/new_topic.html',context)

导入了函数redirect,用户提交主题后将使用这个函数重定向到页面topics。函数redirect将视图名作为参数,并将用户重定向到这个视图,还导入了刚创建的表单TopicForm。

4.Get请求和POST请求

创建Web应用程序时候,将用到的两种主要的请求类型是GET请求和POST请求。对于只是从服务器读取数据的页面,使用GET请求。在用户需要通过表单提交信息时候,通常使用POST请求。处理所有表单时候,都将使用POST方法,还有一些其他类型的请求。
函数new_topic()将请求对象作为参数,用户初次请求页面时候,其浏览器发送GET请求;用户填写表单并提交表单时候,其浏览器发送POST请求,根据请求类型,可确定用户请求的是空表单(GET请求)还是要对填写好的表单进行处理(POST)请求。
上述if测试确定请求的方法是GET还是POST。如果请求方法不是POST,请求就可能是GET,因此需要返回一个空表单,(即便请求是其他类型的,返回表单也不会有任何问题。之后创建一个TopicForm实例。将其值赋给form,再通过上下文字典将这个表单发送给模板。
由于实例化TopicForm时没有指定任何实参,Django将创建一个空表单,供用户填写。
如果请求方法为POST,将值else代码,对提交的表单数据进行处理。使用用户输入的数据(存储在reques.POST中)创建一个TopicForm实例,这样对象form将包含用户提交的信息。
要将提交的信息保存到数据库,必须先通过检查确认它们是有效的。方法is_valid()核实用据填写了所有必不可少的字段(表单字段默认都是必不可少的。)且输入的数据与要求的字段类型一致(例如字段text少于200字符。)这种自动验证避免了大量工作。如果所有字段都有效。就可调用save()将表单中的数据写入数据库中。保存数据后,就可以离开这个页面了。为此,使用redirect()将用户的浏览器重定向到页面topics.在页面topics中,用户将在主题列表中看到刚输入的主题。
在视图的末尾定义了变量context,并使用稍后将创建的模板new_topic.html来渲染页面。这些代码不再if代码块内,因此无论是用户刚进入new_topic页面还是提交的表单数据无效,这些代码都将执行。用户提交的表单数据无效时,将显示一些默认的错误信息,帮助用户提供有效的数据。

5.模板new_topic

下面来创建新模板new_topic.html,用于显示刚创建的表单:

{% extends "learning_logs/base.html" %}

{% block content %}
<p>Add a new topic:</p>

<form action="{% url 'learning.logs:new_topic' %}" method="post">
    {% csrf_token %}
    {{form.as_p}}
    <button name="submit">Add topic</button>
</form>
{% endblock content %}

这个模板继承了base.html,在开始定义了一个html表单,实参action告诉服务器将提交的表单数据发送到哪。这里它发回给视图函数new_topic().实参method让浏览器以POST请求的方式提交数据。
Django使用模板标签{% csrf_token %}来防止攻击者利用表单来获得对方服务器未授权的访问(跨站请求伪造)。{{form.as_p}}显示表单,从中可知Django使得完成显示表单任务有多简单:只需包含模板变量{{form.as_p}},就可以让Django自动创建表单所需的全部字段,修饰符as_p让Django以段落格式渲染所有表单元素,这是一种简洁的显示表单的简单方式.
Django不会为表单创建提交按钮.

6.连接到页面new_topic

下面在页面topics中添加到页面new_topic的链接.

{% extends "learning_logs/base.html" %}

{% block content %}
<p>Topics</p>

<ul>
    {% for topic in topics %}
        <li>
            <a href="{% url 'learning_logs:topic' topic.id %}">{{topic}}</a>
        </li>
    {% empty %}
        <li>No topics have been added yet.</li>
    {% endfor %}
</ul>
    <a href="{% url 'learning_logs:new_topic'%}">Add a new topic</a>
{% endblock content %}

添加新条目

可以添加新主题后,用户还会想添加几个新条目.再次定义URL,编写视图函数和模板,并且链接到新条目的页面.在forms.py中再添加一个类.

1.用于添加新条目的表单.

需要创建一个与模型Entry相关联的表单.

from django import forms

from .models import Topic,Entry

class TopicForm(forms.ModelForm):
    class Meta:
        model = Topic
        fields = ['text']
        labels = {'text': ''}

class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['text']
        labels = {'text':''}
        widgets = {'text': forms.Textarea(attrs={'cols': 80})}

导入Entry类.新类EntryForm继承了forms.ModelForm,它包含Meta类指出了表单基于的模型以及要在表单中包含哪些字段,这里给字段'text'制定了标签'Entry:'
属性widgets(小部件)是一个HTML表单元素,如单行文本框,多行文本区域或下拉列表.通过设置属性widgets.可以覆盖Django选择的默认小部件,通过让Django使用forms.Textarea制定了字段'text'的输入小部件.将文本框的宽度设置为80列,而不是默认的40列.

2.URL模式new_entry

在用于添加新条目的页面的URL模式中,需要包含实参topic_id.

"""定义learning_logs的URL模式"""

from django.urls import path

from . import views

app_name = 'learning_logs'
urlpatterns = [
    # 主页
    path('',views.index,name='index'),
    # 显示所有的主题
    path('topics/',views.topics,name='topics'),
    # 指定主题的详细页面
    path('topics/<int:topic_id>/',views.topic,name='topic'),
    # 用于添加新主题的页面
    path('new_topic/',views.new_topic,name='new_topic'),
    # 用于添加新条目的页面
    path('new_entry/<int:topic_id>/',views.new_entry,name='new_entry'),
]

请求的URL与这个迷失匹配时,Django将请求和ID发送给函数new_entry()

3.视图函数new_entry()

视图函数new_entry()与函数new_topic()很像.

from django.shortcuts import render,redirect
from .models import Topic
from .forms import TopicForm,EntryForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_logs/new_topic.html',context)
def new_entry(request,topic_id):
    """在特定主题中添加新条目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = EntryForm()
    else:
        # Post提交的数据:对数据进行处理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic',topic_id=topic_id)

    # 显示空表单或指出表单数据无效
    context = {'topic':topic,'form':form}
    return render(request,'learning_logs/new_entry.html',context)

new_entry()的定义包含形参topic_id,用于存储从URL中获得的值,渲染页面和处理表单时,都需要知道针对的是哪个主题.
在if处,检查请求方法是POST还是GET,如果是GET请求,就执行if代码块,创建一个空的EntryForm实例,如果请求方法是POST,就对数据进行处理:创建一个EntryForm实例,使用request对象中的POST数据来填充.然后检查表单是否有效.如果有效,就设置条目对象属性topic,再将条目对象保存到数据库.
调用save()时,传递实参commit=False,让Django创建一个新的条目对象,并将其赋给new_entry,但不保存到数据库中,将new_entry的属性topic设置为这个函数开头从数据库中获取的猪头,再调用save()且不指定任何实参,这将把这个条目保存到数据库,并将其与正确的主题相关联.
之后,调用redirect().重定向.
在末尾创建一个上下文字典,使用模板new_entry.html渲染页面.这些代码在用户刚进入或者提交表单无效时执行.

4.模板new_entry

模板new_entry类似于模板new_topic.

{% extends "learning_logs/base.html" %}

{% block content %}
<p><a href="{% url 'learning_logs:topic' topic.id %}">{{topic.id}}</a></p>

<p>Add a new entry</p>
<form action="{% url 'learning_logs:new_entry' topic.id %}" method="post">
    {% csrf_token %}
    {{form.as_p}}
    <button name="submit">Add entry</button>
</form>
{% endblock content %}

为页面顶端显示主题,让用户知道自己是在哪个主题中添加条目.
表单的实参action包含URL中的topic_id值.让视图函数能够将新条目关联到正确的主题.

5.链接到页面new_entry

{% extends "learning_logs/base.html" %}

{% block content %}
<p>Topics</p>

<ul>
    {% for topic in topics %}
        <li>
            <a href="{% url 'learning_logs:topic' topic.id %}">{{topic}}</a>
        </li>
    {% empty %}
        <li>No topics have been added yet.</li>
    {% endfor %}
</ul>
    <a href="{% url 'learning_logs:new_topic'%}">Add a new topic</a>
{% endblock content %}

编辑条目

下面创建能够编辑既有条目的页面。

1.URL模式edit_entry

这个页面的URL需要传递编辑的条目ID。修改后的learning_logs/urls.py如下:

"""定义learning_logs的URL模式"""

from django.urls import path

from . import views

app_name = 'learning_logs'
urlpatterns = [
    # 主页
    path('',views.index,name='index'),
    # 显示所有的主题
    path('topics/',views.topics,name='topics'),
    # 指定主题的详细页面
    path('topics/<int:topic_id>/',views.topic,name='topic'),
    # 用于添加新主题的页面
    path('new_topic/',views.new_topic,name='new_topic'),
    # 用于添加新条目的页面
    path('new_entry/<int:topic_id>/',views.new_entry,name='new_entry'),
    # 用于编辑条目的页面
    path('edit_entry/<int:topic_id>/',views.edit_entry,name='edit_entry'),
]

在URL中传递的ID存储在形参entry_id中,这个URL模式将于其他匹配的请求发送给视图函数edit_entry().

3.视图函数edit_entry()

页面edit_entry收到GET请求时,edit_entry()返回一个表单,让用户能够对条目进行编辑。收到POST请求,则将修改后的文本保存到数据库。

from django.shortcuts import render,redirect
from .models import Topic,Entry
from .forms import TopicForm,EntryForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_logs/new_topic.html',context)
def new_entry(request,topic_id):
    """在特定主题中添加新条目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = EntryForm()
    else:
        # Post提交的数据:对数据进行处理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic',topic_id=topic_id)

    # 显示空表单或指出表单数据无效
    context = {'topic':topic,'form':form}
    return render(request,'learning_logs/new_entry.html',context)
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:
        # POST提交的数据:对数据进行处理
        form = EntryForm(instance=entry,data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic',topic_id=topic.id)

    context = {'entry':entry,'topic':topic,'form':form}
    return render(request,'learning_logs/edit_entry.html',context)

首先导入模拟Entry,开始,获取用户要修改的条目对象以及其相关联的主题,在请求方法为GET时将执行的if代码块中,使用instance=entry创建一个EntryForm实例。这个实参让Django创建一个表单,并使用既有条目对象中的信息填充它。用户将能够看到数据,并且能顾编辑。
处理POST请求时,传递实参instanc=entry和data=request.POST,让Django根据既有条目对象创建一个表单实例,并根据request.POST中相关数据对其进行修改。然后检查表单是否有效。如果有效,就调用save()且不指定任何实参,因为条目已关联到特定的主题。然后,重定向到显示条目所属主题的页面,用户将在其中看到编辑的条目的新版本。
如果要显示表单让用户编辑条目或者用户提交的表单无效,就创建上下文字典并使用模板edit_entry.html渲染页面。

3.模板edit_entry

下面来创建模板edit_entry.html,它与模板new_entry.html类似。

{% extends "learning_logs/base.html" %}

{% block content %}
<p><a href="{% url 'learning_logs:topic' topic.id %}">{{topic}}</a></p>

<p>Edit entry</p>
<form action="{% url 'learning_logs:edit_entry' entry.id %}" method="post">
    {% csrf_token %}
    {{form.as_p}}
    <button name="submit">Save change</button>
</form>
{% endblock content %}

4.链接到页面edit_entry

现在,需要在显示特定主题的页面中给每个条目添加到页面edit_entry的链接:

{% extends "learning_logs/base.html" %}

{% block content %}

<p>Topic:{{topic}}</p>

<p>Entries:</p>
<p>
    <a href="{% url 'learning_logs:new_entry' topic.id %}">Add new entry</a>
</p>
<ul>
    {% for entry in entries %}
    <p>{{entry.date_add|date:'M d,Y H:i'}}</p>
    <p>{{entry.text|linebreaks}}</p>
    <p>
        <a href="{% url 'learning_logs:edit_entry' entry.id %}">Edit entry</a>
    </p>
{% empty %}
<li>There are no entries for this topic yet.</li>
{% endfor %}
</ul>
{% endblock content %}

创建用户账号

建立用户注册和身份验证系统,让用户能够注册账户,进而登录和注销。创建一个新的应用程序,其中包含与处理用户账号相关的所有功能,这个应用程序将尽可能使用Django自带的用户身份验证系统来完成工作。本节还将对模型Topic稍微修改,让每个主题都归属于特定用户。

应用程序users

首先stratapp创建一个名为users的应用程序:
file
file
这个命令新建一个名为users的目录。

将users添加到settings.py中

INSTALLED_APPS = [
    # 我的应用程序
    'learning_logs',
    'users',
    # 默认添加的应用程序
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

这样,Django将把应用程序users包含到项目中。

包含users的URL

需要修改项目根目录中的urls.py,使其包含将为应用程序的URL:


from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include('users.urls')),
    path('', include('learning_logs.urls')),
]

添加了一行代码,包含应用程序users中的文件urls.py,这行代码与任何单词users打头的URL(http://localhost:8000/users/login/)都匹配

登录页面

首先实现登录页面。
使用Django提供的默认视图login。因此这个应用程序的URL模式稍有不同。
在目录learning_logs/users/中,新建一个urls.py文件,

"""为应用程序users定义URL模式。"""

from django.urls import path,include

app_name = 'users'
urlpatterns = [
    # 包含默认的身份验证URL
    path('',include('django.contrib.auth.urls')),
]

导入函数path和include,以便包含Django定义的一些默认的身份验证URL。这些默认的URL包含具名的URL模式,如'login'和'logout'。将变量app_name设置成为'users',让Django能够将这些URL与其他应用程序的URL区分。即便是Django提供的默认URL,将其包含在应用程序users的文件中后,也可以通过命名空间users进行访问。
登录页面的URL模式与URL http://localhost:8000/users/login/
匹配。这个URL中的单词users让Django在users/users/url.py中查找,而单词login让它请求发送给Django的默认视图login。

1.模板login.html

用户请求登录页面时,Django将使用一个默认的视图函数,但仍然需要为这个页面提供模板。默认的身份验证视图在文件夹registration中查找模板,因此创建这个文件夹。
在目录learning_logs/users/中新建一个名为templates的目录。再在这个目录中新建一个名为templates的目录,再在这个目录中新建一个名为registration的目录。下面是login.html。

{% extends "learning_logs/base.html" %}

{% block content %}
<% if form.errors %>
<p>Your username and password didn't match.Please try again.</p>
<% endif %>
<form method="post" action="{% url 'user:login' %}">
    {% csrf_token %}
    {{form.as_p}}
    <button name="submit">Login in</button>
    <input type="hidden" name="next"
        value="{% url 'learning_logs:index' %}">
</form>
{% endblock content %}

这个模板继承了base.html,旨在确保登录页面的外观与网站的其他页面相同,一个应用程序中的模板可继承另外一个应用程序中的模板。
如果设置表单的errors属性,就显示一条错误信息,指出输入的用户名和密码与数据库中存储的任何用户名密码都不匹配。
要让登录视图对表单进行处,因此将实参action设置为登录页面的URL。登录视图将一个表单发送给模板。在模板中,显示这个表单并添加一个提交按钮。在之后的隐含了一个隐藏的表单元素'next',其中的实参value告诉Django在用户登录后将其重定向刀什么地方。

2.连接到登录页面

下面在base.html中添加刀登录页面的链接,让所有页面都包含它。当已登录的时候,不显示这个链接。

<p>
    <a href="{% url 'learning_logs:index' %}">Home</a>  -
    <a href="{% url 'learning_logs:topics' %}">Topics</a> -
    {% if user.is_authenticated %}
        Hello,{{user.username}}
    {% else %}
    <a href="{% url 'users:login' %}">Log in</a>
    {% endif %}
</p>

{% block content %}{% endblock content %}

在Django身份验证系统中,每个模板都可以使用变量user。这个变量有一个is_authenticated属性。如果用户已经登录,该属性为True,否则为False。。这让能向已经通过身份验证的用户显示一条消息,而向为通过身份验证的用户显示另外一条消息。

3.使用登录页面

file

注销

现在需要提供一个让用户注销的途径,在base.html添加注销链接。

1.在base.html添加注销链接

<p>
    <a href="{% url 'learning_logs:index' %}">Home</a>  -
    <a href="{% url 'learning_logs:topics' %}">Topics</a> -
    {% if user.is_authenticated %}
        Hello,{{user.username}}
    <a href="{% url 'users:logout' %}">Logout</a>
    {% else %}
    <a href="{% url 'users:login' %}">Log in</a>
    {% endif %}
</p>

{% block content %}{% endblock content %}

2.注销确认页面

成功注销后,用户希望获悉这点,因此默认的注销视图使用模板logged_out.html渲染,

{% extends "learning_logs/base.html" %}

{% block content %}
    <p>You have been logged out.Thank you for visiting!</p>
</form>
{% endblock content %}

注册页面

下面来创建一个页面可供新用户注册。使用Django提供的表单UserCreationForm,但编写自己的视图函数和模板。

1.注册页面的URL模式

下面代码定义了注册页面的URL模式,它也包含在users/urls.py中:

"""为应用程序users定义URL模式。"""

from django.urls import path,include

from . import views
app_name = 'users'
urlpatterns = [
    # 包含默认的身份验证URL
    path('',include('django.contrib.auth.urls')),
    # 注册页面
    path('register/',views.register,name='register'),
]

从users中导入模块views,为何需要这样做呢?因为即将为注册页面编写视图函数。
注册页面URL模式与URLhttp://localhost:8000/users/register/匹配
并将发送请求给即将编写的函数register()

2.视图函数register()

在注册页面首次被请求时,视图函数register()需要显示一个空的注册表单,并在用户提交填写好的注册表单时对其进行处理,如果注册成功,这个函数还需让用户自动登录。
在users/views.py中:

from django.shortcuts import render,redirect
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm

# Create your views here.
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)
            return redirect('learning_logs:index')
    # 显示空表单或指出表单无效
    context = {'form':form}
    return render(request,'registration/register.html',context)

首先导入函数render()和redirect(),然后导入函数login().以便在用户正确填写了注册信息时候,让其自动登录。还导入了默认表单UserCreationForm实例,且不给它提供任何初始数据。
如果相应的是POST请求,就根据提交的数据创建了一个UserCreationForm实例,并检查这些数据是否有效。本例而言,有效是指用户名未包含非法字符,输入两个密码相同,以及用户没有试图做恶意的事情。
如果提交的数据有效,就调用表单的方法save(),将用户名和密码的散列值保存到数据库中。方法save()返回新创建的用户对象。将它赋值给了new_user.保存用户信息后,调用函数login()并传入对象request和new_user,为用户创建有效的会话。从而让其自动登录。最后,将用户重定向到主页,而主页的也没中显示了一条个性化的问候语。让用户知道注册成功了。在这个函数的末尾,渲染了注册页面:它要么显示一个空表单,要么显示提交的无效表单。

注册模板

下面来创建注册页面的模板,它与登录页面的模板类似。

{% extends "learning_logs/base.html" %}

{% block content %}

<form method="post" action="{% url 'users:register' %}">
  {% csrf_token %}
  {{form.as_p}}

  <button name="submit">Register</button>
  <input type="hidden" name="text" value="{% url 'learning_logs:index' %}" />

</form>
{% endblock content %}

这里也使用了方法as_p,让Django在表单中正确地显示所有字段,包括错误消息————如果用户没有正确的填写表单。

4.链接到注册页面

下面来添加一些代码,在用户没有登录时显示到注册页面的链接:

<p>
    <a href="{% url 'learning_logs:index' %}">Home</a>  -
    <a href="{% url 'learning_logs:topics' %}">Topics</a> -
    {% if user.is_authenticated %}
        Hello,{{user.username}}
    <a href="{% url 'users:logout' %}">Logout</a>
    {% else %}
    <a href="{% url 'users:register' %}">Register</a> -
    <a href="{% url 'users:login' %}">Log in</a>
    {% endif %}
</p>

{% block content %}{% endblock content %}

让用户拥有自己的数据

用户应该能输入其专有的数据,创建一个系统,确定各项数据所属的用户,再限制对页面的访问,让用户只能使用自己的数据。
修改Topic,让每个主题都归属于特定用户,这也将影响条目,因为每个条目都术语特定主题。

使用@login_required限制访问

Django提供了装饰器@login__required.让我们能够轻松地只允许已登录的用户访问某些页面。
装饰器(decorator)是放在函数定义前面的指令,Python再函数允许前根据它来修改函数代码的行为。

1.限制访问显示所有主题的页面。

每个主题都归特定用户所有,因此只允许已登录的用户请求显示所有主题的页面。为此,在learning_logs/views.py中添加:

from django.shortcuts import render,redirect
from django.contrib.auth.decorators import login_required

from .models import Topic,Entry
from .forms import TopicForm,EntryForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

@login_required
def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_logs/new_topic.html',context)
def new_entry(request,topic_id):
    """在特定主题中添加新条目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = EntryForm()
    else:
        # Post提交的数据:对数据进行处理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic',topic_id=topic_id)

    # 显示空表单或指出表单数据无效
    context = {'topic':topic,'form':form}
    return render(request,'learning_logs/new_entry.html',context)
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:
        # POST提交的数据:对数据进行处理
        form = EntryForm(instance=entry,data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic',topic_id=topic.id)

    context = {'entry':entry,'topic':topic,'form':form}
    return render(request,'learning_logs/edit_entry.html',context)

首先导入函数login_required(),将login_required()作为装饰器用于视图函数topics()---在它前面加上符号@login_required.让Python在允许topics()的代码之前运行login_required()的代码。
login_require()的代码检查用户是否已登录,仅党用户已经登录时,Django才运行topics()的代码,如果用户未登录,就重定向到登录页面。
为实现这种重定向,需要修改settings.py,让Django知道到哪里去查找登录页面。


# 我的设置
LOGIN_URL = 'users:login'

现在,如果为登录的用户请求装饰器@login_required保护的页面,Django将重定向到settings.py中的LOGIN_URL指定的URL.

2.全面限制对项目“学习笔记”的访问

Django让我们能够轻松地限制对页面的访问,但必须确定要保护哪些页面。最好先确定项目的哪些页面不需要保护,再限制对其他所有页面的访问。可以轻松地修改过于严格的访问限制,比起不限制对敏感页面的访问,这样做风险更低。
在本项目中,将不限制对主页和注册页面的访问,并限制对其他所有页面的访问。
在下面learning_logs/views.py中

from django.shortcuts import render,redirect
from django.contrib.auth.decorators import login_required

from .models import Topic,Entry
from .forms import TopicForm,EntryForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

@login_required
def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

@login_required
def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)

@login_required
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_logs/new_topic.html',context)

@login_required
def new_entry(request,topic_id):
    """在特定主题中添加新条目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = EntryForm()
    else:
        # Post提交的数据:对数据进行处理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic',topic_id=topic_id)

    # 显示空表单或指出表单数据无效
    context = {'topic':topic,'form':form}
    return render(request,'learning_logs/new_entry.html',context)

@login_required
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:
        # POST提交的数据:对数据进行处理
        form = EntryForm(instance=entry,data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic',topic_id=topic.id)

    context = {'entry':entry,'topic':topic,'form':form}
    return render(request,'learning_logs/edit_entry.html',context)

将数据关联到用户

现在需要将数据关联到提交它们的用户。
只需将最高增的数据关联到用户,更低层的数据就会自动关联到用户。例如,在项目“学习笔记”中,应用程序的最高层数据是主题,而所有条目都与特定主题相关联。只要每个主题都归属于特定用户,就能确认每个条目的所有者。
下面根据修改模型Topic,在其中添加一个关联到用户的外键,这样做后,必须对数据库进行迁移,最后,必须修改某些视图,使其只显示与当前登录的用户相关联的数据。

1.修改模型Topic

对models.py:

from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class Topic(models.Model):
    """用户学习的主题"""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User,on_delete=models.CASCADE)

    def __str__(self):
        """返回模型的字符串表示。"""
        return self.text

class Entry(models.Model):
    """学到的有关某个主题的具体知识。"""
    topic = models.ForeignKey(Topic,on_delete=models.CASCADE)
    text = models.TextField()
    date_add = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name_plural = 'entries'

    def __str__(self):
        """返回模型的字符串表示。"""
        if len(self.text)>50:
            return f"{self.text[:50]}..."
        else:
            return f"{self.text}"

首先导入django.contrib.auth中的模型User,然后再Topic中添加字段owner,它建立模型User的外键关系,用户被删除时,所有与之相关联的主题也会被删除。

2.确定当有哪些用户

迁移数据库时候,Django将对数据库进行修改,使其能够存储主题和用户之间的关联。为执行迁移,Django需要知道该将各个既有主题关联到哪个用户,最简单方式,将既有主题都关联到同一个用户,如超级用户。为此需要知道用户ID.
下面来查看已创建的所有用户ID。
为此启动Django shell

>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: xiaoan>, <User: xiaoz>]>
>>> for user in User.objects.all():
...     print(user.username,user.id)

file

3.迁移数据库

知道用户ID后,迁移数据库。
file
首先执行命令makemigrations。在只会输出中,Django指出你试图给既有模型Topic添加一个必不可少的(不可为空)字段,而改字段没有默认值。Django提供两种选择;要么现在提供默认中,要么退出并在models.py中添加默认值。
将所有既有主题都关联到管理用户。
接下来,Django使用这个值来迁移数据库。并生成了迁移文件0003_topic_owner.py它在模型中添加字段owner:
现在可以执行迁移了,为此,在活动状态的虚拟环境中执行如下命令:
file
为了验证迁移符合预期,可以在shell中:
file
从learning_ligs.models中导入Topic,再遍历所有的既有主题,并且打印其所属用户。

注意:可以重置数据库而不是迁移它,如果这样做,既有的数据都将流失,一种不错的做法是,学习如何在迁移数据库的同时确保用户数据的完整性。
如果确实想要一个全新的数据库,可以执行命令python manage.py flush,这将重建数据库的结构,如果这样做,就必须重建超级用户,且原来的数据都丢失。

只允许用户访问自己的主题

当前,不管以哪个用户的身份登录。都能够看到所有的主题。下面改变这一点,只向用户显示属于自己的主题。
在views.py中,对函数topics()做出如下修改:

from django.shortcuts import render,redirect
from django.contrib.auth.decorators import login_required

from .models import Topic,Entry
from .forms import TopicForm,EntryForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

@login_required
def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

@login_required
def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)

@login_required
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_logs/new_topic.html',context)

@login_required
def new_entry(request,topic_id):
    """在特定主题中添加新条目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = EntryForm()
    else:
        # Post提交的数据:对数据进行处理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic',topic_id=topic_id)

    # 显示空表单或指出表单数据无效
    context = {'topic':topic,'form':form}
    return render(request,'learning_logs/new_entry.html',context)

@login_required
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:
        # POST提交的数据:对数据进行处理
        form = EntryForm(instance=entry,data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic',topic_id=topic.id)

    context = {'entry':entry,'topic':topic,'form':form}
    return render(request,'learning_logs/edit_entry.html',context)

用户登录后,request对象将有一个user属性,其中存储了有关该用户的信息。
查询topics = Topic.objects.filter(owner=request.user).order_by('date_added')让Django只从数据库中获取ower属性为当前用户的Topic对象。由于没有显示主题的显示方式,无需对显示所有主题的页面的模板做任何修改。

保护用户的主题

还没有限制对显示单个主题的页面访问,因此任何已登录的用户都可以输入与类似于http://localhost:8000/topics/1/的URL来显示相应主题的页面
结果发现并非topic所有者也可以访问该页面
为修复这种问题。在试图函数topic()获取请求的条目前执行检查。

from django.shortcuts import render,redirect
from django.contrib.auth.decorators import login_required
from django.http import Http404

from .models import Topic,Entry
from .forms import TopicForm,EntryForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

@login_required
def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

@login_required
def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    # 确认请求的主题属于当前用户
    if topic.owner != request.user:
        raise Http404

    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)

@login_required
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_logs/new_topic.html',context)

@login_required
def new_entry(request,topic_id):
    """在特定主题中添加新条目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = EntryForm()
    else:
        # Post提交的数据:对数据进行处理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic',topic_id=topic_id)

    # 显示空表单或指出表单数据无效
    context = {'topic':topic,'form':form}
    return render(request,'learning_logs/new_entry.html',context)

@login_required
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:
        # POST提交的数据:对数据进行处理
        form = EntryForm(instance=entry,data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic',topic_id=topic.id)

    context = {'entry':entry,'topic':topic,'form':form}
    return render(request,'learning_logs/edit_entry.html',context)

服务器上没有请求的资源时,标准的做法是返回404响应。这里导入了异常Http404并在用户请求其不应该查看的主题时引发这个异常。收到主题请求后,在渲染页面前检查该主题是否属于当前登录的用户,如果请求的主题不归当前的用户所有,就引发Http404异常,让Django返回一个404错误页面。
现在如果试图查看其他用户的主题条目时,将看到404页面。

保护页面edit_entry

同理配置edit_entry。

from django.shortcuts import render,redirect
from django.contrib.auth.decorators import login_required
from django.http import Http404

from .models import Topic,Entry
from .forms import TopicForm,EntryForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

@login_required
def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

@login_required
def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    # 确认请求的主题属于当前用户
    if topic.owner != request.user:
        raise Http404

    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)

@login_required
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_logs/new_topic.html',context)

@login_required
def new_entry(request,topic_id):
    """在特定主题中添加新条目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = EntryForm()
    else:
        # Post提交的数据:对数据进行处理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic',topic_id=topic_id)

    # 显示空表单或指出表单数据无效
    context = {'topic':topic,'form':form}
    return render(request,'learning_logs/new_entry.html',context)

@login_required
def edit_entry(request,entry_id):
    """编辑既有条目"""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic
    if topic.owner != request.user:
        raise Http404
    if request.method != 'POST':
        # 初次请求,使用当前条目填充表单。
        form = EntryForm(instance=entry)
    else:
        # POST提交的数据:对数据进行处理
        form = EntryForm(instance=entry,data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic',topic_id=topic.id)

    context = {'entry':entry,'topic':topic,'form':form}
    return render(request,'learning_logs/edit_entry.html',context)

将新主题关联到当前用户

当前,用于添加新主题的页面存在问题。——没有将新主题关联到特定用户,如果尝试添加新主题,将看到错误
file
指出了learning_logs_topic.user_id不能为NULL,必须给owner字段指定值。
可以通过request对象获悉当前用户,因此有一个修复该问题的简单方案:

from django.shortcuts import render,redirect
from django.contrib.auth.decorators import login_required
from django.http import Http404

from .models import Topic,Entry
from .forms import TopicForm,EntryForm
# Create your views here.
def index(request):
    """学习笔记的主页。"""
    return render(request,'learning_logs/index.html')

@login_required
def topics(request):
    """显示所有的主题"""
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    context = {'topics':topics}
    return render(request,'learning_logs/topics.html',context)

@login_required
def topic(request,topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    # 确认请求的主题属于当前用户
    if topic.owner != request.user:
        raise Http404

    entries = topic.entry_set.order_by('-date_add')
    context = {'topic': topic,'entries': entries}
    return render(request,'learning_logs/topic.html',context)

@login_required
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = TopicForm()
    else:
        # Post提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            new_topic = form.save(commit=False)
            new_topic.owner = request.user
            new_topic.save()
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request,'learning_logs/new_topic.html',context)

@login_required
def new_entry(request,topic_id):
    """在特定主题中添加新条目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交数据:创建一个新表单。
        form = EntryForm()
    else:
        # Post提交的数据:对数据进行处理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic',topic_id=topic_id)

    # 显示空表单或指出表单数据无效
    context = {'topic':topic,'form':form}
    return render(request,'learning_logs/new_entry.html',context)

@login_required
def edit_entry(request,entry_id):
    """编辑既有条目"""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic
    if topic.owner != request.user:
        raise Http404
    if request.method != 'POST':
        # 初次请求,使用当前条目填充表单。
        form = EntryForm(instance=entry)
    else:
        # POST提交的数据:对数据进行处理
        form = EntryForm(instance=entry,data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic',topic_id=topic.id)

    context = {'entry':entry,'topic':topic,'form':form}
    return render(request,'learning_logs/edit_entry.html',context)

首先调用form.save()并传递实参commit=False,因为要修改新主题,再将其保存到数据库。
接下来将新主题owner属性设置为当前用户。最后,对刚定义的主题实例调用save(),现在,主题包含所有必不可少的数据,将被成功保存。
这个项目目前位置允许任何用户注册,而每个用户想要添加多少个新主题都可以。每个用户只能访问自己的数据,无论查看数据、输入新数据还是修改旧数据都如此。


擦肩而过的概率