• 自学Python第二十二天- Django框架(二) ORM、表单组件


    Django官方文档

    表单组件

    form 表单的用法和 flask 等其他方法大致一样,不一样的地方在于 django 自带 csrf_token 校验。在视图函数获取表单数据时,会比对 csrf_token 的值,所以在 html 中的form 表单内部需要添加 {% csrf_token %} 用来生成随机值以进行比较。如果需要取消 csrf ,可以在settings.py 的中间件注册信息中删除 CsrfViewMiddleware (此种方法将取消全部的 csrf 验证)。如果只是部分的视图处理函数不进行 csrf 验证,则可以使用装饰器 csrf_exempt 修饰需要的视图处理函数(django.views.decorators.csrf.csrf_exempt)。

    但是使用 form 表单会有很多问题,例如数据校验、错误提示、页面字段需要重写、关联表的数据呈现需要手动写入等等。所以在开发时通常使用 django 提供的组件来实现。

    Form 组件

    django 提供的 form 组件能够自动对数据进行校验,能够提供错误提示,能够自动生成字段。

    使用 form 组件需要在 views.py 中创建一个继承自forms.Form的类,用来处理 form 的内容。

    from django import forms
    class MyForm(forms.Form):
    	# 定义 form 的字段,并使用插件
    	user = forms.CharField(widget=forms.TextInput,label='用户名',required=True)
    	pwd = forms.CharField(widget=forms.Input,label='密码',required=True)
    	email = forms.CharField(widget=forms.Input)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后在视图函数中实例化此类,并传递给前端页面

    def index(request):
    	form = MyForm()
    	return render(request, 'index.html', {'form': form})
    
    • 1
    • 2
    • 3

    然后前端页面使用 form 对象创建表单,而不是使用 等标签创建。form 对象会自动生成插件所标识的 html 标签。

    可以进行自动渲染

    <form method="post">
    	
    	
    	{% form.as_table %}
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样就能将 form 渲染为 table 标签,还可以使用 form.as_p、form.as_ul。

    另外,也可以手动渲染字段

    <form method="post">
    	
    	{ form.user }}
    	{{ form.pwd }}
    	{{ form.email }}
    	-->
    	
    	{% for field in form %}
    		{{ field.label }} : {{ field }}
    	{% endfor %}	
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    field.label 可以获取字段的 verbose_name 名称,如果没有定义则取字段名称。

    完整的 label 还可以使用 label_tag 来生成

    <div>
    	{{ form.subject.errors.0 }}
    	{{ form.subject.label_tag }}
    	{{ form.subject }}
    div>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    自定义错误信息

    在表单类的 Meta 元信息中,可以自定义一些字段的错误信息,例如自定义 name 的不能为空信息

    class UserForm(forms.Form):
    	pass
    	class Meta:
    		pass
    		error_messages = {		# 自定义错误信息
    			'name': {			# 字段
    				'required': '名称不能为空',		# 错误类型及提示信息
    				'max_length': '长度不能超过20个字符',
    			}
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    小部件 widget

    widget 可以指定此字段使用的组件类型,例如让元素type='password。也可以自定义一些小部件。

    
    <input type="{{ widget.type }}" name="{{ widget.name }}"
    {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}
    {% for name, value in widget.attrs.items %}
    	{% if value is not False %}
    		{{ name }}
    		{% if value is not True %}
    			="{{ value|stringformat:'s' }}"
    		{% endif %}
    	{% endif %}
    {% endfor %}>
    <p>
    	<button type="button" onclick="send({% if widget.value != None %}"{{ widget.value }}"{% endif %})>发送邮件button>
    p>
    <script>
    	function send(email) {
    		alert(email)
    	}
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    注:模板文件经过格式化,方便阅读,可能实际使用中会报错。

    from django.forms.widgets import input
    
    class SendEmailWidget(input):
    	input_type = 'text'
    	template_name = 'send_email_widget.html'
    	
    	# redner_value 是否渲染现有的值
    	def __init__(self, attrs=None, render_value=True):
            super().__init__(attrs)
            self.render_value = render_value
    
        def get_context(self, name, value, attrs):
            if not self.render_value:
                value = None
            return super().get_context(name, value, attrs)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    ModelForm 组件

    使用 form 组件时,所有字段还是需要手动写一遍(在 views.py 定义的 form 类中),如果觉得麻烦,django 提供了另一个组件: ModelForm 组件。此组件可以根据数据库中各列的情况,自动生成字段。

    生成表单

    使用 ModelForm 组件需要在 views.py 中创建一个继承自ModelForm的类,用来处理 form 的内容。

    from django import forms
    class MyForm(forms.ModelForm):
    	# 也可以使用自定义字段
    	# xx = forms.CharField()
    	# 需要嵌套类 Meta
    	class Meta:
    		model = UserInfo	# UserInfo 是 models.py 中定义的相应数据表的类
    		fields = ["name", "password", "age"]		# 确定要操作的字段
    		# fields = "__all__"	# 使用全部字段
    		# exclude = ["password"]	# 排除特定字段,选择其他字段
    		# 使用自定义字段
    		# fields = ['name', 'password', 'age', 'xx']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    传递数据和前端使用上则和 Form 组件类似

    自动生成的标签会丢失样式,所以实际使用中,会在定义要操作的字段时,定义字段的插件及属性,在插件属性中可以添加样式。

    from django import forms
    class MyForm(forms.ModelForm):
    	class Meta:
    		model = UserInfo
    		fields = ["name", "password", "age"]
    		widgets = {
                'name': forms.TextInput(attrs={'class': 'form-control'}),			# 定义字段使用的插件
                'password': forms.TextInput(attrs={'class': 'form-control'})		# 插件的参数中可以添加属性,将样式添加到这里
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    不过这样也字段多的时候也会比较麻烦。通常项目中会在 MyForm 初始化时,通过遍历字段给各字段添加属性的方式添加样式

    from django import forms
    class MyForm(forms.ModelForm):
    	class Meta:
    		model = UserInfo
    		fields = ["name", "password", "age"]
    	
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            for name, field in self.fields.items():
            	# 设置属性
            	if field.widget.attrs:		# 如果字段中有属性,保留原属性
            		field.widget.attrs['class'] = 'form-control'
            		field.widget.attrs['placeholder'] = '请输入' + field.label
            	else:
            		field.widget.attrs = {'class': 'form-control', 'placeholder': '请输入' + field.label}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    提交表单

    ModelForm 组件的表单时,可以将数据返回定义的类来进行校验等操作

    form = MyForm(data=request.POST)		# 创建对象,将提交的表单数据作为参数
    if form.is_valid():		# 使用 is_valid 方法对数据进行有效性校验,如果有效返回 True
    	print(form.cleaned_data)		# 整理数据成一个字典
    	form.save()		# 保存数据
    else:
    	# 如果校验失败,此时错误信息会保存在 form 里,可以将其返回页面,在页面中接收
    	return render(request, 'user_add_ModelForm.html', {'form': form})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    {% for field in form %}
        
        <div class="form-group">
            <label>{{ field.label }}label>
            {{ field }}
            
            <span style="color: red">{{ field.errors.0 }}span>
        div>
    {% endfor %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    填充数据

    如果需要在表单中填充数据,例如编辑的时候会有默认数据,只需要创建 form 对象时将数据库数据作为参数传给类的 instance 参数即可,其他的完全同生成表单

    def user_edit(request, nid):
        """编辑用户"""
        if request.method == "GET":
            row_obj = models.UserInfo.objects.filter(id=nid).first()
            form = MyForm(instance=row_obj)
            return render(request, 'user_edit.html', {'form': form})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    更新数据

    在提交表单时,使用 form.save() 方法可以添加数据到数据库,但是更新原有数据时必须告诉 form 操作的是哪条数据

    row_obj = models.UserInfo.objects.filter(id=nid).first()
    form = UserModelForm(data=request.POST,instance=row_obj)
    if form.is_valid():  # 检测数据有效性
    	form.save()  # 保存进数据库
    	return redirect('/user/list/')
    else:
    	# 校验失败,显示错误信息
    	return render(request, 'user_add_ModelForm.html', {'form': form})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    保存额外数据

    使用 form.save() 方法保存表单数据到数据库时,实际上保存的是用户提交的表单中的数据。如果有一些其他的数据不在用户提交的保单中,则可以手动添加数据到字段,然后保存

    form = UserModelForm(data=request.POST)
    if form.is_valid():  # 检测数据有效性
    	# 使用 form.instance.字段名 = 值 来手动添加数据,然后保存到数据库
    	form.instance.position = '新入职员工'
    	form.save()  # 保存进数据库
    	return redirect('/user/list/')
    else:
    	# 校验失败,显示错误信息
    	return render(request, 'user_add_ModelForm.html', {'form': form})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    数据校验

    简单校验

    is_valid 方法默认进行空值检测,如果需要更多校验方式,例如字符位数等,需要自定义字段

    from django import forms
    class MyForm(forms.ModelForm):
    	name = forms.Charfield(min_length=3,label="用户名")		# 此字段最小长度为3个字符
    	# name = forms.Charfield(disabled=True,label="用户名")		# 不可用模式,但是提交表单时认为是空
    	password = forms.Charfield(label='密码',required=True)	# 必填校验
    	class Meta:
    		model = models.UserInfo
    		fields = ['name', 'password', 'age']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    使用正则表达式校验

    在自定义字段时,可以添加参数 validators,使用正则表达式 RegexValidator 类进行校验自定义的字段。

    from django.core.validators import RegexValidator
    class PrettyNumForm(forms.ModelForm):
        mobile = forms.CharField(
            label='手机靓号',
            validators=[RegexValidator(r'^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$','手机号格式错误')]	# 参数2表示校验失败显示内容
        )
        class Meta:
            model = models.PrettyNum
            # fields = "__all__"  # 使用所有字段
            # exclude = ['level']   # 排除某个字段,取其他的字段
            fields = ['mobile','price','level','status']
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            for name, field in self.fields.items():
                if type(field.widget) == django.forms.widgets.TextInput or type(field.widget) == django.forms.widgets.NumberInput:
                    field.widget.attrs = {'class': 'form-control', 'placeholder': '请输入' + field.label}
                else:
                    field.widget.attrs = {'class': 'form-control'}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    使用校验函数校验

    可以在类定义中使用 clean_字段名 方法,将相应的字段数据传入并进行校验

        def clean_mobile(self):		# 校验 mobile 字段
            from django.core.exceptions import ValidationError		
            txt_mobile = self.cleaned_data.get('mobile')		# 获取 mobile 字段的数据
            if len(txt_mobile) != 11:				# 进行检验
                raise ValidationError("格式错误")		# 没通过检验,抛出一个错误
            else:
                return txt_mobile			# 通过检验,返回值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    自定义校验,返回错误信息

    可以通过 form.cleaned_data 获取返回的 form 数据字典,所以可以通过 form.cleaned_data[key] 来获取需要的表单数据,来自定义一些数据校验方式。也可以使用 form.cleaned_data.pop(key) 的方式来获取特定键的值,区别在于使用 pop 方法为弹出数据。

    如果自定义校验,或者进行数据核对(例如比较数据库中的信息),则可以主动添加错误信息并返回。

    form = UserModelForm(data=request.POST)
    if form.is_valid():  # 检测数据有效性
    	if models.UserInfo.objects.filter(name=form.cleaned_data['username'], password=form.cleaned_data['password']).exists():
    		# 登录成功,跳转页面
    		return redirect('/user/list/')
    	else:
    		# 用户名和密码对不上
    		form.add_error('password', '用户名或密码错误')  # 主动添加错误信息给 password 字段
    # 校验失败,显示错误信息
    return render(request, 'user_add_ModelForm.html', {'form': form})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    前端可以使用 {{ form.字段名.errors.0 }} 来获取错误信息

    表单校验的步骤

    django 对于表单校验时,遵循这几个步骤:

    • 先使用Fieldto_python()方法,它强制将值转换为正确的数据类型。
    • 使用Field validate()方法处理不适合验证器的特定字段验证。这个方法不会返回任何东西,也不应该改变值。可以覆盖它来处理不能或不想放在验证器中的验证逻辑。
    • Field上的run_validators()方法会运行该字段的所有验证器,不应该覆盖此方法。
    • Field子类上的clean()方法负责以正确的顺序运行to_python()validate()run_validators()并传播它们的错误。该方法返回干净的数据,然后将其插入到表单的cleaned_data字典中。
    • clean_() 方法是在表单子类上调用的——其中 被替换为表单字段属性的名称。这个方法做任何特定属性的清理工作,与字段的类型无关。这个方法不传递任何参数,需要在 self.cleaned_data 中查找字段的值,并且记住,此时它将是一个 Python 对象,而不是在表单中提交的原始字符串(它将在 cleaned_data 中,因为上面的一般字段 clean() 方法已经清理了一次数据)。这个方法可以用来校验特定字段,其返回值会替换 cleaned_data 中的现有值,所以它必须是 cleaned_data 中的字段值(即使这个方法没有改变它)或一个新的干净值。
    • 表单子类的 clean() 方法可以执行需要访问多个表单字段的验证。此方法可以覆写用作整体校验,如果需要可以返回一个完全不同的字典,这个字典将被用作 cleaned_data。覆写时,注意先执行一下父类的此方法,进行一次校验。

    信息提示语言

    通过实例测试可以知道,返回的错误信息是英文的,这是因为 django 在设置上是使用英文 en-us ,可以在 setting.py 中的 LANGUAGE_CODE 设置成中文 zh-hans

    前端自定义表单组件和 Form 的结合

    如果前端使用了自定义的表单组件,在后端 django 中也可以使用 Form 组件或 ModelForm组件。

    通常使用 Form 组件的流程为:

    1. django 将新创建(不包含数据)的 Form 对象传至模板,模板根据此对象进行渲染。
    2. 前端将表单中的数据发回 django,django 根据接收到的数据创建(含数据的) form 对象,根据需要进行验证
    3. 如果验证未通过,则会在 form 对象中产生错误信息,django 将此验证信息传入模板并渲染。

    如果使用自定义表单和 Form 组件相结合的方式,则变为:

    1. 前端需要根据 Form 对象渲染的情况创建表单组件,让提交的表单数据能够被 form 对象接收
    2. 前端提交的表单数据,django 接收此数据并创建含数据的 form 对象,根据需要进行验证
    3. 如果验证未通过,则会在 form 对象中产生错误信息。将此错误信息提取出来传给前端
    4. 前端接收到错误信息,呈现到自定义表单中

    首先需要明确的是,后端使用 Form 或 ModelForm 组件区别不大,主要区别在于 Form 需要手动定义各字段,而 ModelForm 则根据数据库中列的情况自动生成字段。在前端使用自定义表单组件时,字段不会传给前端,所以基本没有区别。

    前端需要创建的表单组件信息

    对于前端来说,首先要创建表单元素,使其提交能够被 Form 对象使用的表单数据。经过分析POST信息(假设 method 为 post),得知需要提交 csrfmiddlewaretoken 和表单数据两种。

    • csrfmiddlewaretoken 是 csrf 校验使用的数据,可以在后台取消校验,或者添加 csrf 校验信息(注:post 请求和 cookie 中都需要有)。
    • 表单数据:表单数据以键值对形式发送(JSON 对象),键名为 form 对象的字段名(字段变量名称),值是一个列表(因为表单中出现同名字段,就会在列表中添加数据),列表成员是字符串(因为序列化的缘故,所有数值也会变成字符串)。

    所以这时候前端需要做的,就是创建需要的表单元素,然后根据字段数据库字段,确定表单中各元素的 name 属性。并且根据需要,选择提交 csrf_token 的方式。

    后端接收前端发送的表单数据

    django 可以正常接收表单发送的数据,数据格式就是之前设置的键值对形式,键为表单字段,由前端元素的 name 属性确定,值为字符串列表,也可以使用 ajax 发送。

    后端接收到数据后,可以使用此数据来创建表单对象并验证,和使用 django 表单组件一样使用。当使用 is_valid() 方法验证过后(也可以自定义验证方法),如果返回真则是通过验证,否则即是未通过验证。未通过验证则会将错误信息存储在 form 对象中。

    经过分析表单对象可以发现,通常渲染模板时使用 {% for item in forms %} 来获取表单中各字段成为 html 元素,使用 {{ item.label }} 获取字段设置的别名,使用 {{ item.errors }} 来获取错误信息列表。相对应的,这些数据也存在于 form 表单对象中:

    • item : 通过表单对象的迭代器获取的对象,实际上可以通过 form.form._bound_fields_cache 获取全部元素。其类型是字典,键为各字段名称,值为 item 对象。
    • item.name : 字段名称,字符串类型,html元素的 name 属性值。
    • item.label : 字段别名,字符串类型
    • item.auto_id : html元素的 id,字符串类型
    • item.errors : 各字段的错误信息列表

    通过这些,则可以获取错误信息返回前端。前端接收到后可以将错误信息呈现给用户,待用户更改后重新发送表单数据给 django。

    以上的方法是基于基于表单模板的使用,其实对于所有的错误均保存在表单对象errors 里,且可以通过其 get_json_data() 方法获取。

    form.errors.get_json_data()
    
    • 1

    返回值是一个字典,键即是字段名(对于全局的错误,键是 '__all__'),值是错误列表。列表的每个成员是一个错误字典,错误信息在字典的message字段,错误代码在code字段。

    权限与后台管理

    Django 项目生成时,会自动生成后台管理应用:Admin。

    创建后台超级用户

    和创建 app 类似,在控制台输入

    python manage.py createsuperuser
    
    • 1

    然后根据提示输入用户名、邮箱、密码,就创建超级用户成功了。

    登录后台

    当创建了超级用户后,会在总路由里注册路由地址 admin,就可以使用这个 url 登录到默认的后台登录界面了。可以在后台界面添加用户、组、权限等。

    通过在 settings.py 中修改语言、时区等设置,可以更改后台界面的

    Xadmin 插件

    Xadmin 是基于原生 admin 的界面,在 github 上有相应的源码。它使用了基于 bootstrap 的样式,界面进行了美化。

    使用 xadmin 需先安装

    pip install xadmin-py3

    后台托管

    如果我们自己写了相应的数据模型,例如用户信息,可以通过后台访问,这就是托管。

    # models.py
    from django.db import models
    
    # 客户用户表
    class UserEntity(models.Model):
    	name = models.CharField(max_length=20)
    	age = models.IntegerField(default=0)
    	phone = models.CharField(max_length=11)
    	
    	class Meta:
    		# 表名
    		db_table = 'user' 
    		# 别名
    		verbose_name = '客户列表'
    		# 复数别名,默认添加个 s
    		verbose_name_plural = verbose_name
    
    	def __str__(self):
    		return self.name
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在 app 目录下的 admin.py 中可以托管本 app 使用的数据模型

    from django.contrib import admin
    # 导入数据模型
    from app.models import UserEntity
    
    # 注册模型到 admin 站点中
    admin.site.register(UserEntity)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后在后台界面就能看到相应 app 下的数据模型了。需要注意的是,每条数据显示对象,所以必须使用 __str__ 方法来定制对象的输出字符串。

    自定义表单

    通过在 admin.py 文件中,定义 admin.ModelAdmin 的子类来自定义表单。

    例如,添加 Store 的表单中只添加 name(自定义表单字段)

    class StoreAdmin(admin.ModelAdmin):
    	fields = ('name',)	# 字段,元组类型
    
    admin.site.register(Store, StoreAdmin)	# 注册模型和其相关配置
    
    • 1
    • 2
    • 3
    • 4

    分栏显示

    class StoreAdmin(admin.ModelAdmin):
    	fieldsets = (['Main', {'fields': ('name',)}],
    				['Advance', {'fields': ('address',),
    						'classes': ('collapse',)}])
    
    • 1
    • 2
    • 3
    • 4

    内联显示

    # 为外表创建内联类
    class FruitInline(admin.TabularInline):
    	model = Fruit
    
    # 在主表设置内联
    class StoreAdmin(admin.ModelAdmin):
    	inlines = [FruitInline]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    设置列表显示与搜索

    在 admin.ModelAdmin 的子类中,可以这样设置:

    class StoreAdmin(admin.ModelAdmin):
    	list_display = ('id', 'name', 'address','custom1')	# 列表显示的字段,元组
    	fields = ('id', 'name', 'address')	# 表单中使用的字段(可以和显示的不同)
    	list_per_page = 2		# 分页显示,每页显示2条记录
    	list_filter = ('id', 'name')	# 过滤器,一般配置分类过滤
    	search_fields = ('name', 'address')			# 搜索关键字
    	
    	# list_display 中定义了自定义的字段,非数据实体的字段
    	# 在此定义自定义字段显示内容,参数 obj 是传入的数据实体对象
    	def custom1(self, obj):
    		return obj.email
    	
    	# 自定义字段显示的标题(字段名)
    	custom1.short_description = 'E-mail'
    
    admin.site.register(Store, StoreAdmin)		# 注册模型和其相关配置
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    权限管理

    django 自带了权限管理模块 auth,提供了用户身份认证、用户组和权限管理

    与 auth 模块有关的数据库表有6个,分别是

    作用备注
    auth_user用户信息,包含 id、password、username、first_name、last_name、email、is_staff、is_active、date_joined
    auth_group组信息每个组拥有id和name两个字段
    auth_user_groupsuser和group之间的关系
    auth_permission用户许可、权限每条权限有id、name、content_type_id、codename四个字段
    auth_user_user_permissionsuser和permission之间的关系
    auth_group_permissions用户组和权限的对应关系使用用户组管理权限是一个更方便的方法,group中包含多对多字段permissions

    需注意的是,django 的缓存机制:django 会缓存每个用户对象,包括其权限 user_permissions。当手动改变某一用户的权限后,必须重新获取改用户对象,才能获取最新的权限。如不重新载入用户对象,则权限还是没有刷新。

    数据模型中的相关操作

    可以直接使用一些接口函数对内建的数据模型进行操作,从而做到权限及用户管理

    用户操作

    创建用户

    from django.contrib.auth.models import User
    user = User.objects.create_user(username, email, password, is_staff)	# is_staff 是个布尔值,表示是否登录admin后台
    user.save()
    
    • 1
    • 2
    • 3

    用户认证

    from django.contrib.auth import authenticate
    # 认证用户的密码是否有效,认证成功返回代表该用户的 user 对象,否则返回 None。
    # 需注意的是,此方法只检查认证用户是否有效,并不检查具体权限和是否激活(is_active)标识
    user = authenticate(username=username, password=password)
    
    from django.contrib.auth.hashers import check_password
    # 或使用 check_password 方法,返回布尔值
    user = User.objects.filter(username=username)
    check_password(password, user.password)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    修改用户密码

    user = auth.authenticate(username=usermane, password=old_password)
    if user:
    	user.set_password(new_password)
    	user.save()
    
    • 1
    • 2
    • 3
    • 4

    登录

    from django.contrib.auth import login
    user = authenticate(username=username, password=password)	# 验证成功返回用户对象
    if user:	
    	if user.is_active:		# 用户已经激活
    		# 登录,向 session 中添加 user 对象,便于对用户进行跟踪
    		login(request, user)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    获取当前用户

    user = request.user
    
    • 1

    判断当前用户是否通过验证(即已经登录)

    request.user.is_authenticated		# 布尔值
    # 如果是视图函数需要判断是否登录,还可以使用装饰器
    @login_required
    def inddex(request):
    	pass
    
    • 1
    • 2
    • 3
    • 4
    • 5

    退出登录

    from django.contrib.auth import logout
    def logout_view(request):
    	logout(request)
    
    • 1
    • 2
    • 3
    组操作

    django.contirb.auth.models.Group 定义了用户组的模型,每个用户组拥有 id 和 name 两个字段,该模型在数据库被映射为 auth_group 表。User 对象中有一个名为 groups 的多对多字段,由 auth_user_group 表维护。Group 对象可以通过 user_set 反向查询用户组中的用户。

    创建组

    group = Group.objects.create(name=group_name)
    group.save()
    
    • 1
    • 2

    删除组

    group.delete()
    
    • 1

    用户加入用户组

    user.groups.add(group)
    # 或者
    group.user_set.add(user)
    
    • 1
    • 2
    • 3

    用户退出用户组

    user.groups.reomve(group)
    # 或者
    group.user_set.remove(user)
    
    • 1
    • 2
    • 3

    用户退出所有用户组

    user.groups.clear()
    
    • 1

    用户组清除所有用户

    group.user_set.clear()
    
    • 1
    权限操作
    自定义模型权限

    在定义 model 时可以使用 Meta 定义此模型的权限

    class Discussion(models.Model):
    	...
    	class Meta:
    		permissions = (
    			('create_discussion', 'Can create a discussion'),
    			('reply_discussion', 'Can reply discussion'),
    		)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    为用户/组增删权限

    每个模型默认拥有增(add)删(delete)的权限

    # 添加用户权限
    user.user_permissions.add(permission)
    # 删除用户权限
    user.user_permissions.delete(permission)
    # 清空用户权限
    user.user_permissions.clear()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    用户拥有所在组的权限,使用用户组管理权限是一个更方便的方法。Group 中包含多对多字段 permissions,在数据库中由 auth_group_permissions 表进行维护

    # 添加组权限
    group.permissions.add(permission)
    # 删除组权限
    group.permissions.delete(permission)
    # 清空组权限
    group.permissions.clear()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    还可以获取某个特定用户的相关权限信息

    # 特定用户所在用户组的权限
    user_A.get_group_permissions()
    # 特定用户的所有权限
    user_A.get_all_permissions()
    
    • 1
    • 2
    • 3
    • 4
    权限验证

    可以在访问视图时,进行一些权限验证:

    登录验证

    登录验证的原始方式就是检查 request.user.is_authenticated 并重定向到登录页面,或返回一个错误信息

    from django.conf import settings
    from django.shortcuts import redirect, render
    
    
    def my_view(request):
        if not request.user.is_authenticated:
            return redirect(f"{settings.LOGIN_URL}?next={request.path}")
            # return render(request, "myapp/login_error.html")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    比较便捷的是使用装饰器 login_required

    from django.contrib.auth.decorators import login_required
    
    @login_required
    def my_view(request):
    	pass
    
    • 1
    • 2
    • 3
    • 4
    • 5

    login_required 会判断是否登录。如果没有登录,会重定向到 settings.LOGIN_URL 定义的路径中,并传递当前绝对路径到查询字符串中。例如: /accounts/login/?next=/polls/3/。如果用户已经登录,则正常执行视图函数。

    默认情况下,未登录跳转时将跳转前的绝对路径添加到名为 next 的查询字符串中,也可以在装饰器中自定义,使用参数 @login_required(redirect_field_name="my_redirect_field")

    另外如果重定向不想使用 settings.LOGIN_URL ,也可以添加到装饰器的参数中:@login_required(login_url="/login/")

    需注意的是 login_required 虽然不检查用户的 is_active 标识,但是默认的 AUTHENTICATION_BACKENDS 会拒绝非正常用户。除非在 settings.py 中进行设置:AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']

    管理人员登录验证

    如果是类似于管理后台,需要验证登录用户是否是管理人员(User.is_staff=True),则可以使用装饰器 @staff_member_requiredstaff_member_required 的使用方式和操作行为类似于 login_required 区别在于 staff_member_required 不仅检查 is_authenticated ,还检查 is_staffis_active

    验证权限

    检查用户权限:user.has_perm 方法用于检查用户是否拥有操作某个视图或模型的权限,若有则返回 True

    user.has_perm('blog.add_article')
    user.has_perm('blog.change_article')
    user.has_perm('blog.delete_article')
    
    • 1
    • 2
    • 3

    抛出异常的权限检查:has_perm 仅是进行权限检查,即使用户没有权限它也不会阻止程序执行相关操作。可以使用 @permission_required 装饰器代替 has_perm 并在用户没有相应权限时重定向到登录页或抛出异常。

    视图中使用这个装饰器验证是一个好的选择,但是如果使用视图类(CBV),而不是视图函数(FBV),则不能使用此装饰器。需要继承 RermissionRequiredMixin 这个类。

    # permission_required(perm[, login_url=None, raise_exception=False])
    
    @permission_required('blog.add_article')
    def post_atricle(request):
    	pass
    
    • 1
    • 2
    • 3
    • 4
    • 5

    permission_required 也可以接受可迭代权限,此时必须拥有所有权限才能访问视图。

    模板中也可以进行权限验证:主要使用 perms 这个全局变量。perms 对当前用户的 user.has_module_perms 和 user.has_perm 方法进行了封装。例如判断当前用户是否拥有 blog 应用下所有的权限:

    {{ perms.blog }}
    
    • 1

    这样结合 if 标签,可以选择性的根据用户权限显示不同内容了

    {% if perms.blog.add_article %}
    You can add atricles.
    {% endif %}
    {% if perms.blog.delete_article %}
    You can delete atricles.
    {% endif %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    自定义验证

    使用装饰器 @user_passes_test 可以自定义一个函数用以验证用户,该函数接收一个参数是当前用户的 django.contrib.auth.models.User 对象。当函数返回 False 时执行重定向。

    # 使用格式为 user_passes_test(test_func, login_url=None, redirect_field_name='next')
    from django.contrib.auth.decorators import user_passes_test
    
    def email_check(user):
        return user.email.endswith("@example.com")
    
    
    @user_passes_test(email_check)
    def my_view(request):
    	pass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Admin 中的权限分配

    当编辑某个 user 信息时,可以在 User permissions 栏为其设置权限。权限的规则是:

    权限显示为:应用 | 模型 | 行为(增删改查等)
    权限数据为:应用.行为_模型
    其中 行为_模型 就是数据库表中的 codename,应用即 app_label

    django guardian

    django 自带的权限管理机制是针对模型的,这就意味着如果一个用户对某一模型有权限,则此模型中的所有数据均有权限。如果希望实现针对单个数据的权限管理,则需要使用第三方库比如 django guardian 库。

    django-guardian 官网英文文档

    guardian 的安装和使用准备

    使用 pipy 安装

    pip install django-guardian

    安装完成后,需要将添加到项目中,首先添加 app,然后添加身份验证后端

    # settings.py
    
    INSTALLED_APPS = ( 
        # ... 
        'guardian',
    )
    
    AUTHENTICATION_BACKENDS = ( 
        'django.contrib.auth.backends.ModelBackend', # 这是Django默认的
        'guardian.backends.ObjectPermissionBackend', # 这是guardian的
    ) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    然后创建 guardian 的数据库表。创建完成后会多出两张表:guardian_groupobjectpermission 和 guardian_userobjectpermission,分别记录了用户组/用户与model及具体object的权限对应关系

    python manage.py migrate

    下面是表中各字段的含义

    字段说明
    id默认主键
    object_pkobject 的 id,标识具体是哪个对象需要授权,对应的是具体某一数据
    content_type_id记录具体哪个表的id,对应的是django系统表django_content_type内的某条数据,django所有注册的model都会在这个表里记录
    group_id/user_id记录是那个组/用户会有权限,对应的是auth_group/auth_user表里的某条记录
    permission_id记录具体的某个权限,对应的是auth_permission表里的某条记录

    需注意的是,一旦将 django-guardian 配置进项目,当调用 migrate 命令时将会创建一个匿名用户的实例(名为 AnonymousUser)。guardina 的匿名用户与 django 的匿名用户不同,django 匿名用户在数据库中没有条目,但 guardian 匿名用户有。这意味着以下代码将会返回意外的结果:

    request.user.is_anonymous = True

    其他的一些配置

    在 settings.py 中可以进行其他的一些配置

    # 如果GUARDIAN_RAISE_403设置为True,guardian将会抛出django.core.exceptions.PermissionDenied异常,而不是返回一个空的django.http.HttpResponseForbidden
    # 需注意的是GUARDIAN_RENDER_403和GUARDIAN_RAISE_403不能同时设置为True。否则将抛出django.core.exceptions.ImproperlyConfigured异常
    GUARDIAN_RAISE_403 = False
    
    # 如果GUARDIAN_RENDER_403设置为True,将会尝试渲染403响应,而不是返回空的django.http.HttpResponseForbidden。模板文件将通过GUARDIAN_TEMPLATE_403来设置。
    GUARDIAN_RENDER_403 = True
    GUARDIAN_TEMPLATE_403 = '403.html'
    # 用来设置匿名用户的用户名,默认为AnonymousUser
    ANONYMOUS_USER_NAME = 'AnonymousUser'
    
    # Guardian支持匿名用户的对象级权限,但是在我们的项目中,我们使用自定义用户模型,默认功能可能会失败。这可能导致guardian每次migrate之后尝试创建匿名用户的问题。将使用此设置指向的功能来获取要创建的对象。一旦获取,save方法将在该实例上被调用。默认值为guardian.ctypes.get_default_content_type
    GUARDIAN_GET_INIT_ANONYMOUS_USER = guardian.ctypes.get_default_content_type
    
    # Guardian允许应用程序提供自定义函数以从对象和模型中检索内容类型。当类或类层次结构以ContentType非标准方式使用框架时,这是有用的。大多数应用程序不必更改此设置。
    # 例如,当使用django-polymorphic适用于所有子模型的基本模型上的权限时,这是有用的。在这种情况下,自定义函数将返回ContentType多态模型的基类和ContentType非多态类的常规模型。默认为guardian.ctypes.get_default_content_type
    GUARDIAN_GET_CONTENT_TYPE = guardian.ctypes.get_default_content_type
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    admin 管理界面中使用

    此节引用的文章来自知乎

    使用Guardian最直观的特色就是在django-admin页面可以图形化地使用对象权限功能。 首先,在admin.py开头,从guardian添加两个导入:

    from guardian.admin import GuardedModelAdminMixin
    from guardian.shortcuts import get_objects_for_user, assign_perm

    GuardedModelAdminMixin 是一个类,包含权限管理的功能,其中Mixin(混入)代表这个类不能单独作为ModelAdmin类使用,需要与其他的ModelAdmin类共同作为子类的父类,新的子类即可既有ModelAdmin的功能也有Guardian权限管理的功能。 但是,GuardedModelAdminMixin本身的功能还是欠缺了点,或者说它本来就是希望开发者自定义重写的。网上有大神将此类继承后重写,完善了其功能,我们将代码抄过来即可(可根据自己项目的特点修改其代码):

    class GuardedMixin(GuardedModelAdminMixin):
        # app是否在主页面中显示,由该函数决定
        def has_module_permission(self, request):
            if super().has_module_permission(request):
                return True
            return self.get_model_objs(request,'view').exists()
    
        # 在显示数据列表时候,哪些数据显示,哪些不显示,由该函数控制
        def get_queryset(self, request):
            if request.user.is_superuser:
                return super().get_queryset(request)
            data = self.get_model_objs(request)
            return data
            
        # 内部用来获取某个用户有权限访问的数据行
        def get_model_objs(self, request, action=None, klass=None):
            opts = self.opts
            actions = [action] if action else ['view', 'change', 'delete']
            klass = klass if klass else opts.model
            model_name = klass._meta.model_name
            data = get_objects_for_user(
                user=request.user, 
                perms=[f'{perm}_{model_name}' for perm in actions],
                klass=klass, any_perm=True
            )
            if hasattr(request.user, 'teacher'):
                data = teacher.objects.filter(id=request.user.teacher.id) | data
            return data
        # 用来判断某个用户是否有某个数据行的权限
        def has_perm(self, request, obj, action):
            opts = self.opts
            codename = f'{action}_{opts.model_name}'
            if hasattr(request.user, 'teacher') and obj == request.user.teacher:
                return True
            if obj:
                return request.user.has_perm(f'{opts.app_label}.{codename}', obj)
            else:
                return self.get_model_objs(request, action).exists()
    
        # 是否有查看某个数据行的权限
        def has_view_permission(self, request, obj=None):
            return self.has_perm(request, obj, 'view')
    
        # 是否有修改某个数据行的权限
        def has_change_permission(self, request, obj=None):
            return self.has_perm(request, obj, 'change')
    
        # 是否有删除某个数据行的权限
        def has_delete_permission(self, request, obj=None):
            return self.has_perm(request, obj, 'delete')
    
        # 用户应该拥有他新增的数据行的所有权限
        def save_model(self, request, obj, form, change):
            result = super().save_model(request, obj, form, change)
            if not request.user.is_superuser and not change:
                opts = self.opts
                actions = ['view', 'add', 'change', 'delete']
                [assign_perm(f'{opts.app_label}.{action}_{opts.model_name}', request.user, obj) for action in actions]
            return result
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    当然,这些代码不是尽善尽美的,我们可根据自己项目的特点适当修改这些代码。 而后,将这个我们自己写的GuardedMixin类作为我们自己原来的模型的ModelAdmin类的父类之一:

    class TeacherAdmin(GuardedMixin,ModelAdmin):
        # 详情表单页
        inlines = [Class_head_yearInline,FamilyMemberInline]
        fieldsets = [
            # ...
        ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    admin.py 就编辑完成了,在admin管理页面的Teacher页面中就可以设置某个管理员针对某个teacher对象的权限了。

    在图形界面具体的数据对象详情页,会有 对象权限 ,即可以设置某一用户针对此对象的权限设置。共有增删改查四项。

    模型操作

    除了图形界面外,在视图等地方需要使用代码来操作数据模型。guardian 使用的用户和组和 django 的一样,只有权限划分中有部分区别

    自定义模型权限

    和 django 一样,可以对模型进行自定义权限

    class CommonTask(models.Model):
    	...
    	class Meta:
            permissions = (
                    ('view_task', '查看任务权限'),
                    ('change_task', '更改任务权限'),
                    ('stop_task', '停止任务权限'),
                )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    为用户/组增删权限

    可以使用 guardina.shortcuts.assign_perm() 方法来分配对象权限

    from django.contrib.auth.models import User, Group
    from guardian.shortcuts import assign_perm
    
    # 获取数据对象
    from models import CommonTask
    obj = CommonTask.objects.get(pk=1)
    
    # 获取用户对象
    user = User.objects.get(name='test_account')
    # 获取用户组对象
    group = Group.objects.get(name='test')
    
    # 确认用户是否对数据对象有权限
    if not user.has_perm('view_task', obj):
    	# 给用户处理数据对象的权限
    	assign_perm('view_task', user, obj)		# 注:这里的 user 和 obj 都可以是 QuerySet,即可以将多个数据对象权限赋给多个用户
    
    # 用户加入组
    user.groups.add(group)
    # 确认用户是否对数据对象有权限
    if not user.has_perm('view_task', obj):
    	# 给组处理数据对象的权限
    	assign_perm('view_task', group, obj)	# 同样能将多个数据对象赋权给多个组
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    需要注意的是,guardian.shortcuts.assign_perm(perm, user_or_group, obj=None) 是针对某一具体对象赋权,但并没有对整个 model 赋权,所以 has_perm('app.view_task') 时,会返回 False。另外 assign_perm 方法的第三个参数如果使用 None,则第一个参数格式必须为 app.perm_codename,此时为赋予 model 的权限而不是具体数据对象。

    可以使用 guardian.shortcuts.remove_perm(perm, user_or_group=None, obj=None) 方法移除授权,需注意的是第二个参数不能是QuerySet而必须是instance,所以不能同时去除多个用户的权限。移除完同样需要刷新用户对象,保证缓存最新的权限。

    from guardian.shortcuts import remove_perm
    
    # 移除用户/组的特定数据对象权限
    remove_perm('view_task', user, obj)
    # 移除用户/组的所有对象(即整个数据模型)权限
    remove_perm('app.view_task', group)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    验证权限

    验证是否有权限可以使用上例中的 user.has_perm() 方法。此外,guardian 还提供了一些其他的方法

    # get_perms(user_or_group,obj) 方法可以根据用户或组以及对象来获取权限(has_perm 不能通过组验证)
    from guardian.shortcuts import get_perms
    
    get_perms(group, obj)		# 返回一个权限列表 ['view_task']
    'permcodename' in get_perms(user_or_group, obj)		# 返回一个布尔值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    # get_objects_for_user(user, perms, klass=None, use_groups=True, any_perm=False) 
    from guardian.shortcuts import get_objects_for_user
    
    # 此方法可以根据用户和权限获取数据对象,获取的是一个 QuerySet
    get_objects_for_user(user, 'app.view_task')
    # 第二个参数可以写成列表,返回同时满足权限的数据对象
    get_objects_for_user(user, ['app.view_task', 'app.stop_task'])
    # 或使用 any_perm=True,满足列表任意权限条件即可
    get_objects_for_user(user, ['app.view_task', 'app.stop_task'], any_perm=True)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    # get_users_with_perms(obj, attach_perms=False, with_superusers=False, with_group_users=True, only_with_perms_in=None)
    from guardian.shortcuts import get_users_with_perms
    
    # 可以根据数据对象的权限获取用户,返回 QuerySet
    get_users_with_perms(obj)
    # 默认返回的用户中没有 superuser,可以通过 with_superusers=True 让超级用户包含在内
    get_users_with_perms(obj, with_superusers=True)
    # 参数 attach_perms=True 可以返回一个字典,可以查看各用户拥有的具体权限
    get_users_with_perms(obj, with_superusers=True, attach_perms=True)
    # 如果仅想查看某个权限的用户,可以使用 only_with_perms_in 参数
    get_users_with_perms(obj, with_superusers=True, only_with_perms_in=['view_task'])
    # 默认用户加入了组后拥有组的权限,如果不想看继承自组的权限,则使用 with_group_users=False
    get_users_with_perms(obj, with_superusers=True, with_group_users=False)
    
    # get_groups_with_perms 与 get_users_with_perms 方法类似,但是只接收2个参数 obj 和 attach_perms
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    guardian 对装饰器 permission_required 做了扩展,能够对对象权限进行校验。使用方式同 permission_required,但是增加了第二个参数,这个参数是一个元组,格式为(model, model_field, value)。通过第二个参数能够获取到一个数据对象 QuerySet (查询逻辑为 model.objects.get(model_field=value)),如果用户对这个对象有第一个参数的权限,则可以使用视图函数

    guardian 也提供了模板标签,方便在模板中对数据对象的权限进行校验

    
    {% load guardian_tags %}
    
    
    {% get_obj_perms request.user for task as 'task_perms' %}
    {% if 'view_task' in task_perms %}
    显示有权限查看的数据
    {% endif %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    孤儿对象许可

    举个例子,用户 A 拥有数据对象 O 的权限,某天用户 A 被删除了,但是分配的权限还存在数据库中,这个就是孤儿对象许可。如果又有一天,创建了用户 A ,则新建立的用户就立刻拥有了数据对象 O 的权限,这是不对的!因此当删除 User 和相关 Object 时,一定要删除相关的所有 UserObjectPermission 和 GroupObjectPermission 对象。

    解决办法有三个:

    1. 显式编写代码
    2. 执行 django 命令

    python manage.py clean_orphan_obj_perms

    1. 定期调用 guardian.utils.clean_orphan_obj_perms()
      该函数会返回删除的对象数目。可以使用 celery 定期调度这个任务

    第二和第三个方法不是合理的生产环境的解决办法,真正想要解决,还是需要手动编码,最优雅的方式是加上 post_delete 信号给User或Object 对象,例如

    from django.contrib.auth.models import User
    from django.contrib.contenttypes.models import ContentType
    from django.db.models import Q
    from django.db.models.signals import pre_delete
    from guardian.models import UserObjectPermission
    from guardian.models import GroupObjectPermission
    from models import Task	# 自定义的模型
    
    def remove_obj_perms_connected_with_user(sender, instance, **kwargs):
        filters = Q(content_type=ContentType.objects.get_for_model(instance), object_pk=instance.pk)
        UserObjectPermission.objects.filter(filters).delete()
        GroupObjectPermission.objects.filter(filters).delete()
    
    pre_delete.connect(remove_obj_perms_connected_with_user, sender=Task)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    生成html代码

    有时候,在前端不好确定的代码,可以通过后端生成,然后传递给前端使用。这里以翻页的手动代码进行举例:

    from django.utils.safestring import mark_safe  # 将后端字符串标记为安全,可以传递到前端做为前端的 HTML 代码使用
        # 前端的翻页页码
        page_list = []
    
        # 计算出当前页的前三后三页
        if page >= 5:  # 需要首页
            ele = f'
  • {q}&order={order}&by={by}&page=1">首页
  • '
    page_list.append(ele) for i in range(page - 3, page + 4): if i <= 0 or i > all_page: continue if i == page: ele = f'
  • {q}&order={order}&by={by}&page={i}">{i}(current)
  • '
    else: ele = f'
  • {q}&order={order}&by={by}&page={i}">{i}
  • '
    page_list.append(ele) if page <= all_page - 4: ele = f'
  • {q}&order={order}&by={by}&page={all_page}">尾页
  • '
    page_list.append(ele) page_str = mark_safe(''.join(page_list)) # 使用 mark_safe() 方法将字符串变为前台代码 return render(request,'pn_list.html', page_str)
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    如果不使用 mark_safe 标记,传到前端的数据会以字符串形式呈现。标记后则前端认为字符串是就是前端代码。

    cookie、session 和 token

    cookie 信息

    创建、设置 cookie

    django 可以通过设置响应来设置 cookie 信息,使用HttpResponse.set_cookie() 方法来设置 Response 的 cookie:

    def set_cookie(request):
    	resp = HttpResponse()
    	resp.set_cookie('username', 'zhangsan', expires=datetime.now()+timedelta(days=3))
    	return resp
    
    • 1
    • 2
    • 3
    • 4

    设置 cookie 时可以设置 max-age 或 expire,来确定 cookie 的寿命周期。如未指定,则表示永久有效。

    • max_age 有效时长,单位为秒。默认为0,关闭浏览器即失效。
    • expire 持续到某一时间节点。使用 datetime.datetime 对象

    获取 cookie

    通过 request 可以获取 cookie 信息

    name = request.COOKIES.get('username')
    
    • 1

    删除 cookie

    响应中发送删除 cookie 的数据,浏览器接收到了就可以删除响应的 cookie 信息

    def del_cookie(request):
    	resp = HttpResponse()
    	resp.delete_cookie('username')
    	return resp
    
    • 1
    • 2
    • 3
    • 4

    注意

    需要注意的有

    • cookie 不支持中文(可能部分浏览器支持,最好不要使用中文)
    • 不能跨浏览器,不能跨域

    session 信息

    django 的 session 依赖于 cookie 技术,因为使用 session 会自动生成 session_id,用来确定 session 信息的归属,而 session_id 会记录在 cookie 中。

    session 的存储

    django 的 session 数据默认存储在数据库的 django_session 表中,通过 cookie 中的 sessionid 获取相应。也可以在 settings.py 里设置,例如保存至 redis。

    # settings.py
    
    # 配置缓存
    CACHES = {
        'session_redis': {  # 保存方案名称
            'BACKEND': 'django_redis.cache.RedisCache',  # 缓存方案
            'LOCATION': 'redis://127.0.0.1:6379/1',  # redis 的主机地址、端口和数据库编号
            'OPTIONS': {  # 其他的一些选项
                'CLIENT_CLASS': 'django_redis.client.DefaultClient',  # 连接客户端
                'PASSWORD': '123456',  # 口令
                'SOCKET_CONNECT_TIMEOUT': 5,  # 连接超时时间,单位秒
                'SOCKET_TIMEOUT': 5,  # 读写超时时间,单位秒
                'CONNECTION_POOL_KWARGS': {'max_connections': 10, }  # 连接池参数
            },
        },
    }
    
    # 配置 SESSION 缓存
    SESSION_ENGINE = "django.contrib.sessions.backends.cache"	# 保存至缓存
    SESSION_CACHE_ALIAS = "session_redis"			# 保存缓存方案
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    启用 session

    django中默认启用了session,如果要自定义添加 session,则需要在 settings.py 中的 INSTALLED_APP 中添加,并在 MIDDLEWARE 中添加 session 的中间件。

    创建 session 信息

    在 django 中,使用 request.session[key] = value 能够记录 session 的信息,同时会自动生成验证 session_id 返回到用户浏览器 cookie 中,并且将 session_id 和验证字符串都存储到数据库的 django_session 表中,以便下次使用时自动验证。

    获取并检查 session 信息

    如果是已经记录 session 信息的用户再次访问,则请求信息的 cookie 中会有 session_id 。django 会从数据库中查询是否存在此 session_id,如果存在则能够获取设置的 session[key] 的 value ,如果不存在则不能够获取。所以使用 request.session.get(key) 来获取设置数据就能够自动检查 session 信息。

    session 超时

    可以使用 request.session.set_expiry(sec) 来设置 session 的超时时间,sec 单位为秒。

    删除、注销 session 信息

    可以使用 del request.session[key] 的方式删除指定的 session 信息,使用 request.session.clear() 方法可以清除 session 信息,即进行注销。

    token

    token 是身份令牌,表示一个有权限的访问用户成功登录。以后再访问时如果有 token (且 token 有效)就不再进行登录验证。token 的信息一般都是自定义的,较简单的方式是在成功登录后,产生一个 uuid 到 cookie 中,这个 uuid 就是 token。

    def add_token(request):
    	# 生成token
    	token = uuid.uuid4().hex
    	resp = HttpResponse('增加了 token 到 cookie 中')
    	resp.set_cookie('token', token, max_age=60*60*24)
    	request.session['token'] = token
    	return resp
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    token 和 sessionid 都是用来确定访问用户的,其最大的区别在于 session 会占用服务器资源,而 token 会交给客户端,通过请求头来维护,节省了服务器资源。session 的主要目的是给无状态的 HTTP 协议添加状态保持,通常在浏览器作为客户端的情况下使用;而 Token 主要目的是鉴权,所以更多用在第三方 API。所以目前基于Token的鉴权机制几乎已经成了前后端分离架构或者对外提供API访问的鉴权标准,得到广泛使用。

    对于 JSON Web Token(JWT) 的方案,详细的可以看这里

    Django+JWT实现Token认证

    在 api 设计中,使用 django-RESTful 是个比较方便的方案,且 django-RESTful 框架包含了方便的 token 验证方案。

  • 相关阅读:
    MAC 搭建vue开发环境,配置环境变量
    关于RNNoise、webrtc_ns、三角带通滤波器、对数能量
    C语言学生成绩管理系统
    安装njnx --chatGPT
    Kafka系列之:深入理解Transformations
    什么让水果店生意好起来,水果店做生意经验分享
    22.07.03.05(Wireshark 抓包并分析)
    .\missyou-0.0.1-SNAPSHOT.jar中没有主清单属性
    [附源码]java毕业设计游戏战队考核系统
    day1_QT
  • 原文地址:https://blog.csdn.net/runsong911/article/details/127936061