初学者Django框架的基本使用,以及项目部署(Docker部署)

Django框架

Django是Python Web应用框架, 基于Python的WSGI(Web Service Gateway Interface)Web服务网关接口, Django从3.0开始运行 ASGI (异步服务网关接口)。

Django三大版本: Django 1.x、Django 2.x, Django 3.x(性能最优, 比较FastAPI/Tornado)。

一、 Django入门

1.1 基本概念

1.2 创建环境与app项目

安装依赖包

pip install django==2.2 -i https://mirrors.aliyun.com/pypi/simple

【注意】如果Python版本(3.7.4+)很高时,SQLite3版本同样很高,则django版本建议使用django==2.1.5+;因为,admin.site 站点管理时,会报auth_user_old表不存在的错误。

进入"终端" CMD命令, 通过 django-admin 命令创建django项目。

django-admin startproject helloDjango

通过django-admin命令创建app应用

django-admin startapp  mainapp

在一个Django项目中,存在很多的app应用(模块), 创建好的app需要注册到主工程中(settings.py)

其它命令,可以通过django-admin help 命令查看:

[django]
    check
    compilemessages
    createcachetable
    dbshell
    diffsettings
    dumpdata
    flush
    inspectdb
    loaddata
    makemessages
    makemigrations
    migrate
    runserver
    sendtestemail
    shell
    showmigrations
    sqlflush
    sqlmigrate
    sqlsequencereset
    squashmigrations
    startapp
    startproject
    test
    testserver

查看某一命令的使用: django-admin help runserver

查看相关的命令: python manage.py help

Available subcommands:

[auth]
    changepassword
    createsuperuser

[contenttypes]
    remove_stale_contenttypes

[django]
    check
    compilemessages
    createcachetable
    dbshell
    diffsettings
    dumpdata
    flush
    inspectdb
    loaddata
    makemessages
    makemigrations
    migrate
    sendtestemail
    shell
    showmigrations
    sqlflush
    sqlmigrate
    sqlsequencereset
    squashmigrations
    startapp
    startproject
    test
    testserver

[sessions]
    clearsessions

[staticfiles]
    collectstatic
    findstatic
    runserver

1.3 django项目结构

项目结构如下:

helloDjango
		|--- helloDjango   主工程目录
				 		|----  settings.py   # 设置文件, 数据库连接、app注册、中间件及模板配置
				 		|----  urls.py       # 总路由
				 		|----  wsgi.py       # Django实现wsgi的脚本
            |----  __init__.py
		|--- mainapp      应用模块(主)
						|----  __init__.py
						|---- admin.py      # 后台管理配置脚本
						|---- models.py     # 数据模型类声明所在脚本
						|---- views.py      # 声明当前应用的视图处理函数或类
						|---- urls.py       # 自已增加的当前应用模块的子路由
						|---- tests.py      # 当前应用模块的单元测试类
						|---- apps.py       # 声明当前应用的基本信息
		|--- manage.py     WEB应用的启动脚本, 项目工程的入口

启动项目的命令: python manage.py runserver

1.4 Django请求流程

1. 到urls分发器 (主路由urls.py -> 子路由 urls.py)
2. urls分发器根据路由规则(正则)分发到views
3. views去调用Model,交互数据
4. views将数据渲染(解析模板标签)到模板中,获取渲染之后HTML文本信息
5. 模板页面呈现给用户(封装HttpResponse对象)
http://127.0.0.1:8000/m/add/?csrfmiddlewaretoken=GQg9ySsT2KKOg2mzFosONCE3WqxaEnqK7UFkwbsGmwo9QLYbajI9PRfAgfHi0pgi&sn=111&name=%E7%8B%84%E5%A4%A7%E5%93%A5

1.5 请求和响应

请求对象: django.http.HttpRequest
响应对象: django.http.HttpResponse | JsonResponse
快捷函数: django.shortcuts.render 渲染 | redirect 重定向  ,  快速生成响应的对象

请求对象的属性:

request.method 请求方法,请求方法有:GET、POST、PUT/PATCH、DELETE、OPTIONS
request.GET  QueryDict 字典类型, get请求的查询参数
request.POST QueryDict 字典类型, post请求的表单form参数
request.META QueryDict 字典类型, 存放客户端环境相关参数, 如REMOTE_ADDR 客户端的IP地址

render函数:

render(request, '模板html文件', {}) 渲染模板,第三个参数是dict类型,可以在渲染模板时,替换{{ }}或{% %}表达式。

二、 数据库连接与ORM模型

ORM(Object Relationship Mapping 对象关系映射): 将类和表进行映射, 针对类的实例操作时,即对表的行数据进行操作。在Python中,使用元类和相关的自省函数(hasattr、getattr、setattr、isinstance)实现的。

2.1 数据库连接配置

默认是sqlite3数据库, 在使用ORM模型之前,需要先生成迁移文件,再执行迁移命令,在数据库中生成这些模型对应的表。

  • 先生成迁移文件
python manage.py  makemigrations
  • 开始迁移(生成表、 修改表、删除表)
python manage.py migrate

注意: 一旦生成了迁移文件并且迁移成功之后,不要删除迁移文件。

sqlite3数据库的文件访问方式可以通过python的内置模块sqlite3(微型关系数据库, 不强调数据类型),还可以是pycharm的数据面板打开。

以下是python的sqlite3模块打开sqlite.db数据库文件的方式:

>>> import sqlite3
>>> db = sqlite3.connect('/Users/apple/PycharmProjects/项目名/db.sqlite3')
>>> cursor = db.cursor()
>>> sql = "select name from sqlite_master where type='table'"
>>> cursor.execute(sql)
<sqlite3.Cursor object at 0x104c80420>
>>> cursor.fetchall()
[('django_migrations',), ('sqlite_sequence',), ('auth_group_permissions',), ('auth_user_groups',), ('auth_user_user_permissions',), ('django_admin_log',), ('django_content_type',), ('auth_permission',), ('auth_user',), ('auth_group',), ('django_session',)]

2.2 初步使用ORM模型

在app模块中的models.py 定义一个用户(客户)模型

from django.db import models

class UserEntity(models.Model):
     # 默认情况下会自动创建id主键
     name = models.CharField(max_length=20)
     age = models.IntegerField(default=0)
     phone = models.CharField(max_lengt=11)
     
     class Meta:
     		 # 指定当前模型类映射成哪一个表
     		 db_table = 'app_user'

模型创建完成后,先后执行生成迁移文件和迁移。

2.3 CURD

查询

UserEntity.objects.all()  # 查询所有, list
UserEngtity.objects.get(pk=id)  # 根据主键值查询一个实体对象

X.objects 对象在X模型类定义时,由它的父类的元类动态添加的cls.add_to_class('objects', manager)。此对象主要用于对模型的查询操作(基于QuerySet类构建)。

增加

u = UserEntity()
u.name = 'disen'
u.age = 20
u.phone = '177'

# 保存模型对象
u.save()

删除

u = UserEntity.objects.get(1)
u.delete()  # 删除

更新

u = UserEntity.objects.get(3)
u.name = '李成'
u.save()

django作业: 完成以上的orm模型练习。

from django.db import models

# Create your models here.
# 声明学生模型类

class Student(models.Model):
    # 如果声明的字段不存在主键primary key ,默认增加id 主键字段
    sn = models.IntegerField(primary_key=True, db_column='sn')
    name = models.CharField(max_length=20, null=False)
    age = models.CharField(max_length=30)
    sex = models.CharField(max_length=2, default='男')

    def __str__(self):
        return f'{self.sn}, {self.name}, {self.age}, {self.sex}'

    class Meta:
        db_table = 'tb_student'
        # ordering = ('-age', )

Python Console中的命令:

>>> from mainapp.models import Student
>>> Student.objects.all()
<QuerySet [<Student: 1, disen, 1991-10-12,>, <Student: 2, jack, 1992-10-12,>]>
>>> Student.objects.get(pk=2)
<Student: 2, jack, 1992-10-12,>
>>> s2 = Student.objects.get(pk=2)
>>> s2
<Student: 2, jack, 1992-10-12,>
>>> s2.delete()
(1, {'mainapp.Student': 1})
>>> Student.objects.all()
<QuerySet [<Student: 1, disen, 1991-10-12,>]>
>>> s1 = Student.objects.get(pk=1)
>>>s1.name = '李阳'
>>>s1.save()
>>>s2.save()

除了Python Console之外,也可以在Terminal中通过Python交互环境进入,但需要将项目的path目录复制,如项目的位置为/Users/apple/PycharmProjects/xpy201/hidjango,则如下操作:

(xpy201) hidjango # python
>>> import os, sys
>>> import django
>>> sys.path.insert(0, '/Users/apple/PycharmProjects/xpy201/hidjango')
>>> os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hidjango.settings')
'hidjango.settings'
>>> django.setup()
>>> from mainapp.models import Student
>>> Student.objects.all()
<QuerySet [<Student: 1, 李阳, 1991-10-12,>, <Student: 2, jack, 1992-10-12,>]>

模型类的objects是Model的元类动态添加的: cls.add_to_class('objects', manager)base.py 360行,它的类型是django.db.models.Manager类(QuerySet查询结果集类)。

2.4 ORM应用于View

ORM定义的模型类,在urls路由对应的处理view函数中使用。根据客户端请求,要么查询数据,要么修改或保存数据,根据相关的业务数据要求,通过ORM实现数据的查询、修改、存储和删除。

2.4.1 表单数据保存

将POST请求的表单数据进行存储。首先,在view函数所在的脚本中,导入模型类,如Student

from .models import Student

# path: /m/add/
def add_stu(request: HttpRequest):
    if request.method == 'POST':
        form = request.POST
        
        s = Student()
        s.sn = form.get('sn', None)
        s.name = form.get('name', None)
        s.age = form.get('age', None)
        s.sex = form.get('sex', '男')
        
        s.save()
        return redirect('/m/list/')
      
     return render(request, 'add_stu.html', locals())

Window 操作系统如果出现debug下的gbk编码问题,则修改django/views/debug.py文件第331和338两行的文件编码为UTF-8,如:

 def get_traceback_html(self):
        """Return HTML version of debug 500 HTTP error page."""
        with Path(CURRENT_DIR, 'templates', 'technical_500.html').open(encoding='utf-8') as fh:
            t = DEBUG_ENGINE.from_string(fh.read())
        c = Context(self.get_traceback_data(), use_l10n=False)
        return t.render(c)

    def get_traceback_text(self):
        """Return plain text version of debug 500 HTTP error page."""
        with Path(CURRENT_DIR, 'templates', 'technical_500.txt').open(encoding='utf-8') as fh:
            t = DEBUG_ENGINE.from_string(fh.read())
        c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False)
        return t.render(c)

