• python 全栈开发,Day98(路飞学城背景,django ContentType组件,表结构讲解)


    昨日内容回顾

    1. 为什么要做前后端分离?
        - 前后端交给不同的人来编写,职责划分明确。
        - API        (IOS,安卓,PC,微信小程序...)
        - vue.js等框架编写前端时,会比之前写jQuery更简单快捷。
    
    2. 对于后端人员,主要为前端提供:API(接口)
        以前的你的接口:
            http://127.0.0.1:8000/index/
            http://127.0.0.1:8000/users/
            http://127.0.0.1:8000/add_users/
            http://127.0.0.1:8000/del_users/
            http://127.0.0.1:8000/edit_users/
        restful 规范:
            http://127.0.0.1:8000/users/ 
            
    3. 谈谈你对restful规范的理解?
        1. 使用https代替http 
            https://www.luffycity.com/course/detail/web/3
            http://www.luffycity.com/course/detail/web/3
            
        2. 在URL中体现自己写的是API
            https://www.luffycity.com/api/
            https://api.luffycity.com/        可能会跨域
        
        3. 在URL中体现版本 
            https://www.luffycity.com/api/v1/users 
            https://www.luffycity.com/api/v2/users
            
        4. 名词(面向资源编程)
            https://www.luffycity.com/api/v1/users 
            https://www.luffycity.com/api/v1/song
        
        5. 行为 
            https://www.luffycity.com/api/v1/users
            method:
                get,获取
                post,新建
                put,更新
                patch,局部更新
                delete,删除
        6. 条件 
            https://www.luffycity.com/api/v1/users?page=1
            https://www.luffycity.com/api/v1/users?page=1&gender=2
            
        7. 状态码
            200
            301
            302
            404
            500
            推荐使用code:
                def xx(request):
                    ret = {'code':1000,'data':None}
                    try:
                        ...
                    except Exptions as e:
                        ret['status'] = 1001 
                        ret['error'] = 'xxxx错误'
                        
                    return JsonResponse(ret)
        8. 错误信息
            {
                code:10001,
                error:'用户名或密码错误'
            }
            
        9. 返回结果:
            GET:
                https://www.luffycity.com/api/v1/users
                    响应:
                        {
                            code: 1000,
                            data: [
                                {'name':'赵森','age':19},
                                {'name':'赵云','age':16},
                                {'name':'赵云','age':16},
                                {'name':'赵云','age':16},
                                {'name':'赵云','age':16},
                            ]
                        }    
            GET:
                https://www.luffycity.com/api/v1/users/1/
                    响应:
                        {
                            code:1000,
                            data:{'name':'赵森','age':19},
                        }
            POST:
                https://www.luffycity.com/api/v1/users
                    请求体:
                        {'name':'大表哥','age':19}
                    响应(不要):
                        {
                            code:1000,
                            data:{'id':9, 'name':'大表哥','age':19}
                        }
                        
            PUT/PATCH:
                https://www.luffycity.com/api/v1/users
                    请求体:
                        {'name':'大表哥','age':19}
                    响应(不要):
                        {
                            code:1000,
                            data:{'id':9, 'name':'大表哥','age':19}
                        }
            
            DELETE:
                ...
        10. hyper link 
            
            访问:https://www.luffycity.com/api/v1/users
                {
                    code:1000,
                    data:[
                        {'id':1,'name':'赵森','age':19, 'depart':https://www.luffycity.com/api/v1/depart/1/},
                        {'id':1,'name':'赵森','age':19, 'depart':https://www.luffycity.com/api/v1/depart/1/},
                        {'id':1,'name':'赵森','age':19, 'depart':https://www.luffycity.com/api/v1/depart/1/},
                        {'id':1,'name':'赵森','age':19, 'depart':https://www.luffycity.com/api/v1/depart/1/},
                        {'id':1,'name':'赵森','age':19, 'depart':https://www.luffycity.com/api/v1/depart/1/},
                    ]
                }
            
            https://www.luffycity.com/api/v1/users
                {
                    code:1000,
                    data:[
                        {'id':1,'name':'赵森','age':19, 'depart_title':'公关部'},
                        {'id':1,'name':'赵森','age':19, 'depart_title':'公关部'},
                        {'id':1,'name':'赵森','age':19, 'depart_title':'公关部'},
                        {'id':1,'name':'赵森','age':19, 'depart_title':'公关部'},
                        {'id':1,'name':'赵森','age':19, 'depart_title':'公关部'},
                    ]
                }
                
            
    4. django rest framework框架的作用?
        帮助开发者可以快速开发出遵循restful规范的API 
        
    5. django rest framework框架都有哪些组件(10)?
        版本
        权限
        认证
        节流
        分页
        解析器      ****
        序列化        *****
        视图        ****
        路由 
        渲染器
    View Code

     补充:

    http传输的数据是明文的,https传输的数据是加密的

    简单情况下,使用status,1表示成功,0表示失败

    复杂情况下,使用code,查看支付宝api的code说明

    https://docs.open.alipay.com/common/105806

    一般写接口的时候,推荐使用code。错误信息是服务器返回的!

    根据restful规范,POST请求,要返回新增的完整数据。但是,如果前端不需要这些信息,可以不返回!

    规定是死的,人是活的。所以要看应用场景!

    delete默认是没有返回信息的,但是,还是得返回一个状态。谁知道你,删除有没有成功呢?

    hyper link,返回信息中包含超链接。这个也是看需求了

    restful规范,不光是python才有。其他语言,也遵循这个规范!

    一、路飞学城背景

    初期

    2012年,有2个人讲python。其中一个是alex,主要是周末班。有7个人,当时没有固定场所,在7个人所在公司的办公室讲课。

    一周换一家公司。

    2014年,alex进入汽车之家。2014下半年,和佩奇。讲Python,也是讲周末班。有了固定场所--沙河。

    线下班,复制难。不好拿投资,规模扩大难。

    第一版:给视频+拉群

    第一版,在线教育。弄一个QQ群,将视频放到网上。给钱买视频,比如1000块,拉群进去讨论。
    学成率比较低,学员自己坚持不下去。10%以下的成功率!

    第二版:51cto合作

    第二版,和51cto合作,将视频交由51cto管理。每周三,周五做直播,直播2小时。主要做在线答疑!

    前期看直播的人,比较多。做了3次直播之后,指数急剧下滑,因为学员技术参差不齐,后面都听不懂了!

    最终以失败告终,51cto下架

    第三版:51cto合作-->微职位

    由alex和51cto谈判,推动51cto的微职位,就是一对一辅导。现今,51cto官网,还保留者微职位。

    还是给视频,并且分配一个导师,协助学员学习。并不是真正的一对一,找兼容人员。比如优秀的毕业生,1000~1500的费用。
    导师主动跟进学员学习!

    好的导师,一个人,可以对接50个人。有些人,遇到问题,会先自己搞定,搞不定,才会找导师。提升自己的解决问题能力,导师就会很轻松。
    学成率也不高,相对于以前的看视频,学成率有提升!


    第一个问题,导师问题。导师每周跟进一次。有些不问问题的学生,可能忽略跟进了
    第二个问题,学生问题。周末在家一天,大多数学不学下的,出去玩,或者打游戏了。学生不积极,没有明确目标
    第三个问题,视频问题。针对的学员的状态,是不同的!还有一些,不录制的视频。跟线上不一样!
    第四个问题,扩张问题。光靠线下,无法壮大。

    第四版:路飞学城

    老男孩,自己做路飞学城。初期,由学生投资,占2%,还有销售投资,2个人,一人50万。路飞学城正式成立!

    视频,单独录制

    由导致单独录制,根据知识点,录制一小段

    导师奖惩

    以前是靠人工,现在是自动化。
    聊天信息,导师上传到服务器。每周都要有,7天聊天没有,就扣钱!
    学员购买课程后,安排导师,导师账号有一定的金额。导师没有安装规定,就扣钱。导师自己肯定不愿意扣钱的,所以会主动跟进!
    学员不做作业,导师差评,都扣钱
    学员换导师,导师分配的金额就没有了,给另外的导师!

    以前导师是兼值的,没有办法做到实时回复,导师也有自己公司的事情要做。

    后来招了全职导师,做了服务升级。这些是需要掏钱的!

    颠覆了在线教育,以前都是看视频,现在是一对一辅导!

    学员奖惩

    学生,有奖学金
    作业做的好,学的快,有奖励金额!
    全线课程6个月,是有时效的。过了6个月,就没有了!需要重新购买,这样可以督促学生学习!
    学习是根据阶段的,一个阶段必须考核通过,才能进入下一阶段。不能超阶段学习!
    每个模块的视频是按照阶段进行的,不是随意看的!

    项目架构

    主站

    使用vue.js + django rest framework的架构,前后端分离。全面采用https,前端和后端在同一台服务器(同域名同端口),避免跨域问题。

    主站,注意是给学员使用的,用来购买课程,查看视频等....

    里面没有退款按钮,但是有签订协议的。可以休学,延长时间。
    退款,需要联系销售人员确认,跟进。为什么不能做到退款瞬时完成呢?因为有竞争对手,它会刷量。

    账户的流入流出,是有手续费的。量大的话,会亏损的。

    淘宝目前是7天退款,也是为了避免刷量。

    导师后台

    主要给导师使用的,账户金额,学员跟进...

    管理后台

    主要是运营使用,用来上传视频,编辑课程相关信息...

    开发人员

    主站:
        - 前端:
            - 前端姑娘 v1,vue.js 1.0
            - 前端姑娘 v2,vue.js 2.0(目前全面使用2.0)
        - 后端:
                - 老村长
                - 产品经理
                - alex/wu sir/文周
    导师: 1人
    管理后台:1人  + 兼职导师
    View Code

    二、django ContentType组件

    请求头ContentType

    ContentType指的是请求体的编码类型,常见的类型共有3种:

    1. application/x-www-form-urlencoded

    这应该是最常见的 POST 提交数据的方式了。浏览器的原生 <form> 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。请求类似于下面这样(无关的请求头在本文中都省略掉了)

    POST http://www.example.com HTTP/1.1
    Content-Type: application/x-www-form-urlencoded;charset=utf-8
    
    user=yuan&age=22
    View Code

    2. multipart/form-data

    这又是一个常见的 POST 数据提交的方式。我们使用表单上传文件时,必须让 <form> 表单的 enctype 等于 multipart/form-data。直接来看一个请求示例:

    POST http://www.example.com HTTP/1.1
    Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
    
    ------WebKitFormBoundaryrGKCBY7qhFd3TrwA
    Content-Disposition: form-data; name="user"
    
    yuan
    ------WebKitFormBoundaryrGKCBY7qhFd3TrwA
    Content-Disposition: form-data; name="file"; filename="chrome.png"
    Content-Type: image/png
    
    PNG ... content of chrome.png ...
    ------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
    View Code

    这个例子稍微复杂点。首先生成了一个 boundary 用于分割不同的字段,为了避免与正文内容重复,boundary 很长很复杂。然后 Content-Type 里指明了数据是以 multipart/form-data 来编码,本次请求的 boundary 是什么内容。消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 --boundary 开始,紧接着是内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 --boundary-- 标示结束。关于 multipart/form-data 的详细定义,请前往 rfc1867 查看。

    这种方式一般用来上传文件,各大服务端语言对它也有着良好的支持。

    上面提到的这两种 POST 数据的方式,都是浏览器原生支持的,而且现阶段标准中原生 <form> 表单也只支持这两种方式(通过 <form> 元素的 enctype 属性指定,默认为 application/x-www-form-urlencoded。其实 enctype 还支持 text/plain,不过用得非常少)。

    随着越来越多的 Web 站点,尤其是 WebApp,全部使用 Ajax 进行数据交互之后,我们完全可以定义新的数据提交方式,给开发带来更多便利。

    3. application/json

    application/json 这个 Content-Type 作为响应头大家肯定不陌生。实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON 字符串。由于 JSON 规范的流行,除了低版本 IE 之外的各大浏览器都原生支持 JSON.stringify,服务端语言也都有处理 JSON 的函数,使用 JSON 不会遇上什么麻烦。

    JSON 格式支持比键值对复杂得多的结构化数据,这一点也很有用。记得我几年前做一个项目时,需要提交的数据层次非常深,我就是把数据 JSON 序列化之后来提交的。不过当时我是把 JSON 字符串作为 val,仍然放在键值对里,以 x-www-form-urlencoded 方式提交。

    举例:

    新建一个项目untitled1

    修改urls.py

    from django.contrib import admin
    from django.urls import path
    from app01 import views
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('index/', views.index),
    ]
    View Code

    修改views.py

    from django.shortcuts import render,HttpResponse
    from django.views.decorators.csrf import csrf_exempt,csrf_protect
    
    # Create your views here.
    @csrf_exempt  # 跳过csrf
    def index(request):
        print(request.POST)
        return HttpResponse('...')
    View Code

    启动项目,访问首页

    这个时候,boss,发了一个post请求,给index

    新建一个boss.py,放在项目根目录中

    import requests
    
    requests.post(
        url='http://127.0.0.1:8000/index/',
        data={"user":"root","pwd":123}
    )
    View Code

    执行boss.py,查看Pycharm控制台输出:

    <QueryDict: {'pwd': ['123'], 'user': ['root']}>

    收到了一条post请求

    假设boss发送的是json数据呢?

    再次执行boss.py,查看控制台

    <QueryDict: {}>

    发现得到是空字典?为什么呢?boss说,已经给你发过去了,你怎么回答?

    注意:request.POST不是万能的,因为它只接收请求头为application/x-www-form-urlencoded,其他类型一概不支持。

    查看源码,只有为application/x-www-form-urlencoded,才会解析数据

        def _load_post_and_files(self):
            """Populate self._post and self._files if the content-type is a form type"""
            if self.method != 'POST':
                self._post, self._files = QueryDict(encoding=self._encoding), MultiValueDict()
                return
            if self._read_started and not hasattr(self, '_body'):
                self._mark_post_parse_error()
                return
    
            if self.content_type == 'multipart/form-data':
                if hasattr(self, '_body'):
                    # Use already read data
                    data = BytesIO(self._body)
                else:
                    data = self
                try:
                    self._post, self._files = self.parse_file_upload(self.META, data)
                except MultiPartParserError:
                    # An error occurred while parsing POST data. Since when
                    # formatting the error the request handler might access
                    # self.POST, set self._post and self._file to prevent
                    # attempts to parse POST data again.
                    # Mark that an error occurred. This allows self.__repr__ to
                    # be explicit about it instead of simply representing an
                    # empty POST
                    self._mark_post_parse_error()
                    raise
            elif self.content_type == 'application/x-www-form-urlencoded':
                self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict()
            else:
                self._post, self._files = QueryDict(encoding=self._encoding), MultiValueDict()
    View Code

    使用request.body,它是万能。它直接接收请求体的原始数据,是一个bytes类型的数据

    修改views.py,改为body

    from django.shortcuts import render,HttpResponse
    from django.views.decorators.csrf import csrf_exempt,csrf_protect
    
    # Create your views here.
    @csrf_exempt  # 跳过csrf
    def index(request):
        print(request.body)
        return HttpResponse('...')
    View Code

    再次执行脚本,查看控制台输出:

    b'{"pwd": 123, "user": "root"}'

     如果需要获取user,需要对数据进行解码

    from django.shortcuts import render,HttpResponse
    from django.views.decorators.csrf import csrf_exempt,csrf_protect
    import json
    
    # Create your views here.
    @csrf_exempt  # 跳过csrf
    def index(request):
        print(request.body)
        data = (request.body).decode('utf-8')  # 解码
        dict_data = json.loads(data)  # 反序列化
        print(dict_data,type(dict_data))  # 查看类型为dict
    
        print(dict_data.get("user"))  # 获取user
    
        return HttpResponse('...')
    View Code

    再次执行脚本,查看控制台输出:

    b'{"user": "root", "pwd": 123}'
    {'pwd': 123, 'user': 'root'} <class 'dict'>
    root

    ContentType组件

    contenttypes 是Django内置的一个应用,可以追踪项目中所有app和model的对应关系,并记录在ContentType表中。

    ContentTypes做了什么?

    当使用django-admin初始化一个django项目的时候,可以看到在默认的INSTALL_APPS已经包含了django.contrib.contenttypes:

    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    ]

    而且注意django.contrib.contenttypes是在django.contrib.auth之后,这是因为auth中的permission系统是根据contenttypes来实现的。

    接着查阅了一下django.contrib.contenttypes.models文件:

    class ContentType(models.Model):
        app_label = models.CharField(max_length=100)
        model = models.CharField(_('python model class name'), max_length=100)
        objects = ContentTypeManager()
    
        class Meta:
            verbose_name = _('content type')
            verbose_name_plural = _('content types')
            db_table = 'django_content_type'
            unique_together = (('app_label', 'model'),)
    
        def __str__(self):
            return self.name
    View Code

    可以看到ContentType就是一个简单的django model,而且它在数据库中的表的名字为django_content_type。 

    在第一次对Django的model进行migrate之后,就可以发现在数据库中出现了一张默认生成的名为django_content_type的表。 
    如果没有建立任何的model,默认django_content_type是这样的:

    +----+--------------+--------------+
    | id | app_label    | model        |
    +----+--------------+--------------+
    |  1 | admin        | logentry     |
    |  2 | auth         | group        |
    |  3 | auth         | user         |
    |  4 | auth         | permission   |
    |  5 | contenttypes | contenttype  |
    |  6 | sessions     | session      |
    +----+--------------+--------------+
    View Code

    因此,django_content_type记录了当前的Django项目中所有model所属的app(即app_label属性)以及model的名字(即model属性)。 
    当然,django_content_type并不只是记录属性这么简单,contenttypes是对model的一次封装,因此可以通过contenttypes动态的访问model类型,而不需要每次import具体的model类型

    每当我们创建了新的model并执行数据库迁移后,ContentType表中就会自动新增一条记录。比如我在应用app01的models.py中创建表class Electrics(models.Model): pass。从数据库查看ContentType表,显示如下:

    idapp_labelmodel
    admin, auth等内置应用…
    5 contenttypes contenttype
    6 app01 electrics

    举例说明

    方式1:适用于1张表和另一张表要关联的时候。

    那么这个表有什么作用呢?这里提供一个场景,路飞学城

    1.路飞学城表设计

    2.将2个价格策略表合并1张表。

    3.如果再加一张表,那价格策略表的表结构会发生改变。  这样不合理的,我们的表结构一般设计完就不会改变。

     

    方式2:适用于1张表和多张表关联的时候。

     4.接下来换一种方式。表名+id   数据库表结构不会改变。

     5.创建一个新项目

     

    6.创建表

    修改models.py

    from django.db import models
    
    
    # Create your models here.
    class Course(models.Model):
        """
        普通课程
        """
        title = models.CharField(max_length=32)
    
    class DegreeCourse(models.Model):
        """
        学位课程
        """
        title = models.CharField(max_length=32)
    
    class PricePolicy(models.Model):
        """
        价格策略
        """
        price = models.IntegerField()
        period = models.IntegerField()
        table_name = models.CharField(verbose_name="关联的表名称")
        object_id = models.CharField(verbose_name="关联的表中的数据行ID")
    View Code

    方式3:ContentType组件

    7.settings.py,默认就有contenttypes

    8. 使用ContentType组件

    修改models.py

    主要改动部分如下:

    from django.contrib.contenttypes.fields import GenericForeignKey,GenericRelation
    from django.contrib.contenttypes.models import ContentType
    content_type = models.ForeignKey(ContentType, verbose_name="关联的表名称",on_delete=models.CASCADE)
    object_id = models.IntegerField(verbose_name="关联的表中的数据行ID")
    View Code

    完整代码如下:

    from django.db import models
    from django.contrib.contenttypes.fields import GenericForeignKey,GenericRelation
    from django.contrib.contenttypes.models import ContentType
    
    # Create your models here.
    class Course(models.Model):
        """
        普通课程
        """
        title = models.CharField(max_length=32)
    
    class DegreeCourse(models.Model):
        """
        学位课程
        """
        title = models.CharField(max_length=32)
    
    class PricePolicy(models.Model):
        """
        价格策略
        """
        price = models.IntegerField()
        period = models.IntegerField()
        content_type = models.ForeignKey(ContentType, verbose_name="关联的表名称",on_delete=models.CASCADE)
        object_id = models.IntegerField(verbose_name="关联的表中的数据行ID")
    View Code

    9.假设,表数据很多,有个关联表的名字改了,需要改所有的数据很麻烦。那就再创建一张表,专门存放表名字。

    10. 第三张表不用自己创建     ContentType 组件已经帮我们创建好了,专门用来存放表名字。

    11.生成表,查看表数据

    使用2个命令生成表

    python manage.py makemigrations
    python manage.py migrate

    效果如下:

     注意:价格策略表,它是万能的。可以适用于任何课程的价格策略。

    contenttypes 应用

    这里提供一个场景,网上商城购物时,会有各种各样的优惠券,比如通用优惠券,满减券,或者是仅限特定品类的优惠券。在数据库中,可以通过外键将优惠券和不同品类的商品表关联起来:

    from django.db import models
    from django.contrib.contenttypes.fields import GenericForeignKey,GenericRelation
    from django.contrib.contenttypes.models import ContentType
    
    # Create your models here.
    class Electrics(models.Model):
        """
        id  name
        1   日立冰箱
        2   三星电视
        3   小天鹅洗衣机
        """
        name = models.CharField(max_length=32)
    
    
    class Foods(models.Model):
        """
        id   name
        1    面包
        2    烤鸭
        """
        name = models.CharField(max_length=32)
    
    
    class Clothes(models.Model):
        name = models.CharField(max_length=32)
    
    
    class Coupon(models.Model):
        """
        id     name            Electrics        Foods           Clothes        more...
        1     通用优惠券       null              null            null           
        2     冰箱满减券         2               null            null
        3     面包狂欢节        null              1              null
    
        """
        name = models.CharField(max_length=32)
        electric_obj = models.ForeignKey(to='Electrics', null=True,on_delete=models.CASCADE)
        food_obj = models.ForeignKey(to='Foods', null=True,on_delete=models.CASCADE)
        cloth_obj = models.ForeignKey(to='Clothes', null=True,on_delete=models.CASCADE)
    View Code

    如果是通用优惠券,那么所有的ForeignKey为null,如果仅限某些商品,那么对应商品ForeignKey记录该商品的id,不相关的记录为null。但是这样做是有问题的:实际中商品品类繁多,而且很可能还会持续增加,那么优惠券表中的外键将越来越多,但是每条记录仅使用其中的一个或某几个外键字段。

    通过使用contenttypes 应用中提供的特殊字段GenericForeignKey,我们可以很好的解决这个问题。只需要以下三步:

    • 在model中定义ForeignKey字段,并关联到ContentType表。通常这个字段命名为“content_type”
    • 在model中定义PositiveIntegerField字段,用来存储关联表中的主键。通常这个字段命名为“object_id”
    • 在model中定义GenericForeignKey字段,传入上述两个字段的名字。

    为了更方便查询商品的优惠券,我们还可以在商品类中通过GenericRelation字段定义反向关系。

    创建一个新的项目tmall

    修改models.py

    from django.db import models
    from django.contrib.contenttypes.models import ContentType
    from django.contrib.contenttypes.fields import GenericForeignKey,GenericRelation
    
    
    class Electrics(models.Model):
        name = models.CharField(max_length=32)
        coupons = GenericRelation(to='Coupon')  # 用于反向查询,不会生成表字段
    
        def __str__(self):
            return self.name
    
    
    class Foods(models.Model):
        name = models.CharField(max_length=32)
        coupons = GenericRelation(to='Coupon')
    
        def __str__(self):
            return self.name
    
    
    class Clothes(models.Model):
        name = models.CharField(max_length=32)
        coupons = GenericRelation(to='Coupon')
    
        def __str__(self):
            return self.name
    
    class Coupon(models.Model):
        name = models.CharField(max_length=32)
    
        content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE,default=None) # step 1
        object_id = models.PositiveIntegerField(default=None) # step 2
        content_object = GenericForeignKey('content_type', 'object_id') # step 3
    
        def __str__(self):
            return self.name
    
    class OftenAskedQuestion(models.Model):
        """常见问题"""
        content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE,default=None)
        object_id = models.PositiveIntegerField(default=None)
        content_object = GenericForeignKey('content_type', 'object_id')
    
        question = models.CharField(max_length=255)
        answer = models.TextField(max_length=1024)
    View Code

    使用2个命令,生成表

    python manage.py makemigrations
    python manage.py migrate

    创建记录和查询

    使用navicat打开sqllite数据库,执行以下sq

    INSERT INTO app01_coupon ("id", "name", "content_type_id", "object_id") VALUES (1, '通用优惠券', 7, 2);
    INSERT INTO app01_coupon ("id", "name", "content_type_id", "object_id") VALUES (2, '冰箱满减券', 7, 1);
    INSERT INTO app01_coupon ("id", "name", "content_type_id", "object_id") VALUES (3, '面包狂欢节', 8, 1);
    
    INSERT INTO app01_electrics ("id", "name") VALUES (1, '日立冰箱');
    INSERT INTO app01_electrics ("id", "name") VALUES (2, '三星电视');
    INSERT INTO app01_electrics ("id", "name") VALUES (3, '小天鹅洗衣机');
    
    INSERT INTO app01_foods ("id", "name") VALUES (1, '面包');
    INSERT INTO app01_foods ("id", "name") VALUES (2, '烤鸭');
    
    INSERT INTO app01_oftenaskedquestion ("id", "object_id", "question", "answer", "content_type_id") VALUES (1, 1, '多少钱', 10, 7);
    View Code

    解释一下,electrics和oftenaskedquestion是如何关联的

    先来看django_content_type表记录,electrics表对应的主键id为7

    再看看oftenaskedquestion表记录

    object_id表示electrics表的主键id=1的记录

    question,表示问题

    answer,表示答案

    content_type_id,表示electrics表在django_content_type表中的主键id

    通过最后一条sql语句,electrics表和oftenaskedquestion表,就建立了一条关联记录!

    修改urls.py

    from django.contrib import admin
    from django.urls import path
    
    from app01 import views
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('test/', views.test),
    ]
    View Code

    修改views.py

    from django.shortcuts import render, HttpResponse
    from app01 import models
    from django.contrib.contenttypes.models import ContentType
    
    
    def test(request):
        if request.method == 'GET':
            # ContentType表对象有model_class() 方法,取到对应model
            content = ContentType.objects.filter(app_label='app01', model='electrics').first()  # 表名小写
            cloth_class = content.model_class() # cloth_class 就相当于models.Electrics
            res = cloth_class.objects.all()
            print(res)
    
            # 为三星电视(id=2)创建一条优惠记录
            s_tv = models.Electrics.objects.filter(id=2).first()
            print(s_tv)
            # models.Coupon.objects.create(name='电视优惠券', content_object=s_tv)
    
            # 查询优惠券(id=1)绑定了哪些商品
            coupon_obj = models.Coupon.objects.filter(id=1).first()
            prod = coupon_obj.content_object
            print(prod)
            #
            # 查询三星电视(id=2)的所有优惠券
            res = s_tv.coupons.all()
            print(res)
            #
            # 查询obj的所有优惠券:如果没有定义反向查询字段,通过如下方式:
            content = ContentType.objects.filter(app_label='app01', model='electrics').first()
            print(content)
            res = models.OftenAskedQuestion.objects.filter(content_type=content, object_id=coupon_obj.pk).all()
            print(res)
            return HttpResponse('....')
    View Code

    启动项目,访问页面

    查看Pycharm控制台输出:

    <QuerySet [<Electrics: 日立冰箱>, <Electrics: 三星电视>, <Electrics: 小天鹅洗衣机>]>
    三星电视
    三星电视
    <QuerySet [<Coupon: 通用优惠券>]>
    electrics
    <QuerySet [<OftenAskedQuestion: OftenAskedQuestion object (1)>]>

    总结:

    当一张表和多个表FK关联,并且多个FK中只能选择其中一个或其中n个时,可以利用contenttypes app,只需定义三个字段就搞定!

    一、路飞学城表结构

    创建项目luffycity,应用名为api。注意:django版本为1.11

    修改models.py

    from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
    from django.contrib.contenttypes.models import ContentType
    from django.db.models import Q
    from django.utils.safestring import mark_safe
    from django.db import models
    import hashlib
    
    
    # ######################## 课程相关 ########################
    
    class CourseCategory(models.Model):
        """课程大类, e.g 前端  后端..."""
        name = models.CharField(max_length=64, unique=True)
    
        def __str__(self):
            return "%s" % self.name
    
        class Meta:
            verbose_name_plural = "01.课程大类"
    
    
    class CourseSubCategory(models.Model):
        """课程子类, e.g python linux """
        category = models.ForeignKey("CourseCategory")
        name = models.CharField(max_length=64, unique=True)
    
        def __str__(self):
            return "%s" % self.name
    
        class Meta:
            verbose_name_plural = "02.课程子类"
    
    
    class DegreeCourse(models.Model):
        """学位课程"""
        name = models.CharField(max_length=128, unique=True)
        course_img = models.CharField(max_length=255, verbose_name="缩略图")
        brief = models.TextField(verbose_name="学位课程简介", )
        total_scholarship = models.PositiveIntegerField(verbose_name="总奖学金(贝里)", default=40000)  # 2000 2000
        mentor_compensation_bonus = models.PositiveIntegerField(verbose_name="本课程的导师辅导费用(贝里)", default=15000)
        period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=150)  # 为了计算学位奖学金
        prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024)
        teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师")
    
        # 用于GenericForeignKey反向查询, 不会生成表字段,切勿删除
        # coupon = GenericRelation("Coupon")
    
        # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除
        degreecourse_price_policy = GenericRelation("PricePolicy")
    
        def __str__(self):
            return self.name
    
        class Meta:
            verbose_name_plural = "03.学位课"
    
    
    class Teacher(models.Model):
        """讲师、导师表"""
        name = models.CharField(max_length=32)
        role_choices = ((0, '讲师'), (1, '导师'))
        role = models.SmallIntegerField(choices=role_choices, default=0)
        title = models.CharField(max_length=64, verbose_name="职位、职称")
        signature = models.CharField(max_length=255, help_text="导师签名", blank=True, null=True)
        image = models.CharField(max_length=128)
        brief = models.TextField(max_length=1024)
    
        def __str__(self):
            return self.name
    
        class Meta:
            verbose_name_plural = "04.导师或讲师"
    
    
    class Scholarship(models.Model):
        """学位课程奖学金"""
        degree_course = models.ForeignKey("DegreeCourse")
        time_percent = models.PositiveSmallIntegerField(verbose_name="奖励档位(时间百分比)", help_text="只填百分值,如80,代表80%")
        value = models.PositiveIntegerField(verbose_name="奖学金数额")
    
        def __str__(self):
            return "%s:%s" % (self.degree_course, self.value)
    
        class Meta:
            verbose_name_plural = "05.学位课奖学金"
    
    
    class Course(models.Model):
        """专题课/学位课模块表"""
        name = models.CharField(max_length=128, unique=True)
        course_img = models.CharField(max_length=255)
        sub_category = models.ForeignKey("CourseSubCategory")
        course_type_choices = ((0, '付费'), (1, 'VIP专享'), (2, '学位课程'))
        course_type = models.SmallIntegerField(choices=course_type_choices)
    
        # 不为空;学位课的某个模块
        # 为空;专题课
        degree_course = models.ForeignKey("DegreeCourse", blank=True, null=True, help_text="若是学位课程,此处关联学位表")
    
        brief = models.TextField(verbose_name="课程概述", max_length=2048)
        level_choices = ((0, '初级'), (1, '中级'), (2, '高级'))
        level = models.SmallIntegerField(choices=level_choices, default=1)
        pub_date = models.DateField(verbose_name="发布日期", blank=True, null=True)
        period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=7)  #
        order = models.IntegerField("课程顺序", help_text="从上一个课程数字往后排")
        attachment_path = models.CharField(max_length=128, verbose_name="课件路径", blank=True, null=True)
        status_choices = ((0, '上线'), (1, '下线'), (2, '预上线'))
        status = models.SmallIntegerField(choices=status_choices, default=0)
        template_id = models.SmallIntegerField("前端模板id", default=1)
    
        # coupon = GenericRelation("Coupon")
    
        # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除
        price_policy = GenericRelation("PricePolicy")
    
        asked_question = GenericRelation("OftenAskedQuestion")
    
    
        def __str__(self):
            return "%s(%s)" % (self.name, self.get_course_type_display())
    
    
        def save(self, *args, **kwargs):
            if self.course_type == 2:
                if not self.degree_course:
                    raise ValueError("学位课程必须关联对应的学位表")
            super(Course, self).save(*args, **kwargs)
    
    
        class Meta:
            verbose_name_plural = "06.专题课或学位课模块"
    
    
    class CourseDetail(models.Model):
        """课程详情页内容"""
        course = models.OneToOneField("Course")
        hours = models.IntegerField("课时")
        course_slogan = models.CharField(max_length=125, blank=True, null=True)
        video_brief_link = models.CharField(verbose_name='课程介绍', max_length=255, blank=True, null=True)
        why_study = models.TextField(verbose_name="为什么学习这门课程")
        what_to_study_brief = models.TextField(verbose_name="我将学到哪些内容")
        career_improvement = models.TextField(verbose_name="此项目如何有助于我的职业生涯")
        prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024)
        recommend_courses = models.ManyToManyField("Course", related_name="recommend_by", blank=True)
        teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师")
    
        def __str__(self):
            return "%s" % self.course
    
        class Meta:
            verbose_name_plural = "07.课程或学位模块详细"
    
    
    class OftenAskedQuestion(models.Model):
        """常见问题"""
        content_type = models.ForeignKey(ContentType)  # 关联course or degree_course
        object_id = models.PositiveIntegerField()
        content_object = GenericForeignKey('content_type', 'object_id')
    
        question = models.CharField(max_length=255)
        answer = models.TextField(max_length=1024)
    
    
        def __str__(self):
            return "%s-%s" % (self.content_object, self.question)
    
    
        class Meta:
            unique_together = ('content_type', 'object_id', 'question')
            verbose_name_plural = "08. 常见问题"
    
    
    class CourseOutline(models.Model):
        """课程大纲"""
        course_detail = models.ForeignKey("CourseDetail")
        title = models.CharField(max_length=128)
        # 前端显示顺序
        order = models.PositiveSmallIntegerField(default=1)
    
        content = models.TextField("内容", max_length=2048)
    
        def __str__(self):
            return "%s" % self.title
    
        class Meta:
            unique_together = ('course_detail', 'title')
            verbose_name_plural = "09. 课程大纲"
    
    
    class CourseChapter(models.Model):
        """课程章节"""
        course = models.ForeignKey("Course", related_name='coursechapters')
        chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
        name = models.CharField(max_length=128)
        summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
        pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
    
        class Meta:
            unique_together = ("course", 'chapter')
            verbose_name_plural = "10. 课程章节"
    
        def __str__(self):
            return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)
    
    
    class CourseSection(models.Model):
        """课时目录"""
        chapter = models.ForeignKey("CourseChapter", related_name='coursesections')
        name = models.CharField(max_length=128)
        order = models.PositiveSmallIntegerField(verbose_name="课时排序", help_text="建议每个课时之间空1至2个值,以备后续插入课时")
        section_type_choices = ((0, '文档'), (1, '练习'), (2, '视频'))
        section_type = models.SmallIntegerField(default=2, choices=section_type_choices)
        section_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填vid,若是文档,填link")
        video_time = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32)  # 仅在前端展示使用
        pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
        free_trail = models.BooleanField("是否可试看", default=False)
    
        class Meta:
            unique_together = ('chapter', 'section_link')
            verbose_name_plural = "11. 课时"
    
        def __str__(self):
            return "%s-%s" % (self.chapter, self.name)
    
    
    class Homework(models.Model):
        chapter = models.ForeignKey("CourseChapter")
        title = models.CharField(max_length=128, verbose_name="作业题目")
        order = models.PositiveSmallIntegerField("作业顺序", help_text="同一课程的每个作业之前的order值间隔1-2个数")
        homework_type_choices = ((0, '作业'), (1, '模块通关考核'))
        homework_type = models.SmallIntegerField(choices=homework_type_choices, default=0)
        requirement = models.TextField(max_length=1024, verbose_name="作业需求")
        threshold = models.TextField(max_length=1024, verbose_name="踩分点")
        recommend_period = models.PositiveSmallIntegerField("推荐完成周期(天)", default=7)
        scholarship_value = models.PositiveSmallIntegerField("为该作业分配的奖学金(贝里)")
        note = models.TextField(blank=True, null=True)
        enabled = models.BooleanField(default=True, help_text="本作业如果后期不需要了,不想让学员看到,可以设置为False")
    
        class Meta:
            unique_together = ("chapter", "title")
            verbose_name_plural = "12. 章节作业"
    
        def __str__(self):
            return "%s - %s" % (self.chapter, self.title)
    
    
    # class CourseReview(models.Model):
    #     """课程评价"""
    #     enrolled_course = models.OneToOneField("EnrolledCourse")
    #     about_teacher = models.FloatField(default=0, verbose_name="讲师讲解是否清晰")
    #     about_video = models.FloatField(default=0, verbose_name="内容实用")
    #     about_course = models.FloatField(default=0, verbose_name="课程内容通俗易懂")
    #     review = models.TextField(max_length=1024, verbose_name="评价")
    #     disagree_number = models.IntegerField(default=0, verbose_name="踩")
    #     agree_number = models.IntegerField(default=0, verbose_name="赞同数")
    #     tags = models.ManyToManyField("Tags", blank=True, verbose_name="标签")
    #     date = models.DateTimeField(auto_now_add=True, verbose_name="评价日期")
    #     is_recommend = models.BooleanField("热评推荐", default=False)
    #     hide = models.BooleanField("不在前端页面显示此条评价", default=False)
    #
    #     def __str__(self):
    #         return "%s-%s" % (self.enrolled_course.course, self.review)
    #
    #     class Meta:
    #         verbose_name_plural = "13. 课程评价(购买课程后才能评价)"
    #
    #
    # class DegreeCourseReview(models.Model):
    #     """学位课程评价
    #     为了以后可以定制单独的评价内容,所以不与普通课程的评价混在一起,单独建表
    #     """
    #     enrolled_course = models.ForeignKey("EnrolledDegreeCourse")
    #     course = models.ForeignKey("Course", verbose_name="评价学位模块", blank=True, null=True,
    #                                help_text="不填写即代表评价整个学位课程", limit_choices_to={'course_type': 2})
    #     about_teacher = models.FloatField(default=0, verbose_name="讲师讲解是否清晰")
    #     about_video = models.FloatField(default=0, verbose_name="视频质量")
    #     about_course = models.FloatField(default=0, verbose_name="课程")
    #     review = models.TextField(max_length=1024, verbose_name="评价")
    #     disagree_number = models.IntegerField(default=0, verbose_name="踩")
    #     agree_number = models.IntegerField(default=0, verbose_name="赞同数")
    #     tags = models.ManyToManyField("Tags", blank=True, verbose_name="标签")
    #     date = models.DateTimeField(auto_now_add=True, verbose_name="评价日期")
    #     is_recommend = models.BooleanField("热评推荐", default=False)
    #     hide = models.BooleanField("不在前端页面显示此条评价", default=False)
    #
    #     def __str__(self):
    #         return "%s-%s" % (self.enrolled_course, self.review)
    #
    #     class Meta:
    #         verbose_name_plural = "14. 学位课评价(购买课程后才能评价)"
    
    
    class PricePolicy(models.Model):
        """价格与有课程效期表"""
        content_type = models.ForeignKey(ContentType)  # 关联course or degree_course
        object_id = models.PositiveIntegerField()
        content_object = GenericForeignKey('content_type', 'object_id')
    
        # course = models.ForeignKey("Course")
        valid_period_choices = ((1, '1天'), (3, '3天'),
                                (7, '1周'), (14, '2周'),
                                (30, '1个月'),
                                (60, '2个月'),
                                (90, '3个月'),
                                (180, '6个月'), (210, '12个月'),
                                (540, '18个月'), (720, '24个月'),
                                )
        valid_period = models.SmallIntegerField(choices=valid_period_choices)
        price = models.FloatField()
    
        class Meta:
            unique_together = ("content_type", 'object_id', "valid_period")
            verbose_name_plural = "15. 价格策略"
    
        def __str__(self):
            return "%s(%s)%s" % (self.content_object, self.get_valid_period_display(), self.price)
    View Code

    关键字解释:

    Course表

    Course表里面有price_policy属性,里面定义了GenericRelation,它是用来做反向查询的

    表示它会从django_content_type表中找到model字段为pricepolicy(价格策略表)

    PricePolicy表

    content_type,约定俗成叫这个命令,它和django_content_type表做了外键关联。ContentType表示django_content_type表

    object_id 表示关联表中的主键。通常这个字段命名为“object_id”。

    content_object 传入上述两个字段的名字,用来建立外键关系。

    Course表,假设id=1,课程名为Python全栈。要加一条价格策略,比如7天30块

    查看django_content_type表,course的id为8

    那么api_pricepolicy表的记录应该这么写

    object_id,表示关联表的主键id,也就是course表的id=1的记录

    vaild_period,表示有效期

    price,表示价格

    content_type_id,表示django_content_type表中主键id。8就表示course表

    通过这样,2个表,就建立了一条关系!

    django_content_type对所有表做了外键,所以它记录了所有表名!

    假设,需要查询Course表中id=1的课程所有价格策略,可以这么查询

    obj = models.Course.objects.filter(id=1).first()print(i.price_policy.all())

    all()表示查询所有记录

    假设,查询所有课程的价格策略

    obj = models.Course.objects.all()
    for i in obj:
        print(i.price_policy.all())

    显示课程列表,并打印每个课程的所有价格信息

    obj = models.Course.objects.all()
    for i in obj:
        print(i.name)
        ps = i.price_policy.all()
        for p in ps:
            print('-->',p.valid_period,p.price)

    执行之后,效果应该是这样的

    Python开发入门7天特训营
    --> 7 10.0
    --> 30 50.0
    Linux系统基础5周入门精讲
    --> 14 20.0
    --> 60 80.0

    Course表里面有专题课和学位课。

    专题课,可以单独售卖。

    Course里面的degree_course。为空,表示专题课。否则为学位课!

    判断为空,使用如下语句

    models.Course.objects.filter(degree_course__isnull=True)

    表结构图关系图如下:

    总结:

    1. django contenttypes组件的作用?
        - 数据库迁移时,找到所有app中的表并录入到django_content_type表
        - 内置两个字段:
            GenericRelation
            GenericForeignKey
            
    2. 使用contenttypes组件
    
    3. 路飞表结构 
        a. 课程分类 
            - 课程大类
            - 课程子类 
        b. 学位课 
            - 学位课
            - 奖学金 
            - 老师 
        c. 专题课 or 学位课模块
            - 专题课 or 学位课模块
            - 课程详细 
            - 课程大纲
            - 常见问题 
            - 章节 
            - 课时
            - 作业 
        d. 价格
            - 价格策略
    View Code

    今日作业:

    一、准备工作:
    1. 通过admin对13张表录入数据
    2. ORM练习 
        a. 查看所有学位课并打印学位课名称以及授课老师
        
        b. 查看所有学位课并打印学位课名称以及学位课的奖学金
        
        c. 展示所有的专题课 
            models.Course.objects.filter(degree_course__isnull=True)
            
        d. 查看id=1的学位课对应的所有模块名称 
        
        e. 获取id=1的专题课,并打印:课程名、级别(中文)、why_study、what_to_study_brief、所有recommend_courses
        
        f. 获取id=1的专题课,并打印该课程相关的所有常见问题 
        
        g. 获取id=1的专题课,并打印该课程相关的课程大纲
        
        h. 获取id=1的专题课,并打印该课程相关的所有章节 
        
        i. 获取id=1的专题课,并打印该课程相关的所有课时 
            第1章·Python 介绍、基础语法、流程控制
                01-课程介绍(一)
                01-课程介绍(一)
                01-课程介绍(一)
                01-课程介绍(一)
                01-课程介绍(一)
            第1章·Python 介绍、基础语法、流程控制
                01-课程介绍(一)
                01-课程介绍(一)
                01-课程介绍(一)
                01-课程介绍(一)
                01-课程介绍(一)
        i.  获取id=1的专题课,并打印该课程相关的所有的价格策略 
        
        
    二、基于django rest framework 写路飞的接口(作业一+rest framework 序列化)
        - 课程列表API
        - 课程详细API 
    View Code

    答案

    一、ORM查询

    a. 查看所有学位课并打印学位课名称以及授课老师
    obj = models.DegreeCourse.objects.values("name","teachers__name")
    print(obj)
    
    b. 查看所有学位课并打印学位课名称以及学位课的奖学金
    obj = models.DegreeCourse.objects.all().values("name","total_scholarship")
    print(obj)
    
    c. 展示所有的专题课 
    obj = models.Course.objects.filter(degree_course__isnull=True)
    d. 查看id=1的学位课对应的所有模块名称 
    obj = models.Course.objects.filter(id=1).values("name","degree_course")
    
    e. 获取id=1的专题课,并打印:课程名、级别(中文)、why_study、what_to_study_brief、所有recommend_courses
    obj = models.Course.objects.filter(degree_course__isnull=True,id=1).values("name","level","coursedetail__why_study",
                                                    "coursedetail__what_to_study_brief",
                                                    "coursedetail__recommend_courses")
                                                    
    f. 获取id=1的专题课,并打印该课程相关的所有常见问题 
    obj = models.Course.objects.filter(id=1)
    for i in obj:
        print(i.asked_question.all())
        
    g. 获取id=1的专题课,并打印该课程相关的课程大纲
    obj = models.CourseDetail.objects.filter(id=1).values("courseoutline__title")
    print(obj)
    
    h. 获取id=1的专题课,并打印该课程相关的所有章节
    obj = models.Course.objects.filter(id=1).values("coursechapters__name")
    print(obj)
    
    i.  获取id=1的专题课,并打印该课程相关的所有的价格策略 
    obj = models.Course.objects.filter(id=1).values("coursechapters__name","coursechapters__coursesections__name")
    print(obj)
    View Code

    二、api

    github下载地址

    https://github.com/987334176/luffycity/archive/v1.zip

  • 相关阅读:
    android头像更换(实现拍照和从手机图片里选择两种形式)
    安卓开发实战-记账本APP(六)
    安卓开发实战-记账本APP(五)
    安卓开发实战-记账本APP(四)
    安卓开发实战-记账本APP(三)
    BaseAdapter的三种表达式分析,startActivityForResult的使用
    使用Bundle在Activity之间交换数据
    深入理解JVM-类加载器深入解析(3)
    深入理解java内存模型--读书笔记
    深入理解JVM-类加载器深入解析(2)
  • 原文地址:https://www.cnblogs.com/xiao987334176/p/9434946.html
Copyright © 2020-2023  润新知