close

Python Django 學習紀錄:架設個人部落格(九) 

修改用戶訊息

 

一.、功能設計

  1. 修改暱稱
  2. 修改信箱
  3. 修改密碼

二.、通用表單

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

22.JPG

在Google安全性設定開啟低安全性登入

23.JPG

在<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)
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 ivankao 的頭像
    ivankao

    IvanKao的部落格

    ivankao 發表在 痞客邦 留言(0) 人氣()