Web应用程序:用户账户

一、让用户输入数据

在建立用于创建用户账户的身份验证系统之前,我们添加几个页面,让用户能够输入数据。用户将能够添加新主题,添加新条目,以及编辑既有的条目。

当前,只有超级用户能够通过管理网站输入数据。我们不想让用户与管理网站交互,因此将使用 Django 的表单创建工具来创建让用户能够输入数据的页面。

1. 添加新主题

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

  • 用于添加主题的表单

让用户输入并提交信息的页面包含名为表单(form)的 HTML 元素。

我们在文件夹learning_logs 中创建一个名为 forms.py 的文件,将其存储到models.py 所在的目录中,并在其中编写第一个表单:

python 复制代码
from django import forms
from  .models import Topic
class TopicForm(forms.ModelForm):
    class Meta:
        model = Topic
        fields = ['title']
        labels = {'title': ''}
  • URL 模式 new_topic

新网页的 URL 应简短且具有描述性,因此在用户要添加新主题时,我们将页面切换到 http://localhost:8000/new_topic/。下面是网页new_topic 的 URL 模式,请将其添加到 learning_logs/urls.py 中:

python 复制代码
urlpatterns = [
    # 用于添加新主题的网页
    path('new_topic/', views.new_topic, name='new_topic'),
]
  • 视图函数 new_topic()

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

python 复制代码
def new_topic(request):
    """添加新主题"""
    
    # 检查请求方法是否为POST
    # 注意:这里是 '!=',表示如果不是POST请求,则执行下面的代码块
    if request.method != 'POST':
        # 情况1:用户第一次访问页面(GET请求)
        # 未提交数据:创建一个空的表单实例(用于显示空白表单)
        form = TopicForm()
    else:
        # 情况2:用户提交了表单数据(POST请求)
        # POST 提交的数据:对数据进行处理
        # 使用用户提交的数据(request.POST)创建表单实例
        form = TopicForm(data=request.POST)
        
        # 检查表单数据是否有效(自动执行字段验证)
        if form.is_valid():
            # 数据有效:将数据保存到数据库
            form.save()
            # 保存成功后,重定向到主题列表页面
            # 'learning_logs:topics' 是URL模式的命名空间
            return redirect('learning_logs:topics')
        # 如果数据无效,继续向下执行,会在模板中显示错误信息

    # 两种情况会执行到这里:
    # 1. GET请求:显示空表单
    # 2. POST请求但数据无效:显示带错误信息的表单
    context = {'form': form}  # 将表单对象传递给模板
    return render(request, 'learning_logs/new_topic.html', context)
  • 模板 new_topic

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

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {% 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_div }}
        <button name="submit">Add topic</button>
    </form>
    {% endblock content %}
</body>
</html>
  • 链接到页面 new_topic

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

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {% 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.text }}</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 %}
</body>
</html>

运行结果如下:

2. 添加新条目

添加新主题之后,用户还会想添加新条目。我们将再次定义 URL,编写视图函数和模板,并链接到添加新条目的网页。但在此之前,需要在 forms.py中再添加一个类。

  • 用于添加新条目的表单

我们需要创建一个与模型 Entry 相关联的表单,这个表单的定制程度比 TopicForm 更高一些:

python 复制代码
class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['text']
        # 这里给字段text 指定了一个空白标签
        labels = {'text': ''}
        # 对于 EntryForm,我们添加了属性 widgets
        # 小部件(widget)是一种 HTML 表单元素,如单行文本框、多行文本区域或下拉列表。通过设置属性 widgets,可覆盖 Django 选择的默认小部件。
        widgets = {'text': forms.Textarea()}
  • URL 模式 new_entry

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

python 复制代码
urlpatterns = [
    # 用于添加新条目的页面
    path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
]
  • 视图函数 new_entry()

视图函数 new_entry() 与 new_topic() 函数很像。在 views.py中添加如下代码:

python 复制代码
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

模板 new_entry 类似于模板 new_topic,如下面的代码所示:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  {% 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_div }}
    <button name='submit'>Add entry</button>
  </form>
  {% endblock content %}
</body>
</html>
  • 链接到页面 new_entry

接下来,需要在显示特定主题的页面中添加页面 new_entry 的链接:

python 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {% extends 'learning_logs/base.html' %}
    {% block content %}
    <p>Topic: {{ topic.text }}</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 %}
