1-Django - form和modelform

before

form和modelform(也称forms和modelforms)组件主要的功能:

  • 数据校验。
  • 前端页面显示错误信息。

这个部分内容非常重要,在传统web项目中,这两个组件能大大的减轻我们的代码量。

比如form功能可以脱离数据库,当我们有对数据校验的需求时,就可以用form组件进行数据校验,校验成功的数据随你拿去干嘛。

而modelfrom则结合数据库,减轻你增删改查的代码。

另外,在前后端分离的项目中, 我们后段通常使用Django restful framework框架,其内部的序列化器内部也是用的form和modelform,所以这部分有必要好好学学。

下面的笔记,是我通过在不同的项目中使用form和modelfrom时遇到的问题,然后将解决方案,摘抄出来的代码片段,所以看起来可能不连贯。

当然,这不是form和modelfrom的所有知识点,我只是记录了我常用的。

Form

快速上手

这里的项目名称叫做demo,在项目目录下新建一个test.py

import os
import django
from django import forms

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")  # demo:项目名称
django.setup()


class UserForm(forms.Form):  # 类名随意,但必须继承forms.Form
    # min_length、max_length都是规则
    # 字段名字和数据库中的表字段保持一致
    user = forms.CharField(min_length=3, max_length=6)
    pwd = forms.CharField()
    email = forms.EmailField()


if __name__ == '__main__':
    """
    user = UserForm({"user": "a", "pwd": 123, "email": "123"})
    # 校验数据,完全合法返回True,否则返回False
    print(user.is_valid())
    # 想要获取干净的数据,必须先经过is_valid校验,否则报错
    # AttributeError: 'UserForm' object has no attribute 'cleaned_data'
    print(user.cleaned_data)  # 返回(校验成功的,可能是部分,也可能是全部)干净的数据{'pwd': '123'}
    print(type(user.errors), user.errors)  # 错误信息类型和错误信息
    """

    # 校验时,只对类中定义的字段做校验,对于多余的字段,不做处理
    user = UserForm({"user": "abc", "pwd": 123, "email": "123@qq.com", "more_field": "xxxx"})
    print(user.is_valid())  # True
    print(user.cleaned_data)  # {'user': 'abc', 'pwd': '123', 'email': '123@qq.com'}
    print(type(user.errors), user.errors)  # <class 'django.forms.utils.ErrorDict'> {}

这里牢记三个方法:

  • is_valid(),form组件类中定义的所有字段,都通过其规则校验的话,返回True,有一项或者若干项校验失败,返回False。
  • cleaned_data,只有先经过is_valid校验后,才能使用该方法进行获取干净的数据。
  • errors,校验失败的错误信息,是个字典,存放一个或多个错误信息。
  • 还有个errors.as_json(),校验失败的错误信息直接转为json数据。

如何将解决错误信息由英文改为中文

form组件校验失败的错误信息,默认是英文,如果改为中文提示呢?这里提供两种解决办法。

修改settings.py

# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'zh-hans'

自定义错误信息error_messages字典

from django import forms

class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"}
    )
    pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
    email = forms.EmailField(error_messages={
        "required": "该字段不能为空",
        "invalid": "邮箱格式错误"
    })

error_messages字典中可以写多种错误信息,以应对多种校验规则。

简单示例

上一小节,大致了解了form组件的基本用法,这次我们结合前端页面来使用。

models.py,然后自行进行数据库迁移:

from django.db import models

class User(models.Model):
    user = models.CharField(max_length=32, verbose_name='用户名')
    pwd = models.CharField(max_length=64, verbose_name='密码')
    email = models.CharField(max_length=32, verbose_name='邮箱')
    def __str__(self):
        return self.user

urls.py

from django.contrib import admin
from django.urls import path, re_path
from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('register/', views.register)
]

views.py

import os
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from app01 import models


class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"}
    )
    pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
    email = forms.EmailField(error_messages={
        "required": "该字段不能为空",
        "invalid": "邮箱格式错误"
    })


def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            # 错误信息包含在forms_obj对象中,我们在前端通过该模板对象进行提取
            return render(request, 'register.html', {"forms_obj": forms_obj})
    return render(request, 'register.html')

register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg{
            color: red;
            margin-left: 10px;
        }
    </style>
</head>
<body>
<form action="" method="post">
    {% csrf_token %}
    <p>用户名 <input type="text" name="user"> <span class="errorMsg">{{ forms_obj.errors.user.0 }}</span></p>
    <p>密 码 <input type="password" name="pwd"> <span class="errorMsg">{{ forms_obj.errors.pwd.0 }}</span></p>
    <p>邮 箱 <input type="text" name="email"> <span class="errorMsg">{{ forms_obj.errors.email.0 }}</span></p>
    <p><input type="submit"></p>
</form>
</body>
</html>

这个是最基础的示例,还有很多问题,比如,返回错误信息时,没有问题的字段内容,由于页面刷新也被清空了,以及我们怎么自定义更多更复杂的校验规则,比如两次密码输入不一致的问题等等,都是我们接下来要解决的。

前端标签的不同渲染方式

这一小节我们来了解下,form组件提供的前端标签的渲染的几种方式。

基础版

也就是上一个小节中,我们用的那种,前端代码自己手扣,后台代码也不用做特殊处理。

后台其他代码不变,views.py

from django.shortcuts import render, HttpResponse, redirect
from django import forms
from app01 import models


class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"}
    )
    pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
    email = forms.EmailField(error_messages={
        "required": "该字段不能为空",
        "invalid": "邮箱格式错误"
    })


def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            return render(request, 'register.html', {"forms_obj": forms_obj})
    return render(request, 'register.html', {"forms_obj": forms_obj})

register.py

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg{
            color: red;
            margin-left: 10px;
        }
    </style>
</head>
<body>
<form action="" method="post">
    {% csrf_token %}
    <p>用户名 <input type="text" name="user"> <span class="errorMsg">{{ forms_obj.errors.user.0 }}</span></p>
    <p>密 码 <input type="password" name="pwd"> <span class="errorMsg">{{ forms_obj.errors.pwd.0 }}</span></p>
    <p>邮 箱 <input type="text" name="email"> <span class="errorMsg">{{ forms_obj.errors.email.0 }}</span></p>
    <p><input type="submit"></p>
</form>
</body>
</html>

这种方式最大的问题,就是数据校验不通过,前端页面因为刷新,导致没问题的input的值也没了.....

进阶 省心省力版

这个版本倒是省心省力,前端input标签不需要自己写,form组件帮我们做了,但也因此少了灵活性,用的也少,后端代码不需要做特殊处理,记得form标签需要加novalidate(目的是避免浏览器帮我们做数据校验),csrf_token和提交按钮不会自动生成,还需要我们自己处理。

特点就是错误信息需要我们手动处理,但解决前端页面因为刷新,导致没问题的input的值没了的问题。

需要后台代码配合。

views.py

from django.shortcuts import render, HttpResponse, redirect
from django import forms
from app01 import models


class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"}
    )
    pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
    email = forms.EmailField(error_messages={
        "required": "该字段不能为空",
        "invalid": "邮箱格式错误"
    })


def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            # 前端干净的数据之所以保留,全在这里返回时,forms_obj对象在之前一次数据提交请求中,
            # 校验后的干净数据,又给前端返回了,所以前端才保留了
            return render(request, 'register.html', {"forms_obj": forms_obj})
    # 需要在给页面的的时候,就传递一个forms_obj对象,方便生成相应的标签
    forms_obj = UserForm()
    return render(request, 'register.html', {"forms_obj": forms_obj})

register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg{
            color: red;
            margin-left: 10px;
        }
    </style>
</head>
<body>

<form action="" method="post" novalidate>
    {% csrf_token %}
    <!-- 直接as_p 啥都有了 -->
    {{ forms_obj.as_p }}
    <p><input type="submit"></p>
</form>
</body>
</html>

进阶版 plus

这个版本就是前端代码部分手扣,用的较多,当有错误提示时,没有错误的字段内容还会保留,但需要后端配合,用的较多,记得form标签需要加novalidate。

views.py

from django.shortcuts import render, HttpResponse, redirect
from django import forms
from app01 import models


class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"}
    )
    pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
    email = forms.EmailField(error_messages={
        "required": "该字段不能为空",
        "invalid": "邮箱格式错误"
    })


def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            # 前端干净的数据之所以保留,全在这里返回时,forms_obj对象在之前一次数据提交请求中,
            # 校验后的干净数据,又给前端返回了,所以前端才保留了
            return render(request, 'register.html', {"forms_obj": forms_obj})
    # 需要在给页面的的时候,就传递一个forms_obj对象,方便生成相应的标签
    forms_obj = UserForm()
    return render(request, 'register.html', {"forms_obj": forms_obj})

register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg{
            color: red;
            margin-left: 10px;
        }
    </style>