修改完成后,确认保存。或者降低django的版本为pip install django==2.0.1

2.4.2 查询数据渲染

在新的view函数中,查询所有的数据:

# 处理客户端发送url为 /m/list/ 的请求
def list_stu(request):
    datas = Student.objects.all()
    return render(request, 'list_stu.html', 
                  context={'datas': datas, 'title': '列出所有学生'})

将数据渲染到html中,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>
head>
<body>
<h1>{{ title }}h1>
<table border="1" cellspacing="0" cellpadding="5" width="80%">
    <thead>
        <th>学号th>
        <th>姓名th>
        <th>生日th>
        <th>性别th>
        <th width="200px">操作th>
    thead>
    {% for stu in datas %}
       <tr>
           <td>{{ stu.sn }}td>
           <td>{{ stu.name }}td>
           <td>{{ stu.age }}td>
           <td>{{ stu.sex }}td>
           <td>
               <button onclick="">删除button>
               <button onclick="">编辑button>
           td>
       tr>
    {% endfor %}

table>

body>
html>

2.4.3 API接口数据

API接口: 前端页面的数据接口(URL),通过ajax(XMLHttpRequest)或 fetch()异步请求的方式获取API接口数据。API接口的数据,一般使用是json格式的文本字符串,在View函数中,对响应的json数据通过JsonResponse进行封装。

API接口说明文档:

base_url = ‘http://localhost:8000’

  1. 删除学生接口
  • url : /m/del/

  • 请求方法: GET

  • 请求参数(查询参数):

    sn 表示学号
    
  • 响应的数据

    • 正确(成功)

      {"data": "haha", "code": 0}
      
    • 失败

      {"code": 1, "msg": "必须指定sn查询参数"}
      

测试接口的前端js代码:

...
<button onclick="del_stu('{{ stu.sn }}')">删除button>
...

<script>

    function del_stu(sn) {
        if(confirm('确认是否删除学号为: '+sn + " 的学生?")){
            // alert('正在删除学号为: '+sn)
            // url = 'http://locahost:8000/m/del/?sn=123'
            url = location.origin+'/m/del/?sn='+sn
            fetch(url).then(resp=>resp.json()).then(data=>{
                if (data.code == 0){
                    // 刷新当前页面的数据
                    open('/m/list/', target='_self')
                }else{
                    alert('操作失败')
                }
            })
        }
    }
script>

view函数的写法:

def del_stu(request):
    # 获取查询参数中的sn
    sn = request.GET.get('sn', None)
    if not sn:
        return JsonResponse({'code': 1, 'msg': '必须指定sn查询参数'})

    s = Student.objects.get(pk=sn)  # pk表示主键的列名
    s.delete()
    return JsonResponse({'data': 'OK', 'code': 0})

扩展:增加列表页面的搜索功能

views.py脚本的内容:

def list_stu(request):
    wd = None
    if request.method == 'POST':
        # wd 可能是学号,可能是姓名
        # django.db.models.Q 可以多条件查询
        wd = request.POST.get('wd', '')

    if wd:  # '' 为 False, None为False
       if wd.isdigit():  # 判断搜索的内容是否为数字 
           datas = Student.objects.filter(sn=wd).all()
       else:
           datas = Student.objects.filter(name__contains=wd).all()
    else:
        datas = Student.objects.all()

    return render(request, 'list_stu.html', 
                  context={'datas': datas, 'title': '列出所有学生'})

list_stu.html模板内容:

<h1>{{ title }}h1>

<form method="post">
    {% csrf_token %}
    <div style="text-align: center">
        <input type="text" placeholder="输入学号或姓名" name="wd" size="30"> <button>搜索button>
    div>
form>

...

作业: 完成修改功能。

2.4.4 跨域请求配置

2.4.4.1 自定义中间件方式

在项目创建middles.py脚本,并定义CorsMiddleware类

ALLOWED_ORIGINS = '*'
ALLOWED_METHODS = ['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE']
# ALLOWED_METHODS = ['POST', 'GET']
ALLOWED_HEADERS = ['Content-Type', '*']
ALLOWED_CREDENTIALS = 'true'

class CorsMiddleware(object):
    """
    This middleware allows cross-domain XHR using the html4/5 post data API.
                                                                                                                                                                     
    Access-Control-Allow-Origin: http://foo.example
    Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
    """
    def process_request(self, request):
        if 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' in request.META:
            response = http.HttpResponse()
            response['Access-Control-Allow-Origin']  = ALLOWED_ORIGINS
            response['Access-Control-Allow-Methods'] = ",".join(ALLOWED_METHODS )
            response['Access-Control-Allow-Headers'] = ",".join(ALLOWED_HEADERS )
            response['Access-Control-Allow-Credentials'] =  ALLOWED_CREDENTIALS
            return response
                                                                                                                                                                  
        return None
                                                                                                                                                                  
    def process_response(self, request, response):
        response['Access-Control-Allow-Origin']  = ALLOWED_ORIGINS
        response['Access-Control-Allow-Methods'] = ",".join( ALLOWED_METHODS )
        response['Access-Control-Allow-Headers'] = ",".join( ALLOWED_HEADERS )
        response['Access-Control-Allow-Credentials'] = ALLOWED_CREDENTIALS
                                                                                                                                                                  
        return response

并配置到settings文件的中间件的位置:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'middles.CorsMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]
2.4.4.2 第三方应用方式

安装第三方的跨域请求的应用:django-cors-headers。

参考文档: https://pypi.org/project/django-cors-headers/

pip install django-cors-headers

安装成功之后, 添加已安装应用列表:

INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]

再配置中间件:

MIDDLEWARE = [  # Or MIDDLEWARE_CLASSES on Django < 1.10
    ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]

最后配置应用中使用的常量:

CORS_ORIGIN_ALLOW_ALL = True  # 默认为False
CORS_ORIGIN_WHITELIST = []  # 白名单, 只有CORS_ORIGIN_ALLOW_ALL为False时有效
CORS_ALLOW_METHODS = [
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
]
CORS_ALLOW_HEADERS = [
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
]
CORS_ALLOW_CREDENTIALS = True

三、 综合案例-水果管理

3.1 模型设计

# Fruit 水果模型类(name 名称, price 价格, source 产地, content 描述, cate_type_id 类型ID)
# FruitImage 水果图片模型类(fruit_id 水果ID, url 图片的路径, width 宽度, height 高度, name 标题)
# CateType 水果分类(name 名称, order_num 排序号)

# Store 水果商店(name 商店名称,boss_name 店主姓名, phone 联系电话, address 详细地址, city 城市, lat 纬度, lon 经度)  
# 经纬度: 百度地图拾取坐标系统

# StoreDetail 水果商店的详情, 广告、精选水果、特价水果

模型和模型之间的关系:

  • 一对一的关系 models.OneToOneField()
  • 一对多的关系 models.Foreignkey()
  • 多对一的关系
  • 多对多的关系 models.ManyToManyField()

【注】在一端模类中,访问多端模型的对象时: obj.小写的多端类名_set, 或在多端类中定义关系时指定了反向引用的属性名。

from django.db import models

# Create your models here.
class FruitCategory(models.Model):
    # 如果不存在主键字段时,默认新增一个id字段
    name = models.CharField(verbose_name='分类名', max_length=20, unique=True)
    num = models.IntegerField(verbose_name='序号')

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = verbose_name = '水果分类'
        db_table = 'tb_category'
        ordering = ('-num', )


class Fruit(models.Model):
    name = models.CharField(max_length=20, verbose_name='水果名')
    price = models.FloatField(verbose_name='价格', default=0)
    source = models.CharField(verbose_name='源产地', max_length=50)

    # blank和verbose_name两个参数描述字段在admin站点显示的信息和必填验证
    content = models.TextField(verbose_name='描述', null=True, blank=True)


    # ForeignKey属性的字段(关联主表模型类的实例),自动增加`字段名_id` 外键字段
    # related_name 指定关联父模型引用当前类的名称,默认为`小写的当前类名_set`
    category = models.ForeignKey('FruitCategory',
                                 on_delete=models.SET_NULL,  # 级联删除时设置为NUll
                                 null=True, blank=True, verbose_name='所属分类',
                                 related_name='fruits')

    def __str__(self):
        return self.name

    class Meta:
        db_table = 'tb_fruit'
        verbose_name_plural = verbose_name = '水果信息'
        ordering = ('source', 'price')
c1 = FruitCategory(name='水果',num=100)
c1.save()

f1 = Fruit(name='红富士', price=4.5, source='延安', category_id=c1.id)
f1.save()

f2 = Fruit(name='小青', price=3.5, source='山东', category_id=c1.id)
f2.save()

f1.category.name  # 查看水果的分类名称

c1.fruits.all()  # 查看当前分类下的所有水果

设计水果图片模型类:

class FruitImage(models.Model):
    fruit = models.OneToOneField('Fruit',
                                 on_delete=models.CASCADE,
                                 verbose_name='水果')

    title = models.CharField(max_length=50, verbose_name='标题')

    # 使用ImageField时,需要安装pillow库
    # 配置静态资源 static和媒体资源 media
    # upload_to 是相对于settings.MEDIA_ROOT 路径
    img = models.ImageField(upload_to='fruits',
                            width_field='width',
                            height_field='height')

    width = models.IntegerField(verbose_name='宽度')
    height = models.IntegerField(verbose_name='高度')

安装pillow库:

pip install pillow

设置静态资源的目录:

# 静态资源文件: css/js/image/fonts等
STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static')
]

# 媒体资源文件
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'static/media')

将媒体资源访问的URL添加到主路由中:

from fruitpro import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('fruit/', include('mainapp.urls')),
    path('', index)
]+static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

3.2 模板的基本使用

循环语句块

{% for item in items %}
	
{% endfor %}

分支循环

{% if 条件表达式 %}

{% endif %}

变量

{{ 变量名 }}

在views.py视图函数中,渲染模板

return render(request, 'fruit/list.html', locals() )

3.3 页面设计

块标签

{% block name %}

{% endblock %}

继承标签

{% extends "base/base.html" %}

【注意】模板文件路径是相对于templates目录的。

包含标签

{% include 'base/top.html' %}

3.4 admin站点管理

admin是django自带的后台管理的应用模块,位置: django.contrib.admin。

在每一个app应用中,都存在一个admin.py脚本,在此脚本中,可以将模型类添加到后台站点中,由后台去管理这些模型的数据。

3.4.1 创建超级用户

命令:

python manager.py createsuperuser
  • 根据提示输入用户名、邮箱和口令
  • 启动服务后,可以在/admin页面进行登录

3.4.2 定制admin管理

3.4.2.1 注册模型类

