Python编程-数据可视化:第19章-用户账户

让用户输入数据

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

添加新主题

  1. 用于添加主题的表单
    让用户输入并提交信息的页面都是表单,

forms.py

  1. 创建forms 类。
from django import forms
from .models import Topic

class TopicForm(forms.ModelForm):
     class Meta:
        model = Topic
        fields = ['text']
        labels = {'text': ''}
  1. URL模式new_topic
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'),
]
  1. 视图函数new_topic()
from django.shortcuts import render, redirect  
  
from .models import Topic  
from .forms import TopicForm  
  
--snip--  
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)
  1. 模板new_topic
{% 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 %}
  1. 链接到页面new_topic

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

添加新条目

  1. 用于添加新条目的表单
    我们需要创建一个与模型Entry 相关联的表单,但这个表单的定制程度比TopicForm 更高一些:

forms.py

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})}
  1. URL模式new_entry
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'),
]
  1. 视图函数new_entry()
from django.shortcuts import render, redirect  
  
from .models import Topic  
from .forms import TopicForm, EntryForm
--snip--
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)
  1. 模板new_entry
{% 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 entry</button>
    </form>

  {% endblock content %}
  1. 链接到页面new_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 %}

    <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 %}

编辑条目

  1. URL模式edit_entry
    urls.py
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:entry_id>/', views.edit_entry, name='edit_entry'),
]
  1. 视图函数edit_entry()
    页面edit_entry 收到GET请求时,edit_entry() 将返回一个表
    单,让用户能够对条目进行编辑;收到POST请求(条目文本经过修
    订)时,则将修改后的文本保存到数据库:
from django.shortcuts import render, redirect  
  
from .models import Topic, Entry  
from .forms import TopicForm, EntryForm  
--snip--  
  
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)
  1. 模板edit_entry
 {% 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 changes</button>
    </form>
 
  {% endblock content %}
  1. 链接到页面edit_entry

创建用户账户

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

应用程序创建users

(ll_env) [python@node1 learning_log]$ python manage.py  startapp users

安装users APP

---snip----
INSTALLED_APPS = [
    'learning_logs',
    'users',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
---snip----

包含users 的URL

vi learning_log/urls.py

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

配置登录页面

首先来实现登录页面。我们将使用Django提供的默认视图login ,因此这个应用程序的URL模式稍有不同。在目录learning_log/users/中,新建一个名为urls.py的文件,并在其中添加如下代码:

配置users APP 页面

"""为应用程序users定义URL模式。"""  
  
from django.urls import path, include  
  
app_name = 'users'  
urlpatterns = [  
    # 包含默认的身份验证URL。  
    path('', include('django.contrib.auth.urls')),  
]
模板login.html
cd ~/learning_log
mkdir -p users/templates/registration
{% 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 in</button> 
     <input type="hidden" name="next" 
        value="{% url 'learning_logs:index' %}" />
    </form>

  {% endblock content %}

这个模板继承了base.html,旨在确保登录页面的外观与网站的其他页面相同。请注意,一个应用程序中的模板可继承另一个应用程序中的模板。如果设置表单的errors 属性,就显示一条错误消息(见❶)
指出输入的用户名密码对与数据库中存储的任何用户名密码对都不匹配

我们要让登录视图对表单进行处理,因此将实参action 设置为登录页面的URL(见❷)。登录视图将一个表单发送给模板。在模板中,我们显示这个表单(见❸)并添加一个提交按钮(见❹)。在❺处,包含了一个隐藏的表单元素'next' ,其中的实参value 告诉Django在用户成功登录后将其重定向到什么地方。在本例中,用户将返回主页。

链接到登录页面
<p>  
<a href="{% url 'learning_logs:index' %}">Learning Log</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 %}



注销

向 base.html 添加注销链接。

[python@node1 learning_log]$ vi learning_logs/templates/learning_logs/base.html 
<p>
  <a href="{% url 'learning_logs:index' %}">Learning Log</a>
  <a href="{% url 'learning_logs:topics' %}">Topics</a>
  {% if user.is_authenticated %}
      Hello, {{ user.username }}.
      <a href="{% url 'users:logout' %}">Log out</a>
  {% else %}
      <a href="{% url 'users:login' %}">Log in</a>
{% endif %}
</p>

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

注销时确认页面

成功注销后,用户希望获悉这一点。因此默认的注销视图使用模板logged_out.html渲染注销确认页面,我们现在就来创建该模板。下面这个简单的页面确认用户已注销,请将其存储到目录templates/registration(login.html所在的目录)中:

[python@node1 registration]$ vi logged_out.html
{% extends "learning_logs/base.html" %}

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

注销后,确认页面提示用户已成功注销。

用户注册页面

注册页面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'), 
]

视图函数register()

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

views.py

from django.shortcuts import render
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)

让用户拥有自己的数据

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

使用@login_required 限制访问

Django提供了装饰器@login_required ,让你能够轻松地只允许已登录用户访问某些页面。装饰器 (decorator)是放在函数定义前面的指令,Python在函数运行前根据它来修改函数代码的行为。下面来看一个示例。

  1. 限制访问显示所有主题的页面。
    views.py
from django.shortcuts import render, redirect  
from django.contrib.auth.decorators import login_required  
  
from .models import Topic, Entry  
--snip--  
  
@login_required  
def topics(request):  
"""显示所有的主题。"""  
--snip--

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

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

为实现这种重定向,需要修改settings.py,让Django知道到哪里去查找登录页面。请在settings.py末尾添加如下代码:

settings.py

--snip--  
  
# 我的设置  
LOGIN_URL = 'users:login'
  1. 全面限制对项目“学习笔记”的访问
    Django让你能够轻松地限制对页面的访问,但你必须确定要保护哪些页面。最好先确定项目的哪些页面不需要保护,再限制对其他所有页面的访问。你可轻松地修改过于严格的访问限制。比起不限制对敏感页面的访问,这样做的风险更低。

在项目“学习笔记”中,将不限制对主页和注册页面的访问,并限制对其他所有页面的访问。
views.py

--snip--  
@login_required  
def topics(request):  
--snip--  
  
@login_required  
def topic(request, topic_id):  
--snip--  
  
@login_required  
def new_topic(request):  
--snip--  
  
@login_required  
def new_entry(request, topic_id):  
--snip--  
  
@login_required  
def edit_entry(request, entry_id):  
--snip--

将数据关联到用户

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

  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.CASCAE)
    
    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 f"{self.text[:50]}..."
  1. 确定当前有哪些用户

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

(ll_env)learning_log$ python manage.py shell  
❶ >>> from django.contrib.auth.models import User  
❷ >>> User.objects.all()  
<QuerySet [<User: ll_admin>, <User: eric>, <User: willie>]>  
❸ >>> for user in User.objects.all():  
... print(user.username, user.id)  
...  
ll_admin 1  
eric 2  
willie 3  
>>>