</head>
<body>
<form action="" method="post" novalidate>
    {% csrf_token %}
    <!-- 这个版本呢,就是想渲染啥字段,就forms对象点啥字段 -->
    <p>用户名 {{ forms_obj.user }}<span class="errorMsg">{{ forms_obj.errors.user.0 }}</span></p>
    <p>密 码 {{ forms_obj.pwd }}<span class="errorMsg">{{ forms_obj.errors.pwd.0 }}</span></p>
    <p>邮 箱 {{ forms_obj.email }}<span class="errorMsg">{{ forms_obj.errors.email.0 }}</span></p>
    <p><input type="submit"></p>
</form>
</body>
</html>

进阶版 plus max

推荐这个版本,在这个版本中,后端加上了更多的内容,比如渲染标签时,添加一些属性值,让其具有相应的样式。

这里用到了booststrap,我引用的是在线的,省事儿。

views.py

from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from app01 import models


class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.TextInput(attrs={"class": "form-control"}),
        label="用户名"  # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字user
    )
    pwd = forms.CharField(
        error_messages={"required": "该字段不能为空"},
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
        label="密码"
    )
    email = forms.EmailField(
        error_messages={
            "required": "该字段不能为空",
            "invalid": "邮箱格式错误"
        },
        widget=widgets.EmailInput(attrs={"class": "form-control"}),
        label="邮箱"
    )


def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            return render(request, 'register.html', {"forms_obj": forms_obj})
    forms_obj = UserForm()
    return render(request, 'register.html', {"forms_obj": forms_obj})

register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg {
            color: red;
            margin-left: 10px;
        }
    </style>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in forms_obj %}
                <div class="form-group">
                    <label for="">{{ field.label }}</label>
                    <!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
                    {{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
                </div>
                {% endfor %}
                <p><input type="submit" class="btn btn-success pull-left"></p>
            </form>
        </div>
    </div>
</div>
</body>
</html>

自定义规则:钩子方法

这部分就比较重要了,主要学习,在现有的校验基础上,比如最大长度、最短长度限制、不能输入为空这些基础上,根据需求实现一些具体的校验规则,我们通常将这些自定义的校验规则,称之为钩子方法。

钩子方法又可以范围:

  • 局部钩子方法
  • 全局钩子方法

加上基础校验的话,就有了三种,它们的执行流程是这样的:

  • 基础校验,无论如何,都会执行,且是最开始执行,如果某个字段没有通过基础校验的话,该字段的自定义钩子方法也就不执行了。
  • 自定义钩子方法,当该字段的基础校验通过后,才执行的自定义的钩子校验方法。
  • 全局钩子校验,无论基础校验和和自定义钩子方法是否通过,最后都会走全局钩子方法。

先来看局部的钩子方法的用法。

局部钩子

局部钩子就是针对某个字段设定的自定义校验规则,钩子方法在form组件类中以方法的形式呈现,想要对哪个字段进行自定义钩子,就以clean_开头,拼接上字段名就可以了,如clean_user就是对user字段添加自定义钩子方法。

来两个示例,views.py

from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from app01 import models


class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.TextInput(attrs={"class": "form-control"}),
        label="用户名"  # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字
    )
    pwd = forms.CharField(
        error_messages={"required": "该字段不能为空"},
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
        label="密码"
    )
    email = forms.EmailField(
        error_messages={
            "required": "该字段不能为空",
            "invalid": "邮箱格式错误"
        },
        widget=widgets.EmailInput(attrs={"class": "form-control"}),
        label="邮箱"
    )

    def clean_user(self):
        """ 自定义关于user字段的钩子方法,作用:用户名不能重复 """
        # 前端传过来的user字段的值
        value = self.cleaned_data.get('user')
        user_obj = models.User.objects.filter(user=value).first()
        if user_obj:  # 用户名已存在
            # 固定写法
            raise ValidationError("用户名已存在")
        else:
            # 如果用户名不存在,表示该字段值没问题,正常返回该值就行了
            return value

    def clean_pwd(self):
        """ 自定义关于pwd字段的钩子方法,作用:密码不能是纯数字 """
        value = self.cleaned_data.get('pwd')
        if value.isdecimal():  # 如果用户输入是纯数字表示校验不通过
            # 固定写法
            raise ValidationError("密码不能是纯数字")
        else:
            return value


def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            return render(request, 'register.html', {"forms_obj": forms_obj})
    forms_obj = UserForm()
    return render(request, 'register.html', {"forms_obj": forms_obj})

其他代码不变。

使用局部钩子对密码一致性进行校验

在注册时,通常有密码和确认密码选项,但是数据库一般只有一个密码字段,所以,在开发中,要对用户输入的两个密码进行一致性校验。在校验时,注意点:

  • form组件类中要重新生成一个确认密码的输入框字段,且该字段必须在密码字段下面,这是因为form组件中,校验是for循环每个字段校验的,在自定义钩子方法中,一般只能从cleaned_data中获取到当前字段和之前字段校验成功的数据,后面的字段数据由于还没循环到,导致没有添加到cleaned_data中,获取不到。
  • 如果校验都通过了,写入到数据库之前,要把确认密码字段的数据删除掉,因为数据库中没有确认密码这个字段,不删除,可能会导致报错,因为多了个键值对,对应不上了!当然,这还要看你具体的orm语句是如何处理的。

上代码,models.pyurls.py都不变。

views.py

import os
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from app01 import models


class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.TextInput(attrs={"class": "form-control"}),
        label="用户名"  # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字
    )
    pwd = forms.CharField(
        min_length=3,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
        label="密码"
    )
    # 注意确认密码字段,要在密码字段下面
    # 这是为了保证自定义确认密码的钩子中,能取到密码字段的值,既让密码字段先被校验成功
    re_pwd = forms.CharField(
        min_length=3,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
        label="确认密码"
    )

    email = forms.EmailField(
        error_messages={
            "required": "该字段不能为空",
            "invalid": "邮箱格式错误"
        },
        widget=widgets.EmailInput(attrs={"class": "form-control"}),
        label="邮箱"
    )

    def clean_user(self):
        # 前端传过来的user字段的值
        value = self.cleaned_data.get('user')
        user_obj = models.User.objects.filter(user=value).first()
        if user_obj:  # 用户名已存在
            # 固定写法
            raise ValidationError("用户名已存在")
        else:
            # 如果用户名不存在,表示该字段值没问题,正常返回该值就行了
            return value

    def clean_pwd(self):
        pwd = self.cleaned_data.get('pwd')
        if pwd.isdecimal():  # 如果用户输入是纯数字表示校验不通过
            # 固定写法
            raise ValidationError("密码不能是纯数字")
        else:
            return pwd

    def clean_re_pwd(self):
        re_pwd = self.cleaned_data.get('re_pwd')
        pwd = self.cleaned_data.get('pwd')
        if pwd is None:  # 表示密码字段没有输入值,这里直接放行,反正密码钩子校验也通不过,这里钩子不校验也无所谓了
            return re_pwd
        # 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
        if re_pwd and pwd and re_pwd == pwd:
            return re_pwd
        else:
            raise ValidationError("两次密码输入不一致")
            
    # def clean_email(self):
    #     email = self.cleaned_data.get('email')
    #     if not email:
    #         raise ValidationError("邮箱不能为空")
    #     else:
    #         return email

def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            print(forms_obj.cleaned_data)
            # {'user': 'zhangkai123', 'pwd': '123a', 're_pwd': '123a', 'email': 'zhangkai123@qq.com'}
            if forms_obj.cleaned_data.get('re_pwd'):
                del forms_obj.cleaned_data['re_pwd']
            print(forms_obj.cleaned_data)
            # {'user': 'zhangkai123', 'pwd': '123a', 'email': 'zhangkai123@qq.com'}
            models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            return render(request, 'register.html', {"forms_obj": forms_obj})
    forms_obj = UserForm()
    return render(request, 'register.html', {"forms_obj": forms_obj})

