django基础之form组件

一:Django表单功能

Django中的表单丰富了传统的HTML语言中的表单。在Django中的表单,主要有以下两大功能:

  1. 渲染表单模板
  2. 表单验证数据是否合法
    Django基础之form组件

1.1 渲染表单

在app目录下新建一个forms.py文件用来校验表单中的数据

from django import forms
class MessageBoardForm(forms.Form):
    title = forms.CharField(max_length=100, min_length=2, label = '标题:', error_messages={
        "min_length": "不能少于2个字符"
    })
    content = forms.CharField(widget=forms.Textarea, label = '内容:', error_messages={
        "required": '请填写内容(必须)'
    })
    email = forms.EmailField(label = '邮箱:')
    reply = forms.BooleanField(required = False, label = '是否回复:')

HTML页面:

<form action="" method="post">
    <table>
        {{ form.as_table }} # 渲染表单
        <tr>
            <td></td>
            <td><input type="submit" value="submit"></td>
        </tr>
    </table>
</form>

渲染的表单HTML:

<form action="" method="post">
    <table>
        <tr>
            <th>
                <label for="id_title">标题::</label>
            </th>
            <td><input type="text" name="title" maxlength="100" minlength="2" required id="id_title" /></td>
        </tr>
        <tr>
            <th>
                <label for="id_content">内容::</label>
            </th>
            <td><textarea name="content" cols="40" rows="10" required id="id_content"></textarea></td>
        </tr>
        <tr>
            <th>
                <label for="id_email">邮箱::</label>
            </th>
            <td><input type="email" name="email" required id="id_email" /></td>
        </tr>
        <tr>
            <th>
                <label for="id_reply">是否回复::</label>
            </th>
            <td><input type="checkbox" name="reply" id="id_reply" /></td>
        </tr>
        <tr>
            <td></td>
            <td><input type="submit" value="submit"></td>
        </tr>
    </table>
</form>

视图函数:

class IndexView(View):
    def get(self,request):
        form = MessageBoardForm()
        return render(request, 'index.html', context = {'form': form})
    def post(self, request):
        form = MessageBoardForm(request.POST)
        if form.is_valid():
            title = form.cleaned_data.get('title')
            content = form.cleaned_data.get('content')
            email = form.cleaned_data.get('email')
            reply = form.cleaned_data.get('reply')
            print(title, content, email, reply)
            return HttpResponse('success')
        else:
            print(form.errors.get_json_data())
            return HttpResponse('failed')

如果数据验证不成功,报错格式如下:

{'title': [{'message': '不能少于2个字符', 'code': 'min_length'}], 'content': [{'message': '请填写内容(必须)', 'code': 'required'}]}

二:Django表单常用的Field

CharField
单行文本输入字段, 对应模型的CharField字段
表单中的样式就是input type=text
max_length: 最大长度
min_length: 最小长度
strip: 是否过滤左右的空格
empty_value: 为空时的值, 默认是空字符串

EmailField
邮箱输入文本字段, 对应模型的EmailField字段
标案中是input type=text
但是会自动增加一个邮箱格式的校验

ChoiceField
下拉单选字段, 这个在模型中是没有的
对应表单的select标签
choices参数也是二维元组的格式

BooleanField
选择字段, 对应表单中的checkbox

DateField
日期选择字段
input_formats: 定义时间格式, 默认是:

['%Y-%m-%d',      # '2006-10-25'
 '%m/%d/%Y',      # '10/25/2006'
 '%m/%d/%y']      # '10/25/06'

DateTimeField
日期时间字段, 同DateField

TimeField
时间字段, 同DateField

DecimalField
十进制数字字段
max_value: 最大值
min_value: 最小值
max_digits: 前置0被去除后的最大位数
decimal_places: 允许的小数位的长度

FileField
文件上传字段

IntegerField
整数字段
max_value: 最大值
min_value: 最小值

FloatField
用来接收浮点类型,并且如果验证通过后,会将这个字段的值转换为浮点类型
max_value: 最大值
min_value: 最小值
错误信息的Key:required, invalid, max_value, min_value

三:常用的校验器

在验证某个字段的时候,可以传递一个 validators 参数用来指定验证器,进一步对数据进行过滤。验证器有很多,但是很多验证器我们其实已经通过这个 Field 或者一些参数就可以指定了。比如 EmailValidator ,我们可以通过 EmailField 来指定,比如 MaxValueValidator ,我们可以通过 max_value 参数来指定。

