资讯专栏INFORMATION COLUMN

Python学习之路18-用户账户

bovenson / 2438人阅读

摘要:通过的定制字段的输入小部件,将文本框的宽度设置为列,而不是默认的列。为此将创建一个新的应用程序,其中包含处理用户账户相关的所有功能。该函数将会为通过了身份验证的用户对象创建会话。

《Python编程:从入门到实践》笔记。
本篇记录如何创建用户注册系统,如何实现用户输入自己的数据。
1. 前言

在本篇中,我们将:

创建一些表单,让用户能够添加主题和条目,以及编辑既有的条目;

实现一个身份验证系统。

2. 让用户能够输入数据

先添加几个页面,让用户能够添加新主题,新条目以及编辑条目。

2.1 添加新主题

和之前创建网页的步骤一样:定义URL,编写视图函数,编写模板。主要区别是,这里需要一个包含表单的模块forms.py

2.1.1 创建forms.py模块

用户输入信息时,需要进行验证,确保提交的信息是正确的数据类型,且不是恶意信息,如中断服务器的代码。然后再处理信息,并保存到数据库中。当然,这些工作很多都由Django自动完成。

models.py所在的目录中新建forms.py模块。创建表单的最简单方法是继承Django的ModelForm类:

from django import forms
from .models import Topic

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

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

2.1.2 URL模式new_topic

当用户要添加新主题时,将切换到http://localhost:8000/new_topic/ 。在learning_logs/urls.py中添加如下代码:

urlpatterns = [
    -- snip --
    # 用于添加新主题的网站
    path("new_topic/", views.new_topic, name="new_topic"),
]
2.1.3 视图函数new_topic()

该函数需要处理两种情形:①刚进入new_topic网页,显示一个空表单;②对提交的表单数据进行处理,并将用户重定向到网页topics。修改views.py文件:

from django.http import HttpResponseRedirect
from django.urls import reverse
from .forms import TopicForm

def new_topic(request):
    """添加新主题"""
    if request.method != "POST":
        # 为提价数据:创建一个新表单
        form = TopicForm()
    else:
        # POST提交的数据,对数据进行处理
        form = TopicForm(request.POST)
        if form.is_valid():
            form.save()
            # 该类将用户重定向到网页topics,函数reverse()根据指定的URL模型确定URL
            return HttpResponseRedirect(reverse("learning_logs:topics"))

    context = {"form": form}
    return render(request, "learning_logs/new_topic.html", context)
2.1.4 GET请求和POST请求

创建Web应用程序时,将用到两种主要数据请求类型:GET请求和POST请求。从这俩英文单词可以看出,如果只从服务器读取数据页面,则使用GET请求;如果要提交用户填写的表单,通常使用POST请求。当然还有一些其他的请求类型,但这个项目中没有使用。本项目中处理表单都使用POST方法。

request.method存储了请求的类型(第7行代码)。

当不是POST请求时,我们生成一个空表单传递给模板new_topic.html,然后返回给用户;当请求是POST时,我们从request.POST这个变量中获取用户提交的数据,并暂存到form变量中。

通过is_valid()方法验证表单数据是否满足要求:用户是否填写了所有必不可少的字段(表单字段默认都是必填的),且输入的数据与字段类型是否一致。当然这些验证都是Django自动进行的。如果表单有效,在通过formsave()方法存储到数据库,然后通过reverse()函数获取页面topics的URL,并将其传递给HTTPResponseRedirect()以重定向到topics页面。如果表单无效,把这些数据重新传回给用户。

2.1.5 模板new_topic.html
{% extends "learning_logs/base.html" %}

{% block content %}
  

Add a new topic:

{% csrf_token %} {{ form.as_p }}
{% endblock content %}

模板继承了base.html,因此其基本结构和项目中的其他页面相同。第6行中,参数action告诉服务器将提交的表单数据送到什么位置去处理,参数method浏览器POST请求的方式提交数据。

Django使用模板标签csrf_token(第7行)来防止攻击者利用表单获得对服务器未经授权的访问(跨站请求伪造)。

Django显示表单非常方便:只需要使用模板变量form.as_p,修饰符as_p让Django以段落格式渲染所有表单元素,这是一种整洁地显示表单的简单方法。

Django不自动创建提交表单的按钮,需自行创建。

