1.需求分析
1).登录,图片验证码(ajax)
https://docs.geetest.com/(极验验证码)
2).注册:ajax提交,错误信息的展示,头像的预览上传(基于forms)
3).首页文章的展示
4).个人站点(不同人,不同的样式),标签、分类、归档
5).文章详情页(母版的继承,文章样式)
6).点赞点踩
7).评论的展示和提交
-子评论
8).后台管理(文章展示)
9).文章发布(富文本编辑器的使用),通过H2自动汇总目录
2.设计程序的架构,数据库
-基于django
-设计数据库(一对多的关系一旦确立,关联字段写在多的一方,一对一的关系,关联字段写在哪里都可以,多对多关系,需要创建第三张表)
-用户表:userInfo---auth(继承)
https://www.cnblogs.com/liuqingzheng/articles/9628105.html
-个人站点表:blog,和userInfo一对一
-文章:分类跟文章一对多,标签跟文章是多对多
-分类表:跟用户表是一对多,userid
-标签表(文章关键字):跟用户的表是一对多,userid
-点赞,点踩表
-评论表:
-子评论
3.分任务开发程序
1.登录
2.注册
-register页面渲染(form组件渲染)
-头像预览(label的for属性)
-文件阅读器 var filereader=new FileReader();
-onload等它读完再进行处理
-错误信息渲染
-用each循环,把错误信息,拼到span中
-定时器
-form组件(数据校验功能用的比较多)
-form.clean_data:校验通过的正确的数据
-form.error:所有错误的数据
-form.is_valid();判断是否校验通过
-局部钩子函数:def clean_字段名():
-全局钩子函数:def clean():
-错误信息的中文显示error_messages={'max_length':'最大长度为18',
'min_length': '最小长度为3',
'required':'这个必填'}
-可以控制前端页面显示的样式attrs={'class':'form-control'}
-页面渲染:for 循环form对象 {% for foo in form %} {% endfor %}
-ajax提交数据
-$("#form").serializeArray()把form表单的数据序列化(里面需要带着csrf_token)
-上传文件:需要借助FormData这个类
-contentType:false //不要给我编码
-processData:false //不要预处理数据
-FileField:
-把文件对象直接给它,它会自动保存,并且把文件地址,放到该字段下(文件不存在,会自动创建文件夹)
4.测试
5.上线运行
举例:
(1).-BBS_new-settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
#ORM不能创建数据库
'NAME': 'BBS', #数据库的名字
'USER':'root',
'PASSWORD':'123456',
'HOST':'192.168.10.5',
'PORT':3306,
}
}
STATIC_URL = '/static/' #静态文件路径 STATICFILES_DIRS=[ os.path.join(BASE_DIR,'static') ]
(2). -blog-urls.py
from django.conf.urls import url from django.contrib import admin from blog import views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^register/', views.register), url(r'^login/', views.login), url(r'^get_code/', views.get_code), ]
(3).-blog-__init__.py
import pymysql pymysql.install_as_MySQLdb()
(4).-blog-models.py
from django.db import models # Create your models here. from django.contrib.auth.models import AbstractUser # from django.contrib.auth.models import User #用auth组件,需要继承AbstractBaseUser class UserInfo(AbstractUser): ''' 用户信息 ''' nid=models.AutoField(primary_key=True) # telephone=models.CharField(max_length=11,null=False,unique=True) #存用户头像(FileField用来存文件,存了一个文件的地址,对到数据库,是一个varchar类型) #直接给它一个文件对象,它会自动的去保存,保存到avatars文件夹下,没有自动创建文件夹 #default 如果没有传,字段存成avatars/default.png avatar=models.FileField(upload_to='avatars/',default="avatars/default.png") #创建用户时间,时间类型DateTimeField,年月日,时分秒 verbose_name 在admin中使用,admin中录入数据使用到 #auto_now_add :创建数据记录的时候会把当前的时间添加到数据库 #auto_add:每次更新数据记录的时候会更新改字段 create_time=models.DateTimeField(verbose_name='创建时间',auto_now_add=True) #null=True表示可以为空,on_delete=models.CASCADE级联删除 blog=models.OneToOneField(to='Blog',to_field='nid',null=True,on_delete=models.CASCADE) #重写了 __str,打印的时候,会显示 username def __str__(self): return self.username class Blog(models.Model): ''' 博客信息 ''' nid=models.AutoField(primary_key=True) title=models.CharField(verbose_name='个人博客标题',max_length=64) site_name=models.CharField(verbose_name='站点名称',max_length=64) theme=models.CharField(verbose_name='博客主题',max_length=32) def __str__(self): return self.title class Category(models.Model): ''' 博主个人文章分类表 ''' nid=models.AutoField(primary_key=True) title=models.CharField(verbose_name='分类标题',max_length=32) #跟blog一对多 blog=models.ForeignKey(verbose_name='所属博客',to='Blog',to_field='nid',on_delete=models.CASCADE) def __str__(self): return self.title class Tag(models.Model): nid=models.AutoField(primary_key=True) title=models.CharField(verbose_name='标签名称',max_length=32) blog=models.ForeignKey(verbose_name='所属博客',to='Blog',to_field='nid',on_delete=models.CASCADE) def __str__(self): return self.title class Article(models.Model): nid=models.AutoField(primary_key=True) title=models.CharField(verbose_name='文章标题',max_length=50) desc=models.CharField(verbose_name='文章描述',max_length=255) create_time=models.DateTimeField(verbose_name='创建时间',auto_now_add=True) #TextField 大文本 content=models.TextField() # comment_count=models.IntegerField(default=0) # up_count=models.IntegerField(default=0) # down_count=models.IntegerField(default=0) #文章和作者是一对多关系 user=models.ForeignKey(verbose_name='作者',to=UserInfo,to_field='nid',on_delete=models.CASCADE) category=models.ForeignKey(verbose_name='分类标签',to=Category,to_field='nid',on_delete=models.CASCADE) #文章跟标签,多对多,手动指定第三张表 tags=models.ManyToManyField( to='Tag', through='Article2Tag', through_fields=('article','tag'), ) def __str__(self): return self.title class Article2Tag(models.Model): nid=models.AutoField(primary_key=True) article=models.ForeignKey(verbose_name='文章',to='Article',to_field='nid',on_delete=models.CASCADE) tag=models.ForeignKey(verbose_name='标签',to='Tag',to_field='nid',on_delete=models.CASCADE) # class Meta: #联合唯一 # unique_together=[ # ('article','tag'), # ] def __str__(self): v=self.article.title + "---" + self.tag.title return v class ArticleUpDown(models.Model): ''' 点赞表 ''' nid=models.AutoField(primary_key=True) user=models.ForeignKey('UserInfo',null=True,on_delete=models.CASCADE) article=models.ForeignKey('Article',null=True,on_delete=models.CASCADE) #default=True 默认为真 is_up=models.BooleanField(default=True) class Meta: unique_together=[ ('article','user') ] class Comment(models.Model): ''' 评论表 ''' nid=models.AutoField(primary_key=True) article=models.ForeignKey(verbose_name='评论文章',to='Article',to_field='nid',on_delete=models.CASCADE) user=models.ForeignKey(verbose_name='评论者',to='UserInfo',to_field='nid',on_delete=models.CASCADE) content=models.CharField(verbose_name='评论内容',max_length=255) create_time=models.DateTimeField(verbose_name='创建时间',auto_now_add=True) #自关联,为了方便查询和约束 parent_comment=models.ForeignKey('self',null=True,on_delete=models.CASCADE) def __str__(self): return self.content # 1 lqz 111文章 写的很好 null # 2 egon 111文章 写的就是很好 null # 3 sls 111文章 你说的对 2
-blog-myForms.py
from django import forms from django.forms import widgets from blog import models from django.core.exceptions import ValidationError class RegForm(forms.Form): name=forms.CharField(max_length=18,min_length=3,label='用户名', widget=widgets.TextInput(attrs={'class':'form-control'}), error_messages={'max_length':'最大长度为18', 'min_length': '最小长度为3', 'required':'这个必填'}) pwd=forms.CharField(max_length=18,min_length=3,label='密码', widget=widgets.PasswordInput(attrs={'class':'form-control'}), error_messages={'max_length':'最大长度为18', 'min_length': '最小长度为3', 'required':'这个必填'}) re_pwd=forms.CharField(max_length=18,min_length=3,label='确认密码', widget=widgets.PasswordInput(attrs={'class':'form-control'}), error_messages={'max_length':'最大长度为18', 'min_length': '最小长度为3', 'required':'这个必填'}) email=forms.EmailField(label='邮箱',widget=widgets.TextInput(attrs={'class':'form-control'}), error_messages={'invalid':'格式不合法', 'required':'这个必填'}) #局部钩子函数 #检验name是不是在数据库存在 # 函数名字必须:clean_字段名 def clean_name(self): #cleaned_data通过校验的数据 name=self.cleaned_data.get('name') #如果能查出结果,说明数据库已经存在这个名字 user=models.UserInfo.objects.filter(username=name).first() # if name.startswith('sb'): # raise ValidationError('不能以sb开头') # else: # return name if user: #如果校验失败,抛出ValidationError异常 raise ValidationError('用户名已经存在') else: #如果校验成功,返回name return name #全局钩子函数 def clean(self): pwd=self.cleaned_data.get('pwd') re_pwd=self.cleaned_data.get('re_pwd') if pwd==re_pwd: #通过了 return self.cleaned_data else: raise ValidationError('两次密码不一致')
(5).-blog-views.py
from django.shortcuts import render,HttpResponse # Create your views here. from blog.myForms import RegForm from blog import models from django.contrib import auth from django.http import JsonResponse # 生成图片包 from PIL import Image #安装Pillow资源包 # 生成随机数 import random # 内存管理器 from io import BytesIO #在图片上写文字 from PIL import ImageDraw #生成字体对象 from PIL import ImageFont #django提供验证 from django.contrib import auth def register(request): if request.method == 'GET': form = RegForm() return render(request, 'register.html', {"form": form}) if request.method=='POST': #生成form对象,把要校验的数据传递过去 form = RegForm(request.POST) response={'status':100,'msg':'注册成功'} #form.is_valid() 如果是true,说明校验通过,就可以保存数据,如果是false,校验不通过,不能保存 if form.is_valid(): #取出数据,创建用户 name=request.POST.get('name') pwd=request.POST.get('pwd') re_pwd=request.POST.get('re_pwd') email=request.POST.get('email') #拿到前台传过来的文件对象 myfile=request.FILES.get('myfile') #如果myfile是None就不能给avatar赋值 if myfile: models.UserInfo.objects.create_user(username=name, password=pwd, email=email, avatar=myfile) else: models.UserInfo.objects.create_user(username=name,password=pwd,email=email) return JsonResponse(response) else: print(form.errors) # from django.forms.utils import ErrorDict # print(type(form.errors)) response['msg']=form.errors response['status']=101 return JsonResponse(response) # 生成随机颜色函数 def get_random_color(): return (random.randint(0,255),random.randint(0,255),random.randint(0,255)) def get_code(request): # 方案一:直接放入图片 # with open('egon.jpg','rb') as f: # data=f.read() # # 方案二:生成一张图片Pillow,虚拟可以环境可以使用不同版本python # #生成一张新图片(三个参数:第一个,模式:RGB,第二个:长高,第三个:图片颜色) # new_img=Image.new('RGB',(400,40),color=get_random_color()) # #打开一个空文件,把它保存进去 # with open('test.png','wb') as f: # #第二个参数是图片类型 # new_img.save(f,'png') # with open('test.png','rb') as f: # data=f.read() # # 方案三:生成图片放入内存 # new_img = Image.new('RGB', (400, 40), color=get_random_color()) # # 打开一个内存管理器,保存进去 # img=BytesIO() # new_img.save(img,'png') # #从内存管理器中取出img # data=img.getvalue() # 方案四:生成图片放入内存 new_img = Image.new('RGB', (400, 40), color=get_random_color()) #把图片放到ImageDraw内 draw=ImageDraw.Draw(new_img) #构造字体对象,第一个参数是字体文件(ttf),第二个是字体大小 font=ImageFont.truetype('static/font/kumo.ttf',30) # draw.text((0,5),'python',get_random_color(),font=font) # ASCII对照表:https: // baike.baidu.com / pic / ASCII / 309296 / 0 / c2fdfc039245d688c56332adacc27d1ed21b2451?fr = lemma & ct = single # aid=0&pic=c2fdfc039245d688c56332adacc27d1ed21b2451 # 保存验证码 valid_code='' for i in range(5): num_str=str(random.randint(0,9)) upper_str=chr(random.randint(65,90)) low_str=chr(random.randint(97,122)) random_str=random.choice([num_str,upper_str,low_str]) draw.text((i*70+70, 5),random_str, get_random_color(), font=font) valid_code+=random_str print(valid_code) #把验证码存储到session,设置全局变量进行提交验证 request.session['valid_code']=valid_code # 打开一个内存管理器,保存进去 img=BytesIO() new_img.save(img,'png') #从内存管理器中取出img data=img.getvalue() return HttpResponse(data) def login(request): if request.method=='GET': return render(request,'login.html') if request.method=='POST': response={'status':100,'msg':'登录成功'} name=request.POST.get('name') pwd=request.POST.get('pwd') code=request.POST.get('code') #忽略大小写 if code.upper()==request.session.get('valid_code').upper(): #如果校验通过,user就是这个对象,如果校验不通过,user就是None user=auth.authenticate(request,username=name,password=pwd) if user: #这样之后,所有的request对象中都能拿出request.user来 auth.login(request,user) else: response['status']=101 response['msg']='用户名或密码错误' else: response['status'] = 102 response['msg'] = '验证码错误' return JsonResponse(response)
-static-bs
-static -font
-static -img
-static-jquery
(6).-templates-login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="/static/jquery-3.2.1.min.js"></script> <link rel="stylesheet" href="/static/bs/css/bootstrap.css"> <title>登录</title> <style> .error{ color: red; margin-left: 255px; } </style> </head> <body> <div class="container-fluid"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <h2>登录</h2> {% csrf_token %} <div class="form-group"> <label for="">用户名</label> <input type="text" id="id_name" class="form-control"> </div> <div class="form-group"> <label for="">密码</label> <input type="text" id="id_pwd" class="form-control"> </div> <div class="form-group"> <label for="">验证码</label> <div class="row"> <div class="col-md-6"> <input type="text" id="id_code" class="form-control"> </div> <img src="/get_code/" alt="" width="255" height="35" id="id_img"> </div> </div> <button class="btn btn-danger " id="submit">登录</button><span class="error"></span> </div> </div> </div> </body> <script> $("#id_img").click(function () { $("#id_img")[0].src = $("#id_img")[0].src + '?' }) $("#submit").click(function () { $.ajax({ url:'/login/', type:'post', data:{'name':$("#id_name").val(), 'pwd':$("#id_pwd").val(), 'code':$("#id_code").val(), 'csrfmiddlewaretoken':'{{ csrf_token }}' }, success:function (data) { console.log(data) if (data.status!=100){ $(".error").text(data.msg) }else { location.href='/index/' } } }) }) </script> </html>
(7).-templates-register.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="/static/jquery-3.2.1.min.js"></script> <link rel="stylesheet" href="/static/bs/css/bootstrap.css"> <title>注册</title> <style> #id_myfile{ display: none; } .errors{ color: red; } </style> </head> <body> <div class="container-fluid"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <h2>注册</h2> <form id="form"> {% csrf_token %} {% for foo in form %} <div class="form-group"> <label for="{{ foo.auto_id}}">{{ foo.label}}</label> {{ foo }} <span class="pull-right errors"></span> </div> {% endfor %} <div class="form-group"> <label for="id_myfile">头像 <img src="/static/img/default.png" id="img_file" height="80" width="80" style="margin-left: 10px"> </label> <input type="file" name="myhead" id="id_myfile"> </div> <input type="button" class="btn btn-danger " value="注册" id="id_submit"> </form> </div> </div> </div> </body> <script> //当id_myfile控件发生变化的时候,取出头像,显示在img标签上,实现预览功能 $("#id_myfile").change(function () { // alert('11') //$("#id_myfile")[0] 拿到原生的dom //$("#id_myfile")[0].files 拿到当前控件的所有文件 //$("#id_myfile")[0].files[0] 拿到当前控件的所有文件的第一个文件 var obj=$("#id_myfile")[0].files[0]; //借助文件阅读器,生产一个文件阅读的对象 var filereader=new FileReader(); //把文件读到文件阅读器中 filereader.readAsDataURL(obj); {# $("#img_file").attr('src',filereader.result);#} filereader.onload=function () { $("#img_file").attr('src',filereader.result); } }) $("#id_submit").click(function () { //上传文件,需要生成一个formdata var formdata=new FormData() //$('#id_name').val()取到name对应的值 //formdata.append('name',$('#id_name').val()) var submit_data=$("#form").serializeArray() //取出form的值 //console.log(submit_data) $.each(submit_data,function (index,value) { //console.log(index) //console.log(value) //把数据拼到formdata中 //console.log(value.name,value.value) formdata.append(value.name,value.value) }) //把文件对象放入formdata中 formdata.append('myfile',$("#id_myfile")[0].files[0]) $.ajax({ url:'/register/', type:'post', //如果要上传文件,必须指定下面两个参数为false contentType:false,//不要给我编码,告诉jQuery不要去处理发送的数据 processData:false,//不要预处理数据,告诉jQuery不要去设置Content-Type请求头 data:formdata, success: function (data) { //console.log(data) if (data.status==100){ location.href='/login/' }else { //错误信息渲染 //先找到input这个标签 //console.log(data.msg) $.each(data.msg,function (index,value) { console.log(index) console.log(value) //-----------第一种方法------------------------------------------- ////$("#id_"+index) 拿到是email 对应的那个input标签,下一个标签是span //$("#id_"+index).next().text(value) ////拿到input标签的父空间,也就是div,给它添加上has-error样式 //$("#id_"+index).parent().addClass('has-error') //-----------第二种方法------------------------------------------- $("#id_"+index).next().text(value).parent().addClass('has-error') //两次密码不一致的情况 if (index=='__all__'){ $("#id_re_pwd").next().text(value).parent().addClass('has-error') } //定时器,两个参数,第一个是匿名函数,第二个是时间毫秒 setTimeout(function () { //清理div上的has-error $(".form-group").removeClass('has-error') $(".errors").text("") },3000) }) } } }) }) </script> </html>