Python Django 學習紀錄:架設個人部落格(九)
修改用戶訊息
一.、功能設計
- 修改暱稱
- 修改信箱
- 修改密碼
二.、通用表單
1.通用表單
在mysite專案的templates目錄中創建<form.html>通用表單
其中的標題、按鈕與傳入的表單等等,皆會根據傳入的變數不同而改變:
{% extends "base.html" %} {% load staticfiles %} {% block title %} {{ page_title }} {% endblock %} {% block nav_blog_active %}active{% endblock %} {% block content %} <div class="container"> <div class="row"> <!-- 響應式 --> <div class="col-xs-12 col-md-4 col-md-offset-4"> <!-- 添加面板 --> <div class="panel panel-default"> <!-- 面板標題 --> <div class="panel-heading">{{ form_title }}</div> <div class="panel-body"> <!-- 表單 --> <form action="" method="POST"> {% csrf_token %} <!-- 進行表單細部調整 --> {% for field in form %} <!-- 通用表單,label要特殊處理 --> {% if field.is_hidden %} <label for="{{ field.id_for_label }}">{{field.label}}</label> {% endif %} {{ field }} <!-- 錯誤訊息紅字顯示 --> <p class="text-danger">{{ field.errors.as_text }}</p> {% endfor %} <span class="pull-left text-danger">{{ form.non_field_errors }}</span> <!-- 按鈕 --> <div class="pull-right"> <input type="submit" value="{{ submit_text }}" class="btn btn-primary"> <button class="btn btn-default" onclick="window.location.href='{{ return_back_url }}'">返回</button> </div> </form> </div> </div> </div> </div> </div> {% endblock %}
2.修改暱稱
(1)功能分析
user不一定有對應的profile (尚未創建)
暱稱不能為空(Form表單判斷)
(2)創建form表單
在user應用<forms.py>中添加ChangeNicknameForm表單:
class ChangeNicknameForm(forms.Form): nickname_new = forms.CharField(label='新的暱稱', max_length=20, widget=forms.TextInput(attrs={'class':'form-control', 'placeholder':'請輸入新的暱稱'}))
(3)views初步添加修改暱稱方法
在user應用<views.py>中添加change_nickname方法
傳出的三個變數給通用表單使用:
from .forms import LoginForm, RegForm, ChangeNicknameForm def change_nickname(request): if request.method =='POST': pass else: form = ChangeNicknameForm() context = {} context['form'] = form context['page_title'] = '修改暱稱' context['form_title'] = '修改暱稱' context['submit_text'] = '修改' return render(request, 'form.html', context)
(4)設置url路徑
在user應用<urls.py>中添加change_nickname路徑:
path('change_nickname/', views.change_nickname, name='change_nickname'),
(5)個人資料頁面
在<user_info.html>添加用戶暱稱,與修改暱稱的路徑:
{% extends "base.html" %} {% block title %} 個人資料 {% endblock %} {% block nav_blog_active %}active{% endblock %} {% block content %} <div class="container"> <div class="row"> <!-- 響應式 --> <div class="col-xs-10 col-xs-offset-1"> {% if user.is_authenticated %} <h2>{{ user.username }}</h2> <ul> <li>暱稱:{{ user.profile.nickname }} <a href="{% url 'change_nickname' %}?from={{ request.get_full_path }}">修改暱稱</a></li> <li>信箱:{% if user.email %}{{ user.email }}{% else %}未綁定 <a href="#">綁定信箱</a>{% endif %}</li> <li>上一次登錄時間:{{ user.last_login|date:"Y-m-d H:i:s" }}</li> <li><a href="#">修改密碼</a></li> </ul> {% else %} <span>未登錄,跳轉到首頁...</span> <script type="text/javascript"> window.location.href = '/'; </script> {% endif %} </div> </div> </div> {% endblock %}
(6)form表單驗證
在<forms.py>的change_nickname進行驗證
使用自訂方法引入request.user進行用戶驗證
接著進行欄位不可空的驗證:
class ChangeNicknameForm(forms.Form): nickname_new = forms.CharField(label='新的暱稱', max_length=20, widget=forms.TextInput(attrs={'class':'form-control', 'placeholder':'請輸入新的暱稱'})) #判斷用戶是否登錄 #修改類別,將user寫入方法 def __init__(self, *args, **kwargs): if 'user' in kwargs: self.user = kwargs.pop('user') super(ChangeNicknameForm, self).__init__(*args, **kwargs) #判斷用戶是否登錄 def clean(self): if self.user.is_authenticated: self.cleaned_data['user'] = self.user else: raise forms.ValidationError('用戶尚未登錄') return self.cleaned_data #判斷新暱稱是否為空 def clean_nickname_new(self): nickname_new = self.cleaned_data.get('nickname_new','').strip() if nickname_new == '': raise forms.ValidationError('暱稱不能為空') return nickname_new
(7)完善<views.py>處理
將POST的資料與request.user傳入表單進行驗證
驗證通過則存入資料庫,並導向修改前頁面:
def change_nickname(request): if request.method =='POST': form = ChangeNicknameForm(request.POST, user = request.user) #如果驗證通過 if form.is_valid(): nickname_new = form.cleaned_data['nickname_new'] #呼叫資料庫並儲存 profile, created = Profile.objects.get_or_create(user=request.user) profile.nickname = nickname_new profile.save() return redirect(request.GET.get('from',reverse('home'))) else: form = ChangeNicknameForm() context = {} context['form'] = form context['page_title'] = '修改暱稱' context['form_title'] = '修改暱稱' context['submit_text'] = '修改' context['return_back_url'] = request.GET.get('from',reverse('home')) return render(request, 'form.html', context)
3.判斷顯示暱稱或用戶名稱
有時候需要顯示用戶名稱,時而又需要顯示暱稱
因此添加一些判斷,若有暱稱則只顯示暱稱
或是有暱稱就同時顯示用戶名稱與暱稱
(1)模型添加判斷方法
在user應用的<models.py>添加一些判斷:
- get_nickname:顯示暱稱,若無則為空
- get_nickname_or_username:顯示暱稱,若無則顯示用戶名稱
- has_nickname:判斷是否傭有暱稱
以下為代碼:
from django.db import models from django.contrib.auth.models import User # Create your models here. class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) #一對一關聯 nickname = models.CharField(max_length=20, verbose_name='暱稱') #介面顯示為中文 def __str__(self): #定義物件名稱 return '<Profile: %s for %s>' % (self.nickname, self.user.username) #顯示暱稱,若無則為空 def get_nickname(self): if Profile.objects.filter(user=self).exists(): profile = Profile.objects.get(user=self) return profile.nickname else: return '' #顯示暱稱,若無則顯示用戶名稱 def get_nickname_or_username(self): if Profile.objects.filter(user=self).exists(): profile = Profile.objects.get(user=self) return profile.nickname else: return self.username #判斷是否傭有暱稱 def has_nickname(self): return Profile.objects.filter(user=self).exists() #python附值方法 User.get_nickname = get_nickname User.get_nickname_or_username = get_nickname_or_username User.has_nickname = has_nickname
(2)基礎頁面與個人資料
在<base.html>中,將導航條添加判斷
有暱稱就顯示為用戶名稱(暱稱),無暱稱則顯示用戶名稱
將個人資料的暱稱欄位顯示暱稱,若無則為空白
以下為<base.html>:
<li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> {% if user.has_nickname %} {{ user.username }}({{ user.get_nickname }}) {% else %} {{ user.username}} {% endif %} <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="{% url 'user_info' %}">個人資料</a></li> {% if user.is_staff or user.is_superuser %} <li><a href="{% url 'admin:index' %}">後台管理</a></li> {% endif %} <li><a href="{% url 'logout' %}?from={{ request.get_full_path }}">登出</a></li> </ul> </li>
以下為<user_info.html>:
{% extends "base.html" %} {% block title %} 個人資料 {% endblock %} {% block nav_blog_active %}active{% endblock %} {% block content %} <div class="container"> <div class="row"> <!-- 響應式 --> <div class="col-xs-10 col-xs-offset-1"> {% if user.is_authenticated %} <h2>{{ user.username }}</h2> <ul> <li>暱稱:{{ user.get_nickname1 }} <a href="{% url 'change_nickname' %}?from={{ request.get_full_path }}">修改暱稱</a></li> <li>信箱:{% if user.email %}{{ user.email }}{% else %}未綁定 <a href="#">綁定信箱</a>{% endif %}</li> <li>上一次登錄時間:{{ user.last_login|date:"Y-m-d H:i:s" }}</li> <li><a href="#">修改密碼</a></li> </ul> {% else %} <span>未登錄,跳轉到首頁...</span> <script type="text/javascript"> window.location.href = '/'; </script> {% endif %} </div> </div> </div> {% endblock %}
(3)評論與回復
在comment應用的<views.py>要使用get_nickname_or_username方法來判斷
因此回傳給js的資料要做一些修改:
#返回數據 data['status'] = 'SUCCESS' data['username'] = comment.user.get_nickname_or_username() data['comment_time'] = (comment.comment_time + datetime.timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S") data['text'] = comment.text data['content_type'] = ContentType.objects.get_for_model(comment).model #從comment獲取關聯的的模型 #如果有值(None則為第一層) if not parent is None: #取得評論的用戶名稱 data['reply_to'] = comment.reply_to.get_nickname_or_username()
接著修改<blog_detail.html>頁面
將評論與回復、引用回復的對象,皆改為get_nickname_or_username的回傳值:
<form id="comment_form" action="{% url 'update_comment' %}" method="POST" style="overflow:hidden"> {% csrf_token %} <label>{{ user.get_nickname_or_username }},歡迎評論</label> <!-- 回復時才顯示回復的評論對象內容 --> <div id="reply_content_container" style="display:none"> <p>回復:</p> <div id="reply_content"></div> </div> <!-- 傳入編輯器表單 --> {% get_comment_form blog as comment_form %} {% for field in comment_form %} {{ field }} {% endfor %} <!-- 添加錯誤訊息 --> <span id="comment_error" class="text-danger pull-left"></span> <!-- 加入id,根據js改變值為評論或是回復 --> <input id="comment_btn" type="submit" value="評論" class="btn btn-primary pull-right"> </form> {% else %} 您尚未登錄,登錄之後方可評論 <a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登錄</a> <a class="btn btn-primary" href="{% url 'register' %}?from={{ request.get_full_path }}">註冊</a> {% endif %} </div> <div class="comment-area"> <h3 class="comment-area_title">評論列表</h3> <!-- 添加評論列表id --> <div id="comment_list"> {% get_comment_list blog as comments %} {% for comment in comments %} <div id="root_{{ comment.pk }}" class="comment"> <span>{{ comment.user.get_nickname_or_username }}</span> <span>({{ comment.comment_time|date:"Y-m-d H:i:s" }}):</span> <!-- 設置id,根據傳入的編號改變,並將最終的id的內容顯示到編輯器上方 --> <div id="comment_{{ comment.pk }}"> <span>{{ comment.text |safe }}</span> </div> <div class="like" onclick="LikeChange(this, 'comment', {{ comment.pk }} )"> <span class="glyphicon glyphicon-thumbs-up {% get_like_status comment %} "></span> <span class="liked-num">{% get_like_count comment %}</span> </div> <!-- 點擊回復的同時取得編號 --> <a href="javascript:reply({{ comment.pk }})">回復</a> {% for reply in comment.root_comment.all %} <div id="root_{{ reply.pk }}" class="reply"> <span>{{ reply.user.get_nickname_or_username }}</span> <span>{{ reply.comment_time|date:"Y-m-d H:i:s" }}:</span> <span>回復</span> <span>{{ reply.reply_to.get_nickname_or_username }}</span> <!-- 設置id,根據傳入的編號改變,並將最終的id的內容顯示到編輯器上方 --> <div id="comment_{{ reply.pk }}"> <span>{{ reply.text |safe }} </div>
4.綁定信箱
(1)創建form表單
在user應用<forms.py>中添加BindEmailForm表單:
class BindEmailForm(forms.Form): email = forms.EmailField(label='信箱', widget=forms.EmailInput(attrs={'class':'form-control', 'placeholder':'請輸入正確的信箱'})) verification_code = forms.CharField(label='驗證碼', max_length=20,required=False, widget=forms.TextInput(attrs={'class':'form-control', 'placeholder':'點擊"發送驗證碼"發送到信箱'}))
(2)views初步添加綁定信箱方法
在user應用<views.py>中添加bind_email方法
傳出的三個變數給通用表單使用:
def bind_email(request): if request.method =='POST': form = BindEmailForm(request.POST, request = request) #如果驗證通過 if form.is_valid(): pass return redirect(request.GET.get('from',reverse('home'))) else: form = BindEmailForm() context = {} context['form'] = form context['page_title'] = '綁定信箱' context['form_title'] = '綁定信箱' context['submit_text'] = '綁定' context['return_back_url'] = request.GET.get('from',reverse('home')) return render(request, 'form.html', context)
(3)設置url路徑
在user應用<urls.py>中添加bind_email路徑:
path('bind_email/', views.bind_email, name='bind_email'),
(4)個人資料頁面
在<user_info.html>添加綁定信箱的路徑:
<li>信箱:{% if user.email %}{{ user.email }}{% else %}未綁定 <a href="{% url 'bind_email' %}?from={{ request.get_full_path }}">綁定信箱</a>{% endif %}</li>
(5)form頁面添加按鈕擴充區塊
在<form.html>添加按鈕擴充區塊
將錯誤訊息添加id讓js調用,並使用clearfix清除浮動使表單呈現整齊:
<span id="tip" class="text-danger">{{ form.non_field_errors }}</span> <!-- 清除浮動 --> <div class="clearfix"></div> <div class="pull-left"> {% block other_buttons %}{% endblock %} </div> <!-- 按鈕 --> <div class="pull-right"> <input type="submit" value="{{ submit_text }}" class="btn btn-primary"> <button class="btn btn-default" onclick="window.location.href='{{ return_back_url }}'">返回</button> </div>
(6)創建<bind_email.html>頁面繼承<form.html>
在user應用templates目錄中user資料夾裡建立<bind_email.html>並繼承<form.html>
在按鈕擴充區塊添加發送驗證碼按鈕
並使用js進行點擊按鈕後的處理:
{% extends 'form.html' %} {% block other_buttons %} <button id="send_code" class="btn btn-primary">發送驗證碼</button> {% endblock %} {% block script_extends %} <script type="text/javascript"> $('#send_code').click(function(){ var email = $('#id_email').val(); if(email==''){ $('#tip').text('* 信箱不能為空') return false; } }); </script> {% endblock %}
修改user應用<views.py>中bind_email方法的回傳模板:
return render(request, 'user/bind_email.html', context)
(7)Google信箱SAMP設置
在Google帳戶設定啟用IMAP
在Google安全性設定開啟低安全性登入
在<settings.py>添加以下設置:
#發送信件設置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT = 587 EMAIL_HOST_USER = 'google信箱' EMAIL_HOST_PASSWORD = 'google密碼' EMAIL_SUBJECT_PREFIX = '[部落格架設]' #前墜 EMAIL_USE_TLS = True #是否啟用TLS鏈接(安全鏈接)
(8)發送驗證碼
在user應用<views.py>撰寫發送驗證碼的方法
導入django的send_mail方法來發信
導入string、random來生成驗證碼
透過表單獲取要發送的信箱來發信
並回傳data狀態給前端js處理
以下為代碼:
from django.core.mail import send_mail import string import random def send_verification_code(request): email = request.GET.get('email','') data = {} if email != '': #生成驗證碼 code = ''.join(random.sample(string.ascii_letters + string.digits, 4)) #添加(四個字元 (字母+數字))至code request.session['bind_email_code'] = code #發送信件 send_mail( '綁定信箱', #標題 '驗證碼: %s' % code, #內容 'ivanjo39192@gmail.com', #發送來源 [email], #發送目的(list) fail_silently=False, #是否忽略錯誤 ) data['status'] = 'SUCCESS' else: data['status'] = 'ERROR' return JsonResponse(data)
(9)表單驗證
- 驗證用戶是否登錄
- 驗證用戶是否已經綁定信箱
- 判斷驗證碼是否正確
- 驗證信箱是否以被綁定
- 驗證驗證碼是否為空
這邊要注意的是,導入request而不是user
因為不只會使用request.user還需要使用request.email
所以從<views.py>要直接傳入request
以下為代碼:
#發送信件設置 class BindEmailForm(forms.Form): email = forms.EmailField(label='信箱', widget=forms.EmailInput(attrs={'class':'form-control', 'placeholder':'請輸入正確的信箱'})) verification_code = forms.CharField(label='驗證碼', max_length=20,required=False, widget=forms.TextInput(attrs={'class':'form-control', 'placeholder':'點擊"發送驗證碼"發送到信箱'})) #判斷用戶是否登錄 #修改類別,將request寫入方法,因為還要取用request.email所以不能直接用user def __init__(self, *args, **kwargs): if 'request' in kwargs: self.request = kwargs.pop('request') super(BindEmailForm, self).__init__(*args, **kwargs) #判斷用戶是否登錄 def clean(self): if self.request.user.is_authenticated: self.cleaned_data['user'] = self.request.user else: raise forms.ValidationError('用戶尚未登錄') #判斷用戶是否綁定信箱 if self.request.user.email != '': raise forms.ValidationError('你已經綁定信箱') #判斷驗證碼 code = self.request.session.get('bind_email_code','') verification_code = self.cleaned_data.get('verification_code','') if not (code != '' and code == verification_code): raise forms.ValidationError('驗證碼不正確') return self.cleaned_data #判斷信箱是否已被綁定 def clean_email(self): email = self.cleaned_data['email'] if User.objects.filter(email=email).exists(): raise forms.ValidationError('該信箱已被綁定') else: return email #判斷驗證碼是否為空 def clean_verification_code(self): verification_code = self.cleaned_data.get('verification_code','').strip() if verification_code == '': raise forms.ValidationError('驗證碼不能為空') else: return verification_code
(10)完善<views.py>處理
將驗證過的資料儲存至資料庫
以下為代碼:
def bind_email(request): if request.method =='POST': form = BindEmailForm(request.POST, request = request) #如果驗證通過 if form.is_valid(): email = forms.cleaned_data['email'] request.user.email = email request.user.save() #清除session del request.session['bind_email_code'] return redirect(request.GET.get('from',reverse('home'))) else: form = BindEmailForm() context = {} context['form'] = form context['page_title'] = '綁定信箱' context['form_title'] = '綁定信箱' context['submit_text'] = '綁定' context['return_back_url'] = request.GET.get('from',reverse('home')) return render(request, 'user/bind_email.html', context)
(11)完善js代碼
添加ajax發送驗證碼
並在發送後將按鈕變灰,進行30秒倒數計時
以下為代碼:
{% block script_extends %} <script type="text/javascript"> $('#send_code').click(function(){ var email = $('#id_email').val(); if(email==''){ $('#tip').text('* 信箱不能為空') return false; } //發送驗證碼 $.ajax({ url:"{% url 'send_verification_code' %}", type:"GET", data:{ 'email': email }, cache: false, success: function(data){ if(data['status']=='ERROR'){ alert(data['status']); } } }); //把按鈕變灰,進行倒數計時 $(this).addClass('disabled'); //按鈕變灰 $(this).attr('disabled', true); //按鈕變灰 var time = 30; $(this).text(time + 's'); var interval = setInterval(() => { //setInterval 週期性執行代碼 if(time <= 0){ clearInterval(interval); //取消已設置的動作 $(this).removeClass('disabled'); //按鈕復原 $(this).attr('disabled', false); //按鈕復原 $(this).text('發送驗證碼'); //按鈕文字 return false } time --; //倒數 $(this).text(time + 's'); }, 1000); }); </script> {% endblock %}
(12)驗證碼添加時間判斷
將生成驗證碼的時間使用session記錄下來,在30秒內不可重複發送
以下為代碼:
import time def send_verification_code(request): email = request.GET.get('email','') data = {} if email != '': #生成驗證碼 code = ''.join(random.sample(string.ascii_letters + string.digits, 4)) #添加(四個字元 (字母+數字))至code now = int(time.time()) send_code_time = request.session.get('send_code_time', 0) if now - send_code_time <30: data['status'] = ERROR else: request.session['bind_email_code'] = code request.session['send_code_time'] = now #發送信件 send_mail( '綁定信箱', #標題 '驗證碼: %s' % code, #內容 '', #發送來源 [email], #發送目的(list) fail_silently=False, #是否忽略錯誤 ) data['status'] = 'SUCCESS' else: data['status'] = 'ERROR' return JsonResponse(data)