2.1.6 链接到页面new_topic

在页面topics.html中添加一个到页面new_topic的链接:

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

{% block content %}
  -- snip --
  Add a new topic:
{% endblock content %}
2.1.7 效果

以下是实际效果图:

通过这个页面,随意添加几个主题,如下:

2.2 添加新条目

和前面的步骤相似:创建条目表单,添加URL,添加视图,添加模板,链接到页面

2.2.1 创建条目表单

创建一个与模型Entry相关联的表单,但这个表单的自定义程度比TopicForm要高些,依然是在刚才创建的forms.py中添加:

from .models import Topic, Entry

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

代码中定义了属性widgets。小部件(widget)是一个HTML表单元素,如单行文本框、多行文本框或下拉列表。通过设置属性widgets可以覆盖Django选择的默认小部件。通过Django的forms.Textarea定制字段“text"的输入小部件,将文本框的宽度设置为80列,而不是默认的40列。

2.2.2 添加URL模式new_entry

修改learning_logs/urls.py

urlpatterns = [
    -- snip --
    path("new_entry//", views.new_entry, name="new_entry"),
]

该URL模式与形式为http://localhost:8000/new_entry/topi_id/ 的URL匹配,其中topic_id是主题的ID。

2.2.3 视图函数new_entry()

与函数new_topic()很像:

from .forms import TopicForm, EntryForm

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)

new_entry()的定义包含形参topic_id,用于存储从URL中获得的值。

在调用save()时传递了参数commit=False(第14行),它让Django创建一个新的条目对象,但并不立刻提交数据库,而是暂时存储在变量new_entry中,待为这个新条目对象添加了属性topic之后再提交数据库。

在重定向时,reverse()函数中传递了两个参数,URL模式的名称以及列表argsargs包含要包含在URL中的所有参数。

2.2.4 模板new_entry.html

类似于new_topic

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

{% block content %}
  

{{ topic }}

Add a new entry:

{% csrf_token %} {{ form.as_p }}
{% endblock content %}

注意第4行代码,改行代码返回到特定主题页面。

2.2.5 链接到页面new_entry

在显示特定主题的页面中添加到页面new_entry的链接,修改topic.html

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

{% block content %}

  

Topic: {{ topic }}

Entries:

add new entry

-- snip -- {% endblock content %}
2.2.6 效果

下图是实际效果,请随意添加一些条目:

2.3 编辑条目

创建一个页面,让用户能编辑既有条目。顺序是:添加URL,添加视图,添加模板,链接到页面。

2.3.1 URL模式edit_entry

修改learning_logs/urls.py

urlpatterns = [
    -- snip --
    path("edit_entry//", views.edit_entry, name="edit_entry"),
]
2.3.2 视图函数edit_entry()
from .models import Topic, Entry

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/edit_entry.html", context)

首先获取要被修改的entry以及与该条目相关的主题。处理GET请求时,通过参数instance=entry创建EntryForm实例,该参数让Django创建一个表单,并使用既有条目对象中的信息填充它。处理POST请求时,还传入了data=request.POST参数,Django根据POST中的相关数据对entry进行修改。

2.3.3 模板edit_entry.html
{% extends "learning_logs/base.html" %}

{% block content %}
  

{{ topic }}

Edit entry:

{% csrf_token %} {{ form.as_p }}
{% endblock content %}
2.3.4 链接到页面edit_entry.html

在显示特定主题的页面中,需要给每个条目添加到页面edit_entry.html的链接,为此,修改topic.html