register.html,代码不变。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg {
            color: red;
            margin-left: 10px;
        }
    </style>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in forms_obj %}
                <div class="form-group">
                    <label for="">{{ field.label }}</label>
                    <!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
                    {{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
                </div>
                {% endfor %}
                <p><input type="submit" class="btn btn-success pull-left"></p>
            </form>
        </div>
    </div>
</div>
</body>
</html>

全局钩子

根据form组件的源码执行流程,当基础钩子和局部钩子都通过后,这时的cleaned_data数据也基本上是合法且完整的了,可以在全局钩子做最后的校验规则的补充。

全局钩子的基本使用和注意事项

在form组件类中重写clean方法,即是在定义全局钩子。

全局钩子的校验逻辑是,通过校验,返回cleaned_data,否则raise ValidationError错误,错误信息将会保存在forms对象的errors字典的__all__这个特殊的key中,那么前端展示全局钩子的错误信息,也要手动处理。

使用起来还是需要前端配合的。

接下来的示例,就是通过全局钩子校验两次密码输入不一致。

上代码,models.pyurls.py都不变。

veiws.py

import os
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from app01 import models


class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.TextInput(attrs={"class": "form-control"}),
        label="用户名"  # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字
    )
    pwd = forms.CharField(
        min_length=3,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
        label="密码"
    )
    re_pwd = forms.CharField(
        min_length=3,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
        label="确认密码"
    )

    email = forms.EmailField(
        error_messages={
            "required": "该字段不能为空",
            "invalid": "邮箱格式错误"
        },
        widget=widgets.EmailInput(attrs={"class": "form-control"}),
        label="邮箱"
    )

    def clean_user(self):
        # 前端传过来的user字段的值
        value = self.cleaned_data.get('user')
        user_obj = models.User.objects.filter(user=value).first()
        if user_obj:  # 用户名已存在
            # 固定写法
            raise ValidationError("用户名已存在")
        else:
            # 如果用户名不存在,表示该字段值没问题,正常返回该值就行了
            return value

    def clean_pwd(self):
        pwd = self.cleaned_data.get('pwd')
        if pwd.isdecimal():  # 如果用户输入是纯数字表示校验不通过
            # 固定写法
            raise ValidationError("密码不能是纯数字")
        else:
            return pwd

    # def clean_re_pwd(self):
    #     re_pwd = self.cleaned_data.get('re_pwd')
    #     pwd = self.cleaned_data.get('pwd')
    #     if pwd is None:  # 表示密码字段没有输入值,这里直接放行,反正密码钩子校验也通不过,这里钩子不校验也无所谓了
    #         return re_pwd
    #     # 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
    #     if re_pwd and pwd and re_pwd == pwd:
    #         return re_pwd
    #     else:
    #         raise ValidationError("两次密码输入不一致")

    def clean(self):
        """ 全局钩子 """
        re_pwd = self.cleaned_data.get('re_pwd')
        pwd = self.cleaned_data.get('pwd')
        # 如果这两个字段有一个输入为空,就跳过这个全局钩子,反正基础校验或者自定义钩子检验也通不过,这里跳过无所谓
        if pwd is None or re_pwd is None:
            return self.cleaned_data
        # 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
        if re_pwd and pwd and re_pwd == pwd:
            return self.cleaned_data
        else:
            raise ValidationError("两次密码输入不一致")


def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            print(forms_obj.cleaned_data)
            # {'user': 'zhangkai123', 'pwd': '123a', 're_pwd': '123a', 'email': 'zhangkai123@qq.com'}
            if forms_obj.cleaned_data.get('re_pwd'):
                del forms_obj.cleaned_data['re_pwd']
            print(forms_obj.cleaned_data)
            # {'user': 'zhangkai123', 'pwd': '123a', 'email': 'zhangkai123@qq.com'}
            models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            # 如果全局钩子中有错误信息产生,就单独取出来,交给前端单独处理
            # 没有就返回None
            all_error = forms_obj.errors.get('__all__')
            if all_error:
                all_error = forms_obj.errors.get('__all__')[0]
            else:
                # 没有错误信息,前端就不用显示了
                all_error = ""

            return render(request, 'register.html', {"forms_obj": forms_obj, "all_error": all_error})
    forms_obj = UserForm()
    return render(request, 'register.html', {"forms_obj": forms_obj})

register.html也要单独做处理:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg {
            color: red;
            margin-left: 10px;
        }
    </style>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in forms_obj %}
                <div class="form-group">
                    <label for="">{{ field.label }}</label>
                    <!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
                    {{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
                    <!-- 如果后台使用的是全局钩子的话,要进行单独的判断,让两次密码输入不一致,显示在确认密码输入框下面 -->
                    {% if field.name == "re_pwd" %}
                        <span class="errorMsg">{{ all_error }}</span>
                    {% endif %}
                </div>
                {% endfor %}
                <p><input type="submit" class="btn btn-success pull-left"></p>
            </form>
        </div>
    </div>
</div>
</body>
</html>

重写__init__方法

如果form组件类中,每个字段都要写required,添加属性值之类的,在每个字段中写是非常麻烦的,如下:

from django import forms
from django.forms import widgets

class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5, max_length=13,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.TextInput(attrs={"class": "form-control"}),
        label="用户名"  # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字
    )
    pwd = forms.CharField(
        min_length=3,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
        label="密码"
    )
    re_pwd = forms.CharField(
        min_length=3,
        error_messages={"required": "该字段不能为空"},
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
        label="确认密码"
    )

    email = forms.EmailField(
        error_messages={
            "required": "该字段不能为空",
            "invalid": "邮箱格式错误"
        },
        widget=widgets.EmailInput(attrs={"class": "form-control"}),
        label="邮箱"
    )

为了省事儿,有些相同的值可以在__init__中写:

from django import forms
from django.forms import widgets

class UserForm(forms.Form):
    user = forms.CharField(
        min_length=5,
        max_length=13,
        widget=widgets.TextInput(),
        label="用户名"
    )
    pwd = forms.CharField(
        min_length=3,
        widget=widgets.PasswordInput(),
        label="密码"
    )
    re_pwd = forms.CharField(
        min_length=3,
        widget=widgets.PasswordInput(),
        label="确认密码"
    )

    email = forms.EmailField(
        error_messages={"invalid": "邮箱格式错误"},
        widget=widgets.EmailInput(),
        label="邮箱"
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

forms处理下拉菜单

下拉菜单这里处理起来有多种情况:

  • 单选下拉框:
    • 值是固定的,例如用户表的性别字段,值是固定的,多选一,这种情况前端搞个单选下拉框即可。注意,我在偶然中发现,这种下拉框在form和modelform处理中,对应的模型类字段必须是IntegerField类型,不能是CharField类型,否则会报"选择一个有效的选项。 xxx 不在可用的选项中。"这种通不过校验的错误。
    • 值是动态的,例如下拉框的值是外键字段关联的表中的数据,例如书籍表的出版社字段,它关联出版社表,前端展示书籍对应的出版社字段时,数据是从出版社表中动态获取的,所以这个单选下拉框跟上面的下拉框还不一样。
  • 多选下拉框:
    • 多选下拉框的值通常也来自外键字段,例如书籍的作者字段对应作者表,这种情况的多选下拉框跟上面的下拉框还不一样。

下面的示例,涵盖了上面的三种下拉框。

urls.py

from django.contrib import admin
from django.urls import path, re_path
from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('add_book/', views.add_book, name="add_book"),
    path('add_author/', views.add_author, name="add_author"),
]

models.py,自己手动添加几个出版社和作者,书籍和作者都可以从前端添加。

from django.db import models


class Publish(models.Model):
    name = models.CharField(max_length=32, verbose_name='出版社')
	# 前端下拉框中其实展示的是一个个的出版社对象,但之所以你能看到具体的出版社
    # 全靠下面__str__方法,当你调试时,你可以先把下面的方法注释掉,然后你看前端
    # 下拉框中的数据长啥样,你就知道我在说什么了
    def __str__(self):
        return self.name


class Book(models.Model):
    """ 书籍表 """
    title = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=8, decimal_places=2)  # 999999.99
    pub_date = models.DateTimeField()  # "2012-12-12"
    publish = models.ForeignKey(to="Publish", on_delete=models.CASCADE)  # 级联删除
    authors = models.ManyToManyField(to="Author")

    def __str__(self):
        return self.title


class Author(models.Model):
    user = models.CharField(max_length=32, verbose_name='用户名')
    gender = models.IntegerField(choices=(
        (1, "男"),
        (2, "女"),
    ), default=1, verbose_name='性别')

    def __str__(self):
        return self.user

views.py

from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from django.core.exceptions import ValidationError
from app01.models import Publish, Author, Book


class AuthorForm(forms.Form):
    user = forms.CharField(label="作者名称")
    # 值是固定的单选下拉框
    gender = forms.ChoiceField(choices=((1, "男"), (2, "女")), label="性别")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})


class BookForm(forms.Form):
    title = forms.CharField(label="书籍名称")
    price = forms.DecimalField(max_digits=4, decimal_places=2, label="价格")
    pub_date = forms.DateField(label='出版时间', widget=widgets.TextInput(attrs={"type": "date"}))
    # 值是动态的单选下拉框
    publish = forms.ModelChoiceField(queryset=Publish.objects.all(), label='出版社')
    # 值是动态的多选下拉框
    authors = forms.ModelMultipleChoiceField(queryset=Author.objects.all(), label='作者')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})


