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节继续.