Django搭建個人博客:擴展用戶信息

可能你已經發現了,Django自帶的User模型非常實用,以至於我們沒有寫用戶管理相關的任何模型。

但是自帶的User畢竟可用的字段較少。比方說非常重要的電話號碼、頭像等都沒有。解決的方法有很多,你可以不使用User,自己從零寫用戶模型;也可以對User模型進行擴展。

博客網站的用戶信息並不複雜,因此擴展User就足夠了。

擴展User模型

擴展User模型又有不同的方法。在大多數情況下,使用模型一對一鏈接的方法是比較適合的。

編寫userprofile/models.py如下:

userprofile/models.py

from django.db import models
from django.contrib.auth.models import User
# 引入內置信號
from django.db.models.signals import post_save
# 引入信號接收器的裝飾器
from django.dispatch import receiver


# 用戶擴展信息
class Profile(models.Model):
    # 與 User 模型構成一對一的關係
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    # 電話號碼字段
    phone = models.CharField(max_length=20, blank=True)
    # 頭像
    avatar = models.ImageField(upload_to='avatar/%Y%m%d/', blank=True)
    # 個人簡介
    bio = models.TextField(max_length=500, blank=True)

    def __str__(self):
        return 'user {}'.format(self.user.username)


# 信號接收函數,每當新建 User 實例時自動調用
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)


# 信號接收函數,每當更新 User 實例時自動調用
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

每個Profile模型對應唯一的一個User模型,形成了對User的外接擴展,因此你可以在Profile添加任何想要的字段。這種方法的好處是不需要對User進行任何改動,從而擁有完全自定義的數據表。模型本身沒有什麼新的知識,比較神奇的是用到的信號機制

Django包含一個“信號調度程序”,它可以在框架中的某些位置發生操作時,通知其他應用程序。簡而言之,信號允許某些發送者通知一組接收器已經發生了某個動作。當許多代碼可能對同一事件感興趣時,信號就特別有用。

這裏引入的post_save就是一個內置信號,它可以在模型調用save()方法後發出信號。

有了信號之後還需要定義接收器,告訴Django應該把信號發給誰。裝飾器receiver就起到接收器的作用。每當User有更新時,就發送一個信號啓動post_save相關的函數。

通過信號的傳遞,實現了每當User創建/更新時,Profile也會自動的創建/更新。

當然你也可以不使用信號來自動創建Profile表,而是採用手動方式實現。

爲什麼刪除User表不需要信號?答案是兩者的關係採用了models.CASCADE級聯刪除,已經帶有關聯刪除的功能了。

avatar字段用來存放頭像,暫且不管它,下一章講解。

重建數據庫

前面講過,每次改動模型後都需要進行數據的遷移。由於avatar字段爲圖像字段,需要安裝第三方庫Pillow來支持:

(env) E:\django_project\my_blog> pip install Pillow

安裝成功後,通過makemigrationsmigrate遷移數據:

(env) E:\django_project\my_blog>python manage.py makemigrations

Migrations for 'userprofile':
  userprofile\migrations\0001_initial.py
    - Create model Profile
(env) E:\django_project\my_blog>python manage.py migrate

Operations to perform:
  Apply all migrations: admin, article, auth, contenttypes, sessions, userprofile
Running migrations:
  Applying userprofile.0001_initial... OK

**遷移好數據後,如果你試圖登錄用戶,會得到報錯。**這是因爲之前創建的User數據都沒有對應的Profile模型,違背了現有的模型。一種解決辦法就是乾脆刪除舊的數據,因此就需要用到Django的shell命令。

shell是Django提供的互動解釋器,你可以在這個指令模式中試驗代碼是否能夠正確執行,是相當方便的工具。

在虛擬環境中輸入python manage.py shell就可以進入shell:

(env) E:\django_project\my_blog>python manage.py shell
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

看到>>>表示成功進入shell。

輸入下面兩行指令就可以輕鬆刪除User數據庫:

>>> from django.contrib.auth.models import User
>>> User.objects.all().delete()

注意因爲前面寫的article模型中,與User的外鍵也採用了models.CASCADE級聯刪除模式,因此隨着User的刪除,相關的文章也一併刪除了

輸入exit()退出shell,輸入指令python manage.py createsuperuser重新創建管理員賬戶

對新手來說,修改數據庫經常會導致各種頭疼的問題,比如說字段失效、新字段爲null、賦值錯誤、外鍵鏈接出錯等等,最終導致整個業務邏輯報錯。因此我的建議是,在設計數據庫時儘量考慮周全,避免頻繁修改模型。

如果實在要修改,並且已經導致數據庫混亂了,不妨刪除掉/app/migrations/目錄下最新的幾個文件,清空相關數據庫,重新遷移數據。

接下來編寫MTV模式的剩餘部分。

表單、視圖和模板

有了擴展的Profile模型後,需要新建一個表單類去編輯它的內容:

userprofile/forms.py

...
# 引入 Profile 模型
from .models import Profile

...
class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('phone', 'avatar', 'bio')

然後在userprofile/views.py中寫處理用戶信息的視圖函數:

userprofile/views.py

...

# 別忘了引入模塊
from .forms import ProfileForm
from .models import Profile

...

# 編輯用戶信息
@login_required(login_url='/userprofile/login/')
def profile_edit(request, id):
    user = User.objects.get(id=id)
    # user_id 是 OneToOneField 自動生成的字段
    profile = Profile.objects.get(user_id=id)

    if request.method == 'POST':
        # 驗證修改數據者,是否爲用戶本人
        if request.user != user:
            return HttpResponse("你沒有權限修改此用戶信息。")

        profile_form = ProfileForm(data=request.POST)
        if profile_form.is_valid():
            # 取得清洗後的合法數據
            profile_cd = profile_form.cleaned_data
            profile.phone = profile_cd['phone']
            profile.bio = profile_cd['bio']
            profile.save()
            # 帶參數的 redirect()
            return redirect("userprofile:edit", id=id)
        else:
            return HttpResponse("註冊表單輸入有誤。請重新輸入~")

    elif request.method == 'GET':
        profile_form = ProfileForm()
        context = { 'profile_form': profile_form, 'profile': profile, 'user': user }
        return render(request, 'userprofile/edit.html', context)
    else:
        return HttpResponse("請使用GET或POST請求數據")

業務邏輯與以前寫的處理表單的視圖非常相似(還記得嗎),就不贅述了。

需要注意下面幾個小地方:

  • user_id是外鍵自動生成的字段,用來表徵兩個數據表的關聯。你可以在SQLiteStudio中查看它。
  • 留意redirect()是如何攜帶參數傳遞的。

然後就是新建模板文件/templates/userprofile/edit.html

/templates/userprofile/edit.html

{% extends "base.html" %} {% load staticfiles %}
{% block title %} 用戶信息 {% endblock title %}
{% block content %}
<div class="container">
    <div class="row">
        <div class="col-12">
            <br>
            <div class="col-md-4">用戶名: {{ user.username }}</div>
            <br>
            <form method="post" action=".">
                {% csrf_token %}
                <!-- phone -->
                <div class="form-group col-md-4">
                    <label for="phone">電話</label>
                    <input type="text" class="form-control" id="phone" name="phone" value="{{ profile.phone }}">
                </div>
                <!-- bio -->
                <div class="form-group col-md-4">
                    <label for="bio">簡介</label>
                    <textarea type="text" class="form-control" id="bio" name="bio" rows="12">{{ profile.bio }}</textarea>
                </div>
                <!-- 提交按鈕 -->
                <button type="submit" class="btn btn-primary">提交</button>
            </form>
        </div>
    </div>
</div>
{% endblock content %}
  • 留意模板中如何分別調用UserProfile對象的
  • 行內文本通過value屬性設置了初始值,而多行文本則直接設置{{ profile.bio }}

最後配置熟悉的userprofile/urls.py

userprofile/urls.py

...
urlpatterns = [
    ...
    # 用戶信息
    path('edit/<int:id>/', views.profile_edit, name='edit'),
]

啓動服務器,輸入地址查看功能是否正常。注意舊的用戶都刪除了(id=1的用戶已經沒有了),這裏的/<int:id>必須爲新創建的用戶的id

頁面雖然簡陋,但是方法是類似的。可以在這個基礎上,擴展爲一個美觀、詳細的用戶信息頁面。

**當然最好再給個人信息添加一個入口。**修改/templates/header.html

/templates/header.html

...
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
    <a class="dropdown-item" href='{% url "userprofile:edit" user.id %}'>個人信息</a>
    ...
</div>
...

修改article視圖

在前面新建article的章節中,由於沒有用戶管理的知識,存在一些問題:

  • new_article.author = User.objects.get(id=1)強行把作者指定爲id=1的用戶,這顯然是不對的。
  • 沒有對用戶的登錄狀態進行檢查。

因此稍加修改def article_create()

/article/views.py

...
from django.contrib.auth.decorators import login_required

...

# 檢查登錄
@login_required(login_url='/userprofile/login/')
def article_create(request):
    ...
    # 指定目前登錄的用戶爲作者
    new_article.author = User.objects.get(id=request.user.id)
    ...

重啓服務器,文章正確匹配到登錄的用戶,又可以愉快的寫文章了。

實際上,刪除文章article_delete()、更新文章article_update()都應該對用戶身份進行檢查。就請讀者嘗試修改吧。

配置admin

前面我們已經嘗試過將article配置到admin後臺,方法是非常簡單的,直接在admin.py中寫入admin.site.register(Profile)就可以了。但是這樣寫會導致UserProfile是兩個分開的表,不方便不說,強迫症的你怎麼能受得了。

我們希望能夠在admin中將UserProfile合併爲一張完整的表格。方法如下:

/userprofile/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User

from .models import Profile

# 定義一個行內 admin
class ProfileInline(admin.StackedInline):
    model = Profile
    can_delete = False
    verbose_name_plural = 'UserProfile'

# 將 Profile 關聯到 User 中
class UserAdmin(BaseUserAdmin):
    inlines = (ProfileInline,)

# 重新註冊 User
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

打開admin中的User表,發現Profile的數據已經堆疊在底部了:

總結

本章使用一對一鏈接的方式,擴展並更新了用戶信息。

下一章將學習對圖片的簡單處理。

轉載請告知作者並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章