-- snip --
    {% for entry in entries %}
      
  • {{ entry.date_added|date:"M d, Y H:i" }}

    {{ entry.text|linebreaks }}

    edit entry

  • -- snip --
    2.3.5 效果

    以下是实际效果图:

    3. 创建用户账户

    现在开始建立一个用户注册和身份验证系统。为此将创建一个新的应用程序,其中包含处理用户账户相关的所有功能。对Topic模型也要做稍许修改,让每个主题都归属于特定用户。

    3.1 创建应用程序users

    希望大家还记得如何使用startapp命令还创建APP:

    python manage.py startapp users

    将APP添加到settings.py中

    INSTALLED_APPS = [
        -- snip --
        "users.apps.UsersConfig",
    ]

    在APP根目录下创建urls.py文件,并添加命名空间:

    app_name = "users"

    为APP定义URL,修改项目根目录中的urls.py

    urlpatterns = [
        path("admin/", admin.site.urls),
        path("", include("learning_logs.urls")),
        path("users/", include("users.urls")),
    ]
    3.2 登陆页面

    使用Django提供的默认登陆视图,URL模式会有所不同。在users中的urls.py中添加如下代码:

    """为应用程序users定义URL模式"""
    from django.contrib.auth.views import login
    from django.urls import path
    
    app_name = "users"
    
    urlpatterns = [
        # 登陆页面
        path("login/", login, {"template_name": "users/login.html"}, name="login"),
    ]

    代码中,我们使用Django自带的login视图函数(注意,参数是login,而不是views.login)。从之前的例子可以看出,我们渲染模板的代码都是在自己写的视图函数中。但这里使用了自带的视图函数,无法自行编写进行渲染的代码。所以,我们还传了一个字典给path,告诉Django到哪里查找我们要用到的模板。注意,该模板在users中,而不是在learning_logs中。

    3.2.1 新建模板login.html

    learning_log/users/templates/users中创建login.html

    {% extends "learning_logs/base.html" %}
    
    {% block content %}
    
      {% if form.errors %}
        

    Your username and password didn"t match. Please try again.

    {% endif %}
    {% csrf_token %} {{ form.as_p }}
    {% endblock content %}

    如果表单的errors属性被设置,则显示一条提示账号密码错误的信息。

    3.2.2 链接到登陆页面

    base.html中添加到登陆页面的链接,让所有页面都包含它。将这个链接嵌套在一个 if 标签中,用户已登录时隐藏掉该链接:

    Learning Log - Topics {% if user.is_authenticated %} Hello, {{ user.username }} {% else %} log in {% endif %}

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

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

    3.2.3 使用登陆页面

    首先访问localhost:8000/admin注销超级用户,再访问localhost:8000/users/login/,得到如下页面:

    3.3 注销

    并不为注销创建多带带的页面,而是让用户单击一个连接用于注销并返回主页。因此,需要做如下工作:注销URL模式,新建视图,链接到注销视图

    users/urls.py中添加与http://localhost:8000/users/logout/ 匹配的URL模式:

    -- snip --
    urlpatterns = [
        -- snip --
        path("logout/", views.logout_view, name="logout"),
    ]

    编写视图函数logout_view(),其中,直接调用Django自带的logout()函数,该函数要求request作为参数:

    from django.contrib.auth import logout
    from django.http import HttpResponseRedirect
    from django.urls import reverse
    
    def logout_view(request):
        """注销用户"""
        logout(request)
        return HttpResponseRedirect(reverse("learning_logs:index"))

    base.html中添加注销链接:

    -- snip --
      {% if user.is_authenticated %}
        Hello, {{ user.username }}
        log out
      {% else %}
        log in
      {% endif %}
    -- snip --
    3.4 注册页面

    使用Django提供的表单UserCreationFrom,但编写自己的视图函数和模板。URL->view->template->link

    首先,创建注册页面的URL模式,修改users/urls.py

    -- snip --
    urlpatterns = [
        -- snip --
        path("register/", views.register, name="register"),
    ]

    其次,创建视图register()

    from django.contrib.auth import logout, authenticate, login
    from django.contrib.auth.forms import UserCreationForm
    from django.shortcuts import render
    -- snip --
    
    def register(request):
        """注册新用户"""
        if request.method != "POST":
            # 显示空的注册表单
            form = UserCreationForm()
        else:
            # 处理填写好的表单
            form = UserCreationForm(data=request.POST)
    
            if form.is_valid():
                new_user = form.save()
                # 让用户自动登陆,再重定向到主页
                # 注册是要求输入两次密码,所以有password1和password2
                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()函数实现。该函数将会为通过了身份验证的用户对象创建会话(session)。最后上述代码重定向到主页。

    然后,编写注册页面的模板register.html

    {% extends "learning_logs/base.html" %}
    
    {% block content %}
      
    {% csrf_token %} {{ form.as_p }}
    {% endblock content %}

    最后,在页面中显示注册链接,修改base.html,在用户没有登录时显示注册链接:

    -- snip --
      {% if user.is_authenticated %}
        Hello, {{ user.username }}
        log out
      {% else %}
        register -
        log in
      {% endif %}
    -- snip --

    下面是实际效果:

    这是直接点register按钮时的反馈,不过这里有点疑惑,从上面的register.html中看到,其实代码很简单,但这里有个浮动效果,而且在注册模板中并没有像前面那样的form.errors模板标签,但依然有未注册成功时的反应,而且注册的视图函数也是自己写的,并不是用的自带的注册函数,所以不知道是不是和form.as_p有关。之后再慢慢研究吧,

    4. 让用户拥有自己的数据

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

    4.1 使用@login_required限制访问

    Django提供了装饰器@login_required,使得能轻松实现用户只能访问自己能访问的页面。

    限制对topics.html的访问

    每个主题都归特定用户所有,所以需要加限制,修改learning_logs/views.py

    from django.contrib.auth.decorators import login_required
    
    @login_required
    def topics(request):
        """显示所有的主题"""
        topics = Topic.objects.order_by("date_added")
        # 一个上下文字典,传递给模板
        context = {"topics": topics}
        return render(request, "learning_logs/topics.html", context)

    装饰器也是一个函数,python在运行topics()前会先运行login_required()的代码。

    login_required()函数检查用户是否登录,仅当用户已登录时,Django才运行topics()函数,若未登录,就重定向到登陆界面。而为了实现这个重定向,还需要修改项目settings.py文件,在该文件中添加这样一个常量(其实也是变量),一般在文件末尾添加:

    -- snip --
    LOGIN_URL = "/users/login/"

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

    Django能轻松地限制对页面的访问,但得自己设计需要限制哪些页面。一般先确定哪些页面不需要保护,再限制对其他页面的访问。在该项目中,我们不限制对主页、注册页面和注销链接的访问,其他页面均限制。在learning_logs/views.py中,除了index()外,每个视图函数都加上@login_required

    4.2 将数据关联到用户

    为了禁止用户访问其他用户的数据,需要将用户与数据关联。只需要将最高层的数据关联到用户,这样更低层的数据将自动关联到用户。下面修改Topic模型和相关视图:

    from django.contrib.auth.models import User
    from django.db import models
    
    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)
        -- snip --

    修改模型后,还需要迁移数据库。此时,需要将主题与用户关联。这里并没有通过代码进行关联,我们在迁移数据库时手动进行关联。为此,我们需要先知道有哪些用户,以及这些用户的ID。我们通过Django shell查询用户信息,当然也可以直接查看数据库,这里不再演示。我们将主题都关联到超级用户ll_admin上,它的ID是1。现在我们执行数据迁移:

    (venv)learning_logs$ python manage.py makeimgrations learning_logs
    You are trying to add a non-nullable field "owner" to topic without a default; 
    we can"t do that (the database needs something to populate existing rows).
    Please select a fix:
     1) Provide a one-off default now (will be set on all existing rows with a null value for 
     this column)
     2) Quit, and let me add a default in models.py
    Select an option:  1
    Please enter the default value now, as valid Python
    The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
    Type "exit" to exit this prompt
    >>>  1
    Migrations for "learning_logs":
      learning_logsmigrations003_topic_owner.py
        - Add field owner to topic

    Django指出试图给既有模型Topic添加一个必不可少(不可为空)的字段,而该字段没有默认值,需要我们采取措施:要么现在提供默认值,要么退出并在models.py中添加默认值。我们选择了直接输入默认值。接下来,Django使用这个值来迁移数据库,并生成了迁移文件0003_topic_owner.py,它在模型Topic中添加字段owner

    现在执行迁移命令:

    (venv)learning_logs$ python manage.py migrate
    Operations to perform:
      Apply all migrations: admin, auth, contenttypes, learning_logs, sessions
    Running migrations:
      Applying learning_logs.0003_topic_owner... OK

    执行后可以在Django shell中验证是否迁移成功,这里不再验证。

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

    目前不管以哪个用户身份登录,都能看到所有主题。现在我们添加限制,让用户只能看到自己的主题。在views.py中,对topics()做如下修改:

    -- snip --
    @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)
    -- snip --

    用户登录后,request对象将有一个user属性,这个属性存储了有关该用户的信息。第5行代码让Django只从数据库中读取特定用户的数据。

    4.4 保护用户的主题

    上述代码做到了登录后只显示相应用户的数据,但是,如果登录后直接通过URL访问,如直接输入http://localhost:8000/topics/1/ ,依然可以访问不属于自己的特定主题页面。下面修改views.py中的topic()函数来加以限制:

    from django.http import HttpResponseRedirect, Http404
    
    @login_required
    def topic(request, topic_id):
        """显示单个主题及其所有的条目"""
        topic = Topic.objects.get(id=topic_id)
        # 确认请求的主题属于当前用户
        if topic.owner != request.user:
            raise Http404
        -- snip --
    4.5 保护页面edit_entry

    此时用户也可以像上面一样,登陆后直接通过URL来访问edit_entry.html,现在我们对这个页面也加以限制:

    @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
        
        -- snip --
    4.6 最后一步:将新主题关联到当前用户

    当前用于添加新主题的页面存在问题,因为它没有将新主题关联到特定用户。如果此时尝试添加新主题,将看到错误信息IntegrityError,指出learning_logs_topic.user_id不能为NULL,下面修改new_topic()函数:

    @login_required
    def 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()
                # 该类将用户重定向到网页topics,函数reverse()根据指定的URL模型确定URL
                return HttpResponseRedirect(reverse("learning_logs:topics"))
    
        context = {"form": form}
        return render(request, "learning_logs/new_topic.html", context)

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

    5. 小结

    本篇主要讲述了如何使用表单来让用户添加新主题、添加新条目和编辑既有条目;如何实现注册,登录与注销,如何使用装饰器来限制访问,如何对用户数据进行保护等。下一篇中,我们将使这个项目更漂亮,并且部署到服务器上。


    迎大家关注我的微信公众号"代码港" & 个人网站 www.vpointer.net ~

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/44721.html

    相关文章

    • python初学——面向对象编程作业:学校选课系统

      摘要:如上海北京等管理员自己创建其他地方的学校管理员创建的课程。包含名字性别年龄等学员查询可上课程学员选课,选择学校课程,并付款。课程价格,周期课程价格,周期课程价格,周期创建讲师属于北京校区,属于上海校区。 作业需求 创建北京、上海 2 所学校 创建linux , python , go 3个课程,linuxpy在北京开, go 在上海开 课程包含,周期,价格,通过学校创建课程 通过学校...

      kbyyd24 评论0 收藏0
    • Python学习之路4-if语句

      摘要:本章主要讲述条件语句等结构。是一条包罗万象的语句,只要不满足前面的条件,其中的代码就会执行,这可能会引入无效甚至恶意的数据。使用语句处理列表语句常和循环结构配合使用。 《Python编程:从入门到实践》笔记。本章主要讲述条件语句if, if-else, if-elif, if-elif-else等结构。 1. 条件测试 包括了相等,不等,大于,小于,大于等于,小于等于,存在于,与或非等...

      JouyPub 评论0 收藏0
    • Python学习之路1-变量和简单数据类型

      摘要:本章主要介绍的基本数据类型以及对这些数据的简单操作。入门仪式作为一个合格的程序员,必须精通各种语言的,以下是学习的第一段代码变量变量就是数据的别称,和数学上的变量类似。 《Python编程:从入门到实践》笔记。本章主要介绍Python的基本数据类型以及对这些数据的简单操作。 1. 入门仪式 作为一个合格的程序员,必须精通各种语言的Hello, World!,以下是学习Python的第一...

      MASAILA 评论0 收藏0
    • 实战:从Python分析17-18赛季NBA胜率超70%球队数据开始…

      摘要:作为一个正奋战在之路上的球迷,开始了一次数据分析实战,于是,以分析球赛数据为起点的操作开始了前言作为一个功能强大的编程语言,如今在数据分析机器学习人工智能等方面如日中天。 Casey 岂安业务风险分析师主要负责岂安科技RED.Q的数据分析和运营工作。 12月19日,科比再次站在斯台普斯中心球馆中央,见证自己的两件球衣高悬于球馆上空。作为一个正奋战在 Python 之路上的球迷,...

      denson 评论0 收藏0

    发表评论

    0条评论

    最新活动
    阅读需要支付1元查看
    <