django学习 -- 前后端不分离的 -- part.2

35.时间选择插件

download_link
office_link
usage

  • 使用极其简单:只需要选择dom然后绑定即可,在使用方法里可以设置各种参数
$('#id_presettime').datetimepicker({
   format: 'yyyy-mm-dd hh:ii'
});   

36.modelfield定义成成表单的属性样式

class orderForm(ModelForm):
    class Meta():
        model = Orderform
        fields = "__all__"

    #方法一 原生的属性
    widgets = {
        #为html表单中name类型的input标签添加"class=form-control"
        'name' : forms.TextInput(attrs = {"class" : "form-control"})
        #为html表单中password类型的input标签添加"class=form-control"
        'password' : formsPasswordInput( attrs = {"class" : "form-control"})
        ...
    }

    #方法二 扩写父类函数的属性
        def __init__(self,*arg,**kwargs):
            super().__init__(*arg,**kwargs)
            for name, field in self.fields.items():
                # print(name,field) #这个是展示 self.fields.items()可以直接拿到定义在Meta中的fields字段
                field.widget.attrs = {'class' : 'form-control'} #为生成的表单input赋予css属性 
  • 扩充 这个属性可以继承 不过暂时对我没什么用处 因此pass过

37.修改dajngo的modelform类的inputtype类型为datetime-local

  • 第一种是通过前端js实现,一共三步:第一拿到原始的时间数据,第二选择input框然后修改type,第三讲原始的时间数据修改格式之后在赋予给input框:
function changeDatetimeInput(id){
    var time = $(`#${id}`).val().replaceAll('/','-');
    $(`#${id}`).attr('type','datetime-local');
    $(`#${id}`).val(time)
}
changeDatetimeInput('id_presettime');
changeDatetimeInput('id_endtime');

需要注意的是,后端django传来的之间类型是yyyy/mm/dd hh:ii,但是html内置的时间选择input框的时间字符串格式要求是yyyy-mm-ddThh:ii,因此需要将原来的斜杠(/)替换成短横(-),中间·的T是一个时间字符表示,可以用大写字符T表示,但最终解析的时候空格也可以,具体参考

  • 第二种就是修改django的DateTimeInput
    这种看不懂原因,但是如果只是仅仅修改input_type的话是解析不了的,回答给出的原因是后端会因为输入合法判断和不通过,detail_link_and_solution
from django import forms
class DateTimeLocalInput(forms.DateTimeInput):
    input_type = "datetime-local"

class DateTimeLocalField(forms.DateTimeField):
    # Set DATETIME_INPUT_FORMATS here because, if USE_L10N 
    # is True, the locale-dictated format will be applied 
    # instead of settings.DATETIME_INPUT_FORMATS.
    # See also: 
    # https://developer.mozilla.org/en-US/docs/Web/HTML/Date_and_time_formats
    input_formats = [
        "%Y-%m-%dT%H:%M:%S", 
        "%Y-%m-%dT%H:%M:%S.%f", 
        "%Y-%m-%dT%H:%M"
    ]
    widget = DateTimeLocalInput(format="%Y-%m-%dT%H:%M")

然后再自己的modelform中将想要修改input的typ类型为datetime-local的字段设置为DateTimeLocalField即可:

#用法
class orderEditForm(ModelForm):
    presettime = DateTimeLocalField() #在这里将想要的字段设置即可
    class Meta():
        model = Orderform
        fields = "__all__"
<!-- {{field}}生成的具体样式  -->
<input type="text" name="endtime" value="1899/12/31 00:00" class="form-control" id="id_endtime">

38.通过validationError来为钩子方法设置非法输入提示信息

raise ValidationError

39.密码加密 通过钩子方法定义

  • 钩子方法返回的就是储存到数据库中
  • md5加密 加密需要“盐”,为了方便和效果 使用django在设置中生成的secret_key来当作“盐”
from django.conf import settings
import hashlib

def md5(data_string):
    obj = hashlib.md5(settings.SECRET_KEY.encode('utf-8'))
    obj.update(data_string.encode('utf-8'))
    return obj.hexdigest()

40.对于djanfo的ORM,如果搜索不到

搜索不到就返回None,可以用来判断是否是合法请求

41.对于表单验证

所有验证通过的信息都储存在对应的cleaned_data

42.django报错

'Manager' object is not callable
原因是objects是一个属性而不是一个方法,写成objects()会报这个错

43.用户登录

  • cookies 和 session
    无状态短链接
    通过cookies给用户发放一个“凭证”, 保存在浏览器中的键值对,发送请求的时候自动携带
    session django默认储存在mysql中