def add_book(request):
    """ 添加书籍 """
    if request.method == "POST":
        forms_obj = BookForm(request.POST)
        if forms_obj.is_valid():
            data = forms_obj.cleaned_data
            # print(data)
            # queryset对象
            # print(list(data['authors']))
            # print(data['publish'])
            obj = Book.objects.create(
                title=data['title'],
                price=data['price'],
                pub_date=data['pub_date'],
                publish=data['publish']
            )
            obj.authors.add(*data['authors'])
            return HttpResponse("OK")
        else:
            return render(request, 'register.html', {"forms_obj": forms_obj})
    forms_obj = BookForm()
    return render(request, 'add_author.html', {"forms_obj": forms_obj})


def add_author(request):
    """ 添加作者 """
    if request.method == "POST":
        forms_obj = AuthorForm(request.POST)
        if forms_obj.is_valid():
            # print(forms_obj.cleaned_data)
            Author.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            return render(request, 'register.html', {"forms_obj": forms_obj})
    forms_obj = AuthorForm()
    return render(request, 'add_author.html', {"forms_obj": forms_obj})

add_author.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg {
            color: red;
            margin-left: 10px;
        }
    </style>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in forms_obj %}
                <div class="form-group">
                    <label for="">{{ field.label }}</label>
                    {{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
                </div>
                {% endfor %}
                <p><input type="submit" class="btn btn-success pull-left"></p>
            </form>
        </div>
    </div>
</div>
</body>
</html>

add_book.htmladd_author.html代码一样:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg {
            color: red;
            margin-left: 10px;
        }
    </style>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in forms_obj %}
                <div class="form-group">
                    <label for="">{{ field.label }}</label>
                    {{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
                </div>
                {% endfor %}
                <p><input type="submit" class="btn btn-success pull-left"></p>
            </form>
        </div>
    </div>
</div>
</body>
</html>

forms组件实现下拉框保留默认值

例如在根据下拉框进行搜索时,当返回结果数据的页面中,仍然需要下拉框保留原来的搜索条件,也就是保留搜索的默认值时,就可以通过initial字典来完成这个需求。
来个示例,首先是models.pyurls.py

# -------------- urls.py --------------
from django.contrib import admin
from django.urls import path
from api import views

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

# -------------- models.py --------------
from django.db import models

class UserInfo(models.Model):
    user = models.CharField(max_length=32)
    age = models.IntegerField()
    gender = models.IntegerField(choices=((1, "男"), (2, "女"), (3, "保密")), default=3)
    def __str__(self):
        return self.user

接下来就是views.py

from django.shortcuts import render
from api.models import UserInfo
from django import forms


class UserForms(forms.Form):
    gender = forms.ChoiceField(choices=((1, "男"), (2, "女"), (3, "保密")))
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

def users(request):
    if request.method == "GET":
        form_obj = UserForms()
        user_list = UserInfo.objects.all()
        return render(request, 'users.html', {"user_list": user_list, "form_obj": form_obj})
    # 根据前端传来的gender值,再返回给前端时,通过forms的initial字典作为默认值进行展示,这样前端就能达到保留下拉框搜索条件的目的了
    # 这个套路同样也适用于modelform中
    gender = request.POST.get("gender")
    form_obj = UserForms(initial={"gender": gender})
    user_list = UserInfo.objects.filter(gender=gender)
    return render(request, 'users.html', {"user_list": user_list, "form_obj": form_obj})

最后的users.html文件了:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户列表</title>
    <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
    <script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>

<div class="container">
    <div class="row">

            <form action="" method="post">
                {% csrf_token %}
                <div class="col-lg-4 col-lg-offset-2">
                {{ form_obj.gender }}
                </div>
                <div class="col-lg-3">
                    <input type="submit" class="btn btn-primary">
                </div>
            </form>

    </div>
    <div class="row">
        <div class="col-lg-9 col-lg-offset-2">
            <table class="table table-hover">
                <thead>
                <tr>
                    <th>姓名</th>
                    <th>年龄</th>
                    <th>性别</th>
                </tr>
                </thead>
                <tbody>
                {% for user in user_list %}
                    <tr>
                        <td>{{ user.user }}</td>
                        <td>{{ user.age }}</td>
                        <td>{{ user.get_gender_display }}</td>
                    </tr>
                {% endfor %}

                </tbody>
            </table>
        </div>
    </div>
</div>


</body>
</html>

实现效果:

forms类中使用request对象

例如实现某个功能时, 随机生成的图片验证码是存储在session中的, 我想在clean_code钩子函数中对验证码进行验证,而不是在视图函数中实现, 要达到这个目的clean_code钩子函数里就得有request对象....类似的的场景,我们重写__init__方法,把request传进去。

from django.shortcuts import render
from api.models import UserInfo
from django import forms


class UserForms(forms.Form):
    gender = forms.ChoiceField(choices=((1, "男"), (2, "女"), (3, "保密")))
    def __init__(self,request, *args, **kwargs):
        # 重写__init__方法,把request对象传进来
        self.request = request
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

    def clean_gender(self):
        # 在钩子方法中就能用了
        gender = self.request.POST.get("gender")
        return gender


def users(request):
    if request.method == "GET":
        form_obj = UserForms(request=request)
        user_list = UserInfo.objects.all()
        return render(request, 'users.html', {"user_list": user_list, "form_obj": form_obj})
    gender = request.POST.get("gender")
    form_obj = UserForms(data=request.POST, request=request )
    form_obj.is_valid()
    user_list = UserInfo.objects.filter(gender=gender)
    return render(request, 'users.html', {"user_list": user_list, "form_obj": form_obj})

forms组件补充

django的内置字段如下:

Field
    required=True,               是否允许为空
    widget=None,                 HTML插件
    label=None,                  用于生成Label标签或显示内容
    initial=None,                初始值
    help_text='',                帮助信息(在标签旁边显示)
    error_messages=None,         错误信息 {'required': '不能为空', 'invalid': '格式错误'}
    show_hidden_initial=False,   是否在当前插件后面再加一个隐藏的且具有默认值的插件(可用于检验两次输入是否一直)
    validators=[],               自定义验证规则
    localize=False,              是否支持本地化
    disabled=False,              是否可以编辑
    label_suffix=None            Label内容后缀
 
 
CharField(Field)
    max_length=None,             最大长度
    min_length=None,             最小长度
    strip=True                   是否移除用户输入空白
 
IntegerField(Field)
    max_value=None,              最大值
    min_value=None,              最小值
 
FloatField(IntegerField)
    ...
 
DecimalField(IntegerField)
    max_value=None,              最大值
    min_value=None,              最小值
    max_digits=None,             总长度
    decimal_places=None,         小数位长度
 
BaseTemporalField(Field)
    input_formats=None          时间格式化   
 
DateField(BaseTemporalField)    格式:2015-09-01
TimeField(BaseTemporalField)    格式:11:12
DateTimeField(BaseTemporalField)格式:2015-09-01 11:12
 
DurationField(Field)            时间间隔:%d %H:%M:%S.%f
    ...
 
RegexField(CharField)
    regex,                      自定制正则表达式
    max_length=None,            最大长度
    min_length=None,            最小长度
    error_message=None,         忽略,错误信息使用 error_messages={'invalid': '...'}
 
EmailField(CharField)      
    ...
 
FileField(Field)
    allow_empty_file=False     是否允许空文件
 
ImageField(FileField)      
    ...
    注:需要PIL模块,pip3 install Pillow
    以上两个字典使用时,需要注意两点:
        - form表单中 enctype="multipart/form-data"
        - view函数中 obj = MyForm(request.POST, request.FILES)
 
URLField(Field)
    ...
 
 
BooleanField(Field)  
    ...
 
NullBooleanField(BooleanField)
    ...
 
ChoiceField(Field)
    ...
    choices=(),                选项,如:choices = ((0,'上海'),(1,'北京'),)
    required=True,             是否必填
    widget=None,               插件,默认select插件
    label=None,                Label内容
    initial=None,              初始值
    help_text='',              帮助提示
 
 
ModelChoiceField(ChoiceField)
    ...                        django.forms.models.ModelChoiceField
    queryset,                  # 查询数据库中的数据
    empty_label="---------",   # 默认空显示内容
    to_field_name=None,        # HTML中value的值对应的字段
    limit_choices_to=None      # ModelForm中对queryset二次筛选
     
ModelMultipleChoiceField(ModelChoiceField)
    ...                        django.forms.models.ModelMultipleChoiceField
 
 
     
TypedChoiceField(ChoiceField)
    coerce = lambda val: val   对选中的值进行一次转换
    empty_value= ''            空值的默认值
 
MultipleChoiceField(ChoiceField)
    ...
 
TypedMultipleChoiceField(MultipleChoiceField)
    coerce = lambda val: val   对选中的每一个值进行一次转换
    empty_value= ''            空值的默认值
 
ComboField(Field)
    fields=()                  使用多个验证,如下:即验证最大长度20,又验证邮箱格式
                               fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),])
 