在自己模块的admin.py文件中

from .models import Store

admin.site.register(Store)
3.4.2.2 自定义表单

在admin.py文件中,定义admin.ModelAdmin的子类

class StoreAdmin(admin.ModelAdmin):
    pass

Store的表单中只添加name

class StoreAdmin(admin.ModelAdmin):
    fields = ('name',)

admin.site.register(Store, StoreAdmin)

分栏显示

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

内联显示

  1. 为外表创建内联类
# admin.StackedInline 上下排列
# admin.TabularInline 表格行排列
class FruitInline(admin.TabularInline):
    model = Fruit
  1. 在主表设置内联
class StoreAdmin(admin.ModelAdmin):
    inlines = [FruitInline]
3.4.2.3 列表显示与搜索

在admin.ModelAdmin的子类中,定义list_display和search_fields,如下所示:

class StoreAdmin(admin.ModelAdmin):
    list_display = ('id', 'name','address')
    search_fields = ('name','address')
  • list_display 列表显示
  • search_fields 搜索关键字的字段
3.4.2.4 修改app名称

修改app应用在admin站点中的名称:

在app应用的__init__.py中,添加一行:

default_app_config = 'mainapp.apps.MainappConfig'

在app应用的apps.py中,向类中添加一个verbose_name属性:

class MainappConfig(AppConfig):
    name = 'mainapp'
    verbose_name = '水果管理'
3.4.2.5 自定义字段

在显示列表中,增加自字段的名段,可以在AdminModel子类中,添加一个方法,写法如下:

class CategoryAdmin(admin.ModelAdmin):
    list_display=('id','name', 'cnt')
    def cnt(self, obj):
        return obj.fruits.count()  # 统计从类的实例个数
     
    cnt.short_description = '水果数量'

【问题】cnt新增的字段是可以修改为中文, 指定它的short_description

3.4.3 示例

from django.contrib import admin

from .models import FruitCategory, Fruit


# Register your models here.
class FruitInline(admin.StackedInline):
    # 在主表添加数据时,可以同时添加多个从表模型类的数据,即Fruit
    model = Fruit  # 带有ForeginKey外键的模型


@admin.register(FruitCategory)
class CategoryAdmin(admin.ModelAdmin):
    # 指定表单字段
    fields = ('name', 'num')

    # 指定显示列表的字段
    list_display = ('id', 'name', 'num', 'cnt')
    list_display_links = ('id', 'name')  # 有标签的连接的字段属性

    # 指定搜索的字段
    search_fields = ('id', 'name')

    # 分页(每页显示的记录数)
    list_per_page = 2

    # 问题:修改cnt在列表中显示的中文名称???
    def cnt(self, obj):
        return obj.fruits.count()

    sortable_by = ('num', 'cnt')  # 只限于已有字段,扩展字段除外

    inlines = [FruitInline] # FruitInline必须在此之前声明


@admin.register(Fruit)
class FruitAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'source', 'price', 'category')
    sortable_by = ('price', 'category')  # 可排序的字段
    search_fields = ('id', 'name')  # 搜索字段

    list_editable = ('name', 'price')  # 可编辑字段, 不能包含自定义的

# admin.site.register(FruitCategory, CategoryAdmin)

admin站点实现模型类的CURD简单操作,如果后台页面定制的,重新开发页面。

页面设计: 前后分离(Vue+ElementUI/VantUI/Bootstrap+ Django/Flask/FastAPI RESTful)、 前后端不分(页面模块+Django模块布局)MTV设计。

Web应用开发框架: MVC设计思想(前后端不分离)。

  • M (Model) 数据
  • V(View)视图或页面
  • C (Controller)控制器,业务流

Django框架引入了MVC设计思想, 设计了一套它自己的MTV。

  • M (Model)ORM模型类,数据操作
  • T (Template) 用户交互的视图, Django的模板(具有自己的模板标签)。
  • V (View) 视图处理函数,接收Template模板中发起的请求。

作业:

1. 完成Store商店模型类
2. 将Store类注册到admin站点中,并增加相关的数据
3. 设计一个门户页面,默认分页显示所有的水果(精选)和商店(精选),支持搜索
4. 在水果信息的下方,增加 加入购物车功能
5. 点击商店名,显示商店内的所有水果信息。
6. 设计会员模型和购物车模型

【必做题】1-3, 【选做题】4-6。

四、ORM详解

4.1 字段类型

  • CharField 字符类型, 属性: max_length

  • IntegerField 数值类型,属性:chiose 枚举类型的数据, 元组的元组

  • BooleanField 布尔类型, 数据表中表现是0 或 1

  • NullBooleanField 可以为空的布尔值

  • AutoField int自增列,属性 primary_key=True

  • FloatField 浮点类型

  • DecimalField , 同Python的Decimal类型,属性: max_digits 最大总位数, decimal_places 小数位数

  • TextField 文本类型

  • UUIDField 字符串类型,Django Admin以及ModelForm中提供对UUID格式的验证

  • FileField 文件字段

    属性:

    • upload_to 保存路径
    • storage 存储组件(django.core.files.storage.FileSystemStorage)
  • ImageField 路径保存在数据库,文件上传到指定目录
    属性:

    • upload_to = “” 上传文件的保存路径
    • storage = None 存储组件,默认django.core.files.storage.FileSystemStorage
    • width_field=None, 上传图片的高度保存的数据库字段名(字符串)
    • height_field=None 上传图片的宽度保存的数据库字段名(字符串)
  • DateField 日期类型
    格式:YYYY-MM-DD
    属性:

    • auto_now
      每次保存时,自动设置当前字段为当前时间, 用于“最后一次修改时间”

    • auto_now_add 对象每一次保存时,自动设置为当前时间, 用于“创建时间”

      【注意】auto_now和auto_now_add及default只能设置一个,不能组合

  • DateTimeField 日期时间类型, datetime.datetime
    日期+时间格式 YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]

  • TimeField
    时间类型
    格式:HH:MM[:ss[.uuuuuu]]

  • ForeignKey 外键字段类型

    属性:

    • to 关联的主模型类,也可以是模型类,也可以是字符串中包含模型类。
    • db_column 指定外键字段名

    表中会自动创建属性_id字段,也可以通过db_column属性指定外键字段名,如下所示:

    # to 是主模型类 Store,也可是'主模型类'形式的'Store'(主模型类事先未声明或自关联)
    # 默认的外键字段: store_id
    store = ForeginKey(Store, db_column="外键字段名")
    

    【注意】ForeignKey定义的属性在ORM操作中,被当作指定的主模型类实例对象使用。如下两个模型类:

    class Category(models.Model):
         name = models.CharField(max_length=20)
         
    class Goods(models.Model):
         name = models.CharField(max_length=50)
         price = models.FloatField(default=0)
         category = models.ForeignKey(Category)
    
    # 被当作指定的主模型类实例对象使用示例
    
    g1 = Goods.objects.get(pk=1)
    g1.category.name  # 获取category主类实例的name属性
    
    • related_name 指定主模型中自动增加的反向引用的名称,默认在主类中,添加的是小写的模型类名_set。 如Category.objects.get(pk=1).goods_set.all()查看分类ID为1下的所有商品。

    • on_delete 指定级联删除选项,可以是models.CASCADEmodels.SET_NULL

    • OneToOneField 一对一的关联字段,用法同ForeignKey相似。不同的是主类中添加的反向引用是小写的模型类名,代表的是当前模型类的实例对象。

4.2 元类Meta

是模型类的内部类,成员包含:

  • app_label 应用名, 默认表名是以app_label模型类组合后小写字符拼接的。
  • db_table 表名,替换默认给定的表名。
  • ordering 排序字段, 是[ ] 或( ), 字段名前可使用"-" 表示倒序
  • verbose_name admin站点中显示的名称
  • verbose_name_plural 复数表示,即是verbose_name中带有s
  • unique_together 来设置的不重复的字段组合, 如 ((“name”, “phone”),)

4.3 模型查询

Django中的对数据的查询,都是通过ORM模型类的objects对象进行操作的,objects在自定义模型类中会自动生成,它的类型是Manager,实际上也是QuerySet,即QuerySet存在的方法,objects都具有。

【提示】所有查询方法连接的调用其它的QuerySet对象的方法,即使用了构建器的设计模式。

Car -> CarBuilder

​ -> new1() 返回构建器类的实例 CarBuilder

​ -> new2()

​ -> new3()

​ ->new4()

​ ->build() 输出Car对象

car1 = CarBuilder().new1().new2().new3().new4().build()

4.3.1 查询方式

4.3.1.1 all方式

获取所有数据,all()方法返回一个要迭代的元素为模型类实例的QuerySet对象。

Fruit.objects.all()
4.3.1.2 first方式

查询数据中第一条, 返回一个实例对象。

Fruit.objects.first()

扩展: last()方法返回数据集中最一个实例对象。

4.3.1.3 get方式

get方式只是查询某一个对象,条件必须是定位到某一个对象,如主键查询。

X.objects.get(pk=1)  

【注意】get()方法返回的是一个模型类的实例对象,如果未到找指定的数据,则抛出DoesNotExist异常。

4.3.1.4 filter方式

X.objects.filter()可以增加不同的查询条件, 查询满足所有查询条件的数据,如:

Fruit.objects.filter(price__gt=10)

条件的写法非常多,但比较规范容易理解。

多个filter()的组合成and 与条件,如查询价格在5000以内的,并且是苹果类的水果信息。

Fruit.objects.filter(price__lte=5000).filter(category__name='苹果')

【注意】category是Fruit类属性,同时又是FruitCategory类的实例,可以通过属性对象__属性方式为内部对象的属性指定查询条件。

4.3.1.5 exclude方式

在查询的过程中,通过exclude()方法可以去掉或排除符合条件的数据。

# 查询水果价格在(10, 20]之间
# filter中price__gt=10 即查询价格大于10的
# exclude的price__gt=20 即排除从格大于20的
Fruit.objects.filter(price__gt=10).exclude(price__gt=20)
4.3.1.6 raw方式

Django的ORM框架中,支持原生SQL语句查询,原则上是SQL语句查询的字段存在模型类中。

raw = Fruit.objects.raw('select id,price,name from tb_fruit')

返回一个RawQuerySet对象,这个对象可以被直接迭代。虽然只有三个字段被查询,实际上获取每一个对象时,对象中包含了其他的数据。

raw_data = list(raw)

【注意】raw(query, params) 方法的参数同pymysql.cursor.execute(sql, args)相似,但只支持%s格式,不支持%(name)s格式,如下:

raw = Fruit.objects.raw('select * from tb_fruit where price<%s', (5, ))

【扩展】真正的原生SQL操作

