BBS是一個最簡單的項目.在咱們把本節課程的代碼手敲一遍後,算是實戰項目有一個入門.
首先一個項目的第一步是完成表設計,在沒有完成表結構設計以前,千萬不要動手開發(這是老司機的忠告!)
廢話很少說,如今咱們就一步一步的把bbs系統實施出來:
1.建立一個Django Project
django-admin startproject s12bbs
2.建立app
cd s12bbs/ python3.5 manage.py startapp bbs
3.建立templates(存放模版文件)和statics(存放靜態文件如boostrap組件包)
mkdir templates
mkdir statics
4.爲s12bbs項目建立一個數據庫實例
$ mysql -uroot -p 登入mysql mysql> create database day20_s12bbs default charset UTF8;
5.編輯s12bbs/settings.py文件.更改3處:
1.數據庫連接信息
DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), # } 'default':{ 'ENGINE':' django.db.backends.mysql', 'NAME':'day20_s12bbs', 'HOST':'127.0.0.1', 'PORT':'3307', 'USER':'root', 'PASSWORD':'123456', } }
2.html模版文件的存放路徑
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR,"templates")], #此處添加 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
3.靜態文件的存放路徑
STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "statics"), '/var/www/static/', ]
6. 這個步驟是附加的,關於mysqldb目前還不支持3.0python,使用pymysql驅動連接mysql數據庫
下載pymysql而後進行安裝,跟其它python第三包沒任何區別,同樣的安裝。 關於Django1.6中DATABASES的設置也是同樣不用作任何修改,跟之前MySQLdb的時候同樣,settings.py裏的配置不變,可是要在項目目錄下的__init__.py文件加入下面兩句 這裏是mysite/mysite/__init__.py 1 import pymysql 2 pymysql.install_as_MySQLdb() 作完上述動做後,便可在django中訪問mysql了。
7.完成上述6部,就能夠進行bbs系統開發了.
1.設計表結構
a.列出咱們要建立的表
b.分析業務,添加表字段
from django.db import models from django.contrib.auth.models import User from django.core.exceptions import ValidationError #這個就是Django admin後臺當出錯時,拋出的紅色錯誤提示,要自定義錯誤時,就得引入此方法 import datetime # Create your models here. # 論壇帖子表 class Article(models.Model): title = models.CharField(max_length=255,verbose_name=u"標題") brief = models.CharField(null=True,blank=True,max_length=255,verbose_name=u"描述") category = models.ForeignKey("Category",verbose_name=u"所屬板塊") #因爲Category類在它的下方,因此要引號引發來,Django內部會自動反射去找 content = models.TextField(verbose_name=u"文章內容") author = models.ForeignKey("UserProfile",verbose_name=u"做者") pub_date = models.DateField(blank=True,null=True) last_modify = models.DateField(auto_now=True,verbose_name=u"修改時間") priority = models.IntegerField(default=1000,verbose_name=u"優先級") status_choices = (('draft',u"草稿"), ('published',u"已發佈"), ('hidden',u"隱藏"), ) status = models.CharField(choices=status_choices,default="published") def __str__(self): return self.title # django 的model類在保存數據時,會默認調用self.clean()方法的,因此能夠在clean方法中定義數據的一些驗證 def clean(self): # 若是帖子有發佈時間,就說明是發佈過的帖子,發佈過的帖子就不能夠把狀態在改爲草稿狀態了 if self.status == "draft" and self.pub_date is not None: raise ValidationError((u'已發佈的帖子,不能更改狀態爲 草稿')) # 若是帖子沒有發佈時間,而且保存狀態是發佈狀態,那麼就把發佈日期設置成當天 if self.status == 'published' and self.pub_date is None: self.pub_date = datetime.date.today() # 評論表 class Comment(models.Model): article = models.ForeignKey("Article",verbose_name=u"所屬文章") parent_comment = models.ForeignKey('self',related_name="my_clildren",blank=True,null=True,verbose_name=u"父評論") comment_choices = ((1,u'評論'), (2,u"點贊")) comment_type = models.IntegerField(choices=comment_choices,default=1,verbose_name=u"評論類型") user = models.ForeignKey("UserProfile",verbose_name=u"評論人") commet = models.TextField(blank=True,null=True) #這裏有一個問題,這裏咱們設置了容許爲空,那就意味着咱們在頁面上點了評論,卻又沒有輸入內容,這樣豈不是很不合理.那麼怎麼實現只要你點了評論,內容就不能爲空. # 那麼咱們會問,爲何容許爲空,直接不爲空就行了.由於咱們這裏把評論和點贊放到了一張表中,當爲點贊時,固然就不須要評論內容了.因此能夠爲空. # 咱們會想在前端進行判斷或者在views寫代碼進行判斷,這裏告訴你這裏咱們就能夠實現這個限制.使用Django中clean()方法,models類在保存以前它會調用self.clean方法,因此咱們能夠在這裏定義clean方法,進行驗證 def clean(self): # 若是comment的狀態爲評論,那麼評論內容就不能爲空 if self.comment_type ==1 and self.commet is None: raise ValidationError(u"評論內容不能爲空") # 我想知道這個報錯顯示在什麼位置,咱們看到每個字段有報錯,也只是顯示在form表單的字段上,這裏作了判斷錯誤信息會顯示在什麼地方? # 後面把錯誤信息顯示的位置截圖展現 date = models.DateTimeField(auto_now_add=True,verbose_name=u"評論時間") # 板塊表 class Category(models.Model): name = models.CharField(max_length=64,unique=True,verbose_name=u"板塊名稱") #unique是否惟一 brief = models.CharField(null=True,blank=True,max_length=255,verbose_name=u"描述") set_as_top_menu = models.BooleanField(default=False,verbose_name=u"是否將此板塊設置在頁面頂部") positon_index = models.SmallIntegerField(verbose_name=u"頂部展現的位置") admins = models.ManyToManyField("UserProfile",blank=True,null=True,verbose_name=u"版主") def __str__(self): return self.name # 用戶表繼承Django裏的User class UserProfile(models.Model): user = models.OneToOneField(User,verbose_name=u"關聯Django內部的用戶") name = models.CharField(max_length=32,verbose_name=u"暱稱") signature = models.CharField(max_length=255,blank=True,null=True,verbose_name=u"簽名") head_img = models.ImageField(height_field=150,width_field=150,blank=True,null=True,verbose_name=u"頭像") #ImageFied字段說明https://docs.djangoproject.com/en/1.9/ref/models/fields/ #大概的意思是,ImageField 繼承的是FileField,除了FileField的屬性被繼承了,它還有兩個屬性 ImageField.height_field和ImageField.width_field,設置後當你存入圖片字段時,它會把默認尺寸設置成高height_field寬:width_field # 若是想在前端上傳圖像,須要下載一個Pillow模塊,具體使用後面會用到 # 用戶組表,其實這裏用不到,由於咱們使用Django的 User, class UserGroup(models.Model): pass
8.設置settings.py把bbs項目加到APP裏:
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'bbs', ]
9.建立數據庫
$ python3.5 manage.py makemigrations SystemCheckError: System check identified some issues: ERRORS: bbs.UserProfile.head_img: (fields.E210) Cannot use ImageField because Pillow is not installed. HINT: Get Pillow at https://pypi.python.org/pypi/Pillow or run command "pip install Pillow". 要安裝Pillow,才能使用ImageField字段 $ pip3.5 install Pillow Collecting Pillow Downloading Pillow-3.3.1-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl (3.1MB) 100% |████████████████████████████████| 3.1MB 29kB/s Installing collected packages: Pillow Successfully installed Pillow-3.3.1
在次建立:
$ python3.5 manage.py makemigrations System check identified some issues: WARNINGS: bbs.Category.admins: (fields.W340) null has no effect on ManyToManyField. Migrations for 'bbs': 0001_initial.py: - Create model Article - Create model Category - Create model Comment - Create model UserGroup - Create model UserProfile - Add field user to comment - Add field admins to category - Add field author to article - Add field category to article WARNINGS: bbs.Category.admins: (fields.W340) null has no effect on ManyToManyField.
這裏警告的是由於ManyToMany中設置了null=True形成的,爲何形成?由於ManyToMany原本就不會寫到本表中,紀錄都是保存在第三張表.若是不選就不建立紀錄,因此這裏設置null=True是多餘的.
更改便可
$ python3.5 manage.py migrate Operations to perform: Apply all migrations: bbs, admin, contenttypes, sessions, auth Running migrations: Rendering model states... DONE Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying bbs.0001_initial... OK Applying bbs.0002_auto_20160831_0745... OK Applying sessions.0001_initial... OK
至此bbs系統的表結構設計已經完成,接着既是先後端的結合了.
後臺管理
1.註冊後臺admin
from django.contrib import admin from bbs import models # Register your models here. class ArticleAdmin(admin.ModelAdmin): list_display = ('title','category','author','pub_date','last_modify','status') class CommentAdmin(admin.ModelAdmin): list_display = ('article','parent_comment','comment_type','commet','user') class CategoryAdmin(admin.ModelAdmin): list_display = ('name','set_as_top_menu','positon_index',) admin.site.register(models.Article,ArticleAdmin) admin.site.register(models.Comment,CommentAdmin) admin.site.register(models.Category,CategoryAdmin) admin.site.register(models.UserProfile)
2.建立一個後臺管理的supperuser用戶
$ python3.5 manage.py createsuperuser Username (leave blank to use 'tedzhou'): admin Email address: Password: Password (again): Superuser created successfully.
3. 啓動服務,並訪問後臺
$ python3.5 manage.py runserver 127.0.0.1:8000 http://127.0.0.1:8000/admin
引入圖
4.建立測試數據
1.建立用戶
2.建立板塊
3.建立帖子
4.建立評論
和
後端暫時就這麼多內容了
BBS系統之選擇合適的前端模版
準備前端頁面用到的組件文件
訪問 www.bootcss.com -> 選擇"起步",找到以下框架:
而後咱們在點擊這個框架,進入頁面把頁面的內容下載到本地,以下
下載後會有一個目錄和一個文件,其中目錄爲此前端框架前端代碼中使用到的bootstrap中的一些組件和jquery文件.
咱們能夠直接把這個目錄放到靜態文件目錄下/statics/下,也能夠將全部的bootstrap組件都下載下來,還有jquery下載,一定一個系統可能要用到的前端樣式不止一個.
因而咱們下載bootstrap 和jquery(可從上crm項目中烤拷貝),放置statics/bootstrap/目錄下,
前端頁面用到的jss和css以及字體咱們都準備好了,下面咱們就能夠在templates目錄中建立咱們的前端html模版文件了.
準備bbs系統前端頁面的基礎文件base.html
1.把咱們上面下載的Non-responsive Template for Bootstrap.html文件放到templates/目錄下,建立bbs目錄
2.更改base.html文件,把引用的css和js所有更改爲本地
css引入路徑更改
js引入路徑更改
這裏只是選圖舉例,具體按照你base.html文件中引入少css和js文件
3.更改好後,咱們能夠設置一個urls.py和views來測試訪問
urls.py添加以下:
url(r'^bbs/', views.index),
views.py 添加以下:
def index(request): return render(request,"base.html")
訪問http://127.0.0.1:8000/bbs/測試:
下面就正式開始咱們的前端頁面開發了.
全局urls.py文件更改:
from django.conf.urls import url,include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^bbs/', include("bbs.urls")), ]
bbs/urls.py文件更改:
from django.conf.urls import url,include from bbs import views urlpatterns = [ url(r'^$', views.index), ]
bbs/views.py文件內容:
from django.shortcuts import render,HttpResponse # Create your views here. def index(request): return render(request,"bbs/index.html")
建立templates/bbs/index.html,內容以下:
{% extends 'base.html' %}
訪問http://127.0.0.1:8000/bbs/測試,結果正常.
接下來咱們要實現,板塊在上面的導航欄動態展現.
實現分三部
1.把版塊取出來. (後端)
2.取出來後按順序排列到前端html中 (後端排序+前端for循環)
3.讓他能點 (前端)
對於這個項目,初學者根本不會有具體的實現思路.都是走一步看一步.
一\首先咱們先實現板塊取出來排序:
1.後臺bbs/views.py中把板塊取出來
from django.shortcuts import render,HttpResponse from bbs import models # Create your views here. def index(request): category_list = models.Category.objects.filter(set_as_top_menu =True).order_by('positon_index') print(category_list) return render(request,"bbs/index.html",{'category_list':category_list,}) # return HttpResponse("OK")
2.前端html模版for循環後端傳過來的板塊列表,展現出來(這裏咱們只寫實現上述功能的代碼部分)
代碼以下:
<div id="navbar" class="navbar-collapse collapse"> {% block top-head %} <ul class="nav navbar-nav"> {% for category in category_list %} <li class="active"><a href="#">{{ category.name }}</a></li> {% endfor %} </ul> ... {% endblock %} </div>
3.訪問測試
二\解決上圖中的問題,實現"當點擊對應板塊時,對應板塊的標題纔是active狀態".
咱們會有兩種思路:
1.前端寫js,當點擊每個板塊時,js更改對應的標籤樣式.
老師說這種思路不能實現,緣由是當你點擊一個板塊時,業務上確定是刷新頁面,那麼刷新頁面後js的動做不會保留在新頁面.(我想明白了,頁面刷新後,後臺傳來新的頁面沒有任何動做,因此js腳本沒實現)
2.後端給參數,前端根據參數判斷.
此思路可行,首先咱們在訪問首頁http://127.0.0.1:8000/index/時,試圖函數會把category_list返回給前端頁面.category_list裏是各個板塊的models對象.
那咱們就須要把:
<li class="active"><a href="#">{{ category.name }}</a></li>
修改爲:
<li class="active"><a href="{% url 'category_detail' category.id %}">{{ category.name }}</a></li>
其中{% url 'category_detail' category.id %},這是用到了url的name屬性,若是不用就須要寫成
<li class="active"><a href="/bbs/category/{{category.id}}">{{ category.name }}</a></li>
咱們來總結下何時用name屬性.
1. 前面老師說過一個: 當要對URL進行權限管理的時候
2. a連接的本站地址.
上面更改後,就實現了當點擊板塊時,自動跳轉到http://127.0.0.1:8000/bbs/category/{{category.id}}
這時候咱們要在bbs/urls.py文件裏添加一個新的URL,後臺bbs/views.py文件裏在添加一個新的試圖函數,至於前端的html模版文件,可使用index.html只是多傳入一個參數category.
因而代碼以下:
1.bbs/urls.py文件
from django.conf.urls import url,include from bbs import views urlpatterns = [ url(r'^$', views.index), url(r'category/(\d+)/$',views.category,name='category_detail'), ]
2.bbs/views.py文件,添加試圖category
訪問http://127.0.0.1:8000/bbs/時咱們的板塊是動態得到的,是在index試圖裏,使用下面語句得到的.
category_list = models.Category.objects.filter(set_as_top_menu =True).order_by('positon_index')
因爲動態得到,因此每個頁面要想顯示板塊,就須要給前端html傳category_list.
因此上面的語句最好是作成全局變量,這樣在須要使用的時候,直接傳入便可.更好的辦法是,作在一個默認字典裏,這樣在添加其餘鍵值對就默認有了.這個之後優化代碼時能夠考慮.
代碼以下:
from django.shortcuts import render,HttpResponse from bbs import models # Create your views here. category_list = models.Category.objects.filter(set_as_top_menu =True).order_by('positon_index') def index(request): print(category_list) return render(request,"bbs/index.html",{'category_list':category_list,}) def category(request,id): # id是URL配置中category/(\d+)/$的(\d+),一個括號就是一個參數 category_obj = models.Category.objects.get(id=id) return render(request,"bbs/index.html",{'category_list':category_list, 'category_obj':category_obj,})
3.咱們這裏沿用index.html文件,index.html徹底繼承的base.html因此,咱們更改base.html以下:(仍是隻改實現功能的部分)
{% block top-head %} <ul class="nav navbar-nav"> {% for category in category_list %} {% if category_obj.id == category.id %} #若是當前板塊頁面的ID,和循環的id同樣,那麼顯示爲active <li class="active"><a href="{% url 'category_detail' category.id %}">{{ category.name }}</a></li> {% else %} #不然不是active <li class=""><a href="{% url 'category_detail' category.id %}">{{ category.name }}</a></li> {% endif %} {% endfor %} </ul> ... {% endblock %}
4.至此咱們就能夠訪問http://127.0.0.1:8000/bbs測試
點擊"內地",查看效果:
三\下面咱們實現,點擊相應板塊就顯示相應板塊裏的帖子.
準備工做:在admin後臺管理中添加多個帖子,這裏就不截圖了.
咱們在二步驟中已經可以返回指定板塊的內容了,只是沒返回給前端頁面該板塊下的帖子.因此只須要在bbs/views.py文件裏的category試圖函數中查出該板塊中有哪些帖子便可.
另外還有一個須要注意的點,就是咱們在全部論壇中都會發現有一個板塊"所有",因此當板塊爲所有時應該返回的是全部的帖子.
1.首先URL沒有變化,因此就不須要添加新的url了.
2.bbs/views.py文件更改以下:
from django.shortcuts import render,HttpResponse from bbs import models # Create your views here. category_list = models.Category.objects.filter(set_as_top_menu =True).order_by('positon_index') def index(request): print(category_list) return render(request,"bbs/index.html",{'category_list':category_list}) def category(request,id): category_obj = models.Category.objects.get(id=id) if category_obj.positon_index == 1: #咱們把板塊"所有"認定爲首頁顯示,把全部的文章都顯示出來,首頁就認定當position_index 爲1時既是首頁. article_list = models.Article.objects.filter(status='published')#把全部狀態爲"已發佈"的查出來 else: article_list = models.Article.objects.filter(category_id = category_obj.id,status='published') return render(request,"bbs/index.html",{'category_list':category_list, 'category_obj':category_obj, 'article_list':article_list})
3.前端html模版文件也不用改動了.
4.點擊訪問相應的板塊,查看結果如圖:
四\上述三已經實現了點擊每個板塊都會顯示相應的內容,可是還有一點,就是當咱們訪問首頁的時候,默認最好顯示的是"所有"板塊的內容.
想實現這個,首先咱們在bbs/views.py裏的index視圖裏要返回給index.html頁面 3個參數
1.板塊列表
2."所有"這個板塊對象,(主要用於讓標籤active)
3. 所有的帖子
1.因而url.py文件不用改
2. bbs/views.py文件更改index視圖
from django.shortcuts import render,HttpResponse from bbs import models # Create your views here. category_list = models.Category.objects.filter(set_as_top_menu =True).order_by('positon_index') def index(request): print(category_list) category_obj = models.Category.objects.get(positon_index=1) # 咱們這裏定義positon_index=1時,這個就是"所有"這個板塊 article_list = models.Article.objects.filter(status='published') return render(request,"bbs/index.html",{'category_list':category_list, 'category_obj':category_obj, 'article_list':article_list}) # return HttpResponse("OK") def category(request,id): category_obj = models.Category.objects.get(id=id) if category_obj.positon_index == 1: #咱們把板塊"所有"認定爲首頁顯示,把全部的文章都顯示出來,首頁就認定當position_index 爲1時既是首頁. article_list = models.Article.objects.filter(status='published') else: article_list = models.Article.objects.filter(category_id = category_obj.id,status='published') return render(request,"bbs/index.html",{'category_list':category_list, 'category_obj':category_obj, 'article_list':article_list})
3.訪問查看結果
作這麼一件事情,讓咱們清晰了一點:
1個url(或動態url) 必定要對應 一個視圖views ,html卻不必定非得要新建
五.下面咱們就得把貼子的詳細內容都展現到前端了.(此步驟就須要咱們去扒虎嗅網站的代碼了)
咱們先不考慮前端的樣式,先把圖片標題以及描述顯示出來,這時候咱們就不能在base.html文件裏寫了,牽扯到具體的內容了,就不能在基礎模版文件中寫了,否則其它頁面怎麼引用呢?
首先要把base.html的表示詳細內容的區域block出來,而後再在index.html頁面進行更改.
base.html詳細內容的部分是:
<div class="container"> {% block page-container %} <!-- Main component for a primary marketing message or call to action --> <div class="jumbotron"> <h2>your owner stuff</h2> </div> {% endblock %} </div>
因而index.html頁面以下:
{% extends 'base.html' %} {% block page-container %} {% for article in article_list %} <div>{{article.head_img}}</div> <div>{{article.title}}</div> <div>{{article.brief}}</div> {% endfor %} {{ article_list }} {% endblock %}
咱們訪問頁面http://127.0.0.1:8000/bbs查看下結果:
首先咱們看這裏顯示有圖片,可是倒是一個字符串,咱們應該用img標籤,因而index.html代碼改爲以下:
{% extends 'base.html' %} {% block page-container %} {% for article in article_list %} <img src="/static/{{article.head_img}}"> #這裏之因此用/static/{{article.head_img}},是由於圖片原本屬於靜態文件,將圖片上傳目錄加入靜態列表中就能夠直接訪問了 <div>{{article.title}}</div> <div>{{article.brief}}</div> {% endfor %} {{ article_list }} {% endblock %}
咱們再次訪問頁面http://127.0.0.1:8000/bbs查看下結果:
爲啥不嫩正常顯示,是否是由於咱們並無把uploads目錄加入到靜態目錄,咱們先設置下settings.py以下:
STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "statics"), os.path.join(BASE_DIR, "uploads"), # '/var/www/static/', ]
而後咱們第三次瀏覽結果以下:
結果依然是顯示不了,可是最起碼和上次訪問不同了.並且此次問題已經很明顯了.顯示的路徑是
http://127.0.0.1:8000/static/uploads/xxxx.jpeg
是這個路徑有問題,你想咱們是把uploads目錄加入到了static,也就是說訪問時url不該該在帶uploads了,應該是
http://127.0.0.1:8000/static/xxxx.jpeg,咱們訪問下試試:
果真應該是這樣,可是咱們經過{{article.head_img}}得到的是帶有uploads/xxx.jpeg的,怎麼把這個處理了,只要xxx.jpeg這部分呢?
首前後端很差在處理了,前端能夠處理.那麼前端如何處理呢?可經過自定義tags來處理.咱們在day19中學習過,這裏咱們就使用這個技術實現.
1. 在bbs目錄下建立templatetags目錄,必須是這個名稱.
2. 建立一個自定義tags標籤文件,咱們咱們新建custom_tags.py文件
3. 在裏面定義一個處理方法,使用filter
這三步實現如圖:
完成後要從新啓動Django,這是Django中爲數很少的改動後須要重啓的操做.
4. 前端html模版文件load這個custom.py文件
5. 而後在for循環時調用自定義的tags方法
如圖:
咱們再次訪問看看結果圖片就OK了!!!
至此咱們圖片能夠顯示了,可是咱們看這個效果是否是很醜,接下來就是咱們使用各類扒網頁手法去儘可能模仿咱們虎嗅網的樣式了.
六\美化咱們的前端html模版
首先咱們看,這個頁面的是否是很大,大的緣由是咱們沒有給這個div設置一個寬度,因此任意圖片自身的大小把頁面給撐大了.
因此咱們先給這個頁面的大小固定一下.
base.html裏有這段代碼
<div class="container"> {% block page-container %} <!-- Main component for a primary marketing message or call to action --> <div class="jumbotron"> <h2>your owner stuff</h2> </div> {% endblock %} </div> <!-- /container -->
咱們的頁面內容主要是放在{% block page-container %} ... {% endblcok %}
而{% block page-container %} ... {% endblcok %}的外層有一個div,因此咱們設置這個外層的div的大小就是設置了整個大小
外層div有一個class="container",這是bootstrap裏的container樣式,咱們最好仍是定義一個本身的,這樣好調整.因此咱們在靜態文件目錄下定義一個statics/bootstrap/css/custom.css文件,裏面寫咱們項目的css樣式,因而要在base.html引入這個樣式文件.
代碼以下
在base.html的 head頭部加入下面這句
<link href="/static/bootstrap/css/custom.css" rel="stylesheet">
把關於頁面帖子內容的代碼的div class改爲自定義的如:
<div class="page-container"> {% block page-container %} <!-- Main component for a primary marketing message or call to action --> <div class="jumbotron"> <h2>your owner stuff</h2> </div> {% endblock %} </div> <!-- /container -->
自定義css樣式文件
如圖:
base文件的更改內容以下
templates/bbs/index.html文件的更改內容以下:
這寫都作好後,我們訪問測試看下樣子:
七\進一步美化前端,將標題和描述移動到上圖的指定處
前面咱們自已作了wrap-left和wrap-right 樣式,樣式中用到了flot屬性.
這裏我要告訴你,咱們base.html中引用了bootstrap組件,這種基本的向左向右飄的樣式,bootstrap確定都有,因此咱們在實現上圖的目標就使用bootstrap裏的樣式.
咱們就直接看代碼,以下:
咱們在看訪問界面:
大概是這樣了,咱們在調整下邊距等等,代碼 (這裏實現不難,就不寫思路了)
咱們看虎嗅的樣子
要實現評論數和點贊數的統計,這裏仍是有些難度,和新知識點.
1.首先咱們知道傳過來的文章而不是評論,評論表經過外鍵關聯文章的,因此統計的話須要用到反向查找的知識即: article.外鍵表name_set.select_related()得到此片文章全部評論和讚的對象列表
2.咱們作的bbs系統,評論和點贊是放在同一張表的,咱們要經過在前端頁面取到article對象後,使用自定義的templatetags處理.
3.新知識點: 處理後返回給前端是一個字典好比{評論數:x,點贊數:y},前面咱們說在前端不能建立變量,這裏要推翻了使用 as 變量名,可是tags不能是filter形勢,而是simple_tag形勢. 記住這個有用的知識點.
下面咱們就在前端頁面bbs/index.html和自定義標籤文件bbs/templatetags裏作修改,不須要修改urls.py,views.py文件.
bbs/templatetags/custom.py
from django import template from django.utils.html import format_html # 引入format_html模塊 register = template.Library() @register.filter def truncate_url(img_obj): #由於使用article.head_img得到到的是headfiled對象,並非一個字符串 print(img_obj.name,img_obj.url) #使用.name和.url均可以獲取字符串如:uploads/1133486643273333.jpeg return img_obj.name.split('/',maxsplit=1)[-1] #使用"/"做爲分隔符,maxsplit表示只作一次分割,[-1]獲取文件名 @register.simple_tag def filter_comment(article_obj): query_set = article_obj.comment_set.select_related() comments = { 'comment_count':query_set.filter(comment_type = 1).count(), 'thumb_count':query_set.filter(comment_type=2).count() }
bbs/index.html
這時候咱們來訪問測試便可:
而後咱們從bootstrap中找到 評論和點讚的圖標,加入到前端html模版中便可,這裏就不寫具體怎麼找了.
代碼以下
訪問結果如圖:
八\點擊某一篇文章的時候,但願能跳轉到該文章的詳細界面.
實現起來很簡單
1.在index.html中帖子的a標籤處添加跳轉連接
2.添加URL,既然跳轉到了新的URL,確定要加一條路由條目了,修改bbs/urls.py文件
from django.conf.urls import url,include from bbs import views urlpatterns = [ url(r'^$', views.index), url(r'category/(\d+)/$',views.category,name='category_detail'), url(r'article/(\d+)/$',views.ariticle_detail,name='article_detail'), ]
3.在bbs/urls.py裏添加views.ariticle_detail這個視圖.
# 定義文章明細頁面的視圖函數
def ariticle_detail(request,id): ariticle_obj = models.Article.objects.get(id = id) return render(request,'bbs/article_detail.html',{'article_obj':ariticle_obj,'category_list':category_list})
4. 接下來咱們要定義一個新的html頁面了:bbs/article_detail.html,代碼以下:
{% extends 'base.html' %} {% load custom_tags %} {% block page-container %} <div class="wrap-left"> <div class="article-title-bg"> {{article_obj.title}} </div> <div class="article-title-brief"> <span>做者:{{article_obj.author.name}}</span> <span>{{article_obj.pub_date}}</span> <span>{% filter_comment article_obj as comments %}</span> <span class="glyphicon glyphicon-comment">{{comments.comment_count}}</span> <span class="glyphicon glyphicon-thumbs-up">{{comments.thumb_count}}</span> </div> <div class="article-content"> <img class="article-detail-img" src="/static/{{article_obj.head_img|truncate_url}}" > {{ article_obj.content}} </div> </div> <div class="wrap-right"> sss </div> <div class="clear-both"></div> {% endblock %}
statics/bootstrap/css/custom.css文件也有相應的更改:
.article-title-bg{ font-size: 30px; /*padding-top: 10px;*/ margin-top: 10px; } .article-title-brief{ color: #999; margin-top: 10px; } .article-detail-img { width: 100%; margin-top: 10px; margin-bottom: 10px; } .article-content{ line-height: 30px; }
固然這些樣式都是很簡單的,我的感受html裏的css多用就會了,不要刻意去記住.
咱們訪問首頁,隨便點一個帖子進入查看結果
九\評論的建立和展現
上面的文章和評論都是咱們經過Django的後臺管理系統admin進行添加的.而在實際使用中,這些評論確定都是由用戶登陸後,在前端本身添加建立的.
理論上老師應該先帶着咱們先寫一個添加文章(即帖子)的頁面,而後在寫一個添加評論的頁面.可是時間有限,咱們挑一個比較難寫的評論頁面來寫.
爲何說評論頁面難寫呢?首先評論和點贊是在一塊兒的;其次評論是有多級別的,這個展現的時候對於如今的咱們算是比較複雜的.下面我就把老師帶着咱們寫的過程描述出來.
首先咱們看虎嗅網站的發表評論的例子:
咱們實現提交評論內容有兩種一種經過form表單,一種是經過ajax,form表單再以前day18和day19咱們已經見識過了,這裏老師將經過ajax的方式提交,因此這個知識點要記住.
咱們要在article_detail.html頁面展現和提交評論因此,評論相關的標籤添加的位置如圖:
ps:使用ajax實現,會引入另一個知識點,csrf,往下看把.
1.首先咱們先加入評論的前端代碼:
<div class="comment-box"> {% if request.user.is_authenticated %} <textarea class="form-control" rows="3"></textarea> <button type="button" style="margin-top: 5px" class="btn btn-success pull-right">評論</button> {% endif %} </div>
2.訪問查看效果如圖:
3.要實現上圖中描述的點擊"評論",要對輸入的內容進行驗證.將要使用jquery.
4.你要在article_detail.html中寫js ,那就意味着你要在base.html中又一個block專門寫js的.因而我恩先在base.html中加入下面一段html代碼
5. 另外咱們知道在jquery章節中,曾經有一個知識點是講如何實如今jquery沒加載完時把整個頁面框架顯示給用戶來增長用戶的體驗感.
專業術語稱之爲,加載文檔樹結構.所以咱們要把article_detail.html中要寫的jquery寫到下面的區域內:
具體的js代碼以下:
這裏的callback,就是後臺在接收到ajax提交的數據後,執行完views視圖後return的值.
具體的代碼以下:
<script> $(document).ready(function(){ $(".comment-box button").click(function(){ var comment_text = $(".comment-box textarea").val(); if (comment_text.trim().length < 5){ alert("評論不能少於5個字sb") }else{ //post $.post("{% url 'post_comment' %}", { 'commnet_type':1, 'article_id':"{{article_obj.id}}", parent_commet_id:null, 'comment':comment_text.trim() },//end post args function(callback){ console.log(callback) });//end post }; });//end button click }); </script>
6.接下來咱們來寫一個 用於提交平路的URL
urlpatterns = [ url(r'^$', views.index), url(r'^category/(\d+)/$',views.category,name='category_detail'), url(r'^detail/(\d+)/$',views.ariticle_detail,name='article_detail'), url(r'^comment/$',views.comment,name='post_comment'), ]
7.接下來添加一個視圖函數
def comment(request): print(request.POST) return HttpResponse('dddd')
這裏只是爲了測試.
8.咱們提交測試發現前端和後端後有錯誤如圖:
和
咱們看到報錯的緣由是CSRF的緣由,下面就來講明下CSRF
9.CSRF是什麼東西?
CSRF(Cross Site Request Forgery, 跨站域請求僞造)
CSRF 背景與介紹
CSRF(Cross Site Request Forgery, 跨站域請求僞造)是一種網絡的攻擊方式,它在 2007 年曾被列爲互聯網 20 大安全隱患之一。其餘安全隱患,好比 SQL 腳本注入,跨站域腳本攻擊等在近年來已經逐漸爲衆人熟知,不少網站也都針對他們進行了防護。然而,對於大多數人來講,CSRF 卻依然是一個陌生的概念。即使是大名鼎鼎的 Gmail, 在 2007 年末也存在着 CSRF 漏洞,從而被黑客攻擊而使 Gmail 的用戶形成巨大的損失。
10.咱們在使用form提交post的時候,都會在form標籤內加上{% csrf_token %}
加上去的目的,就是爲了當提交form表單裏的內容的時候,會把form裏的csrf的鍵值對提交.
當不使用form表單提交時就須要先獲取到服務器反給瀏覽器的csrf的value值.
咱們能夠如今頁面中寫上{% csrf_token %}查看源碼.PS:這裏我終於明白了模版語言{{}}和{%%}的區別,明白區別了就好用了.
模版語言中{{}} 裏面的變量直接取出的就是字符串.而{%%} 裏面取出的是對象,如圖
和
PS:寫在base.html中是由於還有其它頁面會用到csrf_token,因此寫在基礎模版文件base.html中
哈哈,知道了能夠模版語言的{{}} 和{% %}的區別,我感受很爽.
既而後臺要驗證我提交ajax時的csrf_token值,那麼我就把csrf_token的key和value傳到後臺就完事了.
咱們看{% csrf_token %}對象是一個html代碼.因此還不能用{{ csrf_token.name }} 和{{ csrf_token.value }}來得到 key和value,而是須要寫js得到到key和value.
{% block bottom-js %} <script> function getCsrf(){ return $("input[name='csrfmiddlewaretoken']").val(); } $(document).ready(function(){ $(".comment-box button").click(function(){ var comment_text = $(".comment-box textarea").val(); if (comment_text.trim().length < 5){ alert("評論不能少於5個字sb") }else{ //post $.post("{% url 'post_comment' %}", { 'commnet_type':1, 'article_id':"{{article_obj.id}}", parent_commet_id:null, 'comment':comment_text.trim(), 'csrfmiddlewaretoken':getCsrf() },//end post args function(callback){ console.log(callback) });//end post }; });//end button click }); </script> {% endblock %}
至此咱們已經能夠經過ajax進行post提交了.只是這裏後臺還未作保存操做.
11.論壇網站評論的前提條件是登陸,若是沒有登陸,會顯示如圖的標籤.
這就須要咱們判斷用戶是否是登陸了.至於上圖這個框,能夠去bootstrap中找.最終代碼以下:
<div class="comment-box"> {% if request.user.is_authenticated %} <textarea class="form-control" rows="3"></textarea> <button type="button" style="margin-top: 5px" class="btn btn-success pull-right">評論</button> {% else %} <div class="jumbotron"> <p style="text-align:center;"><a href="{% url 'login' %}" style="color: blue">登陸</a>後參與評論</p> </div> {% endif %} </div>
這樣就實現了,可是這時候有一個問題,當你登陸後login頁面會跳轉到首頁,而咱們但願的是跳轉到咱們點登陸的頁面.這個如何實現呢?
咱們能夠在跳轉到login頁面時get方式給它傳遞一個參數,login的視圖函數拿到這個參數的值的時候,做爲登陸成功後的跳轉的url就行.
因而要改html代碼和後臺的login視圖函數以下:
<div class="comment-box"> {% if request.user.is_authenticated %} <textarea class="form-control" rows="3"></textarea> <button type="button" style="margin-top: 5px" class="btn btn-success pull-right">評論</button> {% else %} <div class="jumbotron"> <p style="text-align:center;"><a href="{% url 'login' %}?next={{ request.path }}" style="color: blue">登陸</a>後參與評論</p> </div> {% endif %} </div>
這時候咱們在明細頁面點擊登陸時,跳轉到login界面的URL同時帶着參數以下:
下面咱們在來改login的視圖函數
12 .OK,下面就開始把接收到的評論保存到數據庫
def comment(request): print(request.POST) if request.method == 'POST': new_comment_obj = models.Comment( article_id = request.POST.get('article_id'), parent_comment_id = request.POST.get('parent_commet_id' or None), comment_type = request.POST.get("comment_type"), user_id = request.user.userprofile.id, #這裏要主要,咱們在bbs系統用戶驗證用的是Django自帶的用戶驗證模塊,通過驗證的用戶實際上是admin的後臺帳戶,咱們在前臺是userprofile和admin的user作了1對1的外鍵關聯. #因此這裏是 request.user.userprofile.id而不是request.userprofile.id comment = request.POST.get('comment'), ) new_comment_obj.save() return HttpResponse('post-comment-success')
13.到這裏纔是咱們要作的重頭戲.展現評論.(之因此說是重頭戲,是由於有層級,以及有點贊和評論的區別.)
點贊和評論很好區分,在查關於某一篇文章的評論時,直接過濾掉點贊,由於點讚的comment_type =2,
在而後咱們知道評論在從屬上沒有規律而言,可是他在時間上是有規律的.因此查找到關於某一篇文章的評論後咱們按時間排序.
這些評論都按照時間排序了,在一個列表中了.接下來就是,在前端如何顯示這些評論了.
13.1如何顯示呢?咱們假如後臺返回給前端的是一個字典,字典就是按照評論的從屬關係排列的,以下:
拿到上面的結構,咱們就能夠用遞歸的方式,把頁面展現出來了.遞歸的思路就是先深度排列完,在進行廣度排列.
13.2 後端如何把按時間排序的列表,轉化成這種按從屬關係的字典呢?
仍是要用到遞歸.遞歸的思路,當沒有父級評論的時候放到第一級,當有時,就便利整個字典,找到它的父級,把它放到父級的字典元素中.
你會想,萬一找不到父級呢?告訴你不可能. 首先列表是按照時間排序的.子級的位置不可能比父級先出現.因此當你有父級的時候,說明你的父級已經排進字典過了.
下面咱們就寫一個把一個列表排列成字典的函數.這個就不放倒views.py文件裏了.由於他不是視圖函數.單首創建一個文件叫bbs/comment_hander.py
#!/usr/bin/env python3.5 # -*- coding:utf-8 -*- # Author:Zhou Ming def add_node(tree_dic,comment): if comment.parent_comment is None: #若是個人父評論爲None,表明我是頂層 tree_dic[comment] = {} else: # 循環當前整個字典,直到找到爲止 for k,v in tree_dic.items(): if k == comment.parent_comment: #找到了父級評論 print("find dad.",k) tree_dic[k][comment] = {} else: #進入下一層繼續找 print("keep going deeper ...") add_node(v,comment) def build_tree(comment_set): print(comment_set) tree_dic = {} for comment in comment_set: add_node(tree_dic,comment)
總結:實現把列表轉化成有從屬關係的字典主要用到的知識點是遞歸.遞歸函數咱們以前學習過,可是實際應用場景又很模糊.
咱們來看上面的這個例子.一個列表.轉換成有從屬關係的字典.若是沒有參考老師給的例子,我會想直接寫一個函數.遞歸也在這個函數下進行.
可是咱們來分析下咱們實現把列表 轉換成字典 的需求實現思路:
循環每個元素,元素去遍歷一個字典,字典中確定有哪個子元素的key是這個列表元素的父親.那咱們要考慮這個遞歸究竟是哪一步循環須要遞歸.
假設如今只有一個元素,要把這個元素插入到指定的字典中,首先咱們遍歷字典的第一層,第一層沒有緊接着是遍歷下一層.因此這個遞歸函數的做用是把一個元素插入到字典中.也有是上面老師寫的函數add_node函數
因此真正須要遞歸實現的是把每個元素 加入到一個字典.而有多少給元素,就是對這個列表進行for循環了.因此單獨寫一個把元素加入到指定字典的遞歸函數add_node().在寫一個函數循環每個元素也就是build_tree函數,調用這個遞歸函數.
不得不說老師仍是牛啊.
同理咱們前端展現的時候.也要遞歸展現這個函數.那麼咱們考慮對哪個環節進行遞歸呢.
假設只有一個主評論,每個主評論有不少分支.那麼要遞歸展現的就是把這個主評論.因此展現的時候遞歸函數以下:
def render_tree_node(tree_dic,margin_val): html = "" for k,v in tree_dic.items(): ele = "<div class = 'comment-node' style='margin-left:%spx'> "%margin_val + k.comment + "</div>" html += ele html += render_tree_node(v,margin_val+10) return html def render_comment_tree(tree_dic): html = "" for k,v in tree_dic.items(): ele = "<div class = 'root-comment'>" + k.comment + "</div>" html += ele html += render_tree_node(v,10) return html
咱們在前端加一個button按鈕,點擊這個按鈕就使用$.get方法把評論獲取到前端頁面中來.PS:$.get()方法和$.post()方法使用方法差很少.如圖:
具體的代碼以下(可看可不看):
{% extends 'base.html' %} {% load custom_tags %} {% block page-container %} <div class="wrap-left"> <div class="article-title-bg"> {{article_obj.title}} </div> <div class="article-title-brief"> <span>做者:{{article_obj.author.name}}</span> <span>{{article_obj.pub_date}}</span> <span>{% filter_comment article_obj as comments %}</span> <span class="glyphicon glyphicon-comment">{{comments.comment_count}}</span> <span class="glyphicon glyphicon-thumbs-up">{{comments.thumb_count}}</span> </div> <div class="article-content"> <img class="article-detail-img" src="/static/{{article_obj.head_img|truncate_url}}" > {{ article_obj.content}} </div> <div class="comment-box"> {% if request.user.is_authenticated %} <textarea class="form-control" rows="3"></textarea> <button type="button" style="margin-top: 5px" class="btn btn-success pull-right">評論</button> {% else %} <div class="jumbotron"> <p style="text-align:center;"><a href="{% url 'login' %}?next={{ request.path }}" style="color: blue">登陸</a>後參與評論</p> </div> {% endif %} <button type="button" onclick="GetComments()">測試獲取評論</button> <div class="comment-list" style="margin-left: 10px"> </div> </div> </div> <div class="wrap-right"> sss </div> <div class="clear-both"></div> {% endblock %} {% block bottom-js %} <script> function GetComments(){ $.get("{% url 'get_comments' article_obj.id %}",function(callback){ console.log(callback); $(".comment-list").html(callback); }); } function getCsrf(){ return $("input[name='csrfmiddlewaretoken']").val(); } $(document).ready(function(){ $(".comment-box .btn").click(function(){ var comment_text = $(".comment-box textarea").val(); if (comment_text.trim().length < 5){ alert("評論不能少於5個字sb") }else{ //post $.post("{% url 'post_comment' %}", { 'comment_type':1, 'article_id':"{{ article_obj.id }}", parent_commet_id:null, 'comment':comment_text.trim(), 'csrfmiddlewaretoken':getCsrf() },//end post args function(callback){ console.log(callback) if (callback == 'post-comment-success'){ alert('successful') } });//end post }; });//end button click }); </script> {% endblock %}
固然咱們這裏還要加一個url,用於點擊"測試獲取評論"按鈕時返回數據.
from django.conf.urls import url,include from bbs import views urlpatterns = [ url(r'^$', views.index), url(r'^category/(\d+)/$',views.category,name='category_detail'), url(r'^detail/(\d+)/$',views.ariticle_detail,name='article_detail'), url(r'^comment/$',views.comment,name='post_comment'), url(r'^comment_list/(\d+)/$',views.get_comments,name='get_comments'), ]
而後咱們在視圖中添加get_comments函數,以下:
def get_comments(request,article_id): article_obj = models.Article.objects.get(id=article_id) comment_tree = comment_hander.build_tree(article_obj.comment_set.select_related()) tree_html = comment_hander.render_comment_tree(comment_tree) return HttpResponse(tree_html)
返回tree_html時,頁面並無刷新,而是直接展現內容.我想之因此未刷新就是由於返回的不是新頁面.而是字符串.
也許返回字符串也就是ajax吧?
咱們訪問測試,查看結果以下:
至此後臺評論展現算是高一小段落.
可是咱們看評論刷出來的很很差看,接下來咱們來美化下.
咱們在生成評論的時候,遞歸生成的是標籤,而且在標籤中添加了樣式.美化的時候咱們就能夠直接給這個樣式定義些屬性.
編輯statics/bootstrap/css/custom.css文件,添加評論相關的兩個相關的class
.comment-node{ border: 1px solid darkgray; padding: 5px; } .root-comment{ border: 2px solid darkblue; padding: 5px; }
咱們看虎嗅網評論還有時間以及評論者以及是否是有人點贊.因而咱們的生成評論的遞歸函數要改爲以下(主要是加一些字段):
def render_tree_node(tree_dic,margin_val): html = "" for k,v in tree_dic.items(): ele = "<div class = 'comment-node' style='margin-left:%spx'> "%margin_val + k.comment + "<span style='margin-left:10px'>%s</span>"%k.date \ + "<span style='margin-left:10px'>%s</span>"%k.user.name + "</div>" html += ele html += render_tree_node(v,margin_val+10) return html def render_comment_tree(tree_dic): html = "" for k,v in tree_dic.items(): ele = "<div class = 'root-comment'>" + k.comment+ "<span style='margin-left:10px'>%s</span>"%k.date \ + "<span style='margin-left:10px'>%s</span>"%k.user.name + "</div>" html += ele html += render_tree_node(v,10) return html
訪問測試結果:
今天的內容大概就那麼多了.接下來的內容在day21節繼續.