Xây Dựng Dự Án BBS Forum Đơn Giản Với Django

Phát triển một forum BBS đơn giản

Yêu cầu dự án:

1 Tham khảo tổng thể từ "抽屉新热榜" và "虎嗅网"
2 Thực hiện các mục khác nhau trong forum
3 Hiển thị danh sách bài viết
4 Hiển thị số lượng bình luận, số lượt thích của bài viết
5 Hiển thị người dùng trực tuyến
6 Cho phép người dùng đăng nhập tạo bài viết, bình luận, và thích
7 Cho phép tải lên tệp
8 Bài viết có thể được ghim
9 Có thể thực hiện bình luận đa cấp

Kiến thức cần thiết: (Lưu ý: Nếu bạn chưa nắm vững những kiến thức sau, hãy quay lại xem trước khi tiếp tục để tránh bối rối!)

1 Django
2 HTML\CSS\JS
3 BootStrap
4 Jquery

Thiết kế cấu trúc bảng

1. Tầm quan trọng của cấu trúc bảng

Khi phát triển bất kỳ dự án nào liên quan đến cơ sở dữ liệu, điều đầu tiên phải làm là thiết kế cấu trúc bảng. Cấu trúc bảng không tốt thì không nên viết code, vì nó phản ánh mối quan hệ logic kinh doanh của bạn. Dữ liệu của bạn sẽ được lưu vào cơ sở dữ liệu, vì vậy nếu bạn đã sắp xếp rõ ràng cấu trúc bảng, bạn cũng đã xác định được kiến trúc của mình!

2. Thiết kế bảng

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from __future__ import unicode_literals
from django.db import models
from django.contrib.auth.models import User

# Tạo mô hình ở đây

class Post(models.Model):
    '''
    Bảng bài viết
    '''
    title = models.CharField('Tiêu đề bài viết', max_length=255, unique=True)
    category = models.ForeignKey("Category", verbose_name='Tên mục')
    head_img = models.ImageField(upload_to="uploads")
    content = models.TextField("Nội dung")
    author = models.ForeignKey("UserProfile", verbose_name="Tác giả")
    publish_date = models.DateTimeField(auto_now=True, verbose_name="Ngày đăng")
    hidden = models.BooleanField(default=False, verbose_name="Ẩn")
    priority = models.IntegerField(default=1000, verbose_name="Ưu tiên")

    def __unicode__(self):
        return "<%s, tác giả:%s>" % (self.title, self.author)

class Comment(models.Model):
    '''
    Bảng bình luận
    '''
    post = models.ForeignKey("Post")
    user = models.ForeignKey("UserProfile")
    comment = models.TextField(max_length=1000)
    date = models.DateTimeField(auto_now=True)
    parent_comment = models.ForeignKey("self", related_name='p_comment', blank=True, null=True)

    def __unicode__(self):
        return "<user:%s>" %(self.user)

class Like(models.Model):
    '''
    Lượt thích
    '''
    post = models.ForeignKey('Post')
    user = models.ForeignKey('UserProfile')
    date = models.DateTimeField(auto_now=True)

class Category(models.Model):
    '''
    Bảng mục
    '''
    name = models.CharField(max_length=64, unique=True, verbose_name="Tên mục")
    admin = models.ManyToManyField("UserProfile", verbose_name="Quản trị viên mục")

    def __unicode__(self):
        return self.name

class UserProfile(models.Model):
    '''
    Bảng người dùng
    '''
    user = models.OneToOneField(User)
    name = models.CharField(max_length=32)
    groups = models.ManyToManyField("UserGroup")

    def __unicode__(self):
        return self.name

class UserGroup(models.Model):
    '''
    Bảng nhóm người dùng
    '''
    name = models.CharField(max_length=64, unique=True)

    def __unicode__(self):
        return self.name

Cấu hình Django Admin

Cấu hình admin để đăng ký model, đừng quên tạo tài khoản quản trị viên Django