from django.db import connection
cursor = connection.cursor()
# sql 不能使用%(name)s 形式
ret = cursor.execute('select * from tb_fruit where id > %s', (2, ))
data = list(ret) # ret可以直接迭代

4.3.2 values字段选择

values()是 QuerySet对象的方法,因此在objects的all()、filter()或exclude()等方法使用之后,再使用.values()方法进行字段选择。

实际上,values()方法是将对象查询的数据是从模型类实例转成dict字典对象,如下所示:

# 查询所有水果的id和name信息
Fruit.objects.all().values('id', 'name')

查询结果是:


Fruit.objects.filter(price__lte=5000).filter(category__name__contains='果').values('name', 'price')

4.3.3 查询条件

在filter或exclude()方法中,条件的基本格式是: 属性名__运算符=临界值属性名=临界值

条件运算符包含:

gt 大于
gte 大于等于
lt 小于
lte 小于等于
exact 精确内容查找,区分大小写
iexact 忽略大小写, i代表ignore(忽略)
contains 内容包含, 同sql的like子句
icontains 包含xx字符,,忽略大小写
startswith 以什么字符开始
istartswith 以xx字符开始,忽略大小写
endswith 以什么字符结束
iendswith 以xx字符结束,,忽略大小写
in 包含值项, 同sql的in子句
isnull 是null
isnotnull  非null

针对时间字段的查询,有特定的格式: 属性__时间关键字__条件=值属性__时间关键字=值

时间关键字包含:

year  年
month 月
day   日
hour  时
minute 分
second  秒
# 查询7月及之前注册的商店的名称和注册日期
Store.objects.filter(regist_date__month__lte=7).values('name', 'regist_date')

4.3.4 F和Q

4.3.4.1 F用法

F是django.db.models.F 类, 作用: 获取某一个列或属性的数据作为更新的条件。

# 将水果的价格调整为原来1.2倍。
Fruit.objects.filter(price__lte=5).update(price=F('price')*1.2)

【注意】在模型的update()方法中,不能使用Python内置计算的方法

4.3.4.2 Q用法

在一个Filter条件中可以通过Q类,实现多个条件的不同关系的运算( | 或, & 与, ~非)

Fruit.objects.filter(~Q(price__gte=10)).values('name', 'price')
Fruit.objects.filter(Q(price__gte=10) & Q(category__name='苹果')).values('name', 'price')
Fruit.objects.filter(Q(price__lte=10) | Q(category__name='苹果')).values('name', 'price', 'category__name')

作业:

1. 拆分网页模板
2. 分析会员或用户相关的信息,并尝试设计出来它们的模型

五、RESTful设计

参考Django-rest-framework (DRF)文档:https://www.django-rest-framework.org/

5.1 RESTful设计规范

RESTful设计规范是基于HTTP或HTTPs(SSL), 但是有它自己的设计思想:

1. 服务器的每个资源都具有唯一的标识符 ,即 URI (统一资源标识符),同URL概念相同。
2. 每个资源都具有四个标准的动作谓词,即 GET查询、POST 添加、PUT/PATCH(幂等性) 更新、DELETE 删除。
3. 每个资源发起请求时都是无状态的,即短连接,服务器不操作Session会话连接 (Connection: close|keep-alive)。
4. 请求交互的数据是json或xml, 即post和put上传的数据是json, 所有动作的响应数据也是json。

5.2 FBV和CBV设计

5.2.1 FBV设计

FBV( Function Based View) : 基于函数的视图

url路由 ->   def  add_stu(request):   view视图函数

5.2.2 CBV设计

CBV(Class Based View): 基于类的视图设计

CBV声明的类,可以重写get、post、put、delete相关的方法,实现资源的不同请求方法的处理。

django中提供了通用的View,所有请求到达View时,由dispatch()方法进行分发的,函数的内容如下:

 # django.views.generic.View
 class View:
    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

5.2.3 案例分析: 会员资源

会员资源的URI(接口): /user/

注册:/user/regist/ post请求  
	请求的数据: {name: '用户名', auth_str: '口令', code: '验证码'}
	响应的数据: {code: 0, msg: '成功信息', data: '数据', token: '身份令牌'}

短信验证: /user/code/ get请求

登录:/api/login/ post 请求
	请求json参数: {name: "",  auth_str: ""}
	响应json数据: {code: 0, token: "身份令牌"}  或 {code: 1, msg: "原因"}

登出:/api/login/ delete 请求
   请求头: {Authorization: Token "身份令牌"}
   响应数据: {code: 0}  或 {code: 1, msg: "退出的失败原因"}
   
上传头像:/user/avater/ put请求
绑定手机:/user/phone/ put请求
修改口令:/user/password/ put 请求

会员模型类: AppUser

基于FBV设计:

import json

from django.shortcuts import render
from django.http import JsonResponse

# /user/regist/
def regist(data):
    # 验证数据的参数是否完整
    if all(('name' in data, 'auth_str' in data, 'code' in data)):

        return JsonResponse({
            'msg': 'OK',
            'code': 0,
            'data': data
        })

    return JsonResponse({
        'msg': '参数不完整,name, auth_str和code必须提供',
        'code': 2,
        'data': data
    })

# Create your views here.
# /user//
def user(request, action):

    if 'regist' == action and 'POST' == request.method:
        # 上传数据是json ,  Content-Type : application/json
        json_data = json.loads(request.body.decode('utf-8'))  # 获取请求报文中的body数据,是bytes类型
        return regist(json_data)

    return JsonResponse({
        'code': 900,
        'msg': '请求方法存在问题,请详细查看API接口文档的说明'
    })

基于CBV方式设计会员视图:

import json

from django.http import JsonResponse
from django.views.generic import View


class UserView(View):

    def not_found(self):
        return JsonResponse({
            'code': 900,
            'msg': '请求方法存在问题,请详细查看API接口文档的说明'
        })

    def post(self, request, action=None):
        if 'regist' == action:
            # 上传数据是json ,  Content-Type : application/json
            json_data = json.loads(request.body.decode('utf-8'))  # 获取请求报文中的body数据,是bytes类型
            return self.regist(json_data)

        return self.not_found()

    def get(self, request, action=None):
        return self.not_found()
    
    def put(self, request, action=None):
        pass

    def delete(self, request, action=None):
        pass

    def regist(self, data):
        # 验证数据的参数是否完整
        if all(('name' in data, 'auth_str' in data, 'code' in data)):
            return JsonResponse({
                'msg': 'OK',
                'code': 0,
                'data': data
            })

        return JsonResponse({
            'msg': '参数不完整,name, auth_str和code必须提供',
            'code': 2,
            'data': data
        })

# 在路由中
urlpatterns = [
    path('user//', v.UserView.as_view()),
]

新的View的写法:

import json
import uuid

from django.http import JsonResponse, HttpRequest
from django.views.generic import View


# CBV模式
class ApiView(View):
    def dispatch(self, request, *args, **kwargs):
        if request.method == 'GET' or request.method == 'DELETE':
            # 将request.GET或DELETE的QueryDcit类型的数据转成dict
            request.data = {k: v for k, v in request.GET.items()}
        else:
            # 将上传的json字节数据转成dict
            request.data = json.loads(request.body.decode('utf-8'))

        resp_date = super().dispatch(request, *args, **kwargs)

        return JsonResponse(resp_date)


class UserView(ApiView):

    def not_found(self):
        return {
            'code': 900,
            'msg': '请求方法存在问题,请详细查看API接口文档的说明'
        }

    def post(self, request, action=None):
        if 'regist' == action:
            # 上传数据是json ,  Content-Type : application/json
            json_data = json.loads(request.body.decode('utf-8'))  # 获取请求报文中的body数据,是bytes类型
            return self.regist(json_data)

        return self.not_found()

    def get(self, request, action=None):
        print('---Login----', request.data)
        return {
            'code': 1,
            'msg': '登录成功',
            'data': dict(request.data)
        }

    def put(self, request, action=None):
        pass

    def delete(self, request, action=None):
        pass

    def regist(self, data):
        # 验证数据的参数是否完整
        if all(('name' in data, 'auth_str' in data, 'code' in data)):
            return {
                'msg': 'OK',
                'code': 0,
                'data': data
            }

        return self.not_found()


class LoginView(View):
    # View中使用dispatch()方法分发到具体某一个请求方法中

    def dispatch(self, request, *args, **kwargs):
        # 接收目标函数处理的结果
        ret = super().dispatch(request, *args, *kwargs)
        if isinstance(ret, JsonResponse):
            return ret
        if isinstance(ret, dict):
            return JsonResponse(ret)

        return JsonResponse({'code':900,
                             'msg': '服务器返回类型错误,请及时联系后台人员'})

    def post(self, request: HttpRequest):
        # 按接口文档的设计,获取相关的参数
        # 上传的数据是json
        print('请求上传的数据类型:', request.headers.get('Content-Type'))
        data = json.loads(request.body.decode('utf-8'))  # dict
        print(data.get('name'),  data.get('auth_str'))

        # 验证数据
        # 通过模型类从数据库进行查找
        # 生成Token, 并且将Token和用户进行绑定(Redis)
        # 封装JsonResponse对象
        # 返回JsonResponse对象

        return {'code': 0, 'token': uuid.uuid4().hex}

    def delete(self, request):
        if 'Authorization' in request.headers:
            print('Token用户身份', request.headers.get('Authorization'))
            # 查询与Token帮助的用户,取消绑定
            return {'code': 0}

        return {
            "code": 1,
            "msg": "用户之前未登录"
        }
urlpatterns = [
    # 表示为path路径的参数,将传入到视图函数的参数中 ,如get(request, action)
    path('user//', v.UserView.as_view()),  # View.as_view()方法是类方法
    path('login/', v.LoginView.as_view()),
]

5.2.4【扩展】Python 实现接口测试

安装requests第三方库

pip install requests

requests中相关的方法:

requests.get(url, params, headers)
requests.post(url, json,headers, files)
requests.put(url, json,headers,files)
requests.delete(url, params,headers)

测试会员注册接口:

import requests

url = 'http://10.36.172.120/api/user/regist/'
json_params = 
{
	"name2": "disen",
	"password": "disen123",
	"code": "1xad"
}

resp = requests.post(url, json=json_params)
print(resp.json())

5.2.5 设计ApiView

class ApiView(View):
    def dispatch(self, request, *args, **kwargs):
        if request.method == 'GET' or request.method == 'DELETE':
            # 将request.GET或DELETE的QueryDcit类型的数据转成dict
            request.data = {k: v for k, v in request.GET.items()}
        else:
            # 将上传的json字节数据转成dict
            request.data = json.loads(request.body.decode('utf-8'))

        resp_date = super().dispatch(request, *args, **kwargs)

        return JsonResponse(resp_date)

