Flask之Ajax提交WTF表单实例

本文介绍如何使用Flask-WTF简化表单处理和验证,通过实例展示表单模型的创建、表单验证及Ajax提交流程。

以前写前端表单的时候,通常都是直接把表单里的一个个input写在html文件中,这样写form提交方式是可行的,但是对于表单的默认提交是不具有验证效果,除非把值传到后台再进行处理,这样一来其实还是很麻烦;如果采用ajax提交表单,则都需要对每个需要验证的进行正则匹配,然后判断再发起,也一样麻烦。还有一个缺点就是如果在网站中需要多个同样的表单,那么就需要在前端模板中写多个一模一样的form表单。要是有个form模型在想用的时候实例化一下就可以用该多好?

在flask中的WTF刚好可以解决这一问题,一般是建立一个存放form模型的文件form.py,然后在视图函数里进行实例化,再传到前端模板渲染,这样即便在多个地方需要同一个form表单,直接调用实例即可。用这种form模型不仅大大提高效率,还具有较好的表单验证。在前面的Flask实现登录、注册(邮箱验证)中也有写到关于WTF的表单提交,前面提到的用户注册、登录表单都是基于WTF的form模型创建的,这两个form都是submit默认提交到当前页,本文则是用ajax提交表单。

首先先写个我们需要的表单form模型,即在form.py文件中添加一下代码

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, ValidationError, RadioField, DateField, SelectField
from wtforms.validators import Required, Length, Email, Regexp, EqualTo
from app import label
class EditUserInfoForm(FlaskForm):
    """
    # Radio Box类型,单选框,choices里的内容会在ul标签里,里面每个项是(值,显示名)对
    gender = RadioField('Gender', choices=[('m', 'Male'), ('f', 'Female')],
                                  validators=[DataRequired()])
    """
    name = StringField(label('_user_name'),validators=[Required(),Regexp(r"^[\u4E00-\u9FA5]{2,4}$",
               message=label('_input_erro',['真实姓名']))])
    sex = RadioField(label('_sex'), coerce=int, choices=[(0, label('_male')), (1, label('_female'))])
    nickname = StringField(label('_nickname'),validators=[Required(),Regexp(r'^([A-Za-z]+\s?)*[A-Za-z]$',
        message=label('_input_erro',['英文名']))])
    email = StringField(label('_email'), validators=[Email(message=label('_input_erro',['邮箱']))])
    tel = StringField(label('_tel'),validators=[Required()])
    birthday = DateField(label('_birthday'),validators=[Required(message=label('_input_erro',['出生日期']))])
    submit = SubmitField(label('_update'))

其中label用于文字的提示函数,我们放在app目录下的__init__.py中初始化,代码如下:

from werkzeug.utils import import_string
def label(name, values=[], path=None):
    '''
        默认从app目录下的guide导入所需变量
            name 导入的数据变量名
            values 数据类型必须市列表,提示的动态内容
            path 为导入的路径
    '''
    path = ".".join(path.split(".")[:-1]) + ".guide:" if path else "app.guide:"
    msg = import_string(path + name)
    return msg.format(values) if len(values) else msg

并在同级目录下创建存放我们想要引用的文字或者字符(guide.py),其中命名自定义,代码如下:

_update_succ     = '''更新成功'''
_upload_succ     = '''上传成功'''
_sex             = '''性别'''
_male            = '''男'''
_female          = '''女'''
_user_name       = '''真实姓名'''
_nickname        = '''英文名'''
_email           = '''邮箱'''
_tel             = '''电话'''
_birthday        = '''出生日期'''
_update          = '''更新'''
_change_succ     = '''success'''
_change_erro     = '''error'''
_input_erro      ='''{0[0]}格式有误'''

当我们想调用(没)有占位符的时候,直接导入label,然后在指定的位置使用label函数,例如:

#无占位符
sex = RadioField(label('_sex'), coerce=int, choices=[(0, label('_male')), (1, label('_female'))])
#有占位符
name = StringField(label('_user_name'),validators=[Required(),Regexp(r"^[\u4E00-\u9FA5]{2,4}$",
               message=label('_input_erro',['真实姓名']))])

form模型建好之后,就是视图函数,实例化form表单,然后传到前端模板渲染,代码如下:

@auth.route('/information')
@login_required
def show_info():
    #实例化表单的值就是给表单填写默认值
    form = EditUserInfoForm(name=current_user.name,nickname=current_user.nickname,\
        email=current_user.email,tel=current_user.tel,birthday=current_user.birthday,sex=current_user.sex)

    return render_template('auth/users/information.html' form=form)

前端模板如下:

<form action="#" id="changeform" method="POST">
        {{ form.hidden_tag() }}
        <div class="form-body overflow-hide">
            <div class="form-group">
                <label class="control-label mb-10" for="exampleInputuname_1">{{ form.name.label }}</label>
                <div class="input-group">
                    {{ form.name(class="form-control", id='name', required="required") }}
                </div>
            </div>
            <div class="form-group">
                <label class="control-label mb-10">{{ form.sex.label }}</label>
                    <div class="radio">
                        {{ form.sex(required="required",class='sex-radio') }}
                    </div>
            </div>

            <div class="form-group">
                <label class="control-label mb-10" for="exampleInputEmail_1">{{ form.nickname.label }}</label>
    
                <div class="input-group">
                    {{ form.nickname(class="form-control", id='nickname', required="required") }}
                </div>
            </div>
            <div class="form-group">
                <label class="control-label mb-10" for="exampleInputuname_1">{{ form.email.label }}</label>
                <div class="input-group">
                {{ form.email(class="form-control", id='email', required="required") }}
                </div>
            </div>
            <div class="form-group">
                <label class="control-label mb-10" for="exampleInputuname_1">{{ form.tel.label }}</label>
                <div class="input-group">
                {{ form.tel(class="form-control",id='tel', required="required") }}
                </div>
            </div>
            <div class="form-group">
                <label class="control-label mb-10" for="exampleInputuname_1">{{ form.birthday.label }}</label>
                <div class="input-group">
                {{ form.birthday(class="form-control", id='birthday') }}
                </div>
            </div>
        </div>
        <div class="modal-footer">
        {{ form.submit(id="submit_btn", class="btn btn-success waves-effect", required="required") }}
        <button type="button" class="btn btn-default waves-effect" data-dismiss="modal">取消</button>
    </div>
</form>

jq的Ajax代码如下:

$(document).ready(function(){
  $('#submit_btn').on('click',function(e){
      //该命令时取消表单的默认提交
     //防止默认提交后跳转
      e.preventDefault()
      //用POST方法提交,把整个form提交到后台
      var post_data=$('#changeform').serialize();
      $.ajax({
              url:'updateinfo',
              method:'POST',
              dataType:'json',//后台返回json格式的返回值
              data:post_data,
              success:function(response){
               //如果想把后台返回回来的json对象转字符
              //用JSON.stringify(response)转字符
                for (var key in response){
                  // alert(response[key]);
                  console.log(response[key])
                  }
              })
          });
  });
});

后台请求接口代码如下:

@auth.route('/updateinfo', methods=['POST'])
@login_required
def update_info():
    '''
        ajax保存用户的修改信息
    '''
    name=request.form.get('name')
    nickname=request.form.get('nickname')
    sex=request.form.get('sex')
    email=request.form.get('email')
    tel=request.form.get('tel')
    birthday=request.form.get('birthday')
    form = EditUserInfoForm(name=name,nickname=nickname,email=email,tel=tel,birthday=birthday,sex=sex)
    if form.validate_on_submit():
        current_user.name=name
        current_user.nickname=nickname
        current_user.sex=int(sex)
        current_user.email=email
        current_user.tel=tel
        current_user.birthday=birthday
        db.session.add(current_user)
        result = {'success':label('_update_succ')}
    else:
        result = form.errors

    return json.dumps(result)