from django.contrib import admin
import models
# Đăng ký mô hình ở đây

admin.site.register(models.Post)
admin.site.register(models.Category)
admin.site.register(models.Comment)
admin.site.register(models.Like)
admin.site.register(models.UserProfile)
admin.site.register(models.UserGroup)

Tôi đã tạo một số mục, nhưng khi tôi xem mục, tôi chỉ thấy thông tin cơ bản:

Làm cách nào để hiển thị ID hoặc thông tin khác của mục?

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from django.contrib import admin
import models
# Đăng ký mô hình ở đây

# Tạo lớp tùy chỉnh cho bảng cụ thể
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('id','name')

class PostAdmin(admin.ModelAdmin):
    list_display = ('id','title','author','hidden','publish_date')

admin.site.register(models.Post,PostAdmin) # Liên kết lớp tùy chỉnh với lớp đăng ký
admin.site.register(models.Category,CategoryAdmin)  # Liên kết lớp tùy chỉnh với lớp đăng ký
admin.site.register(models.Comment)
admin.site.register(models.Like)
admin.site.register(models.UserProfile)
admin.site.register(models.UserGroup)

Hiệu quả như sau:

Trang web & URL hoặc Views cấu hình

1. Sử dụng tên gọi URL

Cấu hình tên gọi trong URL

url(r'^category/(\d+)/$',views.category,name='category'),

Trong HTML, sử dụng tên gọi này

          <li role="presentation"><a href="{% url 'category' 1 %}">Mục Âu Mỹ</a></li>
          <li role="presentation"><a href="{% url 'category' 2 %}">Mục Nhật Hàn</a></li>
          <li role="presentation"><a href="{% url 'category' 3 %}">Mục Ấn Độ</a></li>

Lợi ích của tên gọi: Nếu muốn thay đổi URL, tất cả trang web phía trước đều phải thay đổi! Khi có nhiều tầng, việc sử dụng tên gọi sẽ rất tiện lợi!

2. Hình ảnh không hiển thị đúng trên trang web

Nguyên nhân: Hệ thống có thể tìm thấy thư mục uploads không? Nó có thể truy cập trực tiếp vào thư mục này không? Không, nó không thể!

  • Một giải pháp là tạo liên kết mềm tới thư mục uploads trong môi trường Linux

Nếu thêm thư mục uploads vào settings, nhưng cách này vẫn có vấn đề! Nó sẽ tìm thư mục /static/uploads/uploads, xem hình dưới!

Nhưng bằng cách sau, bạn có thể truy cập (vì nó sẽ tìm thư mục /static/uploads/uploads)

2.2. Viết phương thức upload riêng

Định nghĩa form xác thực

#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Tim Luo  LuoTianShuai

from django import forms

class PostForm(forms.Form):
    title = forms.CharField(max_length=255,min_length=5)
    summary = forms.CharField(max_length=255,min_length=5)
    head_img = forms.ImageField()
    content = forms.CharField(min_length=10)
    category_id = forms.IntegerField()

Định nghĩa phương thức upload

#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Tim Luo  LuoTianShuai
import os

def handle_upload_file(f,request):  # f nhận file handle
    base_img_upload_path = 'static/Uploads'
    user_path = "%s/%s" % (base_img_upload_path,request.user.userprofile.id)
    if not os.path.exists(user_path):
        os.mkdir(user_path)
    with open('%s/%s'% (user_path,f.name),'wb+') as destinations:
        for chunk in f.chunks():
            destinations.write(chunk)

    return "/static/Uploads/%s/%s" % (request.user.userprofile.id,f.name)

Định nghĩa views

