好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Django入门(二)

跟书《python编程:从入门到实践》,学习用Django编写名为“学习笔记”的Web应用程序。

Web应用程序的核心是让任何用户都能够注册账户并能够使用它,不管用户身处何方。我们可以创建一些表单,让用户能够添加主题和条目,以及编辑既有的条目。

让用户能够输入数据

先来添加几个页面,让用户能够输入数据。可以让用户能够添加新主题、添加新条目以及编辑既有条目,但不能通过管理网站来输入,因为只有超级用户才可以这样做。

添加新主题

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

用于添加主题的表单:

在Django中,创建表单的最简单方式是使用ModelForm。创建一个名为forms.py的文件,将其存储到models.py所在的目录中,并在其中编写第一个表单。

# vim learning_logs/forms.py

from django import formsfrom .models import Topicclass TopicForm(forms.ModelForm):
    class Meta:
        model = Topic
        fields = ['text']
        labels = {'text': ''}

首先导入了模块forms以及要使用的模型Topic。然后定义了一个名为TopicForm的类,它继承了forms.ModelForm。

最简单的ModelForm版本只包含一个内嵌的Meta类,它告诉Django根据哪个模型创建表单,以及在表单中包含哪些字段。我们根据模型Topic创建一个表单,该表单只包含字段text,且让Django不要为字段text生成标签。

URL模式 new_topic :
# vim learning_logs/urls.py

"""定义learning_logs的URL模式"""from django.urls import path, re_pathfrom . import views

app_name='learning_logs'urlpatterns = [
    # 主页
    path('', views.index, name='index'),

    # 显示所有的主题
    path('topics/', views.topics, name='topics'),

    # 特定主题的详细页面
    re_path(r'^topics/(?P\d+)/$', views.topic, name='topic'),
    
    # 用于添加新主题的页面
    path('new_topic/', views.new_topic, name='new_topic'),]

这个URL模式会将请求交给视图函数 new_topic() 。

视图函数 new_topic() :

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

# vim learning_logs/views.py

from django.shortcuts import renderfrom django.http import HttpResponseRedirectfrom django.urls import reversefrom .models import Topicfrom .forms import TopicFormdef 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_added')
    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(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('learning_logs:topics'))

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

导入了 HttpResponseRedirect 类和刚才创建的表单 TopicForm ,用户提交主题后我们将使用 HttpResponseRedirect 类将用户重定向到网页topics。函数 reverse() 根据指定的URL模型确定URL,这意味着Django将在页面被请求时生成URL。

首先判断请求方法是否是POST——如果请求方法不是POST,返回一个空表单;如果请求方法是POST,将使用用户输入的数据(存储在request.POST中)创建一个 TopicForm 实例,这样对象form将包含用户提交的信息。

然后要将提交的信息保存到数据库,必须先通过检查确定它们是有效的。函数 is_valid() 核实用户填写了所有必不可少的字段(表单字段默认都是必不可少的),且输入的数据与要求的字段类型一致。如果所有字段都有效,我们就调用 save() 将表单中的数据写入数据库。

最后使用 reverse() 获取页面topics的URL,并将其传递给 HttpResponseRedirect() ,后者将用户的浏览器重定向到页面topics。在页面topics中,用户将在主题列表中看到他刚输入的主题。

GET请求和POST请求:

创建Web应用程序时,将用到的两种主要请求类型是GET请求和POST请求。对于只是从服务器读取数据的页面,使用GET请求;在用户需要通过表单提交信息时,通常使用POST请求。处理所有表单时,我们都将指定使用POST方法。

函数 new_topic() 将请求对象作为参数。用户初次请求该网页时,其浏览器将发送GET请求;用户填写并提交表单时,其浏览器将发送POST请求。根据请求的类型,我们可以确定用户请求的是空表单(GET请求)还是要求对填写好的表单进行处理(POST请求)。

模板 new_topic :
# vim learning_logs/templates/learning_logs/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 topicbutton>
  form>
    {% endblock content %}

继承了base.html,然后定义了一个HTML表单。实参action告诉服务器将提交的表单数据发送到哪里,这里我们将它发回给视图函数 new_topic() 。实参method让浏览器以POST请求的方式提交数据。