</body>
</html>

运行结果:

3. 编辑条目

下面创建让用户编辑既有条目的页面。

  • URL 模式 edit_entry

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

python 复制代码
urlpatterns = [
    # 用于编辑条目的页面
    path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
]
  • 视图函数 edit_entry()

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

python 复制代码
def edit_entry(request, topic_id, entry_id):
    """编辑既有的条目"""
    entry = Topic.objects.get(id=entry_id)
    topic = entry.topic
    if request.method != 'POST':
    # 初次请求:使用当前的条目填充表单
        form = EntryForm(instance=entry)
    else:
        # 初次请求:使用当前的条目填充表单
        form = EntryForm(instance=entry, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic', topic_id=topic.id)
    context = {'entry': entry, 'topic': topic, 'form': form}
    return render(request, 'learning_logs/edit_entry.html', context)
  • 模板 edit_entry

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

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  {% 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_div }}
    <button name="submit">Save changes</button>
  </form>
  {% endblock content %}
</body>
</html>
  • 链接到页面 edit_entry

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

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {% extends 'learning_logs/base.html' %}
    {% block content %}
    <p>Topic: {{ topic.text }}</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>
                <p>
                    <a href="{% url 'learning_logs:edit_entry' entry.id %}">Edit entry</a>
                </p>
        </li>
            {% empty %}
        <li>There are no entries for this topic yet.</li>
        {% endfor %}
    </ul>
    {% endblock content %}
</body>
</html>

运行结果:

二、创建用户账户

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

1. 应用程序 accounts

首先使用命令 startapp 创建一个名为 accounts 的应用程序:

python 复制代码
python manage.py startapp accounts

2. 将应用程序 accounts 添加到 settings.py

settings.py 中,需要将这个新的应用程序添加到 INSTALLED_APPS中,如下所示:

python 复制代码
INSTALLED_APPS = [
    # 添加我的应用
    'accounts',
]

3. 包含应用程序 accounts 的 URL

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

ll_project/urls.py

python 复制代码
urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('', include('learning_logs.urls')),
]

4. 登录页面

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

python 复制代码
from django.urls import path, include

app_name = 'accounts'
urlpatterns = [
    # 包含默认的身份验证 URL
    path('', include('django.contrib.auth.urls'))
]
  • 模板 login.html

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

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  {% extends 'learning_logs/base.html' %}
  {% block content %}
  {% if form.errors %}
  <p>Your username and password didn't match. Please try again.</p>
  {% endif %}
  <form action="{% url 'accounts:login' %}" method='post'>
    {% csrf_token %}
    {{ form.as_div }}
    <button name="submit">Log in</button>
  </form>
  {% endblock content %}
</body>
</html>
  • 设置 LOGIN_REDIRECT_URL

用户成功登录后,Django 需要知道应该将用户重定向到哪里。我们在设置文件中指定这一点。

为此,在文件夹 ll_project 中的文件 settings.py 的末尾添加如下代码:

python 复制代码
# 我的设置
LOGIN_REDIRECT_URL = 'learning_logs:index'
  • 链接到登录页面

下面在 base.html 中添加登录页面的链接,让所有页面都包含它。在用户已登录时,我们不想显示这个链接,因此将它嵌套在一个 {% if %}标签中:

python 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <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 'accounts:login' %}">Log in</a>
        {% endif %}
    </p>
    {% block content %}{% endblock content %}
</body>
</html>
  • 使用登录页面

前面建立了一个用户账户,下面来进行登录,看看登录页面是否管用。首先访问 http://localhost:8000/admin/,如果你已经以管理员的身份登录了,请在页眉上找到注销链接并单击它。

注销后,访问 http://localhost:8000/accounts/login/,可以看到图所示的登录页面。输入前面设置的用户名和密码,将进入主页。在这个主页的页眉中,显示了一条个性化问候语,其中包含用户名。

5. 注销

现在需要提供一个让用户注销的途径。注销请求应以 POST 请求的方式提交,因此我们将在 base.html 中添加一个小型的注销表单。用户在单击注销按钮时,将进入一个确认自己已注销的页面。

  • 在 base.html 中添加注销表单

下面在 base.html 中添加注销表单,让每个页面都包含它。将注销表单放在一个 if 代码块中,使得只有已登录的用户才能看到它:

  • 设置 LOGOUT_REDIRECT_URL