def new_post(request):
    category_list = models.Category.objects.all()
    if request.method == 'POST':
        form = PostForm(request.POST,request.FILES)
        if form.is_valid():
            form_data = form.cleaned_data
            form_data['author_id'] = request.user.userprofile.id

            # Upload ảnh tùy chỉnh
            new_img_path = handle_upload_file(request.FILES['head_img'],request)
            form_data['head_img'] = new_img_path

            new_post_obj = models.Post(**form_data)
            new_post_obj.save()
            return render(request,'new_post.html',{'new_post_obj':new_post_obj})
        else:
            print form.errors

    return render(request,'new_post.html',{'category_list':category_list})

Thực hiện bình luận đa cấp

Người dùng có thể bình luận trực tiếp vào bài viết, và người dùng khác cũng có thể bình luận vào bình luận của người dùng khác, tức là "đóng góp ý kiến", như hình dưới:

Tất cả các bình luận đều được lưu trong một bảng, và giữa các bình luận có mối quan hệ phụ thuộc, làm thế nào để thể hiện mối quan hệ này trên trang web?

Trước hết, chúng ta lưu trữ dữ liệu như thế nào để ghi nhớ mối quan hệ này? (Hình dưới đây đã được đơn giản hóa, các cột khác đã bị ẩn)

Khi tạo cấu trúc bảng, chúng ta đã định nghĩa một khóa ngoại tự tham chiếu (parent_comment_id), nếu không có ID cha, nghĩa là nó là tầng đầu tiên, nếu có, nghĩa là nó nằm trong một bình luận khác! (Xem kỹ cấu trúc bảng)

Chúng ta có thể đơn giản hóa cấu trúc bình luận thành mô hình sau:

data = [
    (None,'A'),
    ('A','A1'),
    ('A','A1-1'),
    ('A1','A2'),
    ('A1-1','A2-3'),
    ('A2-3','A3-4'),
    ('A1','A2-2'),
    ('A2','A3'),
    ('A2-2','A3-3'),
    ('A3','A4'),
    (None,'B'),
    ('B','B1'),
    ('B1','B2'),
    ('B1','B2-2'),
    ('B2','B3'),
    (None,'C'),
    ('C','C1'),

]

Chuyển đổi thành từ điển:

data_dic = {
    'A': {
        'A1': {
            'A2':{
                'A3':{
                    'A4':{}
                }
            },
            'A2-2':{
                'A3-3':{}
            }
        }
    },
    'B':{
        'B1':{
            'B2':{
                'B3':{}
            },
            'B2-2':{}
        }
    },
    'C':{
        'C1':{}
    }

}

Với từ điển trên, chúng ta có thể sử dụng vòng lặp để lấy số tầng không? Tất nhiên không, vì chúng ta không biết có bao nhiêu tầng, nên chúng ta cần sử dụng đệ quy để tìm kiếm từng tầng!

Khi hiển thị trên trang web, chúng ta cần biết dữ liệu nào thuộc tầng nào, không thể chỉ xếp chồng lên nhau, vì chúng có mối quan hệ phân cấp!

Chúng ta có thể thực hiện ở phía sau: Trả về một từ điển cho phía trước là không đủ, chúng ta cần xây dựng mối quan hệ phân cấp ở phía sau, rồi trả về một đoạn HTML hoàn chỉnh

Sau khi chuyển đổi thành từ điển, chúng ta có thể sử dụng đệ quy để thực hiện! Trước khi chuyển đổi, mối quan hệ phân cấp không rõ ràng!

Trong quá trình lặp, chúng ta liên tục tạo từ điển, bắt đầu từ tầng cao nhất, sau đó đi sâu từng tầng

Ví dụ đơn giản:

#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Tim Luo  LuoTianShuai



data = [
    (None,'A'),
    ('A','A1'),
    ('A','A1-1'),
    ('A1','A2'),
    ('A1-1','A2-3'),
    ('A2-3','A3-4'),
    ('A1','A2-2'),
    ('A2','A3'),
    ('A2-2','A3-3'),
    ('A3','A4'),
    (None,'B'),
    ('B','B1'),
    ('B1','B2'),
    ('B1','B2-2'),
    ('B2','B3'),
    (None,'C'),
    ('C','C1'),

]