以下是一些常用的验证器:

  1. MaxValueValidator :验证最大值。
  2. MinValueValidator :验证最小值。
  3. MinLengthValidator :验证最小长度。
  4. MaxLengthValidator :验证最大长度。
  5. EmailValidator :验证是否是邮箱格式。
  6. URLValidator :验证是否是 URL 格式。
  7. RegexValidator :正则表达式的验证器
from django import forms
from django.core import validators
class RegisterForm(forms.Form):
    email = forms.CharField(validators = [validators.EmailValidator(message = 'invalid email')])
    mobile = forms.CharField(validators = [validators.RegexValidator(r'1[3456789]\d{9}', message = 'invalid mobile')])

四:表单校验

forms.py文件内容:

from django import forms
class RegisterForm(forms.Form):
    email = forms.EmailField(required=True)

视图函数:

class IndexView(View):
    def get(self,request):
        return render(request, 'index.html')

    def post(self, request):
        form = RegisterForm(request.POST)
        if form.is_valid():
            return HttpResponse('success')
        else:
            print(form.errors.get_json_data())
            return HttpResponse('failed')

HTML文件内容:

<form action="" method="post" novalidate>
    <input type="email" name="email">
    <input type="submit" value="submit">
</form>

4.1 校验提示语言问题

现在在HTML页面随意输入一些字符,让邮箱校验不通过,表单提示信息如下:

{'email': [{'message': 'Enter a valid email address.', 'code': 'invalid'}]}

默认的提示是英文提示,将提示语修改为中文:

class RegisterForm(forms.Form):
    email = forms.EmailField(required=True, error_messages={"invalid": "请输入正确的邮箱!"})

再次测试则变成中文提示

{'email': [{'message': '请输入正确的邮箱!', 'code': 'invalid'}]}

4.2 浮点型数据的校验

class RegisterForm(forms.Form):
    price = forms.FloatField(error_messages = {"invalid": '请输入浮点型数据'})

在HTML页面输入:123,然后在视图函数中获取price字段数据且输出数据类型

123.0 <class 'float'>  # 变成浮点型数据

这里注意获取数据的方式

price = form.cleaned_data.get('price')			# 浮点型
price = request.POST.get('price')				# 字符串

4.3 自定义验证器

有时候对一个字段验证,不是一个长度,一个正则表达式能够写清楚的,还需要一些其他复杂的逻辑,那么我们可以对某个字段,进行自定义的验证。比如在注册的表单验证中,我们想要验证手机号码是否已经被注册过了,那么这时候就需要在数据库中进行判断才知道。对某个字段进行自定义的验证方式是,定义一个方法,这个方法的名字定义规则是: clean_fieldname 。如果验证失败,那么就抛出一个验证错误。

class RegisterForm(forms.Form):
    username = forms.CharField(min_length=6, max_length=20)
    telephone = forms.CharField(validators = [validators.RegexValidator(r'1[356789]\d{9}', message = 'invalid telephone')])
    password = forms.CharField(min_length=6, max_length=20)
    cpassword = forms.CharField(min_length=6, max_length=20)

    def clean_telephone(self):  # 方法名:clean_要校验的字段,前面是固定的
        telephone = self.cleaned_data.get('telephone')
        exists = User.objects.filter(telephone = telephone).exists()
        if exists:
            raise forms.ValidationError(message = 'The phone number has been registered')   # 校验失败的异常
        return telephone

    def clean(self):    # 校验多个字段,重写父类clean方法
        # 如果来到了clean方法,说明每个字段都校验成功
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        cpassword = cleaned_data.get('cpassword')
        if password != cpassword:
            raise forms.ValidationError(message = '两次密码输入不一致')
        return cleaned_data

4.4 提取错误信息

如果验证失败了,那么有一些错误信息是我们需要传给前端的。这时候我们可以通过以下属性来获取:

  1. form.errors :这个属性获取的错误信息是一个包含了 html 标签的错误信息。
  2. form.errors.get_json_data() :这个方法获取到的是一个字典类型的错误信息。将某个字段的名字作为 key ,错误信息作为值的一个字典。
  3. form.as_json() :这个方法是将 form.get_json_data() 返回的字典 dump 成 json 格式的字符串,方便进行传输。
  4. 上述方法获取的字段的错误值,都是一个比较复杂的数据。比如以下:
   {'username': [{'message': 'Enter a valid URL.', 'code': 'invalid'}, {'message': 'Ensurethis value has at most 4 characters (it has 22).', 'code': 'max_length'}]}

那么如果我只想把错误信息放在一个列表中,而不要再放在一个字典中。这时候我们可以定义一个方法,把这个数据重新整理一份

