modelform实战案例

modelform案例,(form组件结合model模型使用,大幅减少form类中字段的定义,直接使用models中的字段就行)

form组件数据校验底层逻辑:for循环遍历所有字段,每个字段都要经过自己的属性值校验和局部钩子函数校验.for循环遍历完成之后,进入全局钩子函数校验.
比如第1个字段经过了自己的属性值校验和局部钩子函数校验之后,不管有没有错都不影响下一个字段的校验.第2个字段再进入自己的属性值校验和局部钩子校验,所有字段都通过前两个阶段之后才能进入全局钩子函数):

数据校验的顺序3步

①经过字段的属性值校验 -------如字段中的max_length, validators属性值校验

②经过字段的局部钩子函数校验 ------clean_字段名()返回字段数据或者抛出(添加)错误信息

③经过全局钩子函数的校验 ------所有字段都经过第1,第2阶段之后才会进入第3阶段进行全局钩子校验

0 .ModelForm源码解读
# ModelForm源码解读
# ModelForm继承自BaseModelForm,BaseModelForm继承自BaseForm
class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass):
    pass

class BaseModelForm(BaseForm):
    pass

# 以下是BaseForm部分源码摘抄
class BaseForm:
	def full_clean(self):
        self._clean_fields() #字段的属性值校验和局部钩子函数校验
        self._clean_form()   #全局钩子函数校验

    def _clean_fields(self):
        for name, field in self.fields.items(): 
            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)
                else:
                    value = field.clean(value)  # 经过字段的属性值校验,如经过validators属性值校验
                self.cleaned_data[name] = value
                if hasattr(self, 'clean_%s' % name): # 根据反射查看当前字段是否定义了局部钩子函数
                    value = getattr(self, 'clean_%s' % name)() #根据反射找到对应字段的局部钩子函数并调用,如果没有错误必须要返回字段的值
                    self.cleaned_data[name] = value 
            except ValidationError as e: #局部钩子函数raise ValidationError()之后被异常处理捕获,调用add_error(),
                self.add_error(name, e) #raise ValidationError()和add_error这两个方法都能添加错误信息

    def _clean_form(self):
        try:
            cleaned_data = self.clean() #调用全局钩子函数
        except ValidationError as e:
            #全局钩子函数的raise ValidationError只能添加全局错误信息,若要给单个字段添加错误信息需要手动调用add_error('字段名','错误信息')
            self.add_error(None, e) 
        else:
            if cleaned_data is not None: #全局钩子函数校验成功后必须返回所有字段的信息cleaned_data
                self.cleaned_data = cleaned_data 
1, model代码
from django.db import models


class Category(models.Model):
    def __str__(self):
        return self.name
    name=models.CharField(verbose_name='文章类别',max_length=10)

class Education(models.Model):
    def __str__(self):
        return self.name
    name=models.CharField(verbose_name='学历',max_length=10)
    school=models.CharField(verbose_name='毕业院校',max_length=10,blank=True,null=True)

class User(models.Model):
    def __str__(self):
        return self.name
    name=models.CharField(verbose_name="用户名",max_length=16,default="匿名",)
    password=models.CharField(verbose_name='密码',max_length=20,)
    email=models.CharField(verbose_name='邮箱',max_length=20,)
    sex=models.SmallIntegerField(verbose_name='性别',choices=[(0,"男"),(1,"女")],default=0)
    age=models.SmallIntegerField(verbose_name='年龄',default=21)
    education=models.ForeignKey(verbose_name='学历',to='Education',default=1,on_delete=models.CASCADE)
    cet6=models.BooleanField(verbose_name='是否通过英语六级',default=False)   
    '''
    	使用ManyToManyField生成第三方表user_hobby来保存多选字段,当前表并不会生成hobby字段,数据保存在user_hobby表中,
    	所以实例化User对象时不能传入hobby参数,实例化后也不能通过user.hobby=hobby给user_hobby表添加数据,
    	只能通过user.hobby.set(hobyy)给user_hobby表添加数据
    '''
    hobby=models.ManyToManyField(verbose_name='爱好',to="Category",related_name='user',default=[2,3])
    birth=models.DateField(verbose_name='生日')
    '''
     upload_to设置图片在media文件中的保存位置(所以路径中不要再带前缀media了)
     media文件夹用来保存用户上传的图片/文件等静态文件,static用来保存服务器本身自带的静态文件如js,css,图片
     配置media需要在配置文件中设置访问路径:MEDIA_URL="/media/",存储路径:MEDIA_ROOT=os.path.join(BASE_DIR,'media')
     在urls.py中添加media的路由:urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
     ###保存图片信息最好用ImageField(不仅包含了CharField的功能,还更强大),不要用CharField,虽然两者保存的都只是字符串(图片的地址),
     但ImageField会保存图片到本地后再把图片地址这个字符串保存到数据库,ImageField字段既能接受图片对象也能接受字符串,取出的数据是图片对象,
     而CharField即便接受到图片对象也不会保存图片,只会保存图片名称到数据库,取出的数据也是字符串,而不是图片对象.
    '''
    icon=models.ImageField(verbose_name='头像',upload_to='')
    profiles=models.TextField(verbose_name='自我介绍',max_length=300)
    
    #使用JSONField保存多选字段,值为列表,数据就保存在当前表里面
    skill=models.JSONField(verbose_name='技能',blank=True,null=True) 