5.2.6 详解request

在View视图函数设计中,每一个参数就是request, 代表要处理某一个客户端请求。request对象是django.core.handlers.wsgi.WSGIRequest类的实例,也是HttpRequest类的子类。

5.2.6.1 META信息

META信息又称之为元信息,包含客户端的环境变量、IP地址、请求路径、请求方法、请求数据的类型和HTTP协议请求头报文等信息。它也基于WSGI协议的environ对象扩展的,如源码:

class WSGIRequest(HttpRequest):
    def __init__(self, environ):
        script_name = get_script_name(environ)
        path_info = get_path_info(environ) or '/'
        self.environ = environ
        self.path_info = path_info
        self.path = '%s/%s' % (script_name.rstrip('/'),
                               path_info.replace('/', '', 1))
        self.META = environ
        self.META['PATH_INFO'] = path_info
        ...

请求头报文中,包含请求路径PATH_INFO、请求方法REQUEST_METHOD、数据类型CONTENT_TYPE和数据大小CONTENT_LENGTH等。

对于自定义的请求头,如Authorization,可以通过META['HTTP_AUTHORIZATION']获取。

对于请求的数据时,除了判断请求方法之外,应该获取数据类型Content-Type,可以通过request.content_type属性来获取。

5.2.6.2 请求方法

通过request.method属性可以快速获取本次请求的方法,获取的请求方法都是大写的字符串,如GETPOSTPUTDELETEOPTIONS等。对于不同的请求方法,使用不同的属性来封装请求参数。

5.2.6.3 GET和POST

request.GET属性是QueryDict类的实例,对于GETDELETE两种请求方法,request.GET属性封装了查询参数QUERY_STRING,如源码中的103~107行:

    @cached_property
    def GET(self):
        # The WSGI spec says 'QUERY_STRING' may be absent.
        raw_query_string = get_bytes_from_wsgi(self.environ, 'QUERY_STRING', '')
        return QueryDict(raw_query_string, encoding=self._encoding)

request.POST属性也是QueryDict类的实例,针对POSTPUT请求,来封装form表单参数(Content-Type是application/x-www-form-urlencoded)。

<form enctype="application/x-www-form-urlencoded">
form>
5.2.6.4 body

request.body属性是一个字节类型,针对POSTPUT请求时,封装Content-Type是application/json或其它文本类型的请求体中的数据。在HttpRequest类的源码中:

	  @property
    def body(self):
        if not hasattr(self, '_body'):
            if self._read_started:
                raise RawPostDataException("You cannot access body after reading from request's data stream")

            # Limit the maximum request data size that will be handled in-memory.
            if (settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None and
                    int(self.META.get('CONTENT_LENGTH') or 0) > settings.DATA_UPLOAD_MAX_MEMORY_SIZE):
                raise RequestDataTooBig('Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE.')

            try:
                self._body = self.read()
            except IOError as e:
                raise UnreadablePostError(*e.args) from e
            self._stream = BytesIO(self._body)
        return self._body
5.2.6.5 FILES

request.FILES是QueryDict类的实例,针对POST请求的Content-Type为multipart/form-data类型的,表示客户端上传文件时,从request.FILES中读取。

<form metho='post' enctype="multipart/form-data">
   <input name="title">
   <input type="file" name="img">
   <button>提交button>
form>

它的key是客户端上传文件时的字段名,value是django.core.files.uploadedfile.InMemoryUploadedFile类的实例, 这个类的常用的属性有:

  • name 文件名
  • charset 字符集
  • size 文件的字节长度
  • content_type 数据的类型,如图片image/pngimage/jpeg
  • chunks() 获取字节块的可迭代的生成器。

如,客户端使得requests.post()方法上传文件的代码:

with open('1.png', 'rb') as f:
    resp =requests.post('/api/upload/', files={'avater': ('avater.png', f, 'image/png')})
    
if resp.code == 200:
    print('上传成功')

服务端接收/api/upload/请求的view函数中:

from xxxpro import settings

def upload(request):
  file = request.FILES.get('avater')

  # 获取文件的数据块,并写入到服务器的某一个位置上。
  with open(f'{settings.BASE_DIR}/static/users/{file.name}', 'wb') as f:
  	 for chunk in file.chunks():
          f.write(chunk)
          
  return JsonResponse({'msg': f'上传{file.name}成功'})
5.2.6.6 COOKIES

request.COOKIES 也是QueryDict类型,将客户端存储的cookie信息转成字典方式,便于我们使用。对于MTV开发模式下,同Session组合使用。

一般情况下,服务端将一些代表用户或客户端身份的信息作为Cookie响应给客户端,由客户端浏览器或相关应用进行保存。在view函数中,可以在生成的response响应对象之后,通过响应对象的set_cookie(name, value, max_age, expires)方法来设置。name和value是cookie的键值对,max_age表示最大存活的秒数,expires可以指定过期日期时间(datetime时间元组)。

5.3 DRF基本使用

DRF(django-rest-framework)是django第三方框架,实现了RESTful设计规范的相关组件,包括路由、请求解析器、权限验证、视图类、视图集(viewsets)、分页器和序列化等。

5.3.1 安装与配置

安装djangorestframework库:

pip install djangorestframework

在settings.py中配置app、授权处理类和分页类:

INSTALLED_APPS = [ 
    ...
    'rest_framework',
]