Django使用模板标签 {% csrf_token %} 来防止***者利用表单来获得对服务器未经授权的访问(这种***被称为跨站 请求伪造)。接着显示表单,只需包含模板变量 {{ form.as_p }} ,就可让Django自动创建显示表单所需的全部字段。修 饰符 as_p 让Django以段落格式渲染所有表单元素。

链接到页面 new_topic :
# vim learning_logs/templates/learning_logs/topics.html

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

{% block content %}  <p>Topicsp>

  <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,编写视图函数和模板,并链接到添加新条目的网页。

用于添加新条目的表单:

创建一个与模型Entry相关联的表单。

# vim learning_logs/forms.py

from django import formsfrom .models import Topic, Entryclass TopicForm(forms.ModelForm):
    class Meta:
        model = Topic
        fields = ['text']
        labels = {'text': ''}class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['text']
        labels = {'text': ''}
        wgdgets = {'text': forms.Textarea(attrs={'cols': 80})}

除了导入Topic外,还需要导入Entry。新类EntryForm继承了forms.ModelForm,它包含的Meta类指出了表单基于的模型以及要在表单中包含哪些字段。

后面定义了属性 widgets ,小部件(widget)是一个HTML表单元素,如单行文本框、多行文本区域或下拉列表。通过设置属性 widgets ,可覆盖Django选择的默认小部件。通过让Django使用forms.Textarea,我们定制了字段 'text' 的输入小部件,将文本区域的宽度设置为80列,而不是默认的40列。

URL模式 new_entry :

在用于添加新条目的页面的URL模式中,需要包含实参topic_id ,因为条目必须与特定的主题相关联。

# vim learning_logs/urls.py

"""定义learning_logs的URL模式"""from django.urls import path, re_pathfrom . import views

app_name='learning_logs'urlpatterns = [
    # 主页
    path('', views.index, name='index'),

    # 显示所有的主题
    path('topics/', views.topics, name='topics'),

    # 特定主题的详细页面
    re_path(r'^topics/(?P\d+)/$', views.topic, name='topic'),

    # 用于添加新主题的页面
    path('new_topic/', views.new_topic, name='new_topic'),

    # 用于添加新条目的页面
    re_path(r'^new_entry/(?P\d+)/$', views.new_entry, name='new_entry'),]

这个URL模式与形式为 http://localhost:8000/new_entry/id/ 的URL匹配,其中id是一个与主题ID匹配的数字。代码 (?P 捕获一个数字值,并将其存储在变量 topic_id 中。请求的URL与这个模式匹配时,Django将请求和主题ID发送给函数 new_entry() 。

视图函数 new_entry() :

视图函数 new_entry() 与视图函数 new_topic() 比较类似。

# vim learning_logs/views.py