MultiValueField(Field)
    PS: 抽象类,子类中可以实现聚合多个字典去匹配一个值,要配合MultiWidget使用
 
SplitDateTimeField(MultiValueField)
    input_date_formats=None,   格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y']
    input_time_formats=None    格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
 
FilePathField(ChoiceField)     文件选项,目录下文件显示在页面中
    path,                      文件夹路径
    match=None,                正则匹配
    recursive=False,           递归下面的文件夹
    allow_files=True,          允许文件
    allow_folders=False,       允许文件夹
    required=True,
    widget=None,
    label=None,
    initial=None,
    help_text=''
 
GenericIPAddressField
    protocol='both',           both,ipv4,ipv6支持的IP格式
    unpack_ipv4=False          解析ipv4地址,如果是::ffff:192.0.2.1时候,可解析为192.0.2.1, PS:protocol必须为both才能启用
 
SlugField(CharField)           数字,字母,下划线,减号(连字符)
    ...
 
UUIDField(CharField)           uuid类型
    ...

django的内置插件:

TextInput(Input)
NumberInput(TextInput)
EmailInput(TextInput)
URLInput(TextInput)
PasswordInput(TextInput)
HiddenInput(TextInput)
Textarea(Widget)
DateInput(DateTimeBaseInput)
DateTimeInput(DateTimeBaseInput)
TimeInput(DateTimeBaseInput)
CheckboxInput
Select
NullBooleanSelect
SelectMultiple
RadioSelect
CheckboxSelectMultiple
FileInput
ClearableFileInput
MultipleHiddenInput
SplitDateTimeWidget
SplitHiddenDateTimeWidget
SelectDateWidget

常用选择插件:

# 单radio,值为字符串
# user = fields.CharField(
#     initial=2,
#     widget=widgets.RadioSelect(choices=((1,'上海'),(2,'北京'),))
# )
 
# 单radio,值为字符串
# user = fields.ChoiceField(
#     choices=((1, '上海'), (2, '北京'),),
#     initial=2,
#     widget=widgets.RadioSelect
# )
 
# 单select,值为字符串
# user = fields.CharField(
#     initial=2,
#     widget=widgets.Select(choices=((1,'上海'),(2,'北京'),))
# )
 
# 单select,值为字符串
# user = fields.ChoiceField(
#     choices=((1, '上海'), (2, '北京'),),
#     initial=2,
#     widget=widgets.Select
# )
 
# 多选select,值为列表
# user = fields.MultipleChoiceField(
#     choices=((1,'上海'),(2,'北京'),),
#     initial=[1,],
#     widget=widgets.SelectMultiple
# )
 
 
# 单checkbox
# user = fields.CharField(
#     widget=widgets.CheckboxInput()
# )
 
 
# 多选checkbox,值为列表
# user = fields.MultipleChoiceField(
#     initial=[2, ],
#     choices=((1, '上海'), (2, '北京'),),
#     widget=widgets.CheckboxSelectMultiple
# )

常见问题

AttributeError: 'XxxForm' object has no attribute 'cleaned_data'

django3.2 + python3.9 + win10

这个问题,非常典型,就是没有经过is_valid数据校验,就直接cleaned_data取值导致的报错,如下示例:

解决办法也就简单了,加上校验就行了:

ModelForm

基本用法

from django import forms
from django.core.exceptions import ValidationError


class UserModelForm(forms.ModelForm):
    # 添加额外的字段
    # 注意,在视图函数中,校验通过后,写入数据库之前,还要确认是否要删除这个额外的键值对
    re_pwd = forms.CharField(
        label='确认密码',
        widget=forms.widgets.PasswordInput()
    )
    # 重写某个字段
    pwd = forms.CharField(
        label='密码',
        widget=forms.widgets.PasswordInput(),  # 这里是为了让页面输入是密文
    )

    # 元类
    class Meta:
        # 对应的模型类对象
        model = models.User

        # 展示模型类型中的所有字段
        fields = "__all__"
        # 或者这样写,只展示部分字段,和校验这部分字段
        # fields = ['user', 'pwd', 're_pwd']

        # 排除哪些字段不让其在页面中展示
        # exclude = ['user']

        # 插件,用法可以参考forms
        widgets = {
            # 为指定字段添加小组件,比如重写字段类型,添加placeholder等等属性或者自定义属性
            "user": forms.widgets.TextInput(attrs={"type": "text"})
        }
        # 提示信息:labels,当然这个labels的优先级没有上面重写或者自定义字段中的的label优先级高
        labels = {
            "user": "叫啥",
            "pwd": "输入你嘞密码"
        }
        # 自定义错误信息(整体错误信息from django.core.exceptions import NON_FIELD_ERRORS)
        error_messages = {
            "user": {"required": "输入不能为空!!!"}
        }
        # 帮助信息
        help_texts = {
            "user": "请输入中文"
        }

    # 注意这个__init__应该和元类平齐,不要写错缩进!!!!
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

    def clean_user(self):
        """ 局部钩子 """
        pass
    def clean(self):
        """ 全局钩子 """
        pass

创建数据save()

def add_book(request):
    if request.method == "POST":
        #直接传request.POST,进行ModelForm的实例化传参
        form_obj = forms.BookModelForm(request.POST)
        if form_obj.is_valid(): # 校验数据
            form_obj.save()   #直接就可以保存数据到数据库,包括多对多,多对一,一对一的关系
            return redirect("/book_list/")
    #ModelForm实例化对象
    form_obj = forms.BookModelForm()
    return render(request, "v2/add_book.html", locals())

初始化数据instance=obj

def edit_book(request, pk):
    book_obj = models.Book.objects.filter(id=pk).first()
    #form_obj通过instance设置初始化的值,例如,图书管理系统中的编辑书籍功能,
    #点击编辑后跳转到编辑书籍页面,跳转后需要用要编辑的书籍信息填充页面对应信息。
    #不同于Form组件的是,ModelForm直接就可以传实例化的对象,而不需要将对象转化成字典的形式传。
    form_obj = forms.BookModelForm(instance=book_obj)  
    return render(request, "v2/edit_book.html", locals())

更新 :ModelForm(request.POST, instance=book_obj)

def edit_book(request, pk):
    book_obj = models.Book.objects.filter(id=pk).first()
    if request.method == "POST":
        #修改数据时,直接可以将用户数据包request.POST传进去,
        #再传一个要修改的对象的示例,ModelForm就可以自动完成修改数据了。
        form_obj = forms.BookModelForm(request.POST, instance=book_obj)
        if form_obj.is_valid():  // 数据校验
            form_obj.save()   // 直接保存
        return redirect("/book_list/")
    #form_obj通过instance设置初始化的值,例如,图书管理系统中的编辑书籍功能,
    #点击编辑后跳转到编辑书籍页面,跳转后需要用要编辑的书籍信息填充页面对应信息。
    #不同于Form组件的是,ModelForm直接就可以传实例化的对象,而不需要将对象转化成字典的形式传。
    form_obj = forms.BookModelForm(instance=book_obj)  
    return render(request, "v2/edit_book.html", locals())

参考或摘自:

关于label

页面中展示的label内容,有4种展示方式,并且优先级不同:

  1. 模型类中字段没有添加verbose_name参数,且modelform类也没有指定labels属性,那么前端页面默认显示字段的首字母大写的字段名。这个优先级最低。
  2. 模型类中字段有verbose_name参数,前端默认显示verbose_name参数值。优先级适中。
  3. modelform类中定义了labels属性,那么它的优先级高。
  4. 如果重写了某个字段,然后在字段的配置中的label属性优先级最高。
class Author(models.Model):
    user = models.CharField(max_length=32, verbose_name='用户名')
    
class AuthorModelForm(forms.ModelForm):
    # 例如重写user字段
    user =  forms.CharField(
        label='叫啥',   # 这里的label优先级最高
        widget=forms.widgets.PasswordInput(),  # 这里是为了让页面输入是密文
    )
    class Meta:
        model = Author
        fields = "__all__"
        labels = {
            "user": "第二优先级高的label"
        }

empty_lable

通常对于外键字段,都会有个下拉框,没有设置默认值的话,就会默认显示empty_lable值,也就是你看到的:

如果不想要这个empty_lable,你可以这样:

import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher


class BookModelForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = '__all__'
        widgets = {
            # "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
        }

        # 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
        # 数据录入时单独处理
        exclude = ['pub_date']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

        # 如果你不想看到那个提示的 '---------' 就给下面的值设置为None
        self.fields['publish'].empty_label = None