REST_FRAMEWORK = {
  	'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

【注意】DEFAULT_PERMISSION_CLASSES 授权类可以不配置,则表示所有数据接口操作不验证权限。

5.3.2 定义序列化类

设计两个模型类:

class Department(models.Model):
    name = models.CharField(max_length=20, unique=True)
    tel = models.CharField(max_length=12, unique=True)

    def __str__(self):
        return self.name

    class Meta:
        db_table = 'tb_department'
        verbose_name_plural = verbose_name = '部门信息'

class Person(models.Model):
    name = models.CharField(max_length=20, unique=True)
    sex = models.CharField(max_length=2, default='男')
    phone = models.CharField(max_length=11, null=True, blank=True)
		
    # 管理者, 自关联
    manager = models.ForeignKey('self',
                                on_delete=models.SET_NULL,
                                null=True, blank=True)
    department=models.ForeignKey(Department, 
                                 on_delete=models.SET_NULL, null=True, blank=True)

    def __str__(self):
        return self.name

    class Meta:
        db_table = 'tb_person'

序列化类一般使得ModelSerializer类,代码如下:

class DepartmentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Department
        fields = ('name', 'tel') # 如果是所有字段可以是 '__all__'


class MangerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ('id', 'name', 'sex', 'phone')


class PersonSerializer(serializers.ModelSerializer):
    manager = MangerSerializer(many=False)  # many为False表示“一端”,即查出模型对象
    department = DepartmentSerializer(many=False)

    class Meta:
        model = Person
        fields = '__all__'

5.4.3 定义viewset

viewset即是数据查询接口, 设计两个模型操作(CURD)的视图集:

class DepartmentViewset(viewsets.ModelViewSet):
    queryset = Department.objects.all()  # 查询结果集
    serializer_class = DepartmentSerializer  # 查询的数据序列化,即转为可被json序列化的dict

class PersonViewset(viewsets.ModelViewSet):
    queryset =  Person.objects.all()
    serializer_class = PersonSerializer

ModelViewSet类,已实现与客户端请求方法对应的处理函数,如GET请求方法对应是retrieve()或list(),源码:

class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    """
    pass

5.4.4 注册路由

在apiapp应用的urls.py中创建RESTful的路由,并注册已实现的viewset视图集:

from rest_framework import routers

router = routers.DefaultRouter()

# 将viewset类注册到DRF路由中
router.register('department', v.DepartmentViewset)
router.register('person', v.PersonViewset)

urlpatterns = [
    ...
    path('', include(router.urls))  # 将DRF路由添加到Django的路由中
]

注册完成后,则可以产生如下接口:

/api/department/    # 获取所有部门信息
/api/department//  # 查看某一部门信息
/api/person/        # 获取所有人员信息
/api/person//   # 获取某一人员信息

5.4.5 接口测试

5.4.5.1 未授权测试

由于settings 中配置了DjangoModelPermissionsOrAnonReadOnly权限,则表示只读的数据接口是可以请求成功的,如果进行POST、PUT和DELETE请求,则无权限。

import requests

resp = requests.get('http://localhost:8000/api/department/')
print(resp.json())

resp = requests.get('http://localhost:8000/api/department/1/')
print(resp.json())

请求URL中,可以增加分页的查询参数: page, 表示查看是第几页:

resp = requests.get('http://localhost:8000/api/department/?page=1')
print(resp.json())

如果超过页码,则返回是无效页面

5.4.5.2 授权后测试

默认授权方式,可以通过auth参数设置用户名和口令。

# requests.post()方法中的参数,则参考requests.request()
resp = requests.post('http://localhost:8000/api/department/',
                     json={'name': '人力部', 'tel': '19200121'},
                     auth=('admin3', 'disen123'))
print(resp.text)

5.4 APIView和序列化

以上ViewSet模型视图集,已提交了比较完整的操作,但如果实现发送短信、邮件、登录登出或上传头像等功能时,则需要自已实现。在DRF框架中,提供一个ApiView视图类,可以完成这些工作。

5.4.1 DRF的APIView

如果针对非App或前端用户使用的接口时,在DRF配置的PERMISSION相关权限的类则需要删除,否则无法正则使用。以下是设计一个Fruit水果模型相关的接口APIView接口视图类:

from rest_framework.views import APIView
from rest_framework.response import Response

from django.core.paginator import Paginator

class FruitApiView(APIView):
    def get(self, request, format=None):
        page = request.query_params.get('page', 1)
        
        # 生成分页对象,5即每页显示几条
        paginator = Paginator(Fruit.objects.all().values('name','price', 'id'), 5) 
        pager = paginator.get_page(page) # 获取第几页

        return Response({'code': 0, 'data': pager.object_list, 'page': page})

    def post(self, request, format=None):
        return Response({'code': 1, 'data': request.data})

【注意】Paginator主要实现分页显示。

在urls.py路由配置:

urlpatterns = [
    path('fruit/', v.FruitApiView.as_view())
]

测试脚本:

import requests

resp = requests.get('http://localhost:8000/api/fruit/?page=2')
print(resp.text)

resp = requests.post('http://localhost:8000/api/fruit/', json={
    'user': 'haha',
    'auth_str': 'good'
})
print(resp.json())

5.4.2 Serializer序列化类

DRF提供序列化类,可以将模型类的QuerySet数据转成dict字典。在上面的DRF基本应用中,已体现了。

如果在APIView中使用的话,先设计模型类对应的序列化类:

class FruitSerializer(serializers.ModelSerializer):
    class Meta:
        model = Fruit
        fields = ('id', 'name', 'price', 'source')

在查询中使用:

class FruitApiView(APIView):
    def get(self, request: Request, format=None):

        page = request.query_params.get('page', 1)
        paginator = Paginator(Fruit.objects.all(), 5) # 生成分页对象,5即每页显示几条
        pager = paginator.get_page(page)
				
        # 序列化的第一个参数是实例对象, many=True表示是一个QuerySet集合数据(多条记录)
        serializer = FruitSerializer(pager.object_list, many=True)
        return Response({'code': 0, 'data':serializer.data, 'page': page})

在更新中使用:

class FruitApiView(APIView):
    def put(self, request, format=None):
        id = request.query_params.get('id')
        serializer = FruitSerializer(Fruit.objects.get(pk=id), data=request.data)
        serializer.is_valid(True) # 验证数据是否完整,不完整则报错
        serializer.save()
        return Response({'code': 1, 'data': serializer.data})

【注意】serializer在调用save()之前,必须先验证数据是否完整,否则会抛出异常。

5.5 Token系统用户授权

如果对资源进行添加、删除和修改操作时,默认情况下必须带有请求的auth信息,这样信息不安全。DRF中通过一次授权并返回Token后,之后的请求,只需要请求头中带着Token即可。

5.5.1 配置与迁移

配置settings文件:

INSTALLED_APPS = [
    ...
    'rest_framework.authtoken'
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'res_framework.authentication.TokenAuthentication',
    ],
    ...
}

配置成功后,则需要迁移:python manage.py migrate

配置apiapp的urls.py路由:

from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    path('login/', obtain_auth_token),
    path('', include(router.urls))
]

5.5.3 测试

login_url = 'http://localhost:8000/api/login/'
json_data = {
    'username': 'admin',
    'password': 'disen123'
}
resp = requests.post(login_url, json=json_data)
print(resp.text)

登录成功后,返回:

{"token": "38175758580c26625b3d5191aba7ec6a6fedb4ee"}

5.5.4 定制返回信息

默认情况下,只返回token信息。authtoken应用支持定制返回信息,如下:

from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response

class UserAuthToken(ObtainAuthToken):

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data,
                                           context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({
            'token': token.key,
            'user_id': user.pk,
            'username': user.username
        })

在apiapp.urls.py路由,修改/login/的视图:

from . import views as v

urlpatterns = [
    path('login/', v.UserAuthToken.as_view()),
    path('', include(router.urls))
]

再次执行测试脚本,则返回:

{"token":"38175758580c26625b3d5191aba7ec6a6fedb4ee","user_id":3,"username":"admin3"}

六、PowerDesigner设计工具

用PowerDesigner设计数据表,生成sql语句,运行到mysql数据库,使用pymysql或者mysqlclient或者其他方法连接数据库,最后终端运行:
django-inspectdb命令:

python manage.py inspectdb  > mainapp/models.py 

结果,生成model模型

七、系统用户及权限

7.1 重写系统用户

在重写系统用户模型类时,必须在第一次迁移数据库之前完成。

另外,重写系统的模型类时,应该创建相应的系统模块,如sysapp。

7.1.1 系统用户模型类

django中系统用户的模型类,定义在django.contrib.auth.models.AbstractUser类中,源代码如下:

class AbstractUser(AbstractBaseUser, PermissionsMixin):
    """
    An abstract base class implementing a fully featured User model with
    admin-compliant permissions.

    Username and password are required. Other fields are optional.
    """
    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _('username'),
        max_length=150,
        unique=True,
        help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        validators=[username_validator],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=150, blank=True)
    email = models.EmailField(_('email address'), blank=True)
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        abstract = True

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        """
        Return the first_name plus the last_name, with a space in between.
        """
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        """Return the short name for the user."""
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)

一般情况下,通过python manage.py createsuperuser命令创建的系统用户。

大多数的情况下,AbstractUser并不满足我们的业务需要,如系统用户需要其它相关属性时,如部门、手机号等,则需要重新定义。

定义新的系统用户模型类,代码如下:

from django.contrib.auth.models import AbstractUser
from django.contrib.auth.hashers import make_password

# Create your models here.
class SysUser(AbstractUser):
    nickname = models.CharField(max_length=20, null=True, blank=True)
    phone = models.CharField(max_length=11, null=True, blank=True)

    def __str__(self):
        return self.username

    class Meta:
        db_table = 'tb_sys_user'
        verbose_name_plural = verbose_name = '系统用户'


    def save(self, *args, **kwargs):
        if  len(self.password)<60 or not self.password.startswith('pbkdf2_sha256'):
            self.password = make_password(self.password)

        super().save(*args, **kwargs)

【注意】系统用户模型重定义之后,在修改口令时,不会进行加密处理(make_password()),因此重写save()方法来完善修改口令的功能。

7.1.2 修改settings

AUTH_USER_MODEL = 'sysapp.SysUser'  # 应用名+模型类名

7.1.3 注册到admin站点

新的用户模型类配置完成后,默认情况下,站点中不存在新的系统用户。还需要到app模块的admin.py脚本中,将新的用户模型注册到站点中。

from sysapp.models import SysUser

@admin.register(SysUser)
class SysUserAdmin(admin.ModelAdmin):
    list_display = ('id', 'username', 'nickname', 'phone')

7.2 超级管理员

在AbstractUser模型类,存在一个is_staff属性,即表示职员系统用户身份。另外在这个模型类的父类(PermissionsMixin)中还存在一个is_superuser,即表示超级系统用户的身份。只有两个属性值都为True时,才具有所有权限。关于用户和权限关系,都在PermissionsMixin抽象模型类声明,源码如下:

class PermissionsMixin(models.Model):
    """
    Add the fields and methods necessary to support the Group and Permission
    models using the ModelBackend.
    """
    is_superuser = models.BooleanField(
        _('superuser status'),
        default=False,
        help_text=_(
            'Designates that this user has all permissions without '
            'explicitly assigning them.'
        ),
    )
    groups = models.ManyToManyField(
        Group,
        verbose_name=_('groups'),
        blank=True,
        help_text=_(
            'The groups this user belongs to. A user will get all permissions '
            'granted to each of their groups.'
        ),
        related_name="user_set",
        related_query_name="user",
    )
    user_permissions = models.ManyToManyField(
        Permission,
        verbose_name=_('user permissions'),
        blank=True,
        help_text=_('Specific permissions for this user.'),
        related_name="user_set",
        related_query_name="user",
    )

    class Meta:
        abstract = True

    def get_group_permissions(self, obj=None):
        """
        Return a list of permission strings that this user has through their
        groups. Query all available auth backends. If an object is passed in,
        return only permissions matching this object.
        """
        permissions = set()
        for backend in auth.get_backends():
            if hasattr(backend, "get_group_permissions"):
                permissions.update(backend.get_group_permissions(self, obj))
        return permissions

    def get_all_permissions(self, obj=None):
        return _user_get_all_permissions(self, obj)

    def has_perm(self, perm, obj=None):
        """
        Return True if the user has the specified permission. Query all
        available auth backends, but return immediately if any backend returns
        True. Thus, a user who has permission from a single auth backend is
        assumed to have permission in general. If an object is provided, check
        permissions for that object.
        """
        # Active superusers have all permissions.
        if self.is_active and self.is_superuser:
            return True

        # Otherwise we need to check the backends.
        return _user_has_perm(self, perm, obj)

    def has_perms(self, perm_list, obj=None):
        """
        Return True if the user has each of the specified permissions. If
        object is passed, check if the user has all required perms for it.
        """
        return all(self.has_perm(perm, obj) for perm in perm_list)

    def has_module_perms(self, app_label):
        """
        Return True if the user has any permissions in the given app label.
        Use similar logic as has_perm(), above.
        """
        # Active superusers have all permissions.
        if self.is_active and self.is_superuser:
            return True

        return _user_has_module_perms(self, app_label)

7.3 系统用户权限

7.3.1 权限Permission类

所有app应用的模型类,都具有四个权限名称(codename),如mainapp应用下的Fruit模型的权限:

mainapp.view_fruit  查看权限
mainapp.add_fruit   添加权限
mainapp.change_fruit 修改权限
mainapp.delete_fruit 删除权限
  • 权限中的mainapp是Permission模型类的content_type关系对象的app_label信息
  • view_fruit 是Permission的codename属性字符串

7.3.2 查看权限

系统用户可以通过user_permissions属性(多对多的关系模型),可以查看权限:

SysUser.objects.get(pk=1).user_permissions.all()

7.3.2 添加权限

系统用户可以通过user_permissions属性添加或删除相关的权限。

为ID为1的系统用户增加修改水果(add_fruit)的权限:

from django.contrib.auth.models import Permission

u1 = SysUser.objects.get(pk=1)  # 查找系统用户

add_fruit_perm = Permission.objects.get(codename='add_fruit') # 查找权限

u1.user_permissions.add(add_fruit_perm) # 添加权限

7.3.3 验证权限

系统用户通过has_perm(perm)has_perms(list_perms)方法验证,但perm是app_label+codename的组合字符或Permission类实例,如下所示:

u1 = SysUser.objects.get(pk=1) 
u1.has_perm('mainapp.add_fruit') # 返回True或False
# 验证用户是否具有添加水果和修改水果信息的权限
u1.has_perms(['mainapp.add_fruit', 'mainapp.change_fruit'])  # 返回True或False
add_fruit_perm = Permission.objects.get(codename='add_fruit')
u1.has_perm(add_fruit_perm)

7.3.4 删除权限

同添加的方式相似,先查找权限或判断,再通过user_permissions的remove()方法删除。

add_fruit_perm = Permission.objects.get(codename='add_fruit')
if u1.has_perm(add_fruit_perm):
	 u1.user_permissions.remove(add_fruit_perm)

7.3.5 系统用户的状态

系统用户有三种状态:

is_superuser  是否为超级管理员
is_staff      是否为管理员
is_active     是否有效(未过有效时间)

这三种状态会影响用户的权限。

7.4 自定义权限验证

可以自定义一个用于权限验证的装饰器类,可以在view处理函数上直接使用。

7.4.1 CBV的权限验证

CBV的视图处理类下,如果装饰它的方法,闭包函数wrapper()的第一个参数是View类的对象。

class SysCheckPerm:
    def __init__(self, codename):
        self.codename = codename

    def __call__(self, view_func):
        def wrapper(v, request, *args, **kwargs):
            if hasattr(request, 'user') and isinstance(request.user, SysUser):

                request.user = SysUser.objects.get(pk=request.user.id)
                if request.user.has_perm(Permission.objects.get(codename=self.codename)):
                    return view_func(v, request, *args, **kwargs)

                return JsonResponse({'code': 4, 'msg': '当前用户无权限'})

            return JsonResponse({'code': 5, 'msg': '当前用户未登录'})

        return wrapper

用法:

class FruitView(ApiView):

    @SysCheckPerm('view_fruit')
    def get(self, request):
        return {'data':list(Fruit.objects.all().values())}

    @SysCheckPerm('add_fruit')
    def post(self, request):
        Fruit.objects.create(name=request.data['name'],
                             price=request.data['price'],
                             source=request.data['source'])

        return {'code':0, 'msg': '成功'}

7.4.2 FBV的权限验证

如果在普通的view视图函数中使用,则wrapper()函数的第一个参数则是request。

class SysCheckPerm:
    def __init__(self, codename):
        self.codename = codename

    def __call__(self, view_func):
        def wrapper(request, *args, **kwargs):
            if hasattr(request, 'user') and isinstance(request.user, SysUser):

                request.user = SysUser.objects.get(pk=request.user.id)
                if request.user.has_perm(Permission.objects.get(codename=self.codename)):
                    return view_func(request, *args, **kwargs)

                return JsonResponse({'code': 4, 'msg': '当前用户无权限'})

            return JsonResponse({'code': 5, 'msg': '当前用户未登录'})

        return wrapper

用法:

@CheckPerm('view_fruit')
def get_fruit(request):
    return {'data':list(Fruit.objects.all().values())}

八、Redis缓存

8.1 Python中安装库

pip install redis

8.2 Docker部署Redis服务

docker pull redis
docker run -d --name cache1 -p 6370:6379 redis

8.3 Python中使用redis

8.3.1 创建Redis对象

创建utils的Python包,在__init__.py添加以下内容:

from redis import Redis

# Redis缓存默认存在db为0-15之间16个库, 默认为0号库
# decode_responses 表示是否对响应的数据(bytes)进行解码
rd = Redis(host='116.85.40.159', port=6370, db=1, decode_responses=True)
print('--OK-')

8.3.2 设计验证码缓存

在utils包下创建cache.py脚本,内容如下:

from . import rd

def save_code(phone, code):
  rd.set(phone, code, 120)  # k, v, expires
  
def valid_code(phone, code):
  if rd.exists(phone):
    return rd.get(phone) == code
  return False

8.3.3 设计Token缓存

在utils包下的cache.py,添加如下内容:

def save_token(token, user_id):
    # 用户登录或注册后,和用户绑定
    rd.hset('user_token', token, user_id)


def get_token(token):
    # 获取Token对应的UserID
    return rd.hget('user_token', token)


def has_token(token):
    # 判断当前用户的Token是否已登录
    return rd.hexists('user_token', token)


def delete_token(token):
    # 用户退出登录时,删除Token
    return rd.hdel('user_token', token)

九、git版本管理

git服务器选择https://www.gitee.com/,需要自己注册账号。

其它常用的git服务器: github(国外), 自建gitlab。

9.1 配置公钥

~/.ssh目录,查看id_rsaid_rsa.pub两个文件,如果不存在,则执行如下命令:

ssh-keygen

执行命令后,出现输入位置,则直接回车即可,命令执行完之后,会生成私钥和公钥这两个文件。

查看公钥的内容:

cat ~/.ssh/id_rsa.pub

复制公钥内容。

9.2 创建仓库和团队

9.2.1 创建组织

登录成功后, 在右侧的+菜单中,选择创建组织,在打开的创建组织页面中, 填写组织名称。这个组织名必须是唯一的。填写完组织名之后, 会自动生成组织空间地址,如组织名为readerqf, 则空间地址为https://gitee.com/readerqf

9.2.2 添加开发者

在打开的组织页面中,默认显示概览信息。如果添加开发者,点击设置,打开设置页面。在设置页面中,选择成员管理->添加成员。在添加成员的页面,有两个页签链接邀请直接添加。选择直接添加,并在码云用户的输入框中输入开发者的码云用户名,当搜索到用户后,点击搜索到的用名即可添加。可以同时添加多个用户,添加完成后,点击【添加】按钮,并在弹出的页面中选择【确定】。

9.2.3 创建仓库

在组织的页面中, 点击【创建仓库】按钮,进入创建仓库页面

9.2.3.1 创建私有仓库

在创建仓库页面中,默认选择的是私有仓库。

在仓库名称的输入框中输出仓库名称,如apiserver。

在仓库介绍中,输入仓库的相关说明。

在选择分支模型的下拉框架,默认选择单分支(只创建master分支)。

确认以上信息之后, 点击【创建】即完成。

9.2.3.2 添加私有仓库的开发人员

仓库创建成功后,在仓库的页面中,选择最右侧的管理菜单,进入管理页面。

选择仓库成员管理->开发者,打开添加开发者页面。

点击【添加仓库成员】,在弹出的选项中,选择邀请组织成员邀请用户

9.3 git常用命令

9.3.1 全局配置

无论是主开发者还是普通的开发者,都需要配置。

在gitbash窗口中配置,运行如下命令:

git config --global user.name "disenQF"
git config --global user.email "[email protected]"
  • 用户名即是gitee.com码云的用户名

  • 邮箱也是用户注册时的邮箱。

通过git config -l 命令查看已配置的本地仓库相关信息。

9.3.2 本地仓库操作

【注意】主开发者将本地的项目上传到远程仓库前,需要生成项目的依赖环境文件requirements.txt。

pip freeze > require.txt

主开发者:

cd project   # 进入已创建的项目目录中
git init   # 初始化, 在当前的目录下创建.git目录
git remote add origin [email protected]:readerqf/apiserver.git  # 添加远程仓库的地址
git status  # 查看文件的状态,红色表示未入栈(untracked), 绿色表示已入栈
vi .gitignore  # 编辑哪些文件或目录不需要入栈的
git add .     # 将当前目录下所有的文件及目录添加到内存栈中。如果某些文件不需要,通过git status再次查看,找出文件的全名,再通过git rm --cached 。
git commit -m "本次提交的说明"  # 将缓存栈中的文件提交到本地仓库中,返回一个本次提交的版本号。
git log      # 查看已提交版本的相关信息: 作者、时间、版本号和分支信息。
git push -u origin master  # 将本地的仓库代码推送到origin远程仓库的master分支上

如果remote的远程仓库地址写错了,则删除origin的远程仓库,删除命令:

git remote remove origin

通用的.gitignore文件的内容如下:

.DS_Store
.idea
*.swp*
__pycache__

第一次上传远程仓库时, 必须添加-u--set-upstream参数。

【扩展】如果远程仓库的代码是错误时,可以将本地的正确的代码强制上传,命令如下:

git push -u --force origin master

如果基于ssh方式无效上传Push代码,则尝试使用https的方式。

git remote remove origin
git remote add origin https://gitee.com/readerqf/apiserver.git
git push -u origin master

输入用户名(码云的用户名)和口令(账号的口令)。

开发人员(下载项目-clone):

cd projects  # 进入存放项目的父级目录
git clone https://gitee.com/readerqf/apiserver.git

cloen成功之后,通过Pycharm工具打开,并为此项目创建Python环境,环境创建成功,在Pycharm的Terminal终端上执行如下命令:

pip install -r requirements.txt

代码修改完成后,可以执行如下git命令:

git remote remove origin
git remote add origin https://gitee.com/readerqf/apiserver.git
git add .
git commit -m "本次提交的信息" 
git push origin master

【注意】本次推送(上传)不需要-u,因为远程仓库不是空的。

9.3.3 reset和revert操作

git reset --hard  # 取消所有未提交的所有文件
git reset --hard   # 取消之前的所有操作, 即恢复到指定版本
git revert  # 取消某一个提交的版本

9.3.4 分支操作

git branch  # 查看分支
git checkout -b <分支名>  # 创建并切换分支
git branch <分支名>  # 只创建分支
git checkout <分支名>  # 切换分支
git branch -d <分支名>  # 删除分支

分支的作用: 不影响master主分支的代码完整性和正确性。

git merge <分支名>  # 当子分支的内容合并到当前分支上。如果存在冲突,则自动合并。

当两个本地创建修改同一个文件时,可能会发生突冲,解决的办法:

- 手动修改
- 避免冲突: 提交前先更新 git pull, 再提交。

如果两个分支冲突时,通过git merge 自动合并, 通过rebase自动衍合。

merge 合并分支时,可能会产生突冲
突冲:
 在主分支上创建支分后,又修改了主分支(1. 修改同一文件,2. 产生新的文件)
git rebase <子分支名> 

rebase 衍合: 
将子分支的版本,插入到创建子分支时的版本之后

远程操作:

git checkout dev  # 先切换到子分支
git push -u origin dev  # 第一次上传子分支
git push  #  第二次上传

下载更新:

git pull # 默认从远程分支下载最新的版本信息
git pull origin <分支名称>  # 从指定的分支下载最新的版本

十、Docker部署Django项目

参考: https://blog.csdn.net/ahhqdyh/article/details/105409244

10.1 部署方案

Docker + ubuntu + python3 + git + gunicorn(wsgi web server)  + Nginx
Docker + ubuntu + python3 + 文件同步 + gunicorn(wsgi web server) + Nginx
Docker + ubuntu + python3 + git + Django(python manager.py runserver)  选择此方案

10.2 非gunicorn方式部署

采用的Docker + ubuntu + python3 + 文件同步方案:

sources.list文件内容:

deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse

init.sh初始化脚本:

#!/bin/bash

cd /usr/src
cat sources.list > /etc/apt/sources.list
echo "准备更新 apt"
apt update
apt upgrade -y
apt install python3 python3-pip -y
apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev -y
python3 -V
pip3 install -r requirements.txt  -i https://mirrors.aliyun.com/pypi/simple

run.sh脚本内容:

#!/bin/bash
cd /usr/src
python3 manage.py runserver 0.0.0.0:8000

Dockerfile文件的内容:

FROM ubuntu
MAINTAINER BBQ
ADD . /usr/src
RUN /usr/src/init.sh
CMD /usr/src/run.sh

确保以上的脚本文件及项目的文件已上传到云主机的 /root/projects/BBQ,在构建Dockerfile镜像之前,先切换到BBQ目录中。

构造Docker的镜像:

docker build -t bbq:latest

运行bbq镜像:

docker run -itd --name server1 -v /root/projects/BBQ:/usr/src -p 8000:8000 bbq

查看运行的日志:

[root@10-255-0-173 BBQ]# docker logs server1
Performing system checks...

System check identified no issues (0 silenced).
July 29, 2020 - 07:17:23
Django version 2.1.3, using settings 'hpb.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

最后,在云主机的网页的控制台中配置安全组,放开8000端口。

【扩展】WebSocket应用于AI应答

gitee仓库地址: https://gitee.com/readerqf/aiserver.git

clone:

git clone https://gitee.com/readerqf/aiserver.git

server.py代码如下:

import tornado.ioloop as ioloop
import tornado.web as web
from tornado.websocket import WebSocketHandler


class AIHandler(WebSocketHandler):
    def check_origin(self, origin):
        # 解决跨域请求问题
        return True

    def open(self, *args, **kwargs):
        # 前端打开APP时
        # 保存客户端的连接
        print('---->客户端-->', self.request.remote_ip)

    def on_message(self, message):
        if 'hi' == message:
            self.write_message('您好')
        elif 'good' == message:
            self.write_message('你非常漂亮')
        else:
            self.write_message('我暂时无法理解您的意思')


def make_app():
    return web.Application([
        ('/ai/', AIHandler)
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8001, '0.0.0.0')
    print('AI服务器已启动')
    ioloop.IOLoop.current().start()

ai.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>AI 应答title>
    <style>
        #msg{
            height: 400px;
            width: 600px;
            overflow: scroll;
            border: 1px solid red;
            padding: 10px;
        }

        #msg p{
            margin: 10px;
            font-size: 14px;
            padding: 5px;
        }

        .right{
            border-radius: 5px;
            text-align: right;
        }

        .right span{
            background-color: greenyellow;
            padding: 10px;
        }

        .left{
            border-radius: 5px;
        }

        .left span{
            background-color: palevioletred;
            color: white;
            padding: 10px;
        }

        input{
            display: inline-block;
            height: 40px;
            width: 400px;
            padding: 5px;
            font-size: 16px;
        }
    style>
head>
<body>
<div id="msg">
    <p>欢迎进入AI聊天室p>
div>
<div>
    <input id="content" size="30" onchange="send_msg(this.value)">
    <button onclick="send_msg(content.value)">发送button>
div>

<script>
    var  websocket = new WebSocket('ws://localhost:8001/ai/')
    websocket.onmessage = function (ev) {
        msg.innerHTML += "

"+ev.data+"

"
} function send_msg(content) { websocket.send(content) msg.innerHTML += "

"+content+"

"
}
script> body> html>

关于AI应答的设计思想:

设计AI关键字语义表, 主要定义可能交互或搜索的关键字,表结构如下:
|------------------------------------|
|  id | title   | content            |
|------------------------------------|
|  1 |   hi     |  你好               |
|------------------------------------|
|  2 |   disen  | 千锋最帅的Python的老师|
|------------------------------------|
|  3 |   disen  | 千锋最帅的Python的老师|
|------------------------------------|

当前端发送过来话题时: websocket.send(content), 后端 on_message(self, message) 则收到messgae, 再通过DB类,从数据库中查询title对应的content, 并写给前端: self.write_message(content)。

十一、项目问题解决

11.1 排行问题

实现的方式: 采用Redis(缓存服务)的自增方法

自增的方法: rd.incre(key, amount)

11.1.1 人气排行

说明: 统计某一商品或产品的评论人数

人气:

  • 关注(收藏)的人数
  • 评论

实现: Django中QuerySet统计函数。

在从的模型类(多端)中:

class Bookrack(models.Model):
    bookrack_id = models.IntegerField(primary_key=True)
    # racks 建议外键关联时,动态向Book实例添加一个QuerySet的属性
    book = models.ForeignKey(Book, models.DO_NOTHING,'racks', blank=True, null=True)

在主的模型类(一端)中:

class Book(models.Model):
    book_id = models.IntegerField(primary_key=True)
    # ...
    @property
    def star(self):
        # QuerySet:  count(), filer(xx=x).exists()
        # 统计人气: 加入书架的数量
        return self.racks.count()

在序列化类中:

class BookSerializer(serializers.ModelSerializer):
    category = BookCategorySerializer(many=False)
    class Meta:
        model = Book
        fields = ('book_id', 'category', 'star')

在View视图中:

class BookView(APIView):
    def get(self, request):
        # 查询所有产品(小说)
        page = int(request.query_params.get('page', 1))
        paginator = Paginator(Book.objects.all(), 10)
        pager = paginator.page(page)  # 获取第几页

        s = BookSerializer(pager.object_list, many=True)

        return Response({'code': 0,
                         'pages': paginator.num_pages,
                         'page': page,
                         'data': s.data})

【扩展】统计订单详情中的某一个商品的总数量

class Goods(models.Model):
     name = models.CharField(max_length=20)
     price = models.FloatField(verbose_name='单价')
      
     @property
     def star(self):
        # 查询当前商品的总销量
        ret = OrderDetail.objects.filter(goods_id=self.pk).aggregate(Sum('cnt'))
        return ret['cnt__sum']

class Order(models.Model):
     title = models.CharField(max_length=20)
     price = models.FloatField(verbose_name='总价')
     user = models.ForeignKey('AppUser', models.SET_NULL, null=True, blank=True)
     ...

class OrderDetail(models.Model):
     order = models.ForeignKey('Order', models.SET_NULL, 'order_details', null=True, blank=True)
     goods = models.ForeignKey('Goods', models.SET_NULL, 'order_details', null=True, blank=True)
     cnt = models.IntegerField(verbose='数量')

查询 商品ID为 9 的人气(总销量):

from django.db.models import Max, Min, Sum

ret = OrderDetail.objects.filter(goods_id=9).aggregate(a=Sum('cnt'), b=Max('cnt'))

# {'cnt__sum': 1000, 'cnt__max': 90}
print(ret) # {'a': 1000,  'b': 90}

11.1.2 点击排行

说明: 统计某一商品或产品的点击的次数

11.1.2.1 总排行

说明: 统计某一商品的所有点击次数

涉及的方法:

  • redis的hincrby(‘total_rank’, book_id)
  • 模型QuerySet的in_bulk(id_list) 批量查询, 返回dict {id: , …}
  • sorted(list, key=lambda item: item[1], reverse=True) 排序方法

redis缓存的设计:

def rank(book_id):
    # 总排行
    rd.hincrby('total_rank', book_id)
    
def get_total_rank_topn(n):
    ranks = rd.hgetall('total_rank')
    return [book_id for book_id, score in sorted(ranks.items(), key=lambda item: int(item[1]),reverse=True)][:n]

获取总排行的商品接口:

class RankView(APIView):
    def get(self, request):
         # 总排行
        ids = caches.get_total_rank_topn(3)
        books = Book.objects.in_bulk(ids)  # 返回一个dict, {id: <>,  ...}
        s = BookSerializer([books.get(int(id)) for id in ids], many=True)

        return Response({'code': 0,
                         'total_top_3': s.data})
11.1.2.2 月度排行

说明: 统计某一商品在当月的所有点击次数

def rank(book_id):
    # 总排行
    rd.hincrby('total_rank', book_id)
    # 月排行
    curent_month = datetime.now().strftime('%Y%m')
    name = f'{curent_month}_rank'
    rd.hincrby(name, book_id)

def get_month_rank_topn(n):
    curent_month = datetime.now().strftime('%Y%m')
    name = f'{curent_month}_rank'
    ranks = rd.hgetall(name)
    return [book_id for book_id, score in sorted(ranks.items(), key=lambda item: int(item[1]), reverse=True)][:n]

View的设计

class RankView(APIView):
    def get(self, request):
         # 总排行
        ids = caches.get_total_rank_topn(3)
        books = Book.objects.in_bulk(ids)  # 返回一个dict, {id: <>,  ...}
        s = BookSerializer([books.get(int(id)) for id in ids], many=True)

        # 月排行
        month_ids = caches.get_month_rank_topn(3)
        month_books = Book.objects.in_bulk(month_ids)
        s2 = BookSerializer([month_books.get(int(id)) for id in month_ids], many=True)

        return Response({'code': 0,
                         'total_top_3': s.data,
                         'month_top_3': s2.data})

11.2 用户操作问题

当用户登录之后,发起与用户相关的操作。

在后端接口中,要接收用户的身份令牌Token。

11.2.1 前端请求头

登录之后,将后端返回的Token存储,或者设置到网络请求对象的默认headers。

Authorization: Token XxxxxxxXXXx
11.2.2 后端验证用户权限

在请求的方法中,如get()或post()等,写法中下:

class OrderView(APIView):
    def get(self, request):
				if 'HTTP_AUTHORIZATION' in request.META:
				     token = request.META['HTTP_AUTHORIZATION'].split()[-1]
            
             # 从redis中获取token对应的user_id
             user_id = caches.get_token(token)
             
             ret = Order.objects.filter(user_id=user_id)
             s = OrderSerializer(ret, many=True)
             return Response({'data': s.data})
          
         return Response({"msg": '未登录'})
      
    def post(self, request):
      # 获取用户
      pass
    

思考: 设计一个用户获取的装饰器, 如装饰器名为 AuthUser类。

存在装饰器之后,View中的代码如下:

class OrderView(APIView):
    @AuthUser()
    def get(self, request):
				if request.is_login:
             ret = Order.objects.filter(user_id=request.user.user_id)
             s = OrderSerializer(ret, many=True)
             return Response({'data': s.data})
      
    @AuthUser()
    def post(self, request):
      # 获取用户
      pass

AuthUser装饰器类的设计:

from rest_framework.response import Response

from utils import caches
from bqgapp.models import Appuser

class AuthUser():
    def __call__(self, v_func):
        # 被用到View的某一个函数中
        def wrapper(v, request, **kwargs):
            if 'HTTP_AUTHORIZATION' in request.META:
                token = request.META['HTTP_AUTHORIZATION'].split()[-1]

                # 从redis中获取token对应的user_id
                user_id = caches.get_token(token)
                request.user= Appuser.objects.get(pk=user_id)
                request.is_login = True
            else:
                request.is_login = False
                return Response({'msg': '当前接口需要用户授权,但用户未登录'})
            return v_func(v, request, **kwargs)

        return wrapper
11.2.3 示例:用户书架
class RankView(APIView):
    @decorator.AuthUser()
    def get(self, request):
        # 总排行
        ids = caches.get_total_rank_topn(3)
        books = Book.objects.in_bulk(ids)  # 返回一个dict, {id: <>,  ...}
        s = BookSerializer([books.get(int(id)) for id in ids], many=True)

        # 月排行
        month_ids = caches.get_month_rank_topn(3)
        month_books = Book.objects.in_bulk(month_ids)
        s2 = BookSerializer([month_books.get(int(id)) for id in month_ids], many=True)

        return Response({'code': 0,
                         'total_top_3': s.data,
                         'month_top_3': s2.data})

11.3 模型类与表同步问题

11.3.1 微调模型类

11.3.2 修改模型结构

你可能感兴趣的:(后端框架,Django,django,git,orm,restful,docker)