def tree_search(d_dic,parent,son):
    for k,v in d_dic.items():
        if k == parent:
            d_dic[k][son] = {} 
            return
        else:
            tree_search(d_dic[k],parent,son)



data_dic = {}

for item in data:
    parent,son = item
    if parent is None:
        data_dic[son] = {}  
    else:
        tree_search(data_dic,parent,son) 

for k,v in data_dic.items():
    print(k,v)

Kết quả thực thi: (Hoàn hảo)

('A', {'A1': {'A2': {'A3': {'A4': {}}}, 'A2-2': {'A3-3': {}}}, 'A1-1': {'A2-3': {'A3-4': {}}}})
('C', {'C1': {}})
('B', {'B1': {'B2-2': {}, 'B2': {'B3': {}}}})

2. Trả về phía trước

Khi chúng ta trả về từ điển cho phía trước, template phía trước không có chức năng đệ quy, mặc dù chúng ta đã xác định được mối quan hệ phân cấp! Vì vậy, chúng ta cần định nghĩa một ngôn ngữ template tùy chỉnh, sau đó ghép nối thành HTML và trả về cho phía trước để hiển thị!

2.1. Cấu hình phía trước để truyền dữ liệu cho simple_tag

{% load custom_tags %}

{% build_comment_tree post_obj.comment_set.select_related %}

2.2. simple_tag nhận dữ liệu và chuyển đổi dữ liệu truyền vào thành từ điển

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django import template
from django.utils.safestring import mark_safe

register = template.Library()

def tree_search(d_dic,comment_obj):
    for k,v_dic in d_dic.items():
        if k == comment_obj.parent_comment:
            d_dic[k][comment_obj] = {}
            return
        else:
            tree_search(d_dic[k],comment_obj)




@register.simple_tag
def build_comment_tree(comment_list):
    comment_dic = {}
    for comment_obj in comment_list: 
        if comment_obj.parent_comment is None: 
            comment_dic[comment_obj] = {}
        else:
            tree_search(comment_dic,comment_obj)

    html = "<div class='comment-box'>"
    margin_left = 0
    for k,v in comment_dic.items():
        html += "<div class='comment-node'>" + k.comment + "</div>"
        html += generate_comment_html(v,margin_left+15)
    html += "</div>"
    return mark_safe(html)

Hiệu quả như sau:

2.3. Để cải thiện giao diện, chúng ta có thể thêm margin-left để thể hiện hiệu ứng phân cấp, mỗi lần đệ quy, giá trị này tăng lên.

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django import template
from django.utils.safestring import mark_safe

register = template.Library()

def tree_search(d_dic,comment_obj):
    for k,v_dic in d_dic.items():
        if k == comment_obj.parent_comment:
            d_dic[k][comment_obj] = {}
            return
        else:
            tree_search(d_dic[k],comment_obj)

def generate_comment_html(sub_comment_dic,margin_left_val):
    html = ""
    for k,v_dic in sub_comment_dic.items():
        html += "<div style='margin-left:%spx'  class='comment-node'>" % margin_left_val + k.comment + "</div>"
        if v_dic:
            html += generate_comment_html(v_dic,margin_left_val+15)
    return html

@register.simple_tag
def build_comment_tree(comment_list):
    comment_dic = {}
    for comment_obj in comment_list: 
        if comment_obj.parent_comment is None: 
            comment_dic[comment_obj] = {}
        else:
            tree_search(comment_dic,comment_obj)

    html = "<div class='comment-box'>"
    margin_left = 0
    for k,v in comment_dic.items():
        html += "<div class='comment-node'>" + k.comment + "</div>"
        html += generate_comment_html(v,margin_left+15)
    html += "</div>"
    return mark_safe(html)

Hiệu quả như sau:

Thẻ: Django HTML css JavaScript Bootstrap

Đăng vào ngày 2 tháng 7 lúc 09:31