业务过程:收到用户的提交 -> 校验(在数据库比较) -> 成功 生成随机字符串写入到用户浏览器的cookies以及数据库中(这里只需要调用request.session()方法即可 django会自动处理两个事件),并且会将传入的字符经过处理之后,以{session_key:session_data}键值对的形式保存在数据库中,其中session_key就是传递给浏览器的cookies,而session_data就是在下面代码中定义的字典(经过字符转换,需要使用的时候django会解码)

        else: #登陆成功
            request.session['info'] = { #写入session
                'id':login_object.id,
                'name' : login_object.name,
            }
  • PS. 特别需要注意的是,在后面的调用request.session中的属性的名字都是来自于此!

因此需要在用户访问的时候判断用户是否登录 -> 已登录继续访问 未登录跳转回登陆页面:
用户请求-> 拿到用户cookies并判断:

#第一种方法 在每个请求前都加上对于cookies的判断
    if not request.session.get('info'):
        return redirect('/login/')

太low😎
应该使用django的中间件高效的处理

44.django中间件

官方文档

  • 对于django,每一个请求都需要经过很多中间件之后才能访问到视图函数;同样的,所有的视图函数返回值都需要经过很多中间件的处理才会返回给用户。
  • django的中间件都是类,通过process_request方法对传入的请求进行处理,通过process_response对视图函数的return进行包装。如果某个请求不能通过一个中间件,那么就会直接返回给用户,而不会到达视图函数。
  • 中间件写好之后需要在setting.py文件中的MIDDLEWARE中注册(具体到类名),哪个在前面那个先执行。
  • 中间的process_request方法如果返回为None,那么继续向后执行;如果需要返回,那么返回的类型与视图函数的返回类似(例如render,redirect,HttpResponse等),并且不在向后执行。
  • 应用中间件:
    • 1.注册中间件
    • 2.中间件中返回值
    • 3.通过中间件实现登录校验
      代码实现
#middleware.py文件
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import render,redirect
from django.http import HttpResponse

class SessionChectMiddleware(MiddlewareMixin):
    def process_request(self,request):
        #排除不需要登陆的页面
        allow_url = ['/login/','/register/']
        if request.path_info in allow_url:
            return
        #读取session
        info = request.session.get('info')
        if info:
            return
        else: 
            return redirect('/login/')
#在setting.py文件中注册
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
...... ,
    "cake.middleware.SessionChectMiddleware",
]

PS. request.path_info可以获取到请求的url路径。

45.注销操作 - 用户退出登录

也就是将浏览器的cookies和session删除
同样使用request.session中的方法即可

46.通过session获取用户登录信息显示在导航栏

每次视图函数的中传递的request中都包含session参数,因此可以直接在模板中调用{{request.session.info.属性}}来获取对应的信息(因为request是默认传递的),不用刻意通过别的模板参数来渲染。

    <button class="btn btn-secondary dropdown-toggle btn-sm" type="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-expanded="false">
                        {{ request.session.info.name}}
</button>

47.form和modelform

  • form可以自定义字段
    modeform既可以自定义 还可以在数据库中拿取字段
  • form没有save方法可以直接存入到数据库 但是modelform有

48.图片验证码

-> python中如何动态生成图片并写入值 (不做过多了解 仅仅当成黑盒使用)
参考文献

  • 需要pillow库
import random
from PIL import Image,ImageFont,ImageDraw,ImageFilter
#需要注意!! 这里的字体文件font_file路径组合是基于运行的根目录决定的 需要根据相对位置的不同进行调整!这里我将他放在根目录下面的static/文件夹内
def check_code(width=120, height=30, char_length=5, font_file='./static/kumo.ttf', font_size=28):
    # font = ImageFont.load_default() #使用默认字体
    code = []
    img = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255))
    draw = ImageDraw.Draw(img, mode='RGB')
    def rndChar():
        """
        生成随机字母   
        :return:
        """
        return chr(random.randint(65, 90))
    def rndColor():
        """
        生成随机颜色
        :return:
        """
        return (random.randint(0, 255), random.randint(10, 255), random.randint(64, 255))
    # 写文字
    font = ImageFont.truetype(font_file, font_size)
    for i in range(char_length):
        char = rndChar()
        code.append(char)
        h = random.randint(0, 4)
        draw.text([i * width / char_length, h], char, font=font, fill=rndColor())
    # 写干扰点
    for i in range(40):
        draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
    # 写干扰圆圈
    for i in range(40):
        draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
        x = random.randint(0, width)
        y = random.randint(0, height)
        draw.arc((x, y, x + 4, y + 4), 0, 90, fill=rndColor())
    # 画干扰线
    for i in range(5):
        x1 = random.randint(0, width)
        y1 = random.randint(0, height)
        x2 = random.randint(0, width)
        y2 = random.randint(0, height)

        draw.line((x1, y1, x2, y2), fill=rndColor())

    img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)
    return img,''.join(code)
 
