Django表单和HTML表单(将input标签放在form标签中)相比还是不一样的,一般来说有两种用处:一种是渲染表单模板,例如没有前端工程师,自己又不想写前端样式,可以考虑,但是由于django中是前后端代码糅合,所以管理起来不是很方便;另外一种是通过表单验证数据是否合法(通常使用)。


二十三、Django表单的渲染与验证

先简单介绍一下第一种情况,首先定义表单,在app中新建forms.py(命名尽量按照django的规则吧),定义表单,所有的表单都继承自django.forms,类似模型,这其中定义的CharField和模型也很相似,需要注意的是这里面的max_length和min_length是用于验证前端表单提交的数据是否是我们所设置的规则,label定义前端显示的标题,error_messages定义了当数据不合法返回的错误信息,默认是必须存在内容,除非required=False。另外,表单中定义的属性例如title 将会是前端页面中input的name属性值。

# forms.py
from django import forms 
class MesssageBoardForm(forms.Form):
    title = forms.CharField(max_length=100, min_length=5, label="my title",error_messages={"required":"标题要求有",'min_length':'min_length>3'})
    contents = forms.CharField(widget=forms.Textarea)
    email = forms.EmailField(error_messages={"invalid":" mytest"})
    reply = forms.BooleanField(required=False)

视图层代码,定义了一个类,get请求时返回定义好的form 至前端页面index.html中,post请求则获取用户提交的request.POST数据,调用表单的is_valid方法,获取干净的数据cleaned_data,如果用户的数据非法,那么就利用forms.errors将错误打印出来,forms.errors的类型是django.forms.utils.ErrorDict,可以利用get_json_data()方法将打印出来的错误整理的好看一些。EmailField的类型type为email,那么你利用谷歌浏览器在前端进行测试时,根本无法输入非法的email,所以最好还是将配置文件中的csrf注释掉,然后利用postman进行测试。

关于get_json_data提取出来的错误信息大概是下面这个样子,可以看到我在forms.py中定义的error_messages生效了(error_messages书写规则:取code对应值为键)

# forms.errors.get_json_data
{'email': [{'message': ' mytest', 'code': 'invalid'}]}
# views.py
class IndexView(View):
    def get(self, request):
        form = MesssageBoardForm()
        return render(request, 'index.html', context= {'form_html': form})
    def post(self, request):
        forms = MesssageBoardForm(request.POST)
        if forms.is_valid():
            title = forms.cleaned_data.get('title')
            print(f"title: {title}")
            return HttpResponse("post success!")
        else:
            print(forms.errors.get_json_data())
            return HttpResponse("post fail!")

前端页面只需要将上下文中的form_html提取出来,即可渲染表单

# index.html
<form action="" method="post">
    <table>
        {{form_html.as_table}}
        <tr>
            <td></td>
            <td><input type="submit" value="submit value"></td>
        </tr>
    </table>
</form>

二十四、Django表单中字段验证的原理

表单验证的各种字段,如CharField中的max_length和min_length,底层是通过验证器validators实现的(在模型中,我们也可以使用验证器),当我们想自定义自己的表单验证器时,也是可以的。例如,使用CharField来验证一个邮箱:

email = forms.CharField(validators = (validators.EmailValidator(message='errors'),))
# email = forms.CharField(validators = [validators.EmailValidator(message='errors')])

这两种写法是等价的,我们可以进入Field查看validators这个参数说明,List of additional validators to use,元组或者列表;所有的验证器均在django.core.validators下,如果不添加message,则字段验证失败会使用验证器默认的提示,例如EmailValidator的message = _('Enter a valid email address.'),需要注意的是创建类的实例,是validators.EmailValidator(),后面的括号不要忘了。

一般使用较多的是RegexValidator,看名字就知道是正则表达式验证器,例如用它来验证11位数字:

eleven = forms.CharField(validators = [validators.RegexValidator(regex=r'd{11}')])

那么我们是否可以自定义表单验证方式呢?答案是肯定的。例如有一个需求,我们想在表单验证层面就判断用户注册的手机号是否存在数据库当中,当然我们也可以在视图层进行判断,但是一般来讲,规范的做法中,视图层只去操作模型,凡是验证数据方面都丢到表单层。

# forms.py
from .models import webUser
class MesssageBoardForm(forms.Form):
    username = forms.CharField(max_length=100)
    tel = forms.CharField(validators = [validators.RegexValidator(regex=r'1[56789]d{9}',message='error tel')])
    
    def clean_tel(self):
        tel = self.cleaned_data.get('tel')
        tel_not_unique = webUser.objects.filter(tel= tel).exists()
        if tel_not_unique:
            raise forms.ValidationError(message='tel is not unique!', code='invalid_tel')
        return tel