2, form代码
from django import forms
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from app.models import *
import re

class BootstrapForm2(forms.ModelForm):
    # 指定哪些字段生成的表单的class不使用form-control样式
    no_use_bootstrap_fields= []
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        for name,field in self.fields.items():
            if name not in self.no_use_bootstrap_fields:
                field.widget.attrs['class']=field.widget.attrs.get("class","")+" form-control"

class UserForm2(BootstrapForm2):
    no_use_bootstrap_fields = ['cet6','skill','hobby','icon']
    class Meta:
        # 在Meta类中批量添加widgets插件
        model=User
        # exclude=['sex']
        # fields = '__all__'
        fields = ['name','password','repassword','sex','email','age','education','cet6',
                  'hobby','skill','birth','icon','profiles','xxx']
        widgets = {
            "name": forms.TextInput(attrs={'style': 'color:red'}),
            "cet6": forms.CheckboxInput,
            "password": forms.PasswordInput,
            "birth": forms.DateInput(attrs={'type': 'date'}),
        }

    repassword = forms.CharField(label='确认密码', max_length=20, min_length=5, widget=forms.PasswordInput,
                               validators=[RegexValidator(r"^[a-zA-Z_]\w{5,10}$","确认密码必须是以字母或下划线开头的6-11位字符")])
    xxx=forms.CharField(label='只读字段',initial="哈哈哈哈哈",disabled=True)
    skill=forms.MultipleChoiceField(label='技能*',widget=forms.CheckboxSelectMultiple,
                                    # chioces列表中元素的第一个值是标签中实际显示的值,第二个值是value值,两者可以不同
                                    choices=[('python','python'),('java','java'),('rust','rust'),('js','js')],initial=["python",'rust'],)

    def clean_password(self):
        pwd=self.cleaned_data.get('password')
        pattern=re.compile(r"^[a-zA-Z_]\w{5,10}$")
        if not pattern.match(pwd):
            # 局部钩子中的ValidationError()是给当前字段添加错误信息,以下两种方式效果一样
            raise ValidationError("密码必须是以字母或下划线开头的6-11位字符")
            # self.add_error('password',"密码必须是以字母或下划线开头的6-11位字符")
        else:
            return pwd

    def clean_cet6(self):
        education=self.cleaned_data.get('education') # education获取到的是表对象!
        cet6=self.cleaned_data.get('cet6') #复选框单用,没设置value,所以值是'on'或'',cleaned_data将其转成布尔值,
        print(f'打印education={education}')
        print(f'打印cet6={cet6}')
        # 打印education=初中, education实际是对象可以调用id属性,只是类里面重写了__str__而返回了字符串信息
        # 打印cet6=True
        if education.id <3 and cet6:
            raise ValidationError("中小学生无法过英语6级!")
        return cet6

    def clean(self):
        '''
            在全局钩子函数中校验两次密码是否一致,但校验两次密码是否一致也可以在局部钩子函数中获取!
            因为password字段的校验在repassword前面
            所以不能通过clean_password钩子函数来获取repassword字段的数据.应该在clean_repassword钩子中获取,
            因为走到clean_repassword钩子时password的校验已经完成了,可以通过cleaned_data.get("password")获取password了
        '''
        pwd=self.cleaned_data.get('password')
        repwd=self.cleaned_data.get('repassword') # 注意:repassword不删除直接form.save()也不报错
        if pwd != repwd:
            self.add_error('repassword',"两次密码输入不一致")
            # 全局钩子中的ValidationError()添加全局错误信息,无法通过字段获取,要通过formobj.non_field_errors()获取
            # raise ValidationError("密码必须是以字母或下划线开头的6-11位字符")
        else:
            return self.cleaned_data

