用Django Form做登录出错信息处理的Sample

  |   Source

这个学期用基于Python的Django 1.8.2 后端开发框架做了不少活儿,感觉这真是一个为程序员包办各种脏活累活儿的省心Framework :) 使用django.forms提供的API做表单输入的后端验证时遇到了一些坑,大多是因为不了解API的设计哲学所致,基本通过查阅官方文档+搜索StackOverflow解决。这里呈现一个比较通用的登录界面错误信息提示功能的代码片段作为Sample,来归纳一下使用forms时一些需要注意的细节。

这是一个多APP的教务系统工程,其中登录验证在层次上写到了“基础信息管理子系统(IMS)”中;每个APP都(理应)把自己所辖的URL放到一个固定的前缀下,在顶层的urls.py中include每个APP的urls,并对应相应的匹配前缀(比如/ims/login/在顶层被前缀/ims/匹配,在IMS APP中的urls里只要匹配login/即可)。示例如下:

#djcode/urls.py
urlpatterns += [
    url(r'^ims/', include('IMS.urls')),
]

#IMS/urls.py
from IMS import views
#...
urlpatterns += [
    url(r'^login/$', views.loggingin),
    url(r'^user_auth/', views.user_auth),
    url(r'^home/$', views.home),
]

接下来设计views中的Form类,有两个字段(用户名、密码)和clean方法:

#views.py
from django import forms
from django.forms.util import ErrorList
#...
class LoginForm(forms.Form):
    username = forms.CharField(max_length=20)
    passwd = forms.CharField(max_length=20)

    def clean(self):
        cleaned_data = super(LoginForm, self).clean()
        _username = cleaned_data.get('username', '')
        _passwd = cleaned_data.get('passwd', '')
        if not User.objects.filter(username=_username):
            error_msg = ["用户不存在!"]
            super(LoginForm, self).errors['username'] = ErrorList(error_msg)
        else:
            user = authenticate(username=_username, password=_passwd)
            if user is None:
                error_msg = ["密码错误!"]
                super(LoginForm, self).errors['passwd'] = ErrorList(error_msg)
        return cleaned_data

Form API定义了两种clean函数,像上面这种写法可以同时在clean()方法中获取各个域的内容,才能将“密码错误”的错误与“用户名错误”区分开;还有一种clean_<字段名>的规则命名的函数,比如:

def clean_passwd(self):
    #...
    raise forms.ValidationError('密码错误')
    #...

其中是只能获取到passwd域的内容的(试图获取username的话会得到None= =)!并且forms.ValidationError()的信息似乎并不会显示在重定向以后的Login页面的错误信息栏中……(如果直接暴力渲染{{ form }})使用ErrorList似乎是一个相对比较可控的实现方法。

接下来定义urls调用的各个后端处理函数:

#views.py
def loggingin(request):
    if not request.user.is_authenticated():
        form = LoginForm()
        t = get_template('login.html')
        c = RequestContext(request, {'form': form})
        return HttpResponse(t.render(c))
    else:
        return HttpResponseRedirect('../home/')

@csrf_exempt
def user_auth(request):
    form = ""
    if request.method == 'POST':
        form = LoginForm(request.POST)
    else:
        form = LoginForm()

    if form.is_valid():
        _username = form.cleaned_data['username']
        _passwd = form.cleaned_data['passwd']
        user = authenticate(username = _username, password = _passwd)
        if user is not None:
            #valid, active and authenticated
            if user.is_active: 
                login(request, user)
                return HttpResponseRedirect('../home/')

    #else, back to login page
    t = get_template('login.html')
    c = RequestContext(request, {'form': form})
    return HttpResponse(t.render(c))

user_auth()函数中,首先通过request是否是POST类型来判断是要获取表单中已有的信息,还是渲染一个空表单;要注意最后必须返回一个HttpResponse对象,因此把正常登录的返回出口放在if语句中,把出错处理放在外面,以备if-not-taken时执行。

最后是template中如何渲染Django Form的问题。模板使用了Bootstrap 3的form-control CSS样式类。

我们在View函数中,把Form API的数据放在了Context变量’form’中,使HTML模板能够获取到。直接渲染{{ form }}变量的懒惰做法显然不能合乎要求,这样无法顾及到给ErrorList域预留位置等种种问题;因为事实上大多数情境中所需要渲染的表单域数量都不多,且对于输入形式的要求各不相同(比如下拉框 vs 文本域),用{% for %}中模板里写循环来手动渲染也不是个很好的主意。手动渲染form.field.label_tagform.fieldform.field.errors恐怕可以对模板显示的效果获得最大的控制:

<div class="form-group" style="width: 100%;">
  <form role="form" action="../user_auth/" method="post">
      <div class="input-group input-group-lg" style="padding-right: 5%">
          <span class="input-group-addon">用户名</span>
              {{ form.username }}
      </div>
      {% if form.username.errors %}
          <div class="alert alert-danger">
              {{ form.username.errors }}
          </div>
      {% endif %}
      <br/>

      <div class="input-group input-group-lg" style="padding-right: 5%">
          <span class="input-group-addon"> 密 码</span>
          {{ form.passwd }}
      </div>
      {% if form.passwd.errors %}
          <div class="alert alert-danger">
              {{ form.passwd.errors }}
          </div>
      {% endif %}

      <br/>
      <div class="btn-group-lg" align="center">
          <button type="submit" class="btn btn-default">登录</button>
      </div>
  </form>
</div>

<script>
    //make the size of django-rendered forms suits form-control
    $("#id_username").attr("class", "form-control");
    $("#id_passwd").attr({"class": "form-control", "type": "password"});
</script>

forms渲染出的input field有统一的id命名规则(id_<field_name>)。插入一段JavaScript脚本,来保证Form API渲染的表单输入域能符合Bootstrap的大小和圆角样式。

Comments powered by Disqus
Share