这只是一个示例,如果实际操作肯定是不需这样的,我们之前在小白python建站的一些准备(四)文章中写过字段中unique参数的作用。

上面的例子中有两点需要注意,一个是clean_方法,在视图层调用is_valid方法之前,会默认调用表单层所有以clean_方法开头的函数(据说源码有相关解释,但是很遗憾,现在的我还无法参透),上例手机号不唯一,则抛出forms中的ValidationError,另外就是在写clean_方法时,验证了啥数据就一定要记得return tel返回啥数据,否则视图层会拿不到这个数据。

如果针对多个字段的验证,我们就可以重写clean方法(当进入到clean方法时,则说明前面的数据都已经验证成功),例如,用户注册时需要让其重复输入密码,以确认是否相同。

class MesssageBoardForm(forms.Form):
  ...
  pwd1 = forms.CharField(max_length=10)
  pwd2 = forms.CharField(max_length=10)
  ...
  
  def clean(self):
      res = super().clean()
      pwd1 = res.get('pwd1')
      pwd2 = res.get('pwd2')
      if pwd1 != pwd2:
          raise forms.ValidationError(message='pwd error!', code='invalid_pwd')
      return res

我们可以看到clean函数底层就是return self.cleaned_data,所以不调用super().clean()而采取return self.cleaned_data应该也是可以的,只是这样书写代码少一点。

关于一些错误信息返回的小技巧,我们可以在表单中自定义一个错误类,让其他表单都继承它

class BaseErrorInfo(forms.Form):
    def getErrorInfo(self):
        errors_info = self.errors.get_json_data()
        new_errors ={}
        for key,errors_dict_values in errors_info.items():
            lst = []
            for errors_dict_value in errors_dict_values:
                lst.append(errors_dict_value['message'])
            new_errors[key] = lst
        return new_errors

二十五、Django表单ModelForm

ModelForm可以用来简化表单验证,还可以在通过验证后直接在视图层使用save保存至数据库(不用一个个去用self.cleaned_data.get来取值),属于进阶性表单,运用场景多是模型中的字段和表单中需要验证的字段差不多时,示例如下:

# models.py
from django.core import validators
class Book(models.Model):
    title = models.CharField(max_length=10)
    page = models.IntegerField(max_length=20)
    price = models.FloatField(validators=[validators.MaxValueValidator(limit_value=100)])

模型表单中定义的Meta规定了验证哪个模型哪些字段,并定义了输出的错误信息(模型中规定的验证器),关于clean_page方法和正常表单中应用是一致的。

class BookForms(forms.ModelForm):
    def clean_page(self):
        pages = self.cleaned_data.get('page')
        if pages > 1000:
            raise forms.ValidationError(message='page > 1000')
        return pages
        
    class Meta:
        model = Book
        fields = '__all__'
        # fields = ['title', 'page']
        # exclude = ('title',)
        error_messages = {
            'price':{
                'max_value':'必须小于100'
            }
        }

直接调用forms.save()保存数据至数据库的前提是,在表单中提交了所有模型中需要的字段,例如上面我指定了fields = '__all__',否则会报错

def post(self, request):
    forms = BookForms(request.POST)
    if forms.is_valid():
        forms.save()

但是假如用户提交的表单字段多于模型字段,该如何处理呢?例如,用户注册,需要填写两次相同的密码

# models.py
class MyUser(models.Model):
    username = models.CharField(max_length=100)
    pwd = models.CharField(max_length=10)
    tel = models.CharField(max_length=11,validators=[validators.RegexValidator(regex=r'1[56789]d{9}')])

表单代码中需要注意的是使用exclude 将模型中需要验证的字段排除掉,然后自己定义需要验证的两个字段,同样的可以覆写clean方法,记得将结果返回去

# forms.py
class UserForms(forms.ModelForm):
    pwd1 = forms.CharField(max_length=10)
    pwd2 = forms.CharField(max_length=10)
    def clean(self):
        res = super().clean()
        pwd1 = res.get('pwd1')
        pwd2 = res.get('pwd2')
        if pwd1 != pwd2:
            raise forms.ValidationError(message='pwd not correct!')
        return res
    class Meta:
        model = MyUser
        exclude = ('pwd',)

视图层,forms.save(commit=False)会生成一个不会提交到数据库模型对象(模型对象取决于表单中Meta属性中定义的model ),通过这个对象,我们再将验证过后的密码保存至模型对象

# views.py
def add_user(req):
    if req.method == 'POST':
        forms = UserForms(req.POST)
        if forms.is_valid():
            user = forms.save(commit=False)
            pwd = forms.cleaned_data.get('pwd1')
            user.password = pwd
            user.save()
            return HttpResponse('success')
        else:
            return HttpResponse('fail')