博客系统简要
博客园整体上是用Django框架完成的
Django的生命周期和主要内容在前面已经讲过,现在来详述该项目的主要内容。
建一个Django项目blogCMS1,再创一个APP应用名为blog
首先我们要知道,一个网站的数据库是由多张表组成的,而在Django中一张表对应的是一个类,所以我们首先要做的是建立好类,
这就需要我们花时间去弄明白博客园中需要多少张表去存放多少数据。
以下是我设计的表结构(13张表)
1 #项目下的model.py文件中代码: 2 from django.db import models 3 # Create your models here. 4 from django.contrib.auth.models import AbstractUser 5 6 7 class UserInfo(AbstractUser): 8 """ 9 用户信息,只存放最主要的字段 10 """ 11 nid = models.BigAutoField(primary_key=True) 12 nickname = models.CharField(verbose_name='昵称', max_length=32) 13 telephone = models.CharField(max_length=11, blank=True, null=True, unique=True, verbose_name='手机号码') 14 avatar = models.FileField(verbose_name='头像', upload_to='avatar', default="/avatar/default.jpeg") 15 create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True) 16 17 def __str__(self): 18 return self.username 19 20 21 class Blog(models.Model): 22 """ 23 站点信息,每个注册用户有一个自己的个人站点 24 """ 25 nid = models.BigAutoField(primary_key=True) 26 title = models.CharField(verbose_name='个人博客标题', max_length=64) 27 site = models.CharField(verbose_name='个人博客后缀', max_length=32, unique=True) 28 theme = models.CharField(verbose_name='博客主题', max_length=32) 29 # 与用户信息表一对一关联,关联字段建在blog表中 30 user = models.OneToOneField(to='UserInfo', to_field='nid') 31 32 def __str__(self): 33 return self.title 34 35 36 class Article(models.Model): 37 """ 38 文章表,存放着所有该用户站点发布的文章 39 """ 40 nid = models.BigAutoField(primary_key=True) 41 title = models.CharField(max_length=50, verbose_name='文章标题') 42 desc = models.CharField(max_length=255, verbose_name='文章描述') 43 read_count = models.IntegerField(default=0) 44 comment_count = models.IntegerField(default=0) 45 up_count = models.IntegerField(default=0) 46 down_count = models.IntegerField(default=0) 47 create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True) 48 #一个文章只能属于一个分类,而一个分类下有多个文章,所以外键建在文章表 49 category = models.ForeignKey(verbose_name='文章类型', to='Category', to_field='nid', null=True) 50 #一个用户可以发表多篇文章,而一个文章只能由一个用户发表 51 user = models.ForeignKey(verbose_name='所属用户', to='UserInfo', to_field='nid') 52 #文章中有关键字,关键字可以看做是标签,文章与标签的关系是多对多 53 tags = models.ManyToManyField( 54 to="Tag", 55 through='Article2Tag', 56 through_fields=('article', 'tag'),#自己建的多对多关联的第三张表 57 ) 58 #博客园首页的总的文章大分类下的小分类,与个人站点分类无关,该分类与文章也是一对多关系 59 site_article_category=models.ForeignKey("SiteArticleCategory",null=True) 60 61 def __str__(self): 62 return self.title 63 64 65 class ArticleDetail(models.Model): 66 """ 67 文章详细表 68 """ 69 nid = models.AutoField(primary_key=True) 70 content = models.TextField(verbose_name='文章内容', ) 71 #若文章表将所有文章内容存入,数据量会很大,所以我们用一个详情表存储,与文章表一对一 72 article = models.OneToOneField(verbose_name='所属文章', to='Article', to_field='nid') 73 def __str__(self): 74 return self.article.title 75 76 77 class Category(models.Model): 78 """ 79 博主个人文章分类表 80 """ 81 nid = models.AutoField(primary_key=True) 82 title = models.CharField(verbose_name='分类标题', max_length=32) 83 #与站点进行多对一关联,外键必须建在多的一方 84 blog = models.ForeignKey(verbose_name='所属博客', to='Blog', to_field='nid') 85 86 def __str__(self): 87 return self.title 88 89 class Meta: 90 verbose_name = 'category' 91 verbose_name_plural = 'category' 92 ordering = ['title'] 93 94 95 class Tag(models.Model): 96 """ 97 标签表 98 """ 99 nid = models.AutoField(primary_key=True) 100 title = models.CharField(verbose_name='标签名称', max_length=32) 101 blog = models.ForeignKey(verbose_name='所属博客', to='Blog', to_field='nid') 102 103 104 class Article2Tag(models.Model): 105 """ 106 文章和标签的多对多关系表,自己创第三张表的好处是可以自定义表的字段 107 """ 108 nid = models.AutoField(primary_key=True) 109 article = models.ForeignKey(verbose_name='文章', to="Article", to_field='nid') 110 tag = models.ForeignKey(verbose_name='标签', to="Tag", to_field='nid') 111 112 class Meta: 113 #文章和标签联合唯一 114 unique_together = [ 115 ('article', 'tag'), 116 ] 117 118 119 class Comment(models.Model): 120 """ 121 评论表 122 """ 123 nid = models.BigAutoField(primary_key=True) 124 content = models.CharField(verbose_name='评论内容', max_length=255) 125 create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True) 126 up_count = models.IntegerField(default=0)#点赞数 127 128 user = models.ForeignKey(verbose_name='评论者', to='UserInfo', to_field='nid') 129 article = models.ForeignKey(verbose_name='评论文章', to='Article', to_field='nid') 130 #自关联,用于判断该评论是对文章的评论还是对评论的评论 131 parent_comment = models.ForeignKey('self', blank=True, null=True, verbose_name='父级评论') 132 133 def __str__(self): 134 return self.content 135 136 137 class CommentUp(models.Model): 138 """ 139 点评论赞表 140 """ 141 142 nid = models.AutoField(primary_key=True) 143 user = models.ForeignKey('UserInfo', null=True) 144 comment = models.ForeignKey("Comment", null=True) 145 146 147 class ArticleUp(models.Model): 148 """ 149 点文章赞表 150 """ 151 nid = models.AutoField(primary_key=True) 152 user = models.ForeignKey('UserInfo', null=True) 153 article = models.ForeignKey("Article", null=True) 154 155 156 class ArticleDown(models.Model): 157 """ 158 点down表 159 """ 160 nid = models.AutoField(primary_key=True) 161 user = models.ForeignKey('UserInfo', null=True) 162 article = models.ForeignKey("Article", null=True) 163 164 165 class SiteCategory(models.Model): 166 """ 167 首页的大分类表,与个人站点分类无关,该分类与文章不直接绑定关系, 168 因为该分类下还有小分类,小分类才与文章绑定关系 169 170 大分类(例如计算机编程语言)——> 171 小分类(例如python——>文章1 172 文章2 173 …… 174 java——>文章() 175 …… 176 c++——>文章() 177 …… 178 ……) 179 180 """ 181 name=models.CharField(max_length=32) 182 183 184 def __str__(self): 185 return self.name 186 187 class SiteArticleCategory(models.Model): 188 """ 189 首页的大分类下的小分类表,与大分类是多对一关系,与个人站点的分类无关 190 """ 191 name=models.CharField(max_length=32) 192 site_category=models.ForeignKey('SiteCategory') 193 194 195 def __str__(self): 196 return self.name
建立完表关系后,数据库就有了。接下来我们分配路由。
首先我们知道一个网站必须有一个主网页即首页,其次肯定还有其他的网页内容,所以我们需要匹配很多的URL。原则上是有一个网页的需求配一个URL,然后再分配好视图函数view,最后再关联上相应的模板页面html。
在这里我将所有URL分为两部分:
第一部分:网站的主要网址
第二部分:与个人站点有关的网址
1 #urls: 2 from django.conf.urls import url,include 3 from django.contrib import admin 4 from django.views.static import serve 5 from blog import views 6 from blogCMS1 import settings 7 8 urlpatterns = [ 9 url(r'^admin/', admin.site.urls),#Django自带 10 11 url(r'^reg/', views.reg), # 注册功能 12 url(r'^login/', views.log_in),#登录功能 13 url(r'^log_out/', views.log_out),#注销功能 14 url(r'^get_validCode_img/', views.get_validCode_img), # 验证码功能 15 url(r'^index/', views.index),#首页功能 16 url(r'^$', views.index),#只输入域名的情况下默认进入首页 17 url(r'^cate/(?P<site_article_category>.*)/$', views.index), # 首页的小分类 18 # index(requset,site_article_category=python) 19 20 # 个人站点首页,将个人站点需要用到的路由下发到blog下的urls中 21 url(r'^blog/', include('blog.urls')),#路由分发 22 23 24 #media 配置:我们将网站中上传的文件放置于media文件夹中, 25 # 因为该文件夹是新创建的 26 # 所以我们需要在settings中进行相应配置,稍后会有配置的代码 27 28 url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}), 29 url(r'^uploadFile/$', views.uploadFile),#添加文章时上传图片 30 31 ]
1 #个人应用下的urls(这里应用名为blog): 2 from django.conf.urls import url,include 3 from blogCMS1 import settings 4 from blog import views 5 urlpatterns = [ 6 #用户修改个人资料功能 7 url(r'^edituser/(\d+)$', views.edituser), 8 #个人站点首页,需要渲染个人分类、标签和日期的归档,所以需要用到正则 9 url(r'^(?P<username>.*)/(?P<condition>tag|category|date)/(?P<para>.*)', views.homeSite), 10 #路由反向解析,在模板页面中直接用别名即可 11 url(r'^(?P<username>.*)', views.homeSite,name="aaa"), 12 #文章详情页面 13 url(r'^(?P<username>.*)/article_detail/(?P<article_nid>\d+)', views.article_detail), 14 #文章赞、踩功能 15 url(r'^updown/$', views.updown), 16 url(r'^comment/$', views.comment), #文章评论功能 17 #显示文章已发表的评论,树状结构 18 url(r'^commentTree/(?P<article_id>\d+)$', views.commentTree), 19 url(r'^backend/$', views.backendindex),#后台管理首页 20 url(r'^backend/addarticle/$', views.addarticle),#后台管理添加新文章功能 21 url(r'^backend/editarticle/(\d+)$', views.editarticle),#编辑文章 22 url(r'^backend/del/(\d+)', views.delarticle),#删除文章 23 24 ]
上面提到了media配置,这里我们要提一下。media是静态文件,Django中的静态文件有static和media两种,前者用来存放css、js等文件,后者则是用户上传的图片。这两种静态文件都需要我们自己在settings中进行配置后才能使用。
''' 静态文件的处理又包括STATIC和MEDIA两类,这往往容易混淆,在Django里面是这样定义的: #static的配置: STATIC_URL = '/static/' # 别名 STATICFILES_DIRS = ( os.path.join(BASE_DIR,"static"), #实际名 ,即实际文件夹的名字 ) ''' 注意点1: django对引用名和实际名进行映射,引用时,只能按照引用名来,不能按实际名去找 <script src="/statics/jquery-3.1.1.js"></script> ------error-----不能直接用,必须用STATIC_URL = '/static/': <script src="/static/jquery-3.1.1.js"></script> 注意点2: STATICFILES_DIRS = ( ("app01",os.path.join(BASE_DIR, "app01/statics")), ) <script src="/static/app01/jquery.js"></script> ''' ################################### MEDIA:指用户上传的文件,比如在Model里面的FileFIeld,ImageField上传的文件。如果你定义 MEDIA_ROOT=c:\temp\media,那么File=models.FileField(upload_to="abc/")#,上传的文件就会被保存到c:\temp\media\abc eg: class blog(models.Model): Title=models.charField(max_length=64) Photo=models.ImageField(upload_to="photo") 上传的图片就上传到c:\temp\media\photo,而在模板中要显示该文件,则在这样写 在settings里面设置的MEDIA_ROOT必须是本地路径的绝对路径,一般是这样写: BASE_DIR= os.path.abspath(os.path.dirname(__file__)) MEDIA_ROOT=os.path.join(BASE_DIR,'media/').replace('\\','/') MEDIA_URL是指从浏览器访问时的地址前缀,举个例子: MEDIA_ROOT=c:\temp\media\photo MEDIA_URL="/data/" 在开发阶段,media的处理由django处理: 访问http://localhost/data/abc/a.png就是访问c:\temp\media\photo\abc\a.png 在模板里面这样写<img src="/media/abc/a.png"> 在部署阶段最大的不同在于你必须让web服务器来处理media文件,因此你必须在web服务器中配置, 以便能让web服务器能访问media文件 以nginx为例,可以在nginx.conf里面这样: location ~/media/{ root/temp/ break; } 具体可以参考如何在nginx部署django的资料。 '''
在本项目中,static和media的配置代码如下:
STATIC_URL = '/static/'#别名,引用时的名字 STATICFILES_DIRS=[ os.path.join(BASE_DIR, 'blog','static'),#别名所指的实际文件夹路径 ] #别名所指的实际文件夹路径 MEDIA_ROOT=os.path.join(BASE_DIR,"blog","media","uploads") MEDIA_URL="/media/"#别名 #model中userinfo表继承auth模块 AUTH_USER_MODEL = "blog.UserInfo"
以上全部创建完毕,进行数据库迁移后的项目目录应该是下面这个样子
接下来我们要做的就是对视图函数进行编写,视图函数应该对应URL进行编写,并映射到相应的模板中。
注:并不是一条视图函数对应一个模板,根据实际需求,有的视图函数也许是重定向功能。
网站首页:
url(r'^index/', views.index),#首页功能
url(r'^$', views.index),#只输入域名的情况下默认进入首页
url(r'^cate/(?P<site_article_category>.*)/$', views.index), # 首页的小分类
# index(requset,site_article_category=python)
#views中:
from django.shortcuts import render,HttpResponse,redirect
def index(request,*args,**kwargs): print('首页首页首页首页首页首页首页首页首页') if kwargs:#用户点击超链接,显示指定小分类下的文章 article_list=models.Article.objects.filter(site_article_category__name=kwargs.get("site_article_category")) else:#默认,显示全部文章 article_list=models.Article.objects.all() cate_list=models.SiteCategory.objects.all()#所有大分类 return render(request,'index.html',{'article_list':article_list,'cate_list':cate_list})
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>首页</title> <script src="/static/jquery-3.2.1.min.js"></script> <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.css"> <script src="/static/bootstrap-3.3.7-dist/js/bootstrap.js"></script> <link rel="stylesheet" href="/static/mycss/index.css"> </head> <body> {#导航条#} <nav class="navbar navbar-inverse"> <div class="container"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">博客园</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="/">首页 <span class="sr-only">(current)</span></a></li> <li><a href="#">Link</a></li> <li><a href="#">Link</a></li> <li><a href="#">Link</a></li> </ul> <ul class="nav navbar-nav navbar-right"> {% if request.user.is_authenticated %} {# 用户登录状态,显示用户信息#} <li class="active"><a>欢迎:</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> {{ request.user.username }} <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="/blog/{{ request.user.username }}">个人首页</a></li> <li><a href="/blog/backend/">文章管理</a></li> <li><a href="/blog/edituser/{{ request.user.nid }}">修改资料</a></li> <li role="separator" class="divider"></li> <li><a href="/log_out/">注销</a></li> </ul> </li> {% else %} {# 未登录状态,显示登录、注册按钮#} <li><a href="/login/">登录</a></li> <li><a href="/reg/">注册</a></li> {% endif %} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> {#内容#} <div class="container"> <div class="row"> <div class="left1 col-md-2"> {# 左侧菜单#} <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">网站分类</h3> </div> <div class="panel-body"> {% for cate in cate_list %} {# 文章大分类归档#} <div class="panel panel-info menu"> <div class="panel-heading "> <h3 class="panel-title"> {{ cate.name }} </h3> </div> <div class="panel-body hides"> {% for sitearticlecategory in cate.sitearticlecategory_set.all %} {# 文章小分类#} <p><a href="/cate/{{ sitearticlecategory.name }}">{{ sitearticlecategory.name }}</a> </p> {% endfor %} </div> </div> {% endfor %} </div> </div> </div> {# 文章显示部分#} <div class="middle1 col-md-8"> {% for article in article_list %} <div class="article"> <div class="articlt-head"> <a href="/blog/{{ article.user.username }}/article_detail/{{ article.nid }}">{{ article.title }}</a> </div> <div class="content row"> <div class="col-md-2"> {# 用户头像#} <a href="{% url 'aaa' article.user.username %}"><img src="{{ article.user.avatar.url }}" alt="" width="100" height="100"></a> </div> <div class="col-md-10"> {{ article.desc }} </div> </div> <div class="pub-info row">{#发布信息#} <a href="/blog/{{ article.user.username }}"> {{ article.user.username }} </a> <span>发布于</span> {{ article.user.create_time|date:"Y-m-d H:i" }} <a href="/blog/{{ article.user.username }}/article_detail/{{ article.nid }}"><span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }})</a> <a href="/blog/{{ article.user.username }}/article_detail/{{ article.nid }}"><span class="glyphicon glyphicon-thumbs-up"></span>点赞({{ article.up_count }})</a> </div> </div> {% endfor %} </div> <div class="right1 col-md-2"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Panel title</h3> </div> <div class="panel-body"> Panel content </div> </div> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Panel title</h3> </div> <div class="panel-body"> Panel content </div> </div> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Panel title</h3> </div> <div class="panel-body"> Panel content </div> </div> </div> </div> </div> <script> $('.menu').mouseover(function () { {# 鼠标移动到大分类上时显示它所属的小分类#} $(this).children().slideDown(300) }).mouseleave(function () { {# 鼠标从大分类上移开时隐藏它所属的小分类#} $(this).children(".panel-body").slideUp(300) }) </script> </body> </html>
.left1 { text-align: center; font-size: large; } .articlt-head{ font-size: 20px; margin-bottom: 10px; } .article{ padding: 10px 10px; border-bottom: solid 1px; } .hides{ display: none; }
注册页面:
url(r'^reg/', views.reg), # 注册功能
这里我们会用到一个form组件,form组件可以帮我们更方便的处理有关form表单的操作。在blog下新建一个form.py,专门用来写form组件。
#form.py中: from django import forms from django.forms import widgets,ValidationError from blog import models #注册页面的form组件 class RegForm(forms.Form):#必须继承 username=forms.CharField(max_length=32, min_length=5, error_messages={'required':'用户名不能为空'}, widget=widgets.TextInput(attrs={"class":"form-control","placeholder":"请输入用户名"}) ) password=forms.CharField(max_length=32, min_length=6, widget=widgets.PasswordInput( attrs={"class": "form-control", "placeholder": "请输入密码"} )) repeat_pwd = forms.CharField(min_length=6, widget=widgets.PasswordInput( attrs={"class": "form-control", "placeholder": "请再次输入密码"} )) email = forms.EmailField(widget=widgets.EmailInput( attrs={"class": "form-control", "placeholder": "请输入邮箱"} )) def clean_username(self): """ 钩子函数,判断用户名是否已存在 :return: """ ret = models.UserInfo.objects.filter(username=self.cleaned_data.get("username")) if not ret: return self.cleaned_data.get("username") else: raise ValidationError("用户名已注册") def clean_password(self): """ 钩子函数,规定密码不能是全数字或全字母 :return: """ ret = self.cleaned_data.get("password") if ret.isdigit(): raise ValidationError("密码不能全是数字") elif ret.isalpha(): raise ValidationError("密码不能全是字母") else: return ret def clean(self): """ 全局钩子函数,判断两次密码是否一致 :return: """ if self.cleaned_data.get("password") == self.cleaned_data.get("repeat_pwd"): return self.cleaned_data else: raise ValidationError("两次密码不一致")
#views中:
def reg(request): if request.is_ajax():#ajax提交 form_obj=RegForm(request.POST)#拿到页面提交的form对象 print('................',form_obj) regResponse={"user":None,"errorsList":None} if form_obj.is_valid():#数据正常 username=form_obj.cleaned_data["username"] password=form_obj.cleaned_data["password"] email=form_obj.cleaned_data.get("email") pic=request.FILES.get("pic")#头像 #用户表中添加一条信息,即增加一条表记录 user_obj=models.UserInfo.objects.create_user(username=username,password=password,email=email,avatar=pic,nickname=username) print(user_obj.avatar,"......")#路径 regResponse["user"]=user_obj.username else:#数据异常 regResponse["errorsList"]=form_obj.errors#异常信息 import json #ajax返回的数据必须是json字符串 return HttpResponse(json.dumps(regResponse)) form=RegForm()#实例化组件 return render(request,'reg.html',{'form':form})
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>注册</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.css"> <script src="/static/jquery-3.2.1.min.js"></script> <link rel="stylesheet" href="/static/mycss/reg.css"> <script src="/static/js/jquery.cookie.js"></script> </head> <body> <div class="container"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <form action=""> {% csrf_token %}{#Django自带的防止攻击的中间件#} <div class="form-group"> <label for="username">用户名:</label> {{ form.username }} </div> <div class="form-group"> <label for="password">密码:</label> {{ form.password }} </div> <div class="form-group"> <label for="repeat_pwd">确认密码:</label> {{ form.repeat_pwd }} </div> <div class="form-group"> <label for="email">邮箱:</label> {{ form.email }} </div> <div class="form-group picDiv"> <label for="picBtn">头像:</label> <img id="pic" class="pic" src="/static/img/default.png" alt=""> <input id="picBtn" type="file" class="picBtn"> </div> <div class="sub" style="padding-top: 50px"> <input type="button" value="submit" class="btn btn-primary" id="subBtn"><span class="error"></span> </div> </form> </div> </div> </div> <script> // 头像预览,固定代码 $("#picBtn").change(function () { var ele_file=$(this)[0].files[0]; //this.files var reader=new FileReader(); reader.readAsDataURL(ele_file); reader.onload=function () { $("#pic")[0].src=this.result } }); // ajax提交数据 $("#subBtn").click(function () { var formdata=new FormData(); formdata.append("username",$("#id_username").val()); formdata.append("password",$("#id_password").val()); formdata.append("repeat_pwd",$("#id_repeat_pwd").val()); formdata.append("email",$("#id_email").val()); formdata.append("pic",$("#picBtn")[0].files[0]); $.ajax({ url:"/reg/", type:'POST', data:formdata, contentType:false, processData:false, headers:{"X-CSRFToken":$.cookie('csrftoken')},//中间件的值,必须写 success:function (data) { console.log(data); var data=JSON.parse(data);//将后端传来的json字符串转成js字符串 if (data.user){ // 注册成功自动跳转至登录页面 location.href="/login/" } else { // 查看错误信息 console.log(data.errorsList); $.each(data.errorsList,function (i,j) { console.log(i,j); // 将错误信息打印到相对应的输入框下仿 $span=$("<span>"); $span.addClass("pull-right").css("color","red"); $span.html(j[0]); $("#id_"+i).after($span).parent().addClass("has-error") if (i=="__all__"){ // 全局钩子捕捉到错误 $("#id_repeat_pwd").after($span) } }) } } }) }) </script> </body> </html>
.container{ margin-top: 100px; } .picDiv{ position: relative; width: 40px; height: 40px; } #pic{ position: absolute; top:0; left: 50px; width: 40px; height: 40px; } #picBtn{ opacity: 0; position: absolute; top:0; left: 50px; width: 40px; height: 40px; } .sub{ padding-top: 100px; }
登录页面:
url(r'^login/', views.log_in),#登录功能
url(r'^get_validCode_img/', views.get_validCode_img), # 验证码功能
#views中: def log_in(request): if request.is_ajax(): username=request.POST.get('username') password=request.POST.get('password') validCode=request.POST.get('validCode') login_response={"is_login":False,"error_msg":None} if validCode.upper()==request.session.get("keepValidCode").upper():#检验验证码 user=auth.authenticate(username=username,password=password)#验证码正确时校验用户 if user:#用户正确 login_response["is_login"]=True auth.login(request,user) # session request.session[is_login]=True else:#用户名或密码输入错误 login_response["error_msg"] = "username or password error" else:#验证码错误 login_response["error_msg"]='validCode error' import json #检验通过 return HttpResponse(json.dumps(login_response)) return render(request,"login.html") ############################## import random def get_validCode_img(request): def random_color():#随机颜色 the_color = random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) return the_color def random_int():#随机数字 x_y=random.randint(0,150) return x_y from io import BytesIO from PIL import Image,ImageDraw,ImageFont img=Image.new(mode='RGB',size=(120,40),color=random_color()) # img = Image.new(mode="RGB", size=(120, 40), color=(random.randint(0,255),random.randint(0,255),random.randint(0,255))) draw=ImageDraw.Draw(img,mode='RGB') font=ImageFont.truetype("blog/static/font/kumo.ttf",25) valid_list=[] for i in range(5): random_num=str(random.randint(0,9))#数字 random_lower=chr(random.randint(65,90))#小写字母 random_upper=chr(random.randint(97,122))#大写字母 random_char=random.choice([random_num,random_lower,random_upper])#随机选 draw.text([5+i*24,10],random_char,random_color(),font=font) #(坐标, 随机文本 , 随机颜色, 字体) for i in range(100):#画噪点 draw.point([random_int(), random_int()], fill=random_color()) for i in range(3):#画线 draw.line((random_int(), random_int(), random_int(), random_int()), fill=random_color()) valid_list.append(random_char) #写入内存中 f = BytesIO() img.save(f, "png")#以png格式 data = f.getvalue() valid_str = "".join(valid_list) request.session["keepValidCode"] = valid_str#将验证码的字符串写入session中,方便以后校验 return HttpResponse(data)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>登录</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.css"> <script src="/static/jquery-3.2.1.min.js"></script> <link rel="stylesheet" href="/static/mycss/login.css"> <script src="http://static.geetest.com/static/tools/gt.js"></script> </head> <body> <div class="container popup"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <form method="post"> {% csrf_token %} <div class="form-group"> <label for="username">username</label> <input type="text" class="inp form-control" id="username" placeholder="请输入你的用户名"> </div> <div class="form-group"> <label for="Password">Password</label> <input type="password" class="inp form-control" id="password" placeholder="请输入你的密码"> </div> <div class="row validCode"> <div class="col-md-6"> <div class="form-group"> <label for="validCode">验证码</label> <input type="validCode" class="form-control" id="validCode" placeholder="请输入验证码"> </div> </div> <div class="col-md-6"> {#验证码图片,路由获得#} <img class="validCode_img" src="/get_validCode_img/" alt="" width="200px" height="50px"> </div> </div> <input type="btn" value="登录" class="btn btn-primary"><span class="errorMsg"></span> </form> <a href="/reg/">暂无账号,点我注册</a> </div> </div> </div> <script> $('.btn').click(function () { $.ajax({ url: '/login/', type: 'POST', data: { "username": $('#username').val(), "password": $('#password').val(), "validCode": $('#validCode').val(), "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(), }, success: function (data) { var response = JSON.parse(data); var prevLink = document.referrer; if (response["is_login"]) { alert(prevLink) location.href = prevLink// 登陆后跳转回原页面 } else { $(".errorMsg").html(response["error_msg"]).css("color", "red") } } }) }) $('.validCode_img').click(function () { $(this)[0].src += '?'// 点击验证码图片时刷新验证码 }) </script> </form> </body> </html>
.container{ margin-top: 100px; } .validCode .validCode_img{ margin-top: 20px; }
注销功能:
url(r'^log_out/', views.log_out),#注销功能
利用auth模块可以很方便的进行注销,没有对应的模板,重定向到登录页面
def log_out(request): auth.logout(request) return redirect("/login/")
到这里为止,有关首页的功能我们就基本完善好了。接下来要做的都是与个人站点有关的操作,所以之后我们的路径就都映射到blog文件夹下的urls文件中(新建的.py文件)
需在原来的urls.py文件中加一句路由分发:
url(r'^blog/', include('blog.urls')),#路由分发 #之后的所有以/blog/开头的路由都匹配到blog下的urls.py文件中
好,下面我们来做个人站点的主页
from django.conf.urls import url,include from blogCMS1 import settings from blog import views urlpatterns = [ url(r'^(?P<username>.*)/(?P<condition>tag|category|date)/(?P<para>.*)', views.homeSite), ]
下面来解释一下上段代码:
首先是正则表达式有名分组,因为每个人都有自己的站点,所以我们根据所传的用户名不同来确定用户进入的是哪一个用户的主页(每个用户都可以互相查看主页,并不是用户只能看自己的主页)
第二个有名分组的作用,我们在主页的左侧会有一个菜单栏,菜单栏上要渲染出每个用户站点各自的文章分类归档、文章标签归档、文章日期归档,所以就需要有一个可供选择的正则表达式来接收参数。
最后一个para是指
接下来我们需要在视图函数中处理参数,然后返回给用户一个个人站点的主页
#views中: def homeSite(request,username,**kwargs): # 查询当前用户站点 current_user=models.UserInfo.objects.filter(username=username).first() if not current_user:#如果当前用户异常,返回404页面 return render(request,'notFound.html') # 查询当前用户站点所有文章 article_list=models.Article.objects.filter(user=current_user) # 查询 当前用户的分类归档,格式为: ''' python(3) linux(5) go(1) ''' from django.db.models import Count, Sum ''' 可先在视图函数中利用双下划綫查询的方式取出对象再渲染到模板中, 本文用的方法是直接在模板中用.的方法进行取对象 双下划线方式如下: current_blog=current_user.blog category_list = models.Category.objects.all().filter(blog=current_blog).annotate( c=Count("article__nid")).values_list("title", "c") print(category_list) # <QuerySet [<Category: yuan的go>, <Category: yuan的java>]> ''' # 查询当前用户的标签归档 ''' a(3) b(5) c(1) ''' ''' 双下划线查询方式: tag_list = models.Tag.objects.all().filter(blog=current_blog).annotate(c=Count("article__nid")).values_list("title",c) print(tag_list) # <QuerySet [('基础知识', 3), ('插件框架', 0), ('web开发', 1)]> ''' # 查询当前用户的日期归档 ''' 2016-12(2) 2017-1(3) ''' #因为时间对象用Django原生api方式取无法做到格式化输出,所以这里必须用到extra(数据库查询语句) date_list=models.Article.objects.filter(user=current_user).extra(select={"filter_create_date":"strftime('%%Y/%%m',create_time)"})
.values_list("filter_create_date").annotate(Count("nid")) print(date_list) if kwargs: if kwargs.get("condition") == "category": #分类归档 article_list = models.Article.objects.filter(user=current_user, category__nid=kwargs.get("para")) print(article_list) elif kwargs.get("condition") == "tag": #标签归档 article_list = models.Article.objects.filter(user=current_user, tags__nid=kwargs.get("para")) elif kwargs.get("condition") == "date": #时间归档 year, month = kwargs.get("para").split("/") article_list = models.Article.objects.filter(user=current_user, create_time__year=year, create_time__month=month) return render(request,"homeSite.html",locals())
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{% block title %} 个人首页 {% endblock %}</title> <script src="/static/jquery-3.2.1.min.js"></script> <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.css"> <script src="/static/bootstrap-3.3.7-dist/js/bootstrap.js"></script> <link rel="stylesheet" href="/static/mycss/homeSite.css"> <link rel="stylesheet" href="/static/mycss/article_detail.css"> <link rel="stylesheet" href="/static/user_home_style/{{ current_user.blog.theme }}"> <script src="/static/kindeditor/kindeditor-all.js"></script> </head> <body> <nav class="navbar navbar-inverse" id="nav"> <div class="container"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">博客园</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="/blog/{{ current_user.username }}">{{ current_user.blog.title }} <span class="sr-only">(current)</span></a> </li> <li><a href="#">Link</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="/">首页</a></li> <li><a href="#">新随笔</a></li> <li><a href="#">联系</a></li> <li><a href="#">订阅</a></li> <li><a href="#">管理</a></li> {% if request.user.is_authenticated %} <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> {{ request.user.username }} <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="/blog/{{ request.user.username }}">个人首页</a></li> <li><a href="/blog/backend/">文章管理</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="/log_out/">注销</a></li> </ul> </li> {% else %} <li><a href="/login/">请登录</a></li> {% endif %} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <div class="container"> <div class="row"> <div class="col-md-3"> <div class="panel panel-primary"> <div class="panel-heading">公告</div> <div class="panel-body"> <p><b>Python开发交流群:255012808</b></p> <p><b>Python开发交流群:255012808</b></p> <p><b>Python开发交流群:255012808</b></p> <p><b>Python技术交流群:557877767</b></p> <p><b>开源Tyrion交流群:564068039</b></p> </div> </div> <div class="panel panel-primary"> <div class="panel-heading">个人信息</div> <div class="panel-body"> <p><img class="img-circle " src="{{ current_user.avatar.url }}" alt="" height="100" width="100"></p> <h3>昵称:{{ current_user.username }}</h3> </div> </div> <div class="panel panel-danger"> <div class="panel-heading">我的标签</div> <div class="panel-body"> <ul class="list-group"> {% for foo in current_user.blog.tag_set.all %} <li class="list-group-item"> <span class="badge">{{ foo.article_set.count }}</span> <a href="/blog/{{ current_user.username }}/tag/{{ foo.nid }}">{{ foo.title }}</a> </li> {% endfor %} </ul> </div> </div> <div class="panel panel-info"> <div class="panel-heading">随笔分类</div> <div class="panel-body"> <ul class="list-group"> {% for foo in current_user.blog.category_set.all %} <li class="list-group-item"> <span class="badge">{{ foo.article_set.count }}</span> <a href="/blog/{{ current_user.username }}/category/{{ foo.nid }}">{{ foo.title }}</a> </li> {% endfor %} </ul> </div> </div> <div class="panel panel-success"> <div class="panel-heading">随笔档案</div> <div class="panel-body"> <ul class="list-group"> {% for date in date_list %} <li class="list-group-item"> <span class="badge">{{ date.1 }}</span> <a href="/blog/{{ current_user.username }}/date/{{ date.0 }}">{{ date.0 }}{{ date.1 }}</a> </li> {% endfor %} </ul> </div> </div> </div> <div class="col-md-9 c1"> {% block con %} {% for foo in article_list %} <div class="article"> <div class="head"> <h3> <a href="/blog/{{ current_user.username }}/article_detail/{{ foo.nid }}">{{ foo.title }}</a> </h3> </div> <div class="desc"> {{ foo.desc }} </div> <div class="foot pull-right"> posted@ <span>{{ foo.create_time|date:"Y-m-d" }}</span> <span>评论({{ foo.comment_count }})</span> <span>点赞({{ foo.up_count }})</span> <span>阅读({{ foo.read_count }})</span> </div> <p></p> </div> {% endfor %} {% endblock %} </div> </div> </div> <div> </div> </body> </html>
/*body{*/ /*background-color: #9acfea;*/ /*}*/ .article{ padding-bottom:20px; border-bottom: solid; } .desc{ text-align: justify ; font-size: 16px; } .foot{ font-size: 14px; color: grey; } .c1{ background-color:white; } #comment_form_container{ margin-top: 80px; } .top{ display: inline-block; width: 40px; height: 40px; background-color: whitesmoke; position: fixed; bottom: 20px; right: 20px; text-align: center; }
上面的homesite.html中用到了一个路由反向解析,所以这里我们要增加一条路由匹配:
url(r'^(?P<username>.*)', views.homeSite,name="aaa")
这里我们用到了404页面,可以用浏览器自带的功能,我么也可以自己写一个提示的HTML页面
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Error_404_资源不存在</title> <style type="text/css"> body{margin:8% auto 0;max-width: 550px; min-height: 200px;padding:10px;font-family: Verdana,Arial,Helvetica,sans-serif;font-size:14px;}p{color:#555;margin:10px 10px;}img {border:0px;}.d{color:#404040;} </style> </head> <body> <a href="http://www.cnblogs.com/"><img src="//static.cnblogs.com/images/logo_small.gif" alt="cnblogs"/></a> <p><b>404.</b> 抱歉! 您访问的资源不存在!</p> <p class="d">请确认您输入的网址是否正确,如果问题持续存在,请发邮件至contact@cnblogs.com与我们联系。</p> <p><a href="/">返回网站首页</a></p> </body> </html>
接下来我们要做的页面是个人站点中的文章详情页面,因为访问者会在用户的博客站点中任意点击其已发表的文章。
因为文章详情页面的布局与个人站点首页的布局相似,只是在右侧的内容区变成了文章的详细内容,所以我们可以将个人站点首页的模板设置成基板,上面的homesite.html中已经设好,所以我们接下来在文章详情页中直接继承就好。
文章详情页面是我们重点要做的页面,因为其中包含的知识点最多。主要需实现的功能有:点赞、踩、评论、评论实时更新、评论楼结构与评论树结构(任选一项)。
下面我们会来一一进行实现
文章详情:
具体URL配置:
#文章详情页面 url(r'^(?P<username>.*)/article_detail/(?P<article_nid>\d+)', views.article_detail), #文章赞、踩功能 url(r'^updown/$', views.updown), url(r'^comment/$', views.comment), #文章评论功能 #显示文章已发表的评论,树状结构 url(r'^commentTree/(?P<article_id>\d+)$', views.commentTree),
#views中 ########文章详情######### def article_detail(request,username,article_nid): # 查询当前用户 current_user = models.UserInfo.objects.filter(username=username).first() if not current_user:#用户异常时返回404页面 return render(request, 'notFound.html') from django.db.models import Count, Sum #前端渲染时间时依然需使用extra date_list = models.Article.objects.filter(user=current_user).extra( select={"filter_create_date": "strftime('%%Y/%%m',create_time)"}).values_list( "filter_create_date").annotate(Count("nid")) #得到用户点击的文章标题对应的文章详情 current_article=models.Article.objects.filter(nid=article_nid).first() return render(request,'article_detail.html',locals())
{% extends 'homeSite.html' %} {% block title %}文章详情{% endblock %} {% block con %} <div class="article_title" id="article_title"><h3>{{ current_article.title }}</h3></div> <div class="article_con"> <div class="col-md-offset-1">{{ current_article.articledetail.content|safe }}</div> <div class="warn"> <div class="warn_con"><img src="/static/img/warning.png" alt="" width="70" height="70"></div> <div class="warn_con"> <p></p> <p></p> 作者:{{ current_user.username }} <br> 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。 </div> </div> <div class="post-info"> <img class="img-circle " src="{{ current_user.avatar.url }}" alt="" height="100" width="100"> 作者:{{ current_user.username }} <div id="digg"> <div class="diggit"> <span class="diggnum" id="digg_count">{{ current_article.up_count }}</span> </div> <div class="buryit"> <span class="burynum" id="bury_count">{{ current_article.down_count }}</span> </div> <div class="updownerror"> <div class="up_error_div pull-left"><span class="up_error"></span></div> <div class="down_error_div pull-right"><span class="down_error"></span></div> </div> </div> </div> <div class="comment-info"> <h3>已发表评论(评论树):</h3> <div class="comment_tree_list"> <div class="content-tree-list"> {# 由js代码从数据库中取得#} </div> </div> <h2>----------------------------------------------------</h2> <div class="comment-header"><h3>评论列表</h3></div> <div class="content-list"> {# for循环从数据库中取到已发布的评论#} {% for comment in current_article.comment_set.all %} <div class="comment-time"> #{{ forloop.counter }}楼: <img class="img-circle " src="{{ comment.user.avatar.url}}" alt="" width="30" height="30"> <a href="/blog/{{ comment.user.username }}">{{ comment.user.username }}</a> {{ comment.create_time|date:'Y-m-d,h:i:s' }} <div class="pull-right"> <a class="reply_comment_btn" comment_id="{{ comment.nid }}" conmment_username="{{ comment.user.username }}">回复</a> <a href="">引用</a> </div> </div> <div class="comment-detail"> {% if comment.parent_comment_id %}{#对评论的评论#} <div style="background-color: #c4e3f3"> @<a href="">{{ comment.parent_comment.user.username }}</a> : {{ comment.parent_comment.content }} </div> {% endif %} {{ comment.content }}{#对文章的评论#} </div> {% endfor %} </div> </div> <div class="top"><a href="#nav">返回顶部</a></div> {# 用户在登录状态才显示评论区,方可进行评论#} {% if request.user.is_authenticated %} <div class="add-comment"> <div class="add-icon"> <img src="/static/img/addcomment.gif" alt="" height="20" width="20"> <b><span>发表评论</span></b> </div> <div class="comment-user"> 昵称:<input type="text" disabled value="{{ request.user.username }}"> </div> <div class="comment-text"> <div>评论内容:</div> {# <textarea name="textarea" id="textarea" cols="30" rows="10"></textarea>#} <textarea id="textarea" name="textarea" style="width:500px;height:300px;"></textarea> <p><button class="btn btn-primary comment-btn">提交评论</button></p> </div> </div> {% else %}{#访客未登录时只显示提示信息,不显示评论框#} <div id="comment_form_container"> <div class="login_tips"> <img src="/static/img/addcomment.gif" alt="" height="20" width="20"> 注册用户登录后才能发表评论,请 <a href="/login/" >登录</a> 或 <a href="/reg/">注册</a>,<a href="/">访问</a>网站首页。 </div> </div> {% endif %} </div> {% endblock %}
.article_con{ padding-left: 14px; } .article_title{ text-align: center; color: #2b669a; } .warn{ border: #e0e0e0 1px dashed; margin: 20px 0; font-family: 微软雅黑; font-size: 11px; } .warn_con{ display: inline-block; margin: 10px; } #digg{ float: right; margin-bottom: 10px; margin-right: 30px; font-size: 12px; width: 128px; text-align: center; margin-top: 10px; } .diggit{ background: url("/static/img/upup.gif") no-repeat; float: left; width: 46px; height: 52px; text-align: center; cursor: pointer; margin-top: 2px; padding-top: 5px; } .buryit { float: right; margin-left: 20px; width: 46px; height: 52px; background: url("/static/img/downdown.gif") no-repeat; text-align: center; cursor: pointer; margin-top: 2px; padding-top: 5px; } .updownerror{ margin-top: 60px; color: red; } .comment-info{ padding-top: 20px; } .comment-header{ border-bottom: 1px solid #ddd; } .comment-time{ margin-left: -20px; padding: 10px; font-size: 16px; } .comment-detail{ margin-left: -20px; font-size: 14px; padding: 0px 20px; border-bottom: 1px dotted #ccc; text-align: justify; } .add-icon{ padding: 20px 0 0 25px; } .add-icon span{ margin-left: 10px; } .comment-user input{ background-image: url("/static/img/icon_form.gif"); background-repeat: no-repeat; border: 1px solid #ccc; padding: 4px 4px 4px 30px; width: 300px; font-size: 13px; } textarea{ width: 450px; height: 300px; font-size: 13px; border: 1px solid #ccc; font-family: 'PingFang SC','Helvetica Neue','Helvetica','Arial',sans-serif; } .comment{ /*float: left;*/ margin-left: 40px; }
文章的赞、踩功能:
因为赞、踩功能都是在文章详情中的局部功能,所以对应的模板页面还是article_detail.html。我们需要做的是在模板下方写js代码
这里我们用ajax的方式提交请求,脚本语言用的是jQuery
#在article_detail.html中: <script> $('.diggit').click(function () { $.ajax({ url: '/blog/updown/', type: 'POST', data: { csrfmiddlewaretoken: '{{ csrf_token }}', article_id: '{{ current_article.nid }}', up: 'up' }, success: function (data) { var data = JSON.parse(data) if (data.is_login !== true) { alert(location.href) location.href = '/login/' } else if (data.state) { $('#digg_count').html(parseInt($('#digg_count').html()) + 1) } else if (data.is_repeat) { $('.up_error').html('重复推荐') setTimeout(function () { $('.up_error').html('') }, 3000); } ; } }) }) $('.buryit').click(function () { $.ajax({ url: '/blog/updown/', type: 'POST', data: { csrfmiddlewaretoken: '{{ csrf_token }}', article_id: '{{ current_article.nid }}', 'down': 'down' }, success: function (data) { var data = JSON.parse(data) if (data.is_login !== true) { alert(location.href) location.href = '/login/' } else if (data.state) { $('#bury_count').html(parseInt($('#bury_count').html()) + 1) } else if (data.is_repeat) { $('.down_error').html('重复反对') } } }) }) </script>
import json from django.db.models import F def updown(request): updown_response={'state':True,'is_repeat':None,'is_login':True,'down':None} if not request.user.is_authenticated(): #未登录状态下不可赞和踩 updown_response['is_login']=False return HttpResponse(json.dumps(updown_response)) user_id=request.user.nid#访客已登录,得到访客id article_id=request.POST.get('article_id')#得到访客查看的文章id # the_user=models.UserInfo.objects.filter(nid=user_id) # the_article=models.Article.objects.filter(nid=article_id) if request.POST.get('up')=='up':#判断点的是赞 if models.ArticleUp.objects.filter(user_id=user_id, article_id=article_id): #若用户已点过赞,不允许再点 updown_response['state']= False updown_response['is_repeat']=True else: try:#将用户的点赞请求录入数据库 models.ArticleUp.objects.create(user_id=user_id, article_id=article_id) models.Article.objects.filter(nid=article_id).update(up_count=F("up_count") + 1) print('..........') except:#捕捉到异常返回 updown_response["state"] = False elif request.POST.get('down')=='down':#踩请求(同理) if models.ArticleDown.objects.filter(user_id=user_id, article_id=article_id): updown_response['state']= False updown_response['is_repeat']=True else: try: models.ArticleDown.objects.create(user_id=user_id, article_id=article_id) models.Article.objects.filter(nid=article_id).update(down_count=F("up_count") + 1) print('..........') except: updown_response["state"] = False return HttpResponse(json.dumps(updown_response))
评论功能、评论树与评论楼:
因为评论功能依旧是在文章详情中的局部功能,所以对应的模板页面还是article_detail.html。我们需要做的仍旧是在模板下方写js代码。这里我们继续用ajax的方式提交请求,脚本语言用的是jQuery
<script> $('.comment-btn').click(function () { var content if(parent_comment_id){ var index=$("#textarea").val().indexOf("\n"); // 子评论 content= $("#textarea").val().slice(index+1) } else { content=$("#textarea").val() } $.ajax({ url:'/blog/comment/', type:'POST', data:{ csrfmiddlewaretoken: '{{ csrf_token }}', article_id:'{{ current_article.nid }}', content:content, parent_comment_id:parent_comment_id }, success:function (data) { var data = JSON.parse(data) if (data.is_login !== true) { location.href = '/login/' } else if (parent_comment_id) { s='<div class="comment-time">#新楼: <img class="img-circle " src="{{ request.user.avatar.url}}" alt="" width="30" height="30">'+
' <a href="/blog/{{ request.user.username }}">{{ request.user.username }}</a> '+ data.create_time.slice(0,19)+ '<div class="pull-right"> <a class="reply_comment_btn" comment_id="'+ data.comment_id+ '" conmment_username="{{ request.user.username }}">回复</a> '+
' <a href="">引用</a> </div> </div> <div class="comment-detail">'+ ' <div style="background-color: #c4e3f3"> @<a href="">'+ data.parent_comment_username+'</a> : '+ data.parent_comment_content+'</div>'+' <div>'+content+'</div>'} else { // 根评论 s='<div class="comment-time">#新楼: <img class="img-circle " src="{{ request.user.avatar.url}}" alt="" width="30" height="30">'+
' <a href="/blog/{{ request.user.username }}">{{ request.user.username }}</a> '+ data.create_time.slice(0,19)+ '<div class="pull-right"> <a class="reply_comment_btn" comment_id='+ data.comment_id+ ' conmment_username="{{ request.user.username }}">回复</a> <a href="">引用</a>'+
' </div> </div> <div class="comment-detail">'+ ' <div>'+content+'</div>' } $(".content-list").append(s); $("#textarea").val(""); parent_comment_id=null; } }) }) // 回复按钮 var parent_comment_id=null; $(".comment-info").on("click",".reply_comment_btn",function () { // 文本框中显示父评论的名字 var parent_comment_username=$(this).attr("conmment_username"); $("#textarea").focus(); $("#textarea").val("@"+parent_comment_username+"\n"); // 获取父评论的comment_id parent_comment_id=$(this).attr("comment_id") }); // 获取评论树 $.ajax({ url:"/blog/commentTree/{{ current_article.nid }}", success:function (data) { var data=JSON.parse(data) var s=showCommentTree(data); $(".content-tree-list").append(s); } }); // 展开评论树 var dict={'father':true} function showCommentTree(comment_list) { // comment_list: [{"content":"1","children_list":[{}]},{"content":"2"},{"content":"3"},] var html=""; $.each(comment_list,function (i,comment_dict) { if(dict.father){var num=i+1+'楼';num='#'+num} else{var num='回复楼'} {# var num=i+1#} var val=comment_dict["content"]; var commnent_str= '<div class="comment"><div class="comment-time">'+num+
': <img class="img-circle " src="/media/'+comment_dict["user__avatar"]+
'" alt="" width="30" height="30"> <a href="/blog/'+comment_dict["user__username"]+'">'+
comment_dict["user__username"]+'</a> '+comment_dict["create_time"]+
' <div class="pull-right"> <a class="reply_comment_btn" comment_id="'+comment_dict["nid"]+
'" conmment_username="'+comment_dict["user__username"]+
'">回复</a> <a href="">引用</a> </div></div><div class="comment-detail"><span> '+
val+'</span></div>'; num+=1 if(comment_dict["chidren_commentList"]){ dict.father=false {# var num='评论'#} var s=showCommentTree(comment_dict["chidren_commentList"]); // [{},{}] commnent_str+=s dict.father=true } commnent_str+="</div>"; html+=commnent_str }); return html } </script>
最后我们需要实现的是后台管理的页面:
在这里我们自己写一个后台管理的页面,主要用于用户对自己站点的博客的增删改查。其实可以用Django框架中的admin会可以轻松实现,不过因为是自己的项目,所以我们可以尝试着自己来做一个具有相同功能的。
首先需要的是配置路由,因为是个人站点的功能,所以我们依旧将路由放在blog文件夹下的urls.py中
url(r'^backend/$', views.backendindex),#后台管理首页
url(r'^backend/addarticle/$', views.addarticle),#后台管理添加新文章功能
url(r'^backend/editarticle/(\d+)$', views.editarticle),#编辑文章
url(r'^backend/del/(\d+)', views.delarticle),#删除文章
下面我们进行具体的功能实现
首先要得到一个后台管理的首页
url(r'^backend/$', views.backendindex),#后台管理首页
def backendindex(request): print('-------------------') if not request.user.is_authenticated(): return redirect("/login/")#未登录不可直接进入 #在后台管理需要渲染出自己写的博客,所以要拿到所有的文章 article_list=models.Article.objects.filter(user=request.user).all() return render(request,"backendindex.html",{"article_list":article_list})
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>后台管理</title> <script src="/static/jquery-3.2.1.min.js"></script> <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.css"> <script src="/static/bootstrap-3.3.7-dist/js/bootstrap.js"></script> <script src="/static/kindeditor/kindeditor-all.js"></script> </head> <body> <div class="container c1"> <div class="header"> <h1>后台管理</h1> </div> <hr> <div class="row"> <div class="leftMenu col-md-2"> <ul class="list-group"> <li class="list-group-item list-group-item-success"><a href="/blog/backend/">文章管理</a></li> <li class="list-group-item list-group-item-info"><a href="">标签管理</a></li> <li class="list-group-item list-group-item-warning"><a href="">分类管理</a></li> </ul> </div> <div class="manageCon col-md-8"> {# 因为add页面的排版相似,所以我们要建一个方便继承用的盒子#} {% block manageCon %} <div class="addbutton"> <a href="/blog/backend/addarticle/"><button class="btn btn-primary">添加文章</button></a> </div> <table class="table table-border table-hover"> <tr> <th>标题</th> <th>评论数</th> <th>点赞数</th> <th>操作</th> <th>操作</th> </tr> {% for article in article_list %} <tr> <td>{{ article.title }}</td> <td>{{ article.comment_count }}</td> <td>{{ article.up_count }}</td> <td><a href="/blog/backend/editarticle/{{ article.nid }}"><button class="btn btn-info">编辑</button></a></td> <td><a href="/blog/backend/del/{{ article.nid }}"><button class="btn btn-danger">删除</button></a></td> </tr> {% endfor %} </table> {% endblock %} </div> </div> </div> </body> </html>
接下来实现增删改查的功能,首先说最简单的删,删除只要将选中的文章的id发送到后台,后天将指定文章删除然后redirect到后台首页就好了,没有对应的html模板。
后台管理删除文章
url(r'^backend/del/(\d+)', views.delarticle),#删除文章
def delarticle(request,nid): models.Article.objects.filter(nid=nid).delete() return redirect('/blog/backend/')
后台管理添加文章
这里我们需要用到一个编辑器插件,去网上下载kindeditor,然后照理将kindeditor文件夹放到static文件夹下,具体的使用方法可自行百度,这里直接使用。url(r'^backend/addarticle/$', views.addarticle),#后台管理添加新文章功能
#在views.py中: import datetime def addarticle(request): response={'result':None} if request.method=="POST":#点击保存按钮 article_form = ArticleForm(request.POST)#拿到input框中填入的数据 if article_form.is_valid():#数据没问题 title=article_form.cleaned_data.get("title") content=article_form.cleaned_data.get("content") article_obj=models.Article.objects.create(title=title,desc=content[0:30],create_time=datetime.datetime.now(),user=request.user) models.ArticleDetail.objects.create(content=content,article=article_obj) response['result'] = '添加成功<a href="/blog/backend/">点击返回</a>' else: response['result']='添加失败' #添加成功后弹出添加成功的提示,然后给予一个点击可返回后台管理首页的按钮 return HttpResponse(response['result']) article_form=ArticleForm()#由form组件,生成input框 return render(request,"addarticle.html",{"article_form":article_form})
{% extends "backendindex.html" %} {#继承后台管理首页#} {% block manageCon %} <div class="container"> <div class="row"> <div class="col-md-10"> {% csrf_token %} <div id="Editor_Edit_Header" class="CollapsibleTitle"> <span id="Editor_Edit_headerTitle">添加随笔</span> </div> <form action="/blog/backend/addarticle/" method="post" novalidate> {% csrf_token %} <div class="form-group"> <label for="exampleInputEmail1">标题:</label> {{ article_form.title }} </div> <div> <label for="title">内容:</label> <p> {{ article_form.content }} </p> </div> <p><input type="submit" value=" 提交 " class="btn btn-info"></p> </form> </div> </div> </div> <script> // 使用KindEditor必须写 KindEditor.ready(function(K) { window.editor = K.create('#id_content',{ width:"950px",//宽,可设定 height:"500px",//长,可设定 resizeType:0,//参数为0、1、2,指代长款可不可调 //还有其他许多可设定的功能,自行百度 uploadJson:"/uploadFile/",//传图片时需要用到的路径 extraFileUploadParams:{ csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(), } }); }) </script> {% endblock %}
添加文章时我们肯定会相应的需要上传一些图片,这就需要再写一个url,这里我将url放到全局的urls.py中
url(r'^uploadFile/$', views.uploadFile),#添加文章时上传图片
对应的视图函数:
#在views.py中: def uploadFile(request): file_obj=request.FILES.get("imgFile")#拿到上传的图片 file_name=file_obj.name from blogCMS1 import settings import os #将图片文件写入到本地 path=os.path.join(settings.MEDIA_ROOT,"article_uploads",file_name) with open(path,"wb") as f: for i in file_obj.chunks(): f.write(i) response={ "error":0, "url":"/media/article_uploads/"+file_name+"/" } import json return HttpResponse(json.dumps(response))
编辑文章功能
url(r'^backend/editarticle/(\d+)$', views.editarticle),#编辑文章
#在views.py中: def editarticle(request,nid): response={'result':None} if request.method=="POST": article_form = ArticleForm(request.POST) if article_form.is_valid(): title=article_form.cleaned_data.get("title") content=article_form.cleaned_data.get("content") article_obj=models.Article.objects.filter(nid=nid).update(
title=title,desc=content[0:30],create_time=datetime.datetime.now(),user=request.user) models.ArticleDetail.objects.update(content=content) response['result'] = '添加成功<a href="/blog/backend/">点击返回</a>' else: response['result']='添加失败' return HttpResponse(response['result']) article_obj=models.Article.objects.get(nid=nid)#需要知道被编辑的文章对象 title=article_obj.title content=article_obj.articledetail.content article_form=ArticleForm({"title":title,"content":content})#只允许编辑标题和内容 return render(request,"editarticle.html",{"article_form":article_form,'nid':nid})
{% extends "backendindex.html" %} {% block manageCon %} <div class="container"> <div class="row"> <div class="col-md-10"> {% csrf_token %} <div id="Editor_Edit_Header" class="CollapsibleTitle"> <span id="Editor_Edit_headerTitle">添加随笔</span> </div> <form action="/blog/backend/editarticle/{{ nid }}" method="post" novalidate> {% csrf_token %} <div class="form-group"> <label for="exampleInputEmail1">标题:</label> {{ article_form.title }} </div> <div> <label for="title">内容:</label> <p> {{ article_form.content }} </p> </div> <p><input type="submit" value=" 提交 " class="btn btn-info"></p> </form> </div> </div> </div> <script> KindEditor.ready(function(K) { window.editor = K.create('#id_content',{ width:"950px", height:"500px", resizeType:0, uploadJson:"/uploadFile/", extraFileUploadParams:{ csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(), } }); }) </script> {% endblock %}
基本功能就到这里了,还有一些其他的功能如修改用户名密码、园龄时间的显示、美化的一些插件使用等等可随喜好自行添加。
成品如下: