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.分析业务,添加表字段
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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" οnclick="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节继续.