用户单击注销按钮后,Django 需要知道应该将用户重定向到哪里。我们使用 settings.py 来控制这一点:

6. 注册页面

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

  • 注册页面的 URL 模式

下面的代码定义了注册页面的 URL 模式,应将其放在accounts/urls.py 中:

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

注册页面的 URL 模式与 URL http://localhost:8000/accounts/register/ 匹配,并将请求发送给即将

编写的 register() 函数。

  • 视图函数 register()

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

python 复制代码
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)
  • 注册模板

下面来创建注册页面模板,它与登录页面的模板类似。务必将其保存到 login.html 所在的目录中:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {% extends "learning_logs/base.html" %}
    {% block content %}
    <form action="{% url 'accounts:register' %}" method='post'>
        {% csrf_token %}
        {{ form.as_div }}
        <button name="submit">Register</button>
    </form>
    {% endblock content %}
</body>
</html>
  • 链接到注册页面

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

base.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Learning Log</title>
</head>
<body>
    <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 'accounts:register' %}">Register</a> -
                <a href="{% url 'accounts:login' %}">Log in</a>
        {% endif %}
    </p>
    {% block content %}{% endblock content %}
    {% if user.is_authenticated %}
    <hr />
    <form action="{% url 'accounts:logout' %}" method='post'>
        {% csrf_token %}
        <button name='submit'>Log out</button>
    </form>
    {% endif %}
</body>
</html>

三、让用户拥有自己的数据

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

本节将修改模型 Topic,让每个主题都归属于特定的用户。这也将影响条目,因为每个条目都属于特定的主题。我们先来限制对一些页面的访问。

1. 使用 @login_required 限制访问

Django 提供了装饰器 @login_required,有助于轻松地限制对某些页面的访问。装饰器(decorator)是放在函数定义前面的指令,用于改变函数的行为。下面来看一个示例。

  • 限制对页面 topics 的访问

每个主题都归属于特定的用户,因此应只允许已登录的用户请求页面topics。为此,在 learning_logs/views.py 中添加如下代码:

python 复制代码
from django.contrib.auth.decorators import login_required
@login_required
  • 全面限制对项目"学习笔记"的访问

Django 能够让我们轻松地限制对页面的访问,但是我们必须确定要保护哪些页面。最好先确定项目的哪些页面不需要保护,再限制对其他所有页面的访问。我们可以轻松地修改过于严格的访问限制,这比不限制对敏感页面的访问风险更低。

在项目"学习笔记"中,我们将不限制对主页和注册页面的访问,并限制对其他所有页面的访问。在下面的 learning_logs/views.py 中,对除了 index() 以外的视图都应用装饰器 @login_required:

python 复制代码
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .forms import TopicForm, EntryForm
from .models import Topic

# Create your views here.
def index(request):
    return render(request, 'learning_logs/index.html')
# 函数 topics() 包含一个形参:Django 从服务器那里收到的 request 对象
@login_required
def topics(request):
    # 我们查询数据库:请求提供 Topic 对象,并根据属性 date_added 进行排序
    topics = Topic.objects.order_by('date_added')
    # 接下来,定义一个将发送给模板的上下文
    context = {'topics': topics}
    # 在创建使用数据的网页时,调用了 render(),并向它传递对象 request、要使用的模板和字典 context
    return render(request, 'learning_logs/topics.html', context)
@login_required
def topic(request, topic_id):
    # 我们查询数据库:请求提供 Topic 对象,并根据属性 date_added 进行排序
    topics = Topic.objects.get(id=topic_id)
    entries = topics.entry_set.order_by('-date_added')
    context = {'topic': topics, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)
