基於django的視訊點播網站開發-step5-詳情頁功能

geeeeeeeek發表於2019-02-13

在本講中,我們開始詳情頁功能的開發,詳情頁就是對單個視訊進行播放並展示視訊的相關資訊,比如視訊標題、描述、評論資訊、相關推薦等。我們將會學習到通用檢視類DetailView的使用、評論動態載入、以及如何通過ajax實現喜歡和收藏功能,並通過一段段很酷的程式碼來說明這些功能。

效果展示

基於django的視訊點播網站開發-step5-詳情頁功能

整體功能

大家可先通過 網站演示地址 瀏覽一下網站效果。點選某個視訊即可瀏覽詳情頁。詳情頁實現了是對單個視訊進行展示,使用者可看到視訊的一些元資訊,包括標題、描述、觀看次數、喜歡數、收藏數等等。另外,網站還實現了評論功能,通過上拉網頁即可分頁載入評論列表,使用者還能新增評論。網頁側欄是推薦視訊列表,這裡使用的推薦邏輯比較簡單,就是推薦觀看次數最多的視訊。

我們把詳情頁分為4個小的業務模組來開發,分別是:視訊詳情顯示、喜歡和收藏功能、評論功能、推薦功能。下面我們分別對這四個功能模組進行開發講解。

視訊詳情顯示

因為在上一講中,我們已經建立了video模型,所以不必再新建模型,我們就在video模型的基礎上進行擴充套件。上一講,我們建立的欄位有title、desc、classification、file、cover、status、create_time。這些欄位目前是不夠用的,我們再加幾個欄位,需要加觀察次數喜歡的使用者收藏的使用者。video模型擴充套件後如下

class Video(models.Model):
    STATUS_CHOICES = (
        (`0`, `釋出中`),
        (`1`, `未釋出`),
    )
    title = models.CharField(max_length=100,blank=True, null=True)
    desc = models.CharField(max_length=255,blank=True, null=True)
    classification = models.ForeignKey(Classification, on_delete=models.CASCADE, null=True)
    file = models.FileField(max_length=255)
    cover = models.ImageField(upload_to=`cover/`,blank=True, null=True)
    status = models.CharField(max_length=1 ,choices=STATUS_CHOICES, blank=True, null=True)
    view_count = models.IntegerField(default=0, blank=True)
    liked = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                   blank=True, related_name="liked_videos")
    collected = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                   blank=True, related_name="collected_videos")
    create_time = models.DateTimeField(auto_now_add=True, blank=True, max_length=20)

複製程式碼

新增了3個欄位

  • view_count 觀看次數。資料型別是IntegerField,預設是0
  • liked 喜歡的使用者。資料型別是ManyToManyField,這是一種多對多的關係,表示一個視訊可以被多個使用者喜歡,一個使用者也可以喜歡多個視訊。記得設定使用者表為settings.AUTH_USER_MODEL
  • collected 收藏的使用者。資料型別是ManyToManyField,這是一種多對多的關係,表示一個視訊可以被多個使用者收藏,一個使用者也可以收藏多個視訊。設定使用者表為settings.AUTH_USER_MODEL

更多關於ManyToManyField的使用介紹,可以查詢django官網的介紹。

下面就是詳情展示階段,我們先配置好詳情頁的路由資訊,在video/urls.py中追加detail的路由資訊。


app_name = `video`
urlpatterns = [
    path(`index`, views.IndexView.as_view(), name=`index`),
    path(`search/`, views.SearchListView.as_view(), name=`search`),
    path(`detail/<int:pk>/`, views.VideoDetailView.as_view(), name=`detail`),
]
複製程式碼

path(`detail/<int:pk>/`, views.VideoDetailView.as_view(), name=`detail`)即表示詳情資訊,注意每條視訊都是有自己的主鍵的,所以設定路徑匹配為detail/<int:pk>/,其中<int:pk>表示主鍵,這是django中表示主鍵的一種方法。這樣我們就可以在瀏覽器輸入127.0.0.1:8000/video/detail/xxx來訪問詳情了。

怎麼顯示詳情呢,聰明的django為我們提供了DetailView。urls.py中設定的檢視類是VideoDetailView,我們讓VideoDetailView繼承DetailView即可。

class VideoDetailView(generic.DetailView):
    model = Video
    template_name = `video/detail.html` 
複製程式碼

看起來超級簡單,django就是如此的酷,只需要我們配置幾行程式碼,就能實現很強大的功能。這裡我們配置model為Video模型,模板為video/detail.html,其它的工作都不用管,全都交給django去幹,oh,這棒極了。

模板檔案位於templates/video/detail.html,它的程式碼比較簡單,這裡就不貼了。