def add(request):

    form_obj = BookModelForm()
    if request.method == 'GET':
        return render(request, 'add.html', {"form_obj": form_obj})
    else:
        form_obj = BookModelForm(request.POST)
        if form_obj.is_valid():
            # 为页面中没有填值的字段,这里额外的添加上也可以
            form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
            # 获取从别处传来的出版社id
            pub_pk = 1
            form_obj.instance.publish = Publisher.objects.filter(pk=pub_pk).first()
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'add.html', {"form_obj": form_obj})

效果:

让某些字段不可修改,即设置disable

import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher


class BookModelForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = '__all__'
        widgets = {
            # "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
            # 法1,
            'publish': forms.widgets.Select(attrs={"disabled": "disabled"})
        }

        # 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
        # 数据录入时单独处理
        exclude = ['pub_date']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

        # 法2
        self.fields['title'].widget.attrs.update({"disabled": "disabled"})


def add(request):
    # 要结合initial设置默认值,不然页面中input框中值为空
    pub_pk = 1
    pub_obj = Publisher.objects.filter(pk=pub_pk)
    form_obj = BookModelForm(initial={"title": "book1", 'publish': pub_obj.first()})
    if request.method == 'GET':
        return render(request, 'add.html', {"form_obj": form_obj})
    else:
        form_obj = BookModelForm(request.POST)
        if form_obj.is_valid():
            # 为页面中没有填值的字段,这里额外的添加上也可以
            form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
            # 获取从别处传来的出版社id
            pub_pk = 1
            form_obj.instance.publish = Publisher.objects.filter(pk=pub_pk).first()
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'add.html', {"form_obj": form_obj})

效果:

关于help_text

禁用help_text:

from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User


class AuthModelForm(forms.ModelForm):
    re_password = forms.CharField(
        label='确认密码',
        widget=forms.widgets.PasswordInput()
    )

    class Meta:
        model = User
        fields = [
            "username", 'password', 're_password', 'email',
            'is_active', 'is_staff', 'is_superuser'
        ]
        # 为指定字段添加help_text
        # 注意,如果下面__init__中禁用了help_text,那么这里的配置不会生效
        help_texts = {
            'username': "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for filed in self.fields.values():
            filed.widget.attrs.update({'class': 'form-control'})
            # 禁用每个字段的help_text
            filed.help_text = ''

    def clean(self):
        pwd = self.cleaned_data.get('password')
        re_pwd = self.cleaned_data.get('re_password')
        if pwd == re_pwd:
            return self.cleaned_data
        self.add_error('re_password', '两次密码不一致')
        raise ValidationError('两次密码不一致')

参考:如何禁用django注册密码help_text

initial

为页面中的input框设置初始值。


class AuthModelForm(forms.ModelForm):
    re_password = forms.CharField(
        label='确认密码',
        widget=forms.widgets.PasswordInput()
    )
    password = forms.CharField(
        label='密码',
        widget=forms.widgets.PasswordInput(),
    )
    initial_test = forms.CharField(
        label='初始值示例字段',
        initial='初始值'
    )

    class Meta:
        model = User
        fields = [
            "username", 'password', 're_password', 'email', 'phone', "initial_test",
            'is_active', 'is_staff', 'is_superuser',
        ]
        help_texts = {
            'username': "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
        }

        
# 视图函数中,直接这样写就完了,前端的input框就能展示初始的内容
form_obj = AuthModelForm()

但这样不太方便,因为modelform中通常不会大批量的单独定义字段,那样搞用form多好。

所以通常有另一种写法,定义modelfrom类不管初始值。

而是在视图函数中,去定义初始值:

form_obj = AuthModelForm(initial={"username": "zhangkai", 'email': 'zhangkai@qq.com', "phone": "18211101111"})

这样前端展示的效果也一样。

https://www.icode9.com/content-4-510590.html

modelform处理下拉菜单

django3.2 + python3.9

一般的ForeignKey的外键字段,modelform在前端默认渲染成这样:

如果有需求说,这个外键字段,我要指定让它显示某个出版社,或者固定显示某个出版社,亦或是不显示某个出版社,但保存时又要正常保存。这些需求如何实现。

首先把基本不变的代码列出来。

urls.py:

from django.contrib import admin
from django.urls import path
from api import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('add/', views.add, name='add'),
]

models.py

from django.db import models


class Book(models.Model):
    """ 书籍表 """
    title = models.CharField(max_length=32, verbose_name='书籍名称')
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name='书籍价格')  # 浮点型,9999.99
    pub_date = models.DateField(verbose_name='出版日期')
    publish = models.ForeignKey(
        to='Publisher',
        on_delete=models.CASCADE,
         verbose_name='出版社'
    )  # Django2.x以上版本,必须手动指定on_delete

    def __str__(self):
        return self.title


class Publisher(models.Model):
    """ 出版社表 """
    name = models.CharField(max_length=32, verbose_name='出版社名称')
    # email = models.EmailField(verbose_name='邮箱')  # # 为了省事儿,这个字段就不创建了
    address = models.CharField(max_length=32, verbose_name='出版社地址')

    def __str__(self):
        return self.name

add.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <style>
        input{
            margin-top: 10px;
        }
        ul{
            padding: 0;
        }
        ul li{
            color: red;
            list-style: none;

        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 off-set-2">
            <form action='' method="post" novalidate>
                {% csrf_token %}
                {{ form_obj }}
                <input type="submit" class="btn btn-danger" />
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script></script>
</html>

上面的代码基本不变了,我们来看根据不同需求,需要修改的代码部分。

默认显示某个出版社

最简单的实现,那就是在给页面的时候,赋值上初始值,这里主要是views.py

import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher

class BookModelForm(forms.ModelForm):

    class Meta:
        model = Book
        fields = '__all__'
        # widgets = {
        #     "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
        # }

        # 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
        # 数据录入时单独处理
        exclude = ['pub_date']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

        # 法1(同法3):为下拉菜单中只显示一个值,可以在这里做,但这这么做就写死了,不如在视图中根据条件过滤灵活
        # 通常这么做可以筛选出,固定要在前端显示哪些字段
        # self.fields['publish'].queryset = Publisher.objects.filter(address="上海")

        # 如果你不想看到那个提示的 '---------' 就给下面的值设置为None
        self.fields['publish'].empty_label = None
        
        # 如果你不想让前端标签被选中,直接加disabled属性即可
        self.fields['publish'].widget.attrs.update({"disabled": "disabled"})


def add(request):
    # 例如我们这里接收到传来的书籍对应的出版社,也就是添加的这本书必须属于某个出版社,或者让前端默认显示某个出版社
    pub_pk = 1
    pub_obj = Publisher.objects.filter(pk=pub_pk)
    # 法2,也很灵活
    form_obj = BookModelForm(initial={'publish': pub_obj.first()})
    # 法3,这个就是把上面init中的代码写到这里,可以根据条件进行过滤筛选,相对灵活
    # 可以在这里根据传过来的条件进行筛选,注意一定是queryset,不能是单个模型类对象
    # form_obj.fields['publish'].queryset = pub_obj
    if request.method == 'GET':
        return render(request, 'add.html', {"form_obj": form_obj})
    else:
        form_obj = BookModelForm(request.POST)
        if form_obj.is_valid():
            # 为页面中没有填值的字段,这里额外的添加上也可以
            form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'add.html', {"form_obj": form_obj})

页面效果:

这样的话,用户不选择别的出版社,保存时就是你指定的出版社了。

默认显示指定(多个)出版社

这个需求也是有的,比如外键字段很多,但是我在视图中要根据条件,展示某些记录,就可以这么做。

其它不变,views.py中调整即可:

import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher


class BookModelForm(forms.ModelForm):

    class Meta:
        model = Book
        fields = '__all__'
        widgets = {
            "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
        }

        # 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
        # 数据录入时单独处理
        exclude = ['pub_date']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

def add(request):
    # 例如我们这里接收到传来的书籍对应的出版社名字,也就是添加的这本书必须属于某几个出版社,然后让前端某人显示某几个出版社
    pub_address = '北京'
    pub_obj = Publisher.objects.filter(address=pub_address)
    form_obj = BookModelForm(initial={'publish': pub_obj})
    if request.method == 'GET':
        return render(request, 'add.html', {"form_obj": form_obj})
    else:
        form_obj = BookModelForm(request.POST)
        if form_obj.is_valid():
            # 为页面中没有填值的字段,这里额外的添加上也可以
            form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'add.html', {"form_obj": form_obj})


页面效果:

默认不显示某个出版社,在保存时处理

这个需求,参考处理日期字段的套路即可,就是页面压根不显示出版社信息,在后端保存书籍时,直接根据需求绑定对应的出版社。

其它不变,views.py中调整即可:

import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher


class BookModelForm(forms.ModelForm):

    class Meta:
        model = Book
        fields = '__all__'
        # widgets = {
        #     "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
        # }

        # 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
        # 数据录入时单独处理
        exclude = ['pub_date', 'publish']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})