if __name__ == '__main__':
    # 1. 直接打开
    # img,code = check_code()
    # img.show()
 
    # 2. 写入文件
    img,code = check_code()
    with open('code.png','wb') as f:
        img.save(f,format='png')
 
    # 3. 写入内存(Python3)
    # from io import BytesIO
    # stream = BytesIO()
    # img.save(stream, 'png')
    # stream.getvalue()
 
    # 4. 写入内存(Python2)
    # import StringIO
    # stream = StringIO.StringIO()
    # img.save(stream, 'png')
    # stream.getvalue()

    pass

需要注意!!`check_code()`的字体文件`font_file`路径组合是基于运行的根目录决定的 需要根据相对位置的不同进行调整!这里我将他放在根目录下面的`static/`文件夹内

+ 然后就是部署到djang中:主要是将前端请求图片的url替换为动态的,然后生成验证码的视图函数返回对应的验证码图片。 ```python from .function.checkcode import check_code #这个就是刚才的生成验证码图片的函数 from io import BytesIO #将图片储存到内存中需要用的库

def image_code(request):
'''生成验证码'''
img,str = check_code()
print(str)
stream = BytesIO()
img.save(stream, 'png')

return HttpResponse(stream.getvalue()) #从内存中读取到图片然后以http response的形式传回给前端 前端的img标签解析之后就是一个图片
```html
<img src="/image/code/"> 
<!-- 只要调用对应的url就可以 -->

PS. 需要注意的是:同一个文件中不要出现名字相同的函数,不管是引用的还是本文件的,会导致调用出错

49.如何验证验证码呢?

通过session! - 这样每个用户在登陆的时候对于验证码的验证就不会受到干扰,并且重复刷新也会更新session中对应的验证码。
这里有很多需要注意的点!

  • add_errors()可以向forms中指定字段添加错误提示信息,然后能在前端的{{fields.属性字段.errors.0}}中取到。
  • 由于验证码的字段是没有在用户model/数据库表中存在的,因此如果直接传入request.POST作为data参数会报错。因此应该将验证码字段单独剔除。
  • 验证码验证需要调用之前创建验证码时存入session的字段,将两个字段相比较判断是否正确。
  • 登录成功之后需要重新设置session的过期时间 ,因为为了验证码的登录时效,在生成验证码的时候会设置保存验证码对比字段的session信息时效比较短,因此登录成功之后需要重新设置。
    最终的登录逻辑
from .models import UserInfo #model中的类 是最原本的用户信息类 包括账户名和密码以及自动生成的id
from io import BytesIO #生成图片储存在内存
from .encrypt import md5 #加密

class loginForm(forms.Form):
    name = forms.CharField(label='用户名',widget=forms.TextInput(
        attrs = {
            'class' :"my-input-item"
        }
    ))
    password = forms.CharField(label='密码',widget=forms.PasswordInput(
        attrs = {
            'class' :"my-input-item"
        }
    ))
    code = forms.CharField(label='验证码',widget=forms.TextInput(
        attrs={
            'class':"my-input-item" ,
            'style':"margin-top:0;" ,
            'placeholder':"验证码"
        }
    ),required=True)

    def cleaned_password(self):
        return md5(self.changed_data.get('password')) #md5加密

#登录页面 
def login(request):
    if request.method == 'GET':
        form = loginForm()
        return render(request,'login.html',{
            'form':form
        })

    login_form = loginForm(data=request.POST)
    if login_form.is_valid():
        user_input_code = login_form.cleaned_data.pop('code') 
        #验证码的校验
        code = request.session.get('image_code','') #这个是在生成验证码的时候存入session中的,如果过时或者获取不到默认为空
        if code.upper() != user_input_code.upper(): #不考虑大小写 
            #如果验证码不相等
            login_form.add_error('code','验证码错误')
            return render(request,'login.html',{
                'form' : login_form,
            })

        login_object = UserInfo.objects.filter(**login_form.cleaned_data).first() #校验用户密码
        if not login_object:
            login_form.add_error('name','用户名或密码错误')
            return render(request,'login.html',{
                'form' : login_form,
            })
        else: #登陆成功
            request.session['info'] = { #写入session
                'id':login_object.id,
                'name' : login_object.name,
            }
            request.session.set_expiry(60 * 60 * 24 * 7) #再重新设置session 保存7天
            return redirect('/login/manage/')
    else: #表单验证未通过
        return render(request,'login.html',{
                'form' : login_form,
            }) 

#生成验证码
def image_code(request):
    '''生成验证码'''
    img,str = check_code()
    request.session['image_code'] = str
    request.session.set_expiry(60) #设置验证码session 60s过时
    stream = BytesIO()
    img.save(stream, 'png')
    return HttpResponse(stream.getvalue())

PS. UserInfo是在model.py文件中的ORM类,对应着储存登录用户帐号密码的数据库表。