從效果圖上我們看到還有個觀看次數的展示,這裡的觀看次數本質上就是資料庫裡的一個自增欄位,每次觀看的時候,view_count自動加1。對於這個小需求,我們需要做兩件事情,首先這video模型裡面,新增一個次數自增函式,命名為increase_view_count,這很簡單,如下所示:

    def increase_view_count(self):
        self.view_count += 1
        self.save(update_fields=[`view_count`])
複製程式碼

然後,還需要我們在VideoDetailView檢視類裡面呼叫到這個函式。這個時候get_object()派上用場了。因為每次呼叫DetailView的時候,django都會回撥get_object()這個函式。因此我們可以把increase_view_count()放到get_object()裡面執行。完美的程式碼如下

class VideoDetailView(generic.DetailView):
    model = Video
    template_name = `video/detail.html`

    def get_object(self, queryset=None):
        obj = super().get_object()
        obj.increase_view_count()  # 呼叫自增函式
        return obj
複製程式碼

目前為止,我們就能在詳情頁看到標題、描述、觀看次數、收藏次數、喜歡次數。預覽如下

基於django的視訊點播網站開發-step5-詳情頁功能

雖然可以顯示收藏人數、喜歡人數。但是目前還沒實現點選喜歡/收藏的功能。下面我們來實現。

收藏和喜歡功能

收藏和喜歡是一組動作,因此可以用ajax來實現:使用者點選後呼叫後端介面,介面返回json資料,前端顯示結果。

既然需要介面,那我們先新增喜歡/收藏介面的路由,在video/urls.py追加程式碼如下

path(`like/`, views.like, name=`like`),
path(`collect/`, views.collect, name=`collect`),
複製程式碼

由於喜歡和收藏的功能實現非常類似,限於篇幅,我們只實現喜歡功能。

我們先寫like函式:

@ajax_required
@require_http_methods(["POST"])
def like(request):
    if not request.user.is_authenticated:
        return JsonResponse({"code": 1, "msg": "請先登入"})
    video_id = request.POST[`video_id`]
    video = Video.objects.get(pk=video_id)
    user = request.user
    video.switch_like(user)
    return JsonResponse({"code": 0, "likes": video.count_likers(), "user_liked": video.user_liked(user)})

複製程式碼

首先判斷使用者是否登入,如果登入了則呼叫switch_like(user)來實現喜歡或不喜歡功能,最後返回json。注意這裡新增了兩個註解@ajax_required@require_http_methods(["POST"]),分別驗證request必須是ajax和post請求。

switch_like()函式則寫在了video/model.py裡面

    def switch_like(self, user):
        if user in self.liked.all():
            self.liked.remove(user)
        else:
            self.liked.add(user)
複製程式碼

所有的後端工作都準備好了,我們再把視線轉向前端。前端主要是寫ajax程式碼。

由於ajax程式碼量較大,我們封裝到一個單獨的js檔案中 ==> static/js/detail.js

在detail.js中,我們先實現喜歡的ajax呼叫:

$(function () {

    // 寫入csrf
    $.getScript("/static/js/csrftoken.js");

    // 喜歡
    $("#like").click(function(){
      var video_id = $("#like").attr("video-id");
      $.ajax({
            url: `/video/like/`,
            data: {
                video_id: video_id,
                `csrf_token`: csrftoken
            },
            type: `POST`,
            dataType: `json`,
            success: function (data) {
                var code = data.code
                if(code == 0){
                    var likes = data.likes
                    var user_liked = data.user_liked
                    $(`#like-count`).text(likes)
                    if(user_liked == 0){
                        $(`#like`).removeClass("grey").addClass("red")
                    }else{
                        $(`#like`).removeClass("red").addClass("grey")
                    }
                }else{
                    var msg = data.msg
                    alert(msg)
                }

            },
            error: function(data){
              alert("點贊失敗")
            }
        });
    });
複製程式碼

上述程式碼中,關鍵程式碼是$.ajax()函式,我們傳入了引數:video_idcsrftoken。其中csrftoken可通過/static/js/csrftoken.js生成。在success回撥中,通過判斷user_liked的值來確定自己是否喜歡過,然後改變模板中相應的css。

推薦功能

每個網站都有自己的推薦功能,且都有自己的推薦邏輯。我們視點的推薦邏輯是根據訪問次數最高的n個視訊來降序排序,然後推薦給使用者的。

實現起來非常容易,我們知道詳情頁實現用的是VideoDetailView,我們可以在get_context_data()中把推薦內容傳遞給前端模板。只需要我們改寫VideoDetailView的get_context_date()函式。

    def get_context_data(self, **kwargs):
        context = super(VideoDetailView, self).get_context_data(**kwargs)
        form = CommentForm()
        recommend_list = Video.objects.get_recommend_list()
        context[`form`] = form
        context[`recommend_list`] = recommend_list
        return context
複製程式碼

改寫後,我們新增了一行

recommend_list = Video.objects.get_recommend_list()
複製程式碼

我們把獲取推薦列表的函式get_recommend_list()封裝到了Video模型裡面。在Video/models.py裡面
我們追加程式碼:

class VideoQuerySet(models.query.QuerySet):
    def get_recommend_list(self):
        return self.filter(status=0).order_by(`-view_count`)[:4]
複製程式碼

關鍵是self.filter(status=0).order_by(`-view_count`)[:4],通過order_by把view_count降序排序,並選取前4條資料。

注意此處我們用了VideoQuerySet查詢器,需要我們在Video下面新增一行依賴。表示用VideoQuerySet作為Video的查詢管理器。

objects = VideoQuerySet.as_manager()
複製程式碼

當模板拿到資料後,即可渲染顯示。這裡我們將推薦側欄的程式碼封裝到templates/video/recommend.html裡面。

# templates/video/recommend.html
{% load thumbnail %}
<span class="video-side-title">推薦列表</span>
<div class="ui unstackable divided items">
    {% for item in recommend_list %}
    <div class="item">
        <div class="ui tiny image">
            {% thumbnail item.cover "300x200" crop="center" as im %}
            <img class="ui image" src="{{ im.url }}">
            {% empty %}
            {% endthumbnail %}
        </div>
        <div class="middle aligned content">
            <a class=" header-title" href="{% url `video:detail` item.pk %}">{{ item.title }}</a>
            <div class="meta">
                <span class="description">{{ item.view_count }}次觀看</span>
            </div>
        </div>
    </div>
    {% empty %}
    <h3>暫無推薦</h3>
    {% endfor %}

</div>
複製程式碼

並在detail.html中將它包含進來

{% include "video/recommend.html" %}
複製程式碼

評論功能

評論區位於詳情頁下側,顯示效果如下。共分為兩個部分:評論form和評論列表。

基於django的視訊點播網站開發-step5-詳情頁功能

評論功能是一個獨立的模組,該功能通用性較高,在其他很多網站中都有評論功能,為了避免以後開發其他網站時重複造輪子,我們建立一個新的應用,命名為comment

python3 manage.py startapp comment
複製程式碼

接下來,我們建立comment模型

# 位於comment/models.py

class Comment(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    nickname = models.CharField(max_length=30,blank=True, null=True)
    avatar = models.CharField(max_length=100,blank=True, null=True)
    video = models.ForeignKey(Video, on_delete=models.CASCADE)
    content = models.CharField(max_length=100)
    timestamp = models.DateTimeField(auto_now_add=True) 

    class Meta:
        db_table = "v_comment"
複製程式碼
  • user 使用者。資料型別是ForeignKey,外來鍵是settings.AUTH_USER_MODEL,並設定為級聯刪除on_delete=models.CASCADE
  • nickname 使用者暱稱。資料型別是CharField。
  • avatar 頭像。資料型別是CharField。
  • video 對應的視訊。資料型別是ForeignKey,對應Video模型,級聯刪除 on_delete=models.CASCADE
  • content 評論內容。 資料型別是CharField。
  • timestamp 評論時間。 資料型別是DateTimeField。

有了模型之後,我們就可以專心寫業務程式碼了,首先在comment下建立路由檔案urls.py。並寫入程式碼:

from django.urls import path
from . import views

app_name = `comment`
urlpatterns = [
    path(`submit_comment/<int:pk>`,views.submit_comment, name=`submit_comment`),
    path(`get_comments/`, views.get_comments, name=`get_comments`),
]
複製程式碼

我們配置了兩條路由資訊:評論提交 和 獲取評論。

提交評論,需要一個form,我們把form放到video/forms.py

from django import forms
from comment.models import Comment

class CommentForm(forms.ModelForm):
    content = forms.CharField(error_messages={`required`: `不能為空`,},
        widget=forms.Textarea(attrs = {`placeholder`: `請輸入評論內容` })
    )

    class Meta:
        model = Comment
        fields = [`content`]
複製程式碼

然後在video/views.py的VideoDetailView下新增form的相關程式碼。

class VideoDetailView(generic.DetailView):
    model = Video
    template_name = `video/detail.html`

    def get_object(self, queryset=None):
        obj = super().get_object()
        obj.increase_view_count()
        return obj

    def get_context_data(self, **kwargs):
        context = super(VideoDetailView, self).get_context_data(**kwargs)
        form = CommentForm() 
        context[`form`] = form 
        return context
複製程式碼

在get_context_data()函式裡面,我們把form傳遞給模板。

同樣的,提交評論也是非同步的,我們用ajax實現,我們開啟static/js/detail.js,寫入

    // 提交評論
    var frm = $(`#comment_form`)
    frm.submit(function () {
        $.ajax({
            type: frm.attr(`method`),
            url: frm.attr(`action`),
            dataType:`json`,
            data: frm.serialize(),
            success: function (data) {
                var code = data.code
                var msg = data.msg
                if(code == 0){
                    $(`#id_content`).val("")
                    $(`.comment-list`).prepend(data.html);
                    $(`#comment-result`).text("評論成功")
                    $(`.info`).show().delay(2000).fadeOut(800)
                }else{
                    $(`#comment-result`).text(msg)
                    $(`.info`).show().delay(2000).fadeOut(800);
                }
            },
            error: function(data) {
            }
        });
        return false;
    });
複製程式碼

評論通過ajax提交後,我們在submit_comment()中就能接收到這個請求。處理如下

def submit_comment(request,pk):
    video = get_object_or_404(Video, pk = pk)
    form = CommentForm(data=request.POST)

    if form.is_valid():
        new_comment = form.save(commit=False)
        new_comment.user = request.user
        new_comment.nickname = request.user.nickname
        new_comment.avatar = request.user.avatar
        new_comment.video = video
        new_comment.save()

        data = dict()
        data[`nickname`] = request.user.nickname
        data[`avatar`] = request.user.avatar
        data[`timestamp`] = datetime.fromtimestamp(datetime.now().timestamp())
        data[`content`] = new_comment.content

        comments = list()
        comments.append(data)

        html = render_to_string(
            "comment/comment_single.html", {"comments": comments})

        return JsonResponse({"code":0,"html": html})
    return JsonResponse({"code":1,`msg`:`評論失敗!`})

複製程式碼

在接收函式中,通過form自帶的驗證函式來儲存記錄,然後將這條記錄返回到前端模板。

下面我們開始評論列表的開發。

評論列表部分,我們使用了的是上拉動態載入的方案,即當頁面拉到最下側時,js載入程式碼會自動的獲取下一頁的資料並顯示出來。前端部分,我們使用了一種基於js的開源載入外掛。基於這個外掛,可以很容易實現網頁的上拉動態載入效果。它使用超級簡單,僅需要呼叫$(`.comments`).dropload({})即可。我們把呼叫的程式碼封裝在static/js/load_comments.js裡面。

完整的呼叫程式碼如下:

$(function(){
    // 頁數
    var page = 0;
    // 每頁展示15個
    var page_size = 15;

    // dropload
    $(`.comments`).dropload({
        scrollArea : window,
        loadDownFn : function(me){
            page++;

            $.ajax({
                type: `GET`,
                url: comments_url,
                data:{
                     video_id: video_id,
                     page: page,
                     page_size: page_size
                },
                dataType: `json`,
                success: function(data){
                    var code = data.code
                    var count = data.comment_count
                    if(code == 0){
                        $(`#id_comment_label`).text(count + "條評論");
                        $(`.comment-list`).append(data.html);
                        me.resetload();
                    }else{
                        me.lock();
                        me.noData();
                        me.resetload();
                    }
                },
                error: function(xhr, type){
                    me.resetload();
                }
            });
        }
    });
});
複製程式碼

不用過多的解釋,這段程式碼已經非常非常清晰了,本質還是ajax的介面請求呼叫,呼叫後返回結果更新前端網頁內容。

我們看到ajax呼叫的介面是get_comments,我們繼續來實現它,它位於comment/views.py中。程式碼如下所示,這段程式碼也很簡單,沒有什麼複雜的技術。當獲取到page和page_size後,使用paginator物件來實現分頁。最後通過render_to_string將html傳遞給模板。

def get_comments(request):
    if not request.is_ajax():
        return HttpResponseBadRequest()
    page = request.GET.get(`page`)
    page_size = request.GET.get(`page_size`)
    video_id = request.GET.get(`video_id`)
    video = get_object_or_404(Video, pk=video_id)
    comments = video.comment_set.order_by(`-timestamp`).all()
    comment_count = len(comments)

    paginator = Paginator(comments, page_size)
    try:
        rows = paginator.page(page)
    except PageNotAnInteger:
        rows = paginator.page(1)
    except EmptyPage:
        rows = []

    if len(rows) > 0:
        code = 0
        html = render_to_string(
            "comment/comment_single.html", {"comments": rows})
    else:
        code = 1
        html = ""

    return JsonResponse({
        "code":code,
        "html": html,
        "comment_count": comment_count
    })
複製程式碼

相關文章