class RegisterForm(forms.Form):
    username = forms.CharField(min_length=6, max_length=20)
    telephone = forms.CharField(validators = [validators.RegexValidator(r'1[356789]\d{9}', message = 'invalid telephone')])
    password = forms.CharField(min_length=6, max_length=20)
    cpassword = forms.CharField(min_length=6, max_length=20)

    def get_errors(self):
        errors = self.errors.get_json_data()
        new_errors = {}
        for key,message_dicts in errors.items():
            message_list = []
            for message_dict in message_dicts:
                message = message_dict['message']
                message_list.append(message)
            new_errors[key] = message_list
        return new_errors

视图函数调用get_errors()方法,校验失败信息如下:

{'username': ['Ensure this value has at least 6 characters (it has 1).'], 'telephone': ['invalid telephone'], 'password': ['Ensure this value has at least 6 characters (it has 1).'], 'cpassword': ['Ensure this value has at least 6 characters (it has 1).']}

生产中可以将 get_errors()方法定义在一个类中,其他的校验类都继承自该类

五:is_valid源码分析

先来归纳一下整个流程
(1)首先从is_valid()入手,看self.errors中是否值,只要有值就是flase
(2)接着分析errors.里面判断_errors是都为空,如果为空返回self.full_clean(),否则返回self._errors
(3)现在就要看full_clean(),是何方神圣了,里面设置_errors和cleaned_data这两个字典,一个存错误字段,一个存储正确字段。
(4)在full_clean最后有一句self._clean_fields(),表示校验字段
(5)在_clean_fields函数中开始循环校验每个字段,真正校验字段的是field.clean(value),怎么校验的不管
(6)在_clean_fields中可以看到,会将字段分别添加到_errors和cleaned_data这两个字典中
(7)结尾部分还设置了钩子,找clean_XX形式的,有就执行。执行错误信息也会添加到_errors中
(8)整个校验过程完成

下面分析form组件中is_valid校验的流程,在分析过程中重点关注_erroes和clean_data这两个字典