一开始form.validate_on_submit()不停的返回false,后来经过调试,才发现是因为在建立form模型的,对于性别的RadioField缺少一个属性,导致到后台始终不被识别,后来找到资料,在RadioField里面添加一个coerce属性才能运行成功。

sex = RadioField(label('_sex'), coerce=int, choices=[(0, label('_male')), (1, label('_female'))])

写代码还是要多总结多整理,不然下次再遇到这种很难找的错,估计又要折腾老半天才能解决。

from flask import Flask, render_template, request, send_from_directory, send_file, redirect, url_for, jsonify from flask_wtf.csrf import CSRFProtect from werkzeug.utils import secure_filename from werkzeug.exceptions import RequestEntityTooLarge import os import datetime import zipfile from io import BytesIO app = Flask(__name__) app.config['SECRET_KEY'] = 'your_secure_key_2023' app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(__file__), 'uploads') app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 调整为500MB(避免超时) app.config['APPLICATION_ROOT'] = '/tool/temp_files' csrf = CSRFProtect(app) # 自动创建上传目录 os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) @app.route('/') def index(): # return redirect(url_for('file_manager')) return render_template('upload.html') @app.errorhandler(RequestEntityTooLarge) def handle_large_file(error): return render_template('error.html', message='文件总大小不能超过500MB'), 413 @app.route('/upload', methods=['GET', 'POST']) def file_manager(): try: # 获取文件列表(只显示文件,排除目录) file_list = [] for f in os.listdir(app.config['UPLOAD_FOLDER']): if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f)): file_list.append({'filename': f}) if request.method == 'POST': # 处理文件上传 files = request.files.getlist('files') if not files or all(file.filename == '' for file in files): return '未选择有效文件', 400 # 增加空文件检查 for file in files: if file and file.filename: filename = secure_filename(file.filename) save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) # 处理重名文件 if os.path.exists(save_path): timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') name, ext = os.path.splitext(filename) filename = f"{name}_{timestamp}{ext}" save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(save_path) # 保存文件 return redirect(url_for('file_manager')) # 传给模板的是字符串令牌(不带括号调用) return render_template('upload.html', files=file_list, csrf_token=csrf.generate_csrf()) except RequestEntityTooLarge: return '文件总大小不能超过500MB', 413 except Exception as e: return f'服务器错误: {str(e)}', 500 # 显示具体错误 @app.route('/download/<filename>') def download_file(filename): return send_from_directory( app.config['UPLOAD_FOLDER'], secure_filename(filename), as_attachment=True ) @app.route('/download_all', methods=['POST']) def download_all(): memory_file = BytesIO() with zipfile.ZipFile(memory_file, 'w') as zf: for filename in os.listdir(app.config['UPLOAD_FOLDER']): file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) if os.path.isfile(file_path): # 只打包文件 zf.write(file_path, arcname=filename) memory_file.seek(0) return send_file( memory_file, mimetype='application/zip', as_attachment=True, download_name='all_files.zip' ) @app.route('/get_all_files') def get_all_files(): try: files = [f for f in os.listdir(app.config['UPLOAD_FOLDER']) if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f))] return jsonify(files) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route('/delete', methods=['POST']) def delete_file(): filename = secure_filename(request.form.get('filename')) file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) if os.path.exists(file_path) and os.path.isfile(file_path): try: os.remove(file_path) return '', 204 except Exception as e: return str(e), 500 return '文件不存在', 404 @app.route('/delete_all', methods=['POST']) def delete_all_files(): try: upload_dir = app.config['UPLOAD_FOLDER'] for filename in os.listdir(upload_dir): file_path = os.path.join(upload_dir, filename) if os.path.isfile(file_path): os.remove(file_path) return '', 204 except Exception as e: return f'删除失败: {str(e)}', 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) 帮我改正
最新发布
07-25
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值