@login_required
def new_topic(request):
    """添加新主题"""

    # 检查请求方法是否为POST
    # 注意:这里是 '!=',表示如果不是POST请求,则执行下面的代码块
    if request.method != 'POST':
        # 情况1:用户第一次访问页面(GET请求)
        # 未提交数据:创建一个空的表单实例(用于显示空白表单)
        form = TopicForm()
    else:
        # 情况2:用户提交了表单数据(POST请求)
        # POST 提交的数据:对数据进行处理
        # 使用用户提交的数据(request.POST)创建表单实例
        form = TopicForm(data=request.POST)

        # 检查表单数据是否有效(自动执行字段验证)
        if form.is_valid():
            # 数据有效:将数据保存到数据库
            form.save()
            # 保存成功后,重定向到主题列表页面
            # 'learning_logs:topics' 是URL模式的命名空间
            return redirect('learning_logs:topics')
        # 如果数据无效,继续向下执行,会在模板中显示错误信息

    # 两种情况会执行到这里:
    # 1. GET请求:显示空表单
    # 2. POST请求但数据无效:显示带错误信息的表单
    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, topic_id, entry_id):
    """编辑既有的条目"""
    entry = Topic.objects.get(id=entry_id)
    topic = entry.topic
    if request.method != 'POST':
    # 初次请求:使用当前的条目填充表单
        form = EntryForm(instance=entry)
    else:
        # 初次请求:使用当前的条目填充表单
        form = EntryForm(instance=entry, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic', topic_id=topic.id)
    context = {'entry': entry, 'topic': topic, 'form': form}
    return render(request, 'learning_logs/edit_entry.html', context)

2. 将数据关联到用户

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

下面来修改模型 Topic,在其中添加一个关联到用户的外键。这样做之后,必须对数据库进行迁移。最后,必须对一些视图进行修改,使其只显示与当前登录的用户相关联的数据。

  • 修改模型 Topic

对文件夹 learning_logs 中的 models.py 的修改只涉及两行代码:

python 复制代码
from django.db import models
# 添加代码1
from django.contrib.auth.models import User
# Create your models here.
class Topic(models.Model):
    """用户学习的主题"""
    # 属性 text 是一个 CharField------由字符组成的数据,即文本
    text = models.CharField(max_length=200)
    # 属性 date_added 是一个 DateTimeField------记录日期和时间的数据
    date_added = models.DateTimeField(auto_now_add=True)
    # 添加代码2
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    # __str__() 方法,它返回属性 text 的值
    def __str__(self):
        return self.text
  • 确定当前有哪些用户

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

下面查看已创建的所有用户的 ID。为此,启动一个 Django shell 会话,并执行如下命令:

首先,在 shell 会话中导入模型 User。然后,查看到目前为止都创建了哪些用户。输出中列出了三个用户:ll_admin、eric 和 willie。

接下来,遍历用户列表并打印每个用户的用户名和 ID。当Django 询问要将既有主题关联到哪个用户时,我们将指定其中一个 ID值。

  • 迁移数据库

知道用户 ID 后,就可迁移数据库了。在这样做时,Python 将询问是要暂时将模型 Topic 关联到特定的用户,还是在文件 models.py 中指定默认用户。请选择第一个选项。

首先执行命令 makemigrations。在输出中,Django 指出我们在试图给既有模型 Topic 添加一个必不可少(不可为空)的字段,而该字段没有默认值。Django 提供了两种选择:要么现在提供默

认值,要么退出并在 models.py 中添加默认值。这里选择了第一个选项,因此 Django 让我们输入默认值。

为了将所有既有主题都关联到管理用户 ll_admin,输入用户 ID 值1。这里可以使用已创建的任意用户的 ID,并非必须是超级用户。接下来,Django 使用这个值来迁移数据库,并生成了迁移文件

0003_topic_owner.py,它在模型 Topic 中添加字段 owner。

现在可以执行迁移了。在活动的虚拟环境中执行如下命令:

为了验证迁移符合预期,可在 shell 会话中这样做:

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

当前,不管以哪个用户的身份登录,都能够看到所有的主题。下面将改变这一点,只向用户显示属于其自己的主题。

views.py 中,对 topics() 函数做如下修改:

python 复制代码
# 让 Django 只从数据库中获取 owner 属性为当前用户的 Topic 对象。
topics = Topic.objects.filter(owner=request.user).order_by('date_added')

4. 保护用户的主题

我们还没有限制对显示单个主题的页面的访问,因此任何已登录的用户都可输入形如 http://localhost:8000/topics/1/ 的 URL,来访问显示相应主题的页面。

请你自己试一试。以拥有所有主题的用户的身份登录,访问特定的主题,并复制该页面的 URL 或将其中的 ID 记录下来。然后,注销并以另一个用户的身份登录,再输入显示前述主题的页面的 URL。虽然你是作为另一个用户登录的,但依然能够查看该主题中的条目。

为了修复这个问题,我们在视图函数 topic() 获取请求的条目之前执行检查:

learning_logs/views.py

python 复制代码
def topics(request):
    # 我们查询数据库:请求提供 Topic 对象,并根据属性 date_added 进行排序
    # topics = Topic.objects.order_by('date_added')
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    if topics.owner != request.user:
        raise Http404
    # 接下来,定义一个将发送给模板的上下文
    context = {'topics': topics}
    # 在创建使用数据的网页时,调用了 render(),并向它传递对象 request、要使用的模板和字典 context
    return render(request, 'learning_logs/topics.html', context)

5. 保护页面 edit_entry

页面 edit_entry 的 URL 形如http://localhost:8000/edit_entry/entry_id/,其中 entry_id 是一个数。

下面来保护这种页面,禁止用户通过输入这样的 URL 来访问其他用户的条目:

python 复制代码
def edit_entry(request, topic_id, entry_id):
    """编辑既有的条目"""
    entry = Topic.objects.get(id=entry_id)
    topic = entry.topic
    if topic.owner != request.user:
        raise Http404
    if request.method != 'POST':
    # 初次请求:使用当前的条目填充表单
        form = EntryForm(instance=entry)
    else:
        # 初次请求:使用当前的条目填充表单
        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)