def login(request):
    if request.method == "POST":
        form_obj = LoginForm(request.POST)
        if form_obj.is_valid():
            #如果检验全部通过
            print(form_obj.clean_data) #这里全部都没问题
            return HttpResponse("你好,欢迎回来!")
        else:
            #print(form_obj.clean_data)
            #print(form_obj.errors)
            return render(request, "login.html", {"form_obj": form_obj,)

    form_obj = LoginForm()
    return render(request, "login.html", {"form_obj": form_obj})

钩子代码实例:

def clean_user(self):
    val1 = self.cleaned_data.get("user")
    #从正确的字段字典中取值
    #如果这个字符串全部都是由数组组成
    if not val1.isdigit():
        return val1
    else:
        # 注意这个报错信息已经确定了
        raise ValidationError("用户名不能全部是数字组成")
        #在校验的循环中except ValidationError as e:,捕捉的就是这个异常
        #所以能将错误信息添加到_errors中

代码分析部分:

def is_valid(self):
    """
    Returns True if the form has no errors. Otherwise, False. If errors are
    being ignored, returns False.
    如果表单没有错误,则返回true。否则为假。如果错误是被忽略,返回false。
    """
    return self.is_bound and not self.errors
    #is_bound默认有值
    #只要self.errors中有一个值,not True = false,返回的就是false


def errors(self):
    """
    Returns an ErrorDict for the data provided for the form
    返回一个ErrorDict在form表单存在的前提下
    """
    if self._errors is None:
        self.full_clean()
    return self._errors


def full_clean(self):
    """
    Cleans all of self.data and populates self._errors and self.cleaned_data.
    清除所有的self.data和本地的self._errors和selif.cleaned_data
    """
    self._errors = ErrorDict()
    if not self.is_bound:  # Stop further processing.停止进一步的处理
        return
    self.cleaned_data = {}

    """
    # If the form is permitted to be empty, and none of the form data has
    # changed from the initial data, short circuit any validation.
    #如果表单允许为空,和原始数据也是空的话,允许不进行任何验证
    """

    if self.empty_permitted and not self.has_changed():
        return

    self._clean_fields()   #字面意思校验字段
    self._clean_form()
    self._post_clean()



def _clean_fields(self):
    #每个form组件实例化的过程中都会创建一个fields。fields实质上是一个字典。
    #储存着类似{"user":"user规则","pwd":"pwd的规则对象"}
    for name, field in self.fields.items():
        #name是你调用的一个个规则字段,field是调用字段的规则
        #items是有顺序的,因为他要校验字段的一致性
        """
        # value_from_datadict() gets the data from the data dictionaries.
        # Each widget type knows how to retrieve its own data, because some
        # widgets split data over several HTML fields.
        
        value_from_datadict()从数据字典中获取数据。
        每个部件类型知道如何找回自己的数据,因为有些部件拆分数据在几个HTML字段。
        """
        #现在假设第一个字段是user
        if field.disabled:
            value = self.get_initial_for_field(field, name)
        else:
            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
        try:
            if isinstance(field, FileField):  #判断是不是文件
                #你是文件的时候怎么校验
                initial = self.get_initial_for_field(field, name)
                value = field.clean(value, initial)
                #filed是一个对象,field.clean才是真正的规则校验
            else:
                #你不是文件的时候怎么校验
                #实际中也是走的这一部,value是你输入的字段值
                #如果没有问题,那么原样返回
                value = field.clean(value)
                #如果一旦出现问题,那么就会走except中的代码
            self.cleaned_data[name] = value

            if hasattr(self, 'clean_%s' % name):  #这里找是否有clean_XX这个名字存在
                value = getattr(self, 'clean_%s' % name)()  #如果有执行这个函数
                self.cleaned_data[name] = value  #而在钩子中必须报错的返回值是确定的
                #如果上面有问题,就又把错误添加到了_error中
                #上面这三行代码是我们能添加钩子的原因,而且规定了钩子名的格式

                #如果这个值是正确的话,就会给这个字典添加一个键值对
                #刚才在full_clean中self.cleaned_data = {}已经初始化了。
                #{”pws“:123}
        except ValidationError as e:
            self.add_error(name, e)
            #如果出现错误,就会给_error这个字典添加一个键值对
            #至于add_error这个函数如何添加这个键值对的,我们先不管
            #键就是name,值就是错误信息e
            #在full_clean中已经初始化self._errors = ErrorDict()
            #假设现在user有问题,那么_error就是这样{”user“:e}

六:ModelForm

在写表单的时候,会发现表单中的 Field 和模型中的 Field 基本上是一模一样的,而且表单中需要验证的数据,也就是我们模型中需要保存的。那么这时候我们就可以将模型中的字段和表单中的字段进行绑定。
Model field 与 Form field字段对应表见:https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/

6.1 ModelForm的使用

from django.db import models
from django.core import validators
class Article(models.Model):
    title = models.CharField(max_length=10,validators=[validators.MinLengthValidator(limit_value=3)])	# 可以给字段加校验器
    content = models.TextField()
    author = models.CharField(max_length=100)
    category = models.CharField(max_length=100)
    create_time = models.DateTimeField(auto_now_add=True)

那么在写表单的时候,就不需要把 Article 模型中所有的字段都一个个重复写一遍了。示例代码如下:

from django import forms
class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        # fields = "__all__"    		# 校验所有字段
        # fields = ['title', 'page']    # 校验指定字段
        exclude = ['category']      	# 排除page字段,其他字段都要校验
        error_messages = {
            'page': {
                'required': '请传入page参数',
                'invalid': '请输入一个可用的page参数'
            },
            'title': {
                'max_length': '长度不能超过100',
                'limit_value': '最少3个字符'
            }
        }
    def clean_page(self):   # 自定义验证器该怎么写还怎么写
        pass

6.2 ModelForm save方法

ModelForm 还有 save 方法,可以在验证完成后直接调用 save 方法,就可以将这个数据保存到数据库中了。示例代码如下:

form = MyForm(request.POST)
if form.is_valid():
  form.save()
  return HttpResponse('succes')
else:
  print(form.get_errors())
  return HttpResponse('fail')

这个方法必须要在 clean 没有问题后才能使用,如果在 clean 之前使用,会抛出异常。另外,我们在调用 save 方法的时候,如果传入一个 commit=False ,那么只会生成这个模型的对象,而不会把这个对象真正的插入到数据库中。比如表单上验证的字段没有包含模型中所有的字段,这时候就可以先创建对象,再根据填充其他字段,把所有字段的值都补充完成后,再保存到数据库中。

form = MyForm(request.POST)
if form.is_valid():
  article = form.save(commit=False)
  article.category = 'Python'
  article.save()
  return HttpResponse('succes')
else:
  print(form.get_errors())
  return HttpResponse('fail')
发布了45 篇原创文章 · 获赞 3 · 访问量 1546

猜你喜欢

转载自blog.csdn.net/pcn01/article/details/100125794