def add(request):
    form_obj = BookModelForm()
    if request.method == 'GET':
        return render(request, 'add.html', {"form_obj": form_obj})
    else:
        form_obj = BookModelForm(request.POST)
        if form_obj.is_valid():
            # 为页面中没有填值的字段,这里额外的添加上也可以
            form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
            # 获取从别处传来的出版社id
            pub_pk = 1
            form_obj.instance.publish = Publisher.objects.filter(pk=pub_pk).first()
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'add.html', {"form_obj": form_obj})

页面效果:

参考:https://blog.csdn.net/slamx/article/details/51095066

modelform处理radio

django3.2

这里主要是为了解决modelform在处理radio时,还是会显示---------的问题。

怎么解决呢?
我这里提供个示例。
models.py

class TestDemo(models.Model):
    name = models.CharField(max_length=32, unique=True)
    level = models.IntegerField(choices=((0, "普通会员"), (1, '黄金会员'), (2, "白金会员")), verbose_name='vip等级')

views.py

from django.shortcuts import render, redirect
from django import forms
from django.http import HttpResponse
from .models import TestDemo


class TestModelForm(forms.ModelForm):
    level2 = forms.CharField(widget=forms.RadioSelect(choices=((0, "普通会员"), (1, '黄金会员'), (2, "白金会员"))))

    class Meta:
        model = TestDemo
        fields = ['name', 'level', 'level2']

        widgets = {
            # 在这里重写level字段,页面中还是会渲染出来 ---------
            # 解决方案有两种
            # 1. 在models.py中,给level字段一个default值
            #    default的值在choices中的话,页面会有默认选中效果,如default=1,页面中默认选中黄金会员
            #    default=None 这样写,页面中既没有了---------,也不会默认选中某个radio了
            # 2. 就是不在widgets中写level了,而是在类中直接重写level字段,模型类中也不用定义default值,参考level2字段示例
            "level": forms.RadioSelect(
                attrs={"class": "form-radio"},
                choices=((0, "普通会员"), (1, '黄金会员'), (2, "白金会员")),
            )
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})
        # 下面这两个是控制页面标签的渲染效果
        self.fields['level'].widget.attrs.update({"class": "form-radio"})
        self.fields['level2'].widget.attrs.update({"class": "form-radio"})


def add(request):
    form_obj = TestModelForm()
    if request.method == 'GET':
        return render(request, 'add.html', {"form_obj": form_obj})
    else:
        form_obj = TestModelForm(request.POST)
        if form_obj.is_valid():
            form_obj.cleaned_data.pop('level2')
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'add.html', {"form_obj": form_obj})

add.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <style>
        input, ul, label{
            margin-top: 10px;
        }
        ul{
            padding: 0;
            display: flex;
            flex-direction: row;
        }
        ul li{

            list-style: none;
            padding: 5px;
        }
        .errorlist li{
            color: red;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 off-set-2">
            <form action='' method="post" novalidate>
                {% csrf_token %}
                {{ form_obj }}
                <input type="submit" class="btn btn-danger" />
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script></script>
</html>

注意:django3.x版本,modelform会将radio标签渲染成ul套li的形式;而django4.x的modelform会将radio标签渲染成div套div的形式。

modelform对用户名进行校验

需求是,注册时,如果用户名已存在,就在前端提示下用户名已存在。

这个需求比较好解决,使用局部钩子就行了,对传过来的用户名做个校验。

重点在局部钩子这里:

from django.shortcuts import render, HttpResponse
from django import forms
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from api import models


class UserInfoModelForm(forms.ModelForm):
    class Meta:
        model = models.UserInfo
        fields = "__all__"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

    def clean_user(self):
        """ 自定义关于user字段的钩子方法,作用:用户名不能重复 """
        # 前端传过来的user字段的值
        value = self.cleaned_data.get('user')
        user_obj = models.UserInfo.objects.filter(user=value).first()
        if user_obj:  # 用户名已存在
            # 固定写法
            raise ValidationError("用户名已存在")
        else:
            # 如果用户名不存在,表示该字段值没问题,正常返回该值就行了
            return value



def register(request):
    if request.method == "POST":
        form_obj = UserInfoModelForm(request.POST)
        if form_obj.is_valid():  # 必须先进行is_valid,才能取
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'register.html', {"form_obj": form_obj})
    form_obj = UserInfoModelForm()
    return render(request, 'register.html',{"form_obj": form_obj})

前端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {{ form_obj }}
                <input type="submit" value="提交" class="btn btn-primary" style="margin-top: 10px;">
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<script></script>
</html>

modelform确认密码字段处理

其实这里照抄forms部分即可,views.py

from django.shortcuts import render, HttpResponse
from api import models
from django import forms
from django.core.exceptions import ValidationError


class UserModelForm(forms.ModelForm):
    re_pwd = forms.CharField(
        label='确认密码',
        widget=forms.widgets.PasswordInput()
    )
    pwd = forms.CharField(
        label='密码',
        widget=forms.widgets.PasswordInput(),  # 这里是为了让页面输入是密文
    )

    class Meta:
        model = models.User
        fields = "__all__"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})  # 密码和确认密码不能输入为空的校验就是从这加的

    # ******************* 法1,通过局部钩子对密码和确认密码字段进行校验 *******************
    def clean_pwd(self):
        """ 这里要对密码进行所有的校验,让下面确认密码字段只要校验两次输入不一致就行了 """
        pwd = self.cleaned_data.get('pwd')
        if pwd.isdecimal():  # 如果用户输入是纯数字表示校验不通过
            # 固定写法
            raise ValidationError("密码不能是纯数字")
        else:
            return pwd

    def clean_re_pwd(self):
        """ 如果确认密码不输入值,也就走不到局部钩子这里,在基础钩子就被拦截了 """
        re_pwd = self.cleaned_data.get('re_pwd')
        pwd = self.cleaned_data.get('pwd')
        if pwd is None:  # 表示密码字段没有输入值,这里直接放行,反正密码钩子校验也通不过,这里钩子不校验也无所谓了
            return re_pwd
        # 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
        if re_pwd and pwd and re_pwd == pwd:
            return re_pwd
        else:
            raise ValidationError("两次密码输入不一致")

    # ******************* 法2,通过全局钩子对密码和确认密码字段进行校验 *******************
    # def clean(self):
    #     """ 全局钩子 """
    #     re_pwd = self.cleaned_data.get('re_pwd')
    #     pwd = self.cleaned_data.get('pwd')
    #     # 如果这两个字段有一个输入为空,就跳过这个全局钩子,反正基础校验或者自定义钩子检验也通不过,这里跳过无所谓
    #     if pwd is None or re_pwd is None:
    #         return self.cleaned_data
    #     # 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
    #     if re_pwd and pwd and re_pwd == pwd:
    #         return self.cleaned_data
    #     else:
    #         raise ValidationError("两次密码输入不一致")


def register(request):
    if request.method == "POST":
        form_obj = UserModelForm(request.POST)
        if form_obj.is_valid():  # 必须先进行is_valid,才能取
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'register.html', {"form_obj": form_obj})
    form_obj = UserModelForm()
    return render(request, 'register.html', {"form_obj": form_obj})

register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    <style>
        ul{
            list-style: none;
            padding-left: 0;
            color:red;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {{ form_obj }}
                <input type="submit" value="提交" class="btn btn-primary" style="margin-top: 10px;">
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<script></script>
</html>

modelform处理日期时间字段

django3.2

modelform对于日期时间字段的处理,要用到一个叫做小组件东西,其实就是自定义一个类。
这里提供一个demo。
models.py

from django.db import models

class Test1(models.Model):
    d1 = models.DateField(verbose_name='日期字段')
    d2 = models.DateTimeField(verbose_name='日期时间')

urls.py:

from django.urls import path, re_path
from . import views

urlpatterns = [
    path('add1/', views.add1),
    re_path('edit1/(?P<pk>\d+)/$', views.edit1),
]

views.py,这个是重点,重点观察两个自定义类,和在modelfrom类中进行重写日期时间字段。

from django.shortcuts import render, redirect, HttpResponse
from django import forms
from . import models

class DateInput(forms.DateInput):
    """ 自定义日期类 """
    input_type = "date"  # 日期 2022/9/2

class DateTimeInput(forms.DateTimeInput):
    """ 自定义日期时间类 """
    input_type = "datetime-local"  # 日期时间  2022/9/2 12:12

class Test1ModelForm(forms.ModelForm):
    # 重写日期时间字段,通过widget属性用上咱们定义的两个类
    d1 = forms.DateField(
        widget=DateInput(format='%Y-%m-%d'),  # format中连接符,必须是-,不能是/, 否则编辑时前端渲染不出来值
        label='日期'
    )
    d2 = forms.DateTimeField(
        widget=DateTimeInput(format='%Y-%m-%d %H:%M:%S'),
        label='日期时间'
    )
    class Meta:
        model = models.Test1
        fields = ['d1', 'd2']
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})   
        