from django.shortcuts import renderfrom django.http import HttpResponseRedirectfrom django.urls import reversefrom .models import Topicfrom .forms import TopicForm, EntryFormdef 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_added')
    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(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('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 HttpResponseRedirect(reverse('learning_logs:topic', args=[topic_id]))

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

首先导入了刚创建的EntryForm, new_entry() 的定义包含形参 topic_id ,用于存储从URL中获得的值。渲染页面以及处理表单数据时,都需要知道针对的是哪个主题,因此我们使用 topic_id 来获得正确的主题。

然后还是检查请求方法是POST还是GET。如果是GET请求,将创建一个空的EntryForm实例。如果请求方法为POST,我们就对数据进行处理:创建一个EntryForm实例,使用request对象中的POST数据来填充它;再检查表单是否有效,如果有效则设置条目对象的属性 topic ,再将条目对象保存到数据库。

在调用 save() 时,传递了实参 commit=False ,让Django创建一个新的条目对象,并将其存储到 new_entry 中,但不将它保存到数据库中。我们将 new_entry 的属性 topic 设置为在这个函数开头从数据库中获取的主题,然后调用 save() ,且不指定任何实参。这将把条目保存到数据库,并将其与正确的主题相关联。

最后,将用户重定向到显示相关主题的页面。调用 reverse() 时,需要提供两个实参:要根据它来生成URL的URL模式的名称;列表args ,其中包含要包含在URL中的所有实参。

模板 new_entry :

模板 new_entry 与模板 new_topic 也比较类似。

# vim learning_logs/templates/learning_logs/new_entry.html

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

{% block content %}  <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}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 entrybutton>
  form>
    {% endblock content %}

首先继承base.html。接着在页面顶端显示了主题,让用户知道是在哪个主题中添加条目;该主题名也是一个链接,可用于返回该主题的主页面。

表单的实参action 包含URL中的 topic_id 值,让视图函数能够将新条目关联到正确的主题。除此之外,这个模板与模板new_topic.html完全相同。

链接到页面 new_entry :
# vim learning_logs/templates/learning_logs/topic.html

{% 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 entrya>
  p>
  <ul>
  {% for entry in entries %}    <li>
      <p>{{ entry.date_added|date:'M d, Y H:i' }}p>
      <p>{{ entry.text|linebreaks }}p>
    li>
  {% empty %}    <li>
      There are no entries for this topic yet.    li>
  {% endfor %}  ul>{% endblock content %}

在显示条目前添加链接,因为在这种页面中,执行的最常见的操作是添加新条目。

访问网页:

编辑条目

下面来创建一个页面,让用户可以编辑既有的条目。

URL模式 edit_entry :
# vim learning_logs/urls.py

"""定义learning_logs的URL模式"""from django.urls import path, re_pathfrom . import views

app_name='learning_logs'urlpatterns = [
    # 主页
    path('', views.index, name='index'),

    # 显示所有的主题
    path('topics/', views.topics, name='topics'),

    # 特定主题的详细页面
    re_path(r'^topics/(?P\d+)/$', views.topic, name='topic'),

    # 用于添加新主题的页面
    path('new_topic/', views.new_topic, name='new_topic'),

    # 用于添加新条目的页面
    re_path(r'^new_entry/(?P\d+)/$', views.new_entry, name='new_entry'),

    # 用于编辑条目的页面
    re_path(r'^edit_entry/(?P\d+)/$', views.edit_entry, name='edit_entry'),]

在URL(如 http://localhost:8000/edit_entry/1/ )中传递的ID存储在形参 entry_id 中。这个URL模式将预期匹配的请求发送给视图函数 edit_entry() 。

视图函数 edit_entry() :

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

# vim learning_logs/views.py

from django.shortcuts import renderfrom django.http import HttpResponseRedirectfrom django.urls import reversefrom .models import Topic, Entryfrom .forms import TopicForm, EntryFormdef 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_added')
    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(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('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 HttpResponseRedirect(reverse('learning_logs:topic', args=[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 HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id]))

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

首先导入模型Entry 。接着获取用户要修改的条目对象,以及与该条目相关联的主题。在请求方法为GET时,我们使用实参 instance=entry 创建一个EntryForm实例。该实参让Django创建一个表单,并使用既有条目对象中的信息填充它。用户将看到既有的数据,并能够编辑它们。

处理POST请求时,传递实参 instance=entry 和 data=request.POST ,让Django根据既有条目对象创建一个表单实例,并根据request.POST中的相关数据对其进行修改。然后检查表单是否有效,如果有效就调用 save() 且不指定任何实参。接下来重定向到显示条目所属主题的页面,用户将在其中看到其编辑的条目的新版本。

模板 edit_entry :
# vim learning_logs/templates/learning_logs/edit_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 changesbutton>
  form>{% endblock content %}

链接到页面 edit_entry :
# vim learning_logs/templates/learning_logs/topic.html

{% 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 entrya>
  p>
  <ul>
  {% for entry in entries %}    <li>
      <p>{{ entry.date_added|date:'M d, Y H:i' }}p>
      <p>{{ entry.text|linebreaks }}p>
      <p>
        <a href="{% url 'learning_logs:edit_entry' entry.id %}">edit entrya>
      p>
    li>
  {% empty %}    <li>
      There are no entries for this topic yet.    li>
  {% endfor %}  ul>{% endblock content %}

将编辑链接放在每个条目的日期和文本后面。在循环中,使用模板标签 {% url %} 根据URL模式 edit_entry 和当前条目的ID属性 entry.id 来确定URL。链接文本为 "edit entry" ,它出现在页面中每个条目的后面。

访问网页:

创建用户账户

应用路径:users

接下来建立一个用户注册和身份验证系统,让用户能够注册账户,进而登录和注销。

应用程序users 创建应用程序:
# python manage.py  startapp users# lsdb.sqlite3  learning_log  learning_logs  ll_env  manage.py  users# ls users/admin.py  apps.py  __init__.py  migrations  models.py  tests.py  views.py

可以看到, users目录的结构与应用程序learning_logs相同。

将应用程序users添加到settings.py中:
# vim learning_log/settings.pyINSTALLED_APPS = [
    'django.contrib.admin',    'django.contrib.auth',    'django.contrib.contenttypes',    'django.contrib.sessions',    'django.contrib.messages',    'django.contrib.staticfiles',    # 我的应用程序
    'learning_logs',    'users',                #增加这行]

包含应用程序users的URL:
# vim learning_log/urls.py

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include('users.urls', namespace='users')),               #增加这行
    path('', include('learning_logs.urls', namespace='learning_logs')),]

同样的,这里保存完文件之后会报错 ModuleNotFoundError: No module named 'users.urls' ,不用理会,继续往下做。

登录页面

现在来实现登录页面的功能,可以使用Django提供的默认登录视图。

URL模式:
# vim users/urls.py

"""定义users的URL模式"""from django.urls import path, re_pathfrom django.contrib.auth.views import LoginViewfrom . import views

app_name='users'urlpatterns = [
    # 登录主页
    re_path(r'^login/$', LoginView.as_view(template_name='users/login.html'), name='login'),]

模板 login.html :
# mkdir -p users/templates/users# vim users/templates/users/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 'users:login' %}">
  {% csrf_token %}
  {{ form.as_p }}    
  <button name="submit">log inbutton>
  <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
  form>
    {% endblock content %}

先继承base.html,请注意,一个应用程序中的模板可继承另一个应用程序中的模板。

如果表单的errors属性被设置,我们就显示一条错误消息,指出输入的用户名—密码对与数据库中存储的任何用户名—密码对都不匹配。

我们要让登录视图处理表单,因此将实参 action 设置为登录页面的URL。登录视图将一个表单发送给模板,在模板中,我们显示这个表单并添加一个提交按钮。后面包含了一个隐藏的表单元素’ next ’,其中的实参 value 告诉Django在用户成功登录后将其重定向到主页。

链接到登录页面:
# vim learning_logs/templates/learning_logs/base.html

<p>
  <a href="{% url 'learning_logs:index' %}">Learning Loga> -  <a href="{% url 'learning_logs:topics' %}">Topicsa> - 
  {% if user.is_authenticated %}
    Hello, {{ user.username }}.
  {% else %}    <a href="{% url 'users:login' %}">log ina>
  {% endif %}p>{% block content %}{% endblock content %}

在Django身份验证系统中,每个模板都可使用变量 user ,该变量有一个 is_authenticated 属性:如果用户已登录,该属性将为True,否则为False。

在这里向已登录的用户显示一条问候语。对于已通过身份验证的用户设置了属性 username ,我们使用这个属性来个性化问候语,让用户知道他已经登录。而对于还未通过身份验证的用户,我们再显示一个到登录页面的链接。

使用登录页面:

访问 192.168.30.128:8000/users/login/ 。如果之前使用超级用户登录的,注销之后再访问。

注销

现在需要提供一个让用户注销的途径。我们不需要创建用于注销的页面,而让用户只需单击一个链接就能注销并返回到主页。为此,我们将为注销链接定义一个URL模式,编写一个视图函数,并在base.html中添加一个注销链接。

注销URL:
# vim users/urls.py

"""定义users的URL模式"""from django.urls import path, re_pathfrom django.contrib.auth.views import LoginViewfrom . import views

app_name='users'urlpatterns = [
    # 登录主页
    re_path(r'^login/$', LoginView.as_view(template_name='users/login.html'), name='login'),

    # 注销
    re_path(r'^logout/$', views.logout_view, name='logout'),]

这个URL模式将请求发送给函数 logout_view() ,要将其与我们下面在其中调用的函数 logout() 区分开来。

视图函数 logout_view() :
# vim users/views.py

from django.shortcuts import renderfrom django.http import HttpResponseRedirectfrom django.urls import reversefrom django.contrib.auth import logoutdef logout_view(request):
    """注销用户"""
    logout(request)
    return HttpResponseRedirect(reverse('learning_logs:index'))

先从django.contrib.auth中导入了函数 logout() ;然后调用了函数 logout() ,它要求将request对象作为实参;最后重定向到主页。

链接到注销视图:

现在需要在base.html中添加注销链接,让每个页面都包含它;将它放在标签 {% if user.is_authenticated %} 中,使得仅当用户登录后才能看到它。

# vim learning_logs/templates/learning_logs/base.html

<p>
  <a href="{% url 'learning_logs:index' %}">Learning Loga> -  <a href="{% url 'learning_logs:topics' %}">Topicsa> -
  {% if user.is_authenticated %}
    Hello, {{ user.username }}.    <a href="{% url 'users:logout' %}">log outa>
  {% else %}    <a href="{% url 'users:login' %}">log ina>
  {% endif %}p>{% block content %}{% endblock content %}

访问网页:

使用超级用户登录

注册页面

下面使用Django提供的表单UserCreationForm来创建一个让新用户能够注册的页面。

URL模式:
# vim users/urls.py

"""定义users的URL模式"""from django.urls import path, re_pathfrom django.contrib.auth.views import LoginViewfrom . import views

app_name='users'urlpatterns = [
    # 登录主页
    re_path(r'^login/$', LoginView.as_view(template_name='users/login.html'), name='login'),

    # 注销
    re_path(r'^logout/$', views.logout_view, name='logout'),

    # 注册页面
    re_path(r'^register/$', views.register, name='register'),]

这个模式与URL http://localhost:8000/users/register/ 匹配,并将请求发送给我们即将编写的函数 register() 。

视图函数 register() :

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

# vim users/views.py

from django.shortcuts import renderfrom django.http import HttpResponseRedirectfrom django.urls import reversefrom django.contrib.auth import login, logout, authenfrom django.contrib.auth.forms import UserCreationFormdef logout_view(request):
    """注销用户"""
    logout(request)
    return HttpResponseRedirect(reverse('learning_logs: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('learning_logs:index'))

    context = {'form': form}
    return render(request, 'users/register.html', context)

首先导入了函数 login() 和 authenticate() ,以便在用户正确地填写了注册信息时让其自动登录。还导入了默认表单 UserCreationForm 。在函数 register() 中,我们检查要响应的是否是POST请求。如果不是,就创建一个UserCreationForm实例,且不给它提供任何初始数据。

如果响应的是POST请求,我们就根据提交的数据创建一个UserCreationForm实例,并检查这些数据是否有效:就这里而言,是用户名未包含非法字符,输入的两个密码相同,以及用户没有试图做恶意的事情。如果提交的数据有效,我们就调用表单的方法 save() ,将用户名和密码的散列值保存到数据库中。方法 save() 返回新创建的用户对象,我们将其存储在 new_user 中。

保存用户的信息后,我们让用户自动登录,这包含两个步骤。首先,我们调用 authenticate() ,并将实参 new_user.username 和密码传递给它。用户注册时,被要求输入密码两次;由于表单是有效的,我们知道输入的这两个密码是相同的,因此可以使用其中任何一个。在这里,我们从表单的POST数据中获取与键’password1’相关联的值。如果用户名和密码无误,方法 authenticate() 将返回一个通过了身份验证的用户对象,而我们将其存储在变量 authenticated_user 中。

接下来,我们调用函数 login() ,并将对象 request 和 authenticated_user 传递给它,这将为新用户创建有效的会话。最后,我们将用户重定向到主页,其页眉中显示了一条个性化的问候语,让用户知道注册成功了。

注册模板 register.html :
# vim users/templates/users/register.html

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

{% block content %}  <form method="post" action="{% url 'users:register' %}">
    {% csrf_token %}
    {{ form.as_p }}        
    <button name="submit">registerbutton>
    <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
  form>
    {% endblock content %}

链接到注册页面:
# vim learning_logs/templates/learning_logs/base.html

<p>
  <a href="{% url 'learning_logs:index' %}">Learning Loga> -  <a href="{% url 'learning_logs:topics' %}">Topicsa> -
  {% if user.is_authenticated %}
    Hello, {{ user.username }}.    <a href="{% url 'users:logout' %}">log outa>
  {% else %}    <a href="{% url 'users:register' %}">registera> -    <a href="{% url 'users:login' %}">log ina>
  {% endif %}p>{% block content %}{% endblock content %}

访问网页:

现在,已登录的用户看到的是个性化的问候语和注销链接,而未登录的用户看到的是注册链接和登录链接。

需要注意的是,这里的注册系统允许用户创建任意数量的账户。实际情况下的系统会要求用户确认其身份:发送一封确认邮件或者用手机验证码,用户回复后其账户才生效。通过这样做,系统生成的垃圾账户将比这里使用的简单系统少。

让用户拥有自己的数据

用户应该能够输入其专有的数据,因此我们将创建一个系统,确定各项数据所属的用户,再限制对页面的访问,让用户只能使用自己的数据。

下面将修改模型Topic ,让每个主题都归属于特定用户。这也将影响条目,因为每个条目都属于特定的主题。

使用@login_required限制

Django提供了装饰器 @login_required ,让你能够轻松地实现这样的目标:对于某些页面,只允许已登录的用户访问它们。 装饰器 (decorator)是放在函数定义前面的指令,Python在函数运行前,根据它来修改函数代码的行为。

限制对topics页面的访问:
# vim learning_logs/views.py

from django.shortcuts import renderfrom django.http import HttpResponseRedirectfrom django.urls import reversefrom django.contrib.auth.decorators import login_requiredfrom .models import Topic, Entryfrom .forms import TopicForm, EntryFormdef index(request):
    """学习笔记的主页"""
    return render(request, 'learning_logs/index.html')@login_requireddef 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_added')
    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(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('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 HttpResponseRedirect(reverse('learning_logs:topic', args=[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 HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id]))

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

首先导入函数 login_required() ,将 login_required() 作为装饰器用于视图函数 topics() ——在它前面加上符号 @ 和 login_required ,让Python在运行 topics() 的代码前先运行 login_required() 的代码。

login_required() 的代码检查用户是否已登录,仅当用户已登录时,Django才运行 topics() 的代码。如果用户未登录,就重定向到登录页面。

修改settings.py:

为了实现未登录的重定向,需要修改settings.py。

# vim learning_log/settings.py                  #末尾添加# 我的设置LOGIN_URL = '/users/login/'

访问网页:

可以看到,现在直接点击 topics 会直接跳转到登录页。

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

在项目“学习笔记”中,我们将不限制主页、注册页面和注销页面的访问,同时限制其它所有页面的访问。

# vim learning_logs/views.py

from django.shortcuts import renderfrom django.http import HttpResponseRedirectfrom django.urls import reversefrom django.contrib.auth.decorators import login_requiredfrom .models import Topic, Entryfrom .forms import TopicForm, EntryFormdef index(request):
    """学习笔记的主页"""
    return render(request, 'learning_logs/index.html')@login_requireddef topics(request):
    """显示所有主题"""
    topics = Topic.objects.order_by('date_added')
    context = {'topics': topics}
    return render(request, 'learning_logs/topics.html', context)@login_requireddef topic(request, topic_id):
    """显示一个主题及其详细页面"""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_added')
    context = {'topic': topic, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)@login_requireddef new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单
        form = TopicForm()
    else:
        # POST提交的数据,对数据进行处理
        form = TopicForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('learning_logs:topics'))

    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)@login_requireddef 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 HttpResponseRedirect(reverse('learning_logs:topic', args=[topic_id]))

    context = {'topic': topic, 'form': form}
    return render(request, 'learning_logs/new_entry.html', context)@login_requireddef 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 HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id]))

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

如果你在未登录的情况下尝试访问这些页面,将被重定向到登录页面。此外,你还不能单击到new_topic等页面的链接,如果你输入URL http://192.168.30.128:8000/new_topic/ ,将重定向到登录页面。对于所有与私有用户数据相关的URL,都应限制对它们的访问。

将数据关联到用户

现在将数据关联到提交它们的用户。我们只需将最高层的数据关联到用户,这样更低层的数据将自动关联到用户。在项目“学习笔记”中,应用程序的最高层数据是主题,而所有条目都与特定主题相关联。只要每个主题都归属于特定用户,我们就能确定数据库中每个条目的所有者。

修改模型Topic:
# vim learning_logs/models.py

from django.db import modelsfrom django.contrib.auth.models import Userclass 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_added = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name_plural = 'entries'

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

首先导入了 django.contrib.auth 中的模型User,然后在Topic中添加了字段owner,它建立到模型User的外键关系。

确定当前有哪些用户:

迁移数据库时,Django将对数据库进行修改,使其能够存储主题和用户之间的关联。为执行迁移,Django需要知道该将各个既有主题关联到哪个用户。最简单的办法是,将既有主题都关联到同一个用户,如超级用户。为此,我们需要知道用户的ID。

# python manage.py shell>>> from django.contrib.auth.models import User>>> User.objects.all()<QuerySet [<User: ll_admin>, <User: lzx>]>>>> for user in User.objects.all():...     print(user.username, user.id)... 
ll_admin 1
lzx 2

迁移数据库:
# python manage.py makemigrations learning_logsSelect an option: 1                 #选择第1个选项——现在提供默认值>>> 1               #输入用户IDMigrations for 'learning_logs':
  learning_logs/migrations/0003_topic_owner.py
    - Add field owner to topic

将所有既有主题都关联到管理用户ll_admin,我输入了用户ID值1。并非必须使用超级用户,也可使用已创建的任何用户的ID。

# python manage.py migrate                  #执行迁移# python manage.py shell>>> from learning_logs.models import Topic>>> for topic in Topic.objects.all():...     print(topic, topic.owner)... 
Chess ll_admin
Rock Climbing ll_admin

可以看到,每个主题的所有者是超级用户ll_admin。

注意:也可以重置数据库而不是迁移它,但如果这样做,既有的数据都将丢失。如果确实想要一个新的数据库,可以执行命令 python manage.py flush ,这会重建数据库的结构。如果你这样做,就需要重新创建超级用户,且原来的所有数据都将丢失。

只允许用户访问自己的主题 修改视图函数 views.py :
# vim learning_logs/views.py

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

用户登录后,request对象将有一个 user 属性,这个属性存储了有关该用户的信息。代码 Topic.objects.filter(owner=request.user) 让Django只从数据库中获取 owner 属性为当前用户的Topic对象。

要查看结果,以所有既有主题关联到的用户的身份登录(这里我们是ll_admin)并访问topics页面,你将看到所有的主题。然后,注销并以另一个用户的身份登录,topics页面将不会列出任何主题。

保护用户的主题

接下来限制对单个主题的页面的访问。

修改视图函数 views.py :
# vim learning_logs/views.py

from django.http import HttpResponseRedirect, Http404               #导入Http404@login_requireddef 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_added')
    context = {'topic': topic, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)

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

保护页面 edit_entry

页面 edit_entry 的URL为 http://192.168.30.128:8000/edit_entry/entry_id/ ,其中 entry_id 是一个数字。下面来保护这个页面,禁止用户通过输入类似于前面的URL来访问其他用户的条目。

修改视图函数 views.py :
# vim learning_logs/views.py

@login_requireddef 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 HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id]))

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

获取指定的条目以及与之相关联的主题,然后检查主题的所有者是否是当前登录的用户,如果不是,就引发Http404异常。

将新主题关联到当前用户

当前,用于添加新主题的页面存在问题。如果你尝试添加新主题,将看到错误消息IntegrityError,指出 learning_logs_topic.user_id 不能为NULL 。Django的意思是说,创建新主题时,你必须指定其owner字段的值。

修改视图函数 views.py :
# vim learning_logs/views.py

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

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

首先调用 form.save() ,并传递实参 commit=False ,因为我们先修改新主题,再将其保存到数据库中。

接下来,将新主题的 owner 属性设置为当前用户。最后,对刚定义的主题实例调用 save() 。现在主题包含所有必不可少的数据,将被成功地保存。

现在,这个项目允许任何用户注册,而每个用户想添加多少新主题都可以。但每个用户都只能访问自己的数据,无论是查看数据、输入新数据还是修改旧数据时都如此。

查看更多关于Django入门(二)的详细内容...

  阅读:32次

上一篇: Django入门与实践

下一篇:Django入门(一)