1、数据库表
Django中有User模型
(django.contrib.auth.models.User),不过其中仅包含了有限的
7个固定字段
(username, first_name, last_name, email, is_staff, is_active, date_joined),其中is_staff(models.BooleanField)表示是
否允许此用户进入admin页面,is_active(models.BooleanField)表示该用户的帐号
是否为有效帐号,比如用户将自己注销(删除)了,其状态就可以由is_active=False表示。
扩展用户属性可以通过model的继承来实现,示例代码如下:
class CustomUser(User):
age = models.PositiveIntegerField(blank=True)
job = models.CharField(max_length=300, blank=True)
location = models.CharField(max_length=300, blank=True)
class Meta(User.Meta):
db_table = 'customuser'
如上,CustomUser表示自定义用户,在基础字段上增加了age, job, location三个字段。如果执行python manage.py syncdb,数据库会自动建好一下6个用户直接相关的表:
- auth_group:用户分组表,初始为空,即默认没有分组
- auth_group_permissions:用户分组权限表,初始为空
- auth_permission:用户权限表,初始内容为对各个model的增、删、改的权限
- auth_user:用户表,这里只包含7个固定字段,不包含我们自定义的字段
- auth_user_groups:用户分组关系表,初始为空
- auth_user_user_permissions:用户权限分配表
可见,我们期望的customuser并没有出现,看来我们需要手动建立了,建表语句如下所示:
CREATE TABLE auth_info
(
user_ptr_id integer NOT NULL,
age integer,
job character varying,
location character varying,
CONSTRAINT auth_info_pkey PRIMARY KEY (user_ptr_id),
CONSTRAINT auth_info_user_ptr_id_fkey FOREIGN KEY (user_ptr_id)
REFERENCES auth_user (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT auth_info_age_check CHECK (age >= 0)
)
WITH (
OIDS=FALSE
);
ALTER TABLE auth_info
OWNER TO postgres;
字段
user_ptr_id挺关键的,别忘了。
这样一来,我们就可以像操作普通model一样来操作CustomUser了,也可以将CustomUser当作User类型(继承产生的多态)在后续的用户模块中毫无障碍的使用。
2、用户注册
包括接下来的密码找回、个人信息修改,我们都将借助Django中的表单实现,Django中的表单还是很好用的。
注册表单:
class UserCreateForm(UserCreationForm):
'''
创建新用户
'''
email = forms.EmailField() #邮箱
job = forms.CharField(max_length=300, required=False) #职业
age = forms.IntegerField(required=False) #年龄
location = forms.CharField(max_length=300, required=False) #工作地点
class Meta:
model = User
fields = (
"username", "email", "password1", "password2",
"job", "age", "location"
)
# 判断年龄是否为负数
def clean_age(self):
age = self.cleaned_data['age']
if age < 1:
raise forms.ValidationError("年龄不能小于1岁")
return age
# 判断该email是否已被注册
def clean_email(self):
email = self.cleaned_data["email"]
try:
User._default_manager.get(email=email)
except User.DoesNotExist:
return email
raise forms.ValidationError(
"the email already exists.Plase change one or login",
code='duplicate_email',
)
def save(self, commit=True):
user = super(UserCreateForm, self).save(commit=False)
user.job = self.cleaned_data["job"]
user.age = self.cleaned_data["age"]
user.email = self.cleaned_data["email"]
user.location = self.cleaned_data["location"]
if commit:
user.save()
return user
不一一解释了,代码自身应该能将自身解释的很清楚了,需要注意的是,如果某个字段是非必须的,需注明:
required=False,默认的都是必须的。
注册页面:
考虑到灵活性,我们没有采用Django代码中默认的方式,那样的话使用弹出框真的是很不方便。
<form method="post" id="register-form">
<ul class="soil-user-signup-fields">
<li>
<p>
<span class="soil-user-signup-label">用户名</span>
<input value="1" type="text" name="username" class="form-control soil-user-signup-field" placeholder="必填">
</p>
<p style="display:none">
<span id="username-error-info" class="error-msg"></span>
</p>
</li>
<li>
<p>
<span class="soil-user-signup-label">密码</span>
<input value="123" type="password" name="password1" class="form-control soil-user-signup-field" placeholder="必填">
</p>
</li>
<li>
<p>
<span class="soil-user-signup-label">密码确认</span>
<input value="123" type="password" name="password2" class="form-control soil-user-signup-field" placeholder="必填">
</p>
<p style="display:none">
<span id="password-error-info" class="error-msg"></span>
</p>
</li>
<li>
<p>
<span class="soil-user-signup-label">邮箱</span>
<input value="2@c.cn" type="text" name="email" class="form-control soil-user-signup-field" placeholder="必填">
</p>
<p style="display:none">
<span id="email-error-info" class="error-msg"></span>
</p>
</li>
<li>
<p>
<span class="soil-user-signup-label">职业</span>
<input value="123" type="text" name="job" class="form-control soil-user-signup-field">
</p>
</li>
<li>
<p>
<span class="soil-user-signup-label">单位</span>
<input value="123" type="text" name="location" class="form-control soil-user-signup-field">
</p>
</li>
<li>
<p>
<span class="soil-user-signup-label">年龄</span>
<input value=23 type="text" name="age" class="form-control soil-user-signup-field">
</p>
</li>
</ul>
</form>
<script type="text/javascript">
// 提交注册信息
var signup_submit = function(){
$("#register-form").ajaxSubmit({
url: "/accounts/sign_up",
success: function(){
$("#signup-panel").modal('hide');
$("#signup-success-panel").modal();
},
error: function(msg){
dialogs.error("注册失败!请修改错误信息后重新注册!");
}
});
};
</script>
上面省略了,用户名,邮箱唯一性、有效性检查的代码,这部分可自行添加。为防止页面在提交后跳转,我们使用了异步提交(ajaxSubmit, 来自jquery.form.js)
注册代码:
def sign_up(request):
'''
注册
'''
if request.method == "POST":
form = UserCreateForm(request.POST)
if form.is_valid():
usr = form.save()
else:
raise HttpInternalError()
3、用户登录
表单:django.contrib.auth.forms.AuthenticationForm
登录页面:
<form method="post" id="signin-form" action="/accounts/sign_in">
<ul class="soil-user-signin-fields">
<li>
<p>
<span class="soil-user-signin-label">用户名</span>
<input type="text" name="username" class="form-control soil-user-signin-field">
</p>
<p style="display:none">
<span id="username-signin-error-info" class="error-msg"></span>
</p>
</li>
<li>
<p>
<span class="soil-user-signin-label">密码</span>
<input type="password" name="password" class="form-control soil-user-signin-field">
</p>
</li>
<li>
<p style="padding-right: 25px;font-size: 12px;height: 15px;">
<a href="javascript:void(0)" class="forget-pwd">忘记密码?</a>
<span style="float:right">自动登录</span>
<input name="remember-me" type="checkbox" style="margin-top: 2px;float:right">
</p>
</li>
</ul>
<input type="hidden" name="next" value="/">
</form>
<script>
var signin_submit = function(){
$("#signin-form").ajaxSubmit({
url: "/accounts/sign_in",
dataType:"JSON",
success: function(data){
$("#signin-panel").modal('hide');
$(".signup-btn,.signin-btn").hide();
$(".logout-btn,.pwd-btn").show();
$("#username>a").attr("href","/accounts/profile");
$("#username>a").html(data['username']);
},
error: function(msg){
dialogs.error("登录失败!密码错误!");
}
});
};
</script>
上面的
<input type="hidden" name="next" value="/"> 表示用户登录后的跳转页面,这里让其跳到首页。
登录代码:
def sign_in(request):
'''
登录
'''
params = utils.get_params(request)
redirect_to = utils.get_dict_value(params, 'next', '')
if request.method == "POST":
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
login(request, form.get_user())
rememberme = utils.get_dict_value(params, "remember-me")
if rememberme == "on":
request.session.set_expiry(7*24*3600)
else:
request.session.set_expiry(0)
return utils.dumps_json({"next":redirect_to,"username": utils.get_param(params, "username")})
raise HttpInternalError()
这里需要说明一下用户登录有效期的问题,默认有效期是
2周,但如果在代码里将session的过期设为了浏览器关闭即过期,那么以后用户再登录如果不进行如何设置的话,默认的就是浏览器关闭即过期了,所以这里当用户选择了“自动登录”,还需要显式的设置一次过期时间。
3、信息修改
表单:
class UserProfileForm(UserChangeForm):
'''
个人信息修改
'''
class Meta:
model = User
fields = ('username','email','password','age', 'job', 'location')
password = ReadOnlyPasswordHashField()
def __init__(self, *args, **kwargs):
super(UserProfileForm, self).__init__(*args, **kwargs)
def clean_password(self):
return self.initial["password"]
修改页面与上面大同小异,也是form+ajaxSubmit的组合,不再举例。
修改代码:
def update(request):
'''
修改个人信息
'''
if request.method == 'POST':
form = UserProfileForm(request.POST, instance=request.user.customuser)
if form.is_valid():
form.save()
if 'username' in request.REQUEST:
usr = CustomUser.objects.get(username = request.POST['username'])
return str(usr)
elif 'email' in request.REQUEST:
usr = CustomUser.objects.get(email = request.POST['email'])
return str(usr)
raise HttpInternalError()
4、密码找回
在密码找回里使用了注册码(django-simple-captcha)防止用户恶意提交。基本流程是:用户点击“忘记密码”,弹出密码重置对话框(包括邮箱和注册码两个输入项),用户输入相关信息,提交,如果没问题,向其填写的邮箱中发送密码重置链接,如果有问题,停留在当前对话框,提示相关错误,等待用户重新输入相关信息。
表单:
class CaptchaForm(forms.Form):
email = forms.EmailField()
captcha = CaptchaField()
注册码模块:
urls.py:
urlpatterns += patterns('',
url(r'^captcha/', include('captcha.urls')),
)
settings.py
邮箱配置:
EMAIL_HOST = 'smtp.issas.ac.cn' #SMTP地址
EMAIL_PORT = 25 #SMTP端口
EMAIL_HOST_USER = 'xxx@xxx.xxx' #邮箱地址
EMAIL_HOST_PASSWORD = '******' #邮箱密码
DEFAULT_FROM_EMAIL = <span style="font-family: Arial, Helvetica, sans-serif;">EMAIL_HOST_USER</span>
views.py
def reset_password_dialog(request):
'''
重置密码对话框
'''
form = CaptchaForm()
return render_to_response("accounts/inc/password_reset_dialog.html", locals(),
context_instance=RequestContext(request))
accounts/inc/password_reset_dialog.html
<script>
var exchange = function(a, b){
var n = a.next(), p = b.prev();
b.insertBefore(n);
a.insertAfter(p);
};
$(function(){
exchange($('.captcha'), $('#id_captcha_1'));
$('.captcha').click(function(){
$.get("accounts/get_new_sn", function(result){
$('.captcha').attr("src",result);
$('#id_captcha_0').attr("value",result.split('/')[3]);
});
return false;
});
$("#id_captcha_1").on("focus", function(){
$("#sn-error-msg").html("");
});
});
</script>
<form method="POST" id="captcha-form">
<p>
<span class="soil-user-fw-field">邮箱</span>
{{form.email}}
</p>
<p style="margin-bottom:3px">
<span class="soil-user-fw-field">验证码</span>
{{form.captcha}}
</p>
<span id="sn-error-msg" style="display: inline-block;margin-left: 65px;color: red;"></span>
</form>
如你所见,上述html代码中没有提交代码,这个对话框是ajax方式请求,提交代码在其母页面中。
重置密码模块:
前端代码:
<script>
var forget_pwd_submit = function(){
$("#captcha-form").ajaxSubmit({
url: "/accounts/reset_password",
success: function(data){
if(data == "sn_error"){
$.get("accounts/get_new_sn", function(result){
$('.captcha').attr("src",result);
$('#id_captcha_0').attr("value",result.split('/')[3]);
});
$("#sn-error-msg").html("验证码错误!");
return;
}
$("#forget-pwd-panel").modal("hide");
$("#forget-pwd-done-panel").find(".modal-body").html(data);
$("#forget-pwd-done-panel").modal();
},
error: function(msg){
$("#forget-pwd-panel").modal("hide");
dialogs.error("密码重置失败!");
}
});
};
</script>
后端代码:
urls.py:
urlpatterns += patterns('django.contrib.auth.views',
url(r'^password/reset/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$',
'password_reset_confirm', {"template_name":"accounts/inc/password_reset_confirm.html"}, name="password_reset_confirm"),
url(r'^password/done/$', 'password_reset_complete', {"template_name":"accounts/inc/password_reset_complete.html"}, name="password_reset_complete")
)
views.py
def reset_password(request):
'''
重置用户密码
'''
if request.method == 'POST':
form_sn = CaptchaForm(request.POST)
if form_sn.is_valid():
human = True
else:
return "sn_error" #验证码错误
form_email = PasswordResetForm(request.POST)
email_template_name = "accounts/inc/password_reset_email.html"
if form_email.is_valid():
opts = {
'use_https': request.is_secure(),
'token_generator': default_token_generator,
'from_email': settings.DEFAULT_FROM_EMAIL,
'email_template_name': email_template_name,
'request': request,
}
#form.save(**opts)
from MapCloud.common.ThreadPool import async_execute
async_execute(form_email.save, kwds=opts)
return render_to_response("accounts/inc/password_reset_done.html", locals(),
context_instance=RequestContext(request))
raise HttpInternalError()
这部分代码是参考着django.contrib.auth.forms.PasswordResetForm来写的,是有点麻烦,主要是其中的模板参数不清楚,下面一一解释一下(强烈建议看看django/contrib/admin/templates/registration中的默认模板):
- password_reset_complete.html:用户通过重置链接重置密码成功后跳转到的页面
示例:
<html>
<body>
<p>密码已重置,您现在可以返回<a href="/">首页</a>重新登录!</p>
</body>
</html>
- password_reset_confirm.html:这个就是用户重置密码的页面
示例:
<h3>密码重置</h3>
{% if validlink %}
<form action="" method="post">{% csrf_token %}
{{ form.new_password1.errors }}
<p class="aligned wide"><label for="id_new_password1">新的密码:</label>{{ form.new_password1 }}</p>
{{ form.new_password2.errors }}
<p class="aligned wide"><label for="id_new_password2">再输一遍:</label>{{ form.new_password2 }}</p>
<p><input type="submit" value="重置" /></p>
</form>
{% else %}
<h1>密码重置失败,可能这个链接已经被使用过了,您可以重新申请密码重置</h1>
{% endif %}
- password_reset_done.html:这个是用户输入了正确的邮箱和验证码,系统给出的确认信息。
示例:
<p>我们向您的邮箱里发送了密码重置链接,请按链接中的指示重置密码。</p>
<p>如果没收到相关邮件,请确认您在上一步所填写的邮箱是否时您注册时使用的邮箱。</p>
<p>谢谢!</p>
- password_reset_email.html:这个是向用户发送的邮件的内容,即用户会在其邮箱看到的内容
示例:
{% autoescape off %}
您收到此封邮件是因为您请求重置 {{ site_name }} 上的帐号密码.
请点击下面的链接并设置一个新的密码:
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
另:您的用户名为:{{ user.get_username }}
谢谢您对我们的支持!
{% endautoescape %}
OK,这样这部分基本上就可以使用了。
另外,系统再向用户发送邮箱的会有一个比较明显的延迟,为此我们使用线程池构造了一个异步任务执行模块,这个会再另一篇文章了介绍。
5、密码修改
待续。。。