def add1(request):
    form_obj = Test1ModelForm()
    if request.method == 'GET':
        return render(request, 'add1.html', {"form_obj": form_obj})
    else:
        form_obj = Test1ModelForm(request.POST)
        if form_obj.is_valid():
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'add1.html', {"form_obj": form_obj})


def edit1(request, pk):
    obj = models.Test1.objects.filter(id=pk).first()
    form_obj = Test1ModelForm(instance=obj)

    if request.method == 'GET':
        return render(request, 'add1.html', {"form_obj": form_obj})
    else:
        form_obj = Test1ModelForm(request.POST, instance=obj)
        if form_obj.is_valid():
            form_obj.save()
            return HttpResponse("OK")
        else:
            return render(request, 'add1.html', {"form_obj": form_obj})

add1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <style>
        input, ul, label{
            margin-top: 10px;
        }
        ul{
            padding: 0;
            display: flex;
            flex-direction: row;
        }
        ul li{

            list-style: none;
            padding: 5px;
        }
        .errorlist li{
            color: red;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 off-set-2">
            <form action='' method="post" novalidate>
                {% csrf_token %}
                {{ form_obj }}
                <input type="submit" class="btn btn-danger" />
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script></script>
</html>

效果:


添加和编辑页面没有秒,但是保存到数据库,秒这个单位就有默认值了。

参考:

基于ModelForm的文件上传

有个学生问 modelform中怎么限制file字段上传文件的后缀?比如说一定要上传zip或者rar的压缩文件。

这真是个好问题,所以,我把简单的示例写写。

首先是media的相关配置,settings.py:

# ----------------- media ------------
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = "/media/"

然后是models.py,重点也在这里,在validators对应的列表中,进行文件类型的限制。

from django.db import models
from django.core import validators

class Asset(models.Model):
    user = models.CharField(max_length=32, verbose_name='用户名')
    avatar = models.ImageField(
        upload_to='avatars/',
        verbose_name='用户头像',
        default='avatars/xxx.png',
        validators=[validators.FileExtensionValidator(['jpg', 'png', 'jpeg'])]
    )
    file = models.FileField(
        upload_to='files/',
        verbose_name='用户文件',
        default='files/xxx.txt',
        validators=[validators.FileExtensionValidator(['pdf', 'zip', 'rar'])]
    )

    def __str__(self):
        return self.user

然后就是modelform类和视图函数了,views.py:

from django.shortcuts import render, HttpResponse, redirect
from django.core.exceptions import ValidationError
from django import forms
from app01 import models

class FileModelForm(forms.ModelForm):
    class Meta:
        model = models.Asset
        fields = ['user', 'avatar', 'file']
        error_messages = {
            "file": {"invalid_extension": "必须上传文件, 且上传文件的类型必须是 pdf zip rar"},
            "avatar": {"invalid_extension": "必须上传图片,且上传文件的类型必须是 jpg png jpeg"},
        }
        labels = {
            "file": "文件上传",
            "avatar": "图片上传"
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

def add(request):
    """ 添加用户信息 """
    if request.method == "GET":
        form_obj = FileModelForm()
        return render(request, 'add_user.html', {"form_obj": form_obj})
    else:
        # 必须把request.POST, request.FILES传进去,否则文件上传失败
        form_obj = FileModelForm(request.POST, request.FILES)
        if form_obj.is_valid():
            # print(11, request.POST)
            # print(11, request.FILES)
            # user = request.POST.get('user')
            # avatar = request.FILES.get('avatar')
            # print(222, avatar)
            # file = request.FILES.get('file')
            form_obj.save()
            return HttpResponse('ok')
        else:
            return render(request, 'add_user.html', {"form_obj": form_obj})

路由就不用说了,urls.py:

from django.contrib import admin
from django.urls import path, re_path, include
from django.views.static import serve
from django.conf import settings

from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path('media/(?P<path>.*)$', serve, {"document_root": settings.MEDIA_ROOT}),
    path('add/', views.add),
]

html页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <style>
        ul li{
            color: red;
            list-style: none;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h3>文件上传</h3>
<form action="" method="post" enctype="multipart/form-data" novalidate>
    {{ form_obj }}
    <button type="submit" class="btn btn-danger">提交</button>
</form>
        </div>
    </div>
</div>

</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script></script>
</html>

参考:Django框架学习——14—(ModelForm、save方法、文件上传、限制上传的文件拓展名、cookie和session、上下文处理器)class Test1ModelForm(forms.ModelForm)

validators

Django内置的validators模块内置了一些数据校验类和函数,再结合我们自己定义的校验类,可以很方便的在form和modelform中对各种各样的数据进行校验,比如校验URL、邮箱、IP地址等等,还有比较方便的正则校验类。

导入:

from django.core import validators

常见用法:

import re
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.core import validators
from api import models


class RegexValidator(object):
    def __init__(self, rex):
        self.rex = str(rex)

    def __call__(self, value):
        match_obj = re.match(self.rex, value)
        if not match_obj:  # 校验失败
            raise validators.ValidationError("自定义校验类:email不合法[{}]".format(value))
        return value

class UserForm(forms.Form):
    email1 = forms.EmailField(
        label='邮箱1',
        error_messages={
            "required": "该字段不能为空",
            "invalid": "邮箱格式错误1"  # 这个错误提示是结合EmailField类来实现的,EmailField内部实现了邮箱格式的校验
        })

    email2 = forms.CharField(
        label='邮箱2',
        # 这种是普通的input框,但校验是通过自定义校验类来实现的,一看是个列表,表示可以有多个校验类
        validators=[
            RegexValidator(r"^\w+@\w+\.\w+$")
        ],
        error_messages={
            "required": "该字段不能为空"
        })
    email3 = forms.CharField(
        label='邮箱3',
        # 这种方式是通过validators.EmailValidator来实现的,我们只需要传递错误提示就行,内部有关于邮箱的校验
        validators=[
            # validators.EmailValidator(message='邮箱格式错误')
            # 也可以用validators.RegexValidator这个正则的校验类来实现邮箱格式的校验
            validators.RegexValidator(r"^\w+@\w+\.\w+$", message="邮箱格式错误3")
        ],
        error_messages={
            "required": "该字段不能为空"
        })
    mobile = forms.CharField(
        label='手机号',
        error_messages={
            "required": "该字段不能为空",
        },
        # 而validators的列表中则可以写多个自定义校验规则,如果有多个自定义校验类,则必须所有的都通过才算通过
        # validators.RegexValidator正则校验类相对用途比较广泛,只要能用正则校验的地方,都可以用这个校验类
        validators=[
            validators.RegexValidator(r"^((0\d{2,3}-\d{7,8})|(1[3584]\d{9}))$", message="手机号格式错误")
        ]
    )
    
def register(request):
    if request.method == "POST":
        forms_obj = UserForm(request.POST)
        if forms_obj.is_valid():  # 必须先进行is_valid,才能取
            # 这里只介绍几种校验规则的用法,就不涉及写入数据库了
            # models.User.objects.create(**forms_obj.cleaned_data)
            return HttpResponse("OK")
        else:
            # 错误信息包含在forms_obj对象中,我们在前端通过该模板对象进行提取
            return render(request, 'register.html', {"forms_obj": forms_obj})
    forms_obj = UserForm()
    return render(request, 'register.html', {"forms_obj": forms_obj})

对应的前端页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>form组件</title>
    <style>
        .errorMsg {
            color: red;
            margin-left: 10px;
        }
    </style>
</head>
<body>

<form action="" method="post" novalidate>
    {% csrf_token %}
    {% for field in forms_obj %}
        <div class="form-group">
            <label for="">{{ field.label }}</label>
            <!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
            {{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
        </div>
    {% endfor %}
    <p><input type="submit" class="btn btn-success pull-left"></p>
</form>
</body>
</html>

当然了validators也可以对文件类型和图片类型进行校验,比如实现上传时,对上传的文件类型进行限制,这个示例在本篇博客的上面示例中有用到。

from django.db import models
from django.core import validators

class Asset(models.Model):
    user = models.CharField(max_length=32, verbose_name='用户名')
    avatar = models.ImageField(
        upload_to='avatars/',
        verbose_name='用户头像',
        default='avatars/xxx.png',
        validators=[validators.FileExtensionValidator(['jpg', 'png', 'jpeg'])]
    )
    file = models.FileField(
        upload_to='files/',
        verbose_name='用户文件',
        default='files/xxx.txt',
        validators=[validators.FileExtensionValidator(['pdf', 'zip', 'rar'])]
    )

    def __str__(self):
        return self.user
posted @ 2019-12-14 23:34  听雨危楼  阅读(1134)  评论(0编辑  收藏  举报