6. 将新主题关联到当前用户

当前,用于添加新主题的页面存在问题------没有将新主题关联到特定的用户。如果用户尝试添加新主题,将看到错误消息 IntegrityError,指出learning_logs_topic.user_id 不能为 NULL(NOT NULL constraint failed: learning_logs_topic.owner_id)。Django 的意思是说,在创建新主题时,必须给 owner 字段指定值。由于可以通过 request 对象获悉当前的用户,因此有一个修复该问题的简单方案。添加如下代码,将新主题关联到当前用户:

learning_logs/views.py

python 复制代码
def new_topic(request):
    """添加新主题"""

    # 检查请求方法是否为POST
    # 注意:这里是 '!=',表示如果不是POST请求,则执行下面的代码块
    if request.method != 'POST':
        # 情况1:用户第一次访问页面(GET请求)
        # 未提交数据:创建一个空的表单实例(用于显示空白表单)
        form = TopicForm()
    else:
        # 情况2:用户提交了表单数据(POST请求)
        # POST 提交的数据:对数据进行处理
        # 使用用户提交的数据(request.POST)创建表单实例
        form = TopicForm(data=request.POST)

        # 检查表单数据是否有效(自动执行字段验证)
        if form.is_valid():
            # 数据有效:将数据保存到数据库
            # form.save()
            new_topic = form.save(commit=False)
            new_topic.owner = request.user
            new_topic.save()
            # 保存成功后,重定向到主题列表页面
            # 'learning_logs:topics' 是URL模式的命名空间
            return redirect('learning_logs:topics')
        # 如果数据无效,继续向下执行,会在模板中显示错误信息

    # 两种情况会执行到这里:
    # 1. GET请求:显示空表单
    # 2. POST请求但数据无效:显示带错误信息的表单
    context = {'form': form}  # 将表单对象传递给模板
    return render(request, 'learning_logs/new_topic.html', context)
相关推荐
whuhewei2 小时前
React性能优化
前端·react.js·性能优化
m0_738120722 小时前
渗透知识ctfshow——Web应用安全与防护(三)
android·前端·安全
窝子面2 小时前
NestJs+MongoDB+Deepseek+Langchain实现ai聊天助手
javascript·数据库·人工智能·mongodb
zjshuster2 小时前
流程引擎(Process Engine)简介
java·数据库·servlet
下北沢美食家2 小时前
React面试题2
前端·react.js·前端框架
摇滚侠2 小时前
HTML CSS 演示小米 logo 的变化 border-radius 属性设置圆角
前端·css·html
❆VE❆2 小时前
虚拟列表原理与实战运用场景详解
前端·javascript·css·vue.js·html·虚拟列表
leonkay2 小时前
关于.NET中的队列理解
数据库·性能优化·.net·个人开发·设计规范·队列
weixin_408099672 小时前
【实战教程】EasyClick 调用 OCR 文字识别 API(自动识别屏幕文字 + 完整示例代码)
前端·人工智能·后端·ocr·api·安卓·easyclick