3, views代码
from django.shortcuts import render,HttpResponse
from app.myforms import *


def register_modify(request):
    uid=request.GET.get('uid')
    instance = User.objects.filter(id=uid).first() if uid else None
    # 获取注册页面,或者获取个人信息
    if request.method=='GET':
        form_obj = UserForm2(instance=instance)
        print(f"打印form_obj.fields={form_obj.fields}")
        print(f"打印form_obj['password']={form_obj['password']}")
        # 如果是修改个人信息,不会显示密码,form_obj['password']获取不到密码,页面中显示空
        return render(request,'注册页面.html',locals())

    # 提交注册信息,或者修改个人信息
    elif request.method=="POST":
        form_obj = UserForm2(data=request.POST,files=request.FILES,instance=instance)
        if form_obj.is_valid():
            print("打印cleaned_data=",form_obj.cleaned_data)
            '''
            这是提交过来的修改个人信息的数据,icon没有修改所以类型不是UploadedFile类型
            打印cleaned_data= {'name': '成吉思汗', 'password': 'qq12345', 'sex': 0, 
            'email': '1234214@qq.com', 'age': 25, 'education': <Education: 硕士>,
             'cet6': True, 'hobby': <QuerySet [<Category: 军事>, <Category: 政治>]>, 
             'skill': ['python', 'rust'], 'birth': datetime.date(2023, 8, 9), 
             'icon': <ImageFieldFile: 12.png>, 'profiles': '收到公司当','xxx': '哈哈哈哈哈'}
            '''
            # if userform.has_changed():
            #     print("打印修改的字段form_obj.changed_data=",form_obj.changed_data)
            form_obj.save() #多传了字段过来直接save()也不报错,如repassword
            return HttpResponse('数据插入成功!')
        else:
            # 注册页面和修改个人信息用的是同一个html页面,同一个form类
            return render(request,'注册页面.html',{'form_obj':form_obj})
    return HttpResponse("请求不允许!")
4, html页面
<!DOCTYPE html>
<html lang="en">
<head>
    {% load static %}
    <script src="{% static 'jquery-3.6.0.min.js' %}"></script>
    <script src="{% static 'bootstrap.min.js' %}"></script>
    <link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
    <meta charset="UTF-8">
    <style>
        ul{overflow: hidden}
        ul li{float:left;margin:0 10px;list-style: none;}
        .form-group,button{margin:20px 0;}

    </style>
    <title>注册页面</title>

</head>
<body>
<!--
    {#<form action="" method="post" class="container">#}
       {# form对象.as_xxx 可以一次性生成所有表单标签和错误信息,字段错误信息会显示在字段的上方 #}
    {#    {{ form_obj.as_p }}#}
    {#    {{ form_obj.as_table }}#}
    {#    {{ form_obj.as_ul }}#}
    {#    <button type="submit">提交哈哈哈</button>#}
    {#</form>#}
-->

<div class="container">
    <form action="" method="post" class="row" enctype="multipart/form-data">
        {% for field in form_obj %}
            <div class="col-sm-8">
                <div class="form-group">
                    <label for="{{ field.id_for_label }}"
                           class="col-sm-3">{{ field.label }}:</label>
                    <div class="col-sm-9">
                        {{ field }}
                        {# 只显示单个字段的某一条错误信息:field.errors.索引值 #}
                        {# <span class="text-danger">{{ field.errors.0 }}</span>#}

                        {# 显示单个字段的所有错误信息:field.errors.as_text 全部在一行显示 #}
                        <span class="text-danger">{{ field.errors.as_text }}</span>
                    </div>
                </div>
            </div>
        {% endfor %}

        {# 显示全局的错误信息form_obj.non_field_errors,因为错误信息涉及到多个字段,所以不能通过单个字段对象来获取 #}
        {% for error in form_obj.non_field_errors %}
            <p class="text-danger col-md-8">{{ error }}</p>
        {% endfor %}
        <p class=" col-sm-8 text-center"><button type="submit" class="btn-primary">提交</button></p>
    </form>
</div>
</body>
</html>
5, 表单校验没通过时显示的页面信息

在这里插入图片描述

6, 查看个人信息/修改个人信息页面

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值