Django组件(五) Django之ContentType组件
基础使用
-contenttype组件 -django提供的一个快速连表操作的组件,可以追踪项目中所有的APP和model的对应关系,并记录在ContentType表中。 当我们的项目做数据迁移后,会有很多django自带的表,其中就有django_content_type表 ContentType组件应用: 在model中定义ForeignKey字段,并关联到ContentType表,通常这个字段命名为content_type 在model中定义PositiveIntergerField字段, 用来存储关联表中的主键,通常我们用object_id 在model中定义GenericForeignKey字段,传入上面两个字段的名字 方便反向查询可以定义GenericRelation字段 使用,在models.py中: class Course(models.Model): name = models.CharField(max_length=32) # 不会再数据库生成数据,只是用来连表操作 price_police=GenericRelation(to='PricePolicy') class PricePolicy(models.Model): period = models.IntegerField() price = models.CharField(max_length=32) # 注意不能用外键关联 # course_id = models.IntegerField(null=True) object_id = models.IntegerField(null=True) content_type = models.ForeignKey(to=ContentType,null=True) # 该字段不会在数据库生成字段,只是用来做连表操作 obj=GenericForeignKey() 在view.py中使用: 1 为django入门课,添加三个价格策略 ret = models.PricePolicy.objects.create(period=60, price='99.9', obj=course) 2 查询所有价格策略,并且显示对应的课程名称 ret=models.PricePolicy.objects.all() for i in ret: print(i.price) print(i.obj.name) #课程名称 3 通过课程id,获取课程信息和价格策略 course=models.Course.objects.get(pk=1) price_polices=course.price_police.all() for i in price_polices: print(i.price) print(i.period)
ContentType
在django中,有一个记录了项目中所有model元数据的表,就是ContentType,表中一条记录对应着一个存在的model,所以可以通过一个ContentType表的id和一个具体表中的id找到任何记录,及先通过ContenType表的id可以得到某个model,再通过model的id得到具体的对象
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
这个类主要作用是记录每个app中的model。例如,我们在自己的app中创建了如下几个model:post,event。迁移之后,我们来查看一下ContentType這个数据表中生成的数据:
如上图,生成了app与model的对应关系。那么,這个主要有什么用呢?别急,听我慢慢道来。
我们在View视图中,来这样玩玩:
def demo(request): obj = models.ContentType.objects.get(id=10) print(obj.model_class()) # <class 'app01.models.Post'> return HttpResponse('............')
看到,我通过model_class就可以获取对应的类。也就是说,今后,我们如果自己定义model如果有外键关联到這个ContentType上,我们就能找到对应的model名称。
Django-ContentType-signals
django的signal结合contenttypes可以实现好友最新动态,新鲜事,消息通知等功能。总体来说这个功能就是在用户发生某个动作的时候将其记录下来或者附加某些操作,比如通知好友。要实现这种功能可以在动作发生的代码里实现也可以通过数据库触发器等实现,但在django中,一个很简单的方法的就是使用signals。
当django保存一个object的时候会发出一系列的signals,可以通过对这些signals注册listener,从而在相应的signals发出时执行一定的代码。
使用signals来监听用户的动作有很多好处,1、不管这个动作是发生在什么页面,甚至在很多页面都可以发生这个动作,都只需要写一次代码来监听保存object这个动作就可以了。2、可以完全不修改原来的代码就可以添加监听signals的功能。3、你几乎可以在signals监听代码里写任何代码,包括做一些判断是不是第一次发生此动作还是一个修改行为等等。
想要记录下每个操作,同时还能追踪到这个操作的具体动作。
*首先用信号机制,监听信号,实现对信号的响应函数,在响应函数中记录发生的动作(记录在一张记录表,相当于下文的Event)。
*其次就是为了能追踪到操作的具体动作,必须从这张表中得到相应操作的model,这就得用到上面说的ContentType。
对于新鲜事这个功能来说就是使用GenericRelation来产生一个特殊的外键,它不像models.ForeignKey那样,必须指定一个Model来作为它指向的对象。GenericRelation可以指向任何Model对象,有点像C语言中 void* 指针。
这样关于保存用户所产生的这个动作,比如用户写了一片日志,我们就可以使用Generic relations来指向某个Model实例比如Post,而那个Post实例才真正保存着关于用户动作的完整信息,即Post实例本身就是保存动作信息最好的地方。这样我们就可以通过存取Post实例里面的字段来描述用户的那个动作了,需要什么信息就往那里面去取。而且使用Generic relations的另外一个好处就是在删除了Post实例后,相应的新鲜事实例也会自动删除。
怎么从这张操作记录表中得到相应操作的model呢,这就得用到fields.GenericForeignKey,它是一个特殊的外键,可以指向任何Model的实例,在这里就可以通过这个字段来指向类似Post这样保存着用户动作信息的Model实例。
先来看看model
from django.db import models from django.contrib.auth.models import User from django.contrib.contenttypes import fields from django.db.models import signals class Post(models.Model): author = models.ForeignKey(User) title = models.CharField(max_length=255) content = models.TextField() created = models.DateTimeField(u'发表时间', auto_now_add=True) updated = models.DateTimeField(u'最后修改时间', auto_now=True) events = fields.GenericRelation('Event') def __str__(self): return self.title def description(self): return u'%s 发表了日志《%s》' % (self.author, self.title) class Event(models.Model): user = models.ForeignKey(User) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object= fields.GenericForeignKey('content_type', 'object_id') created = models.DateTimeField(u'事件发生时间', auto_now_add=True) def __str__(self): return "%s的事件: %s" % (self.user, self.description()) def description(self): return self.content_object.description() def post_post_save(sender, instance, signal, *args, **kwargs): """ :param sender:监测的类:Post类 :param instance: 监测的类:Post类 :param signal: 信号类 :param args: :param kwargs: :return: """ post = instance event = Event(user=post.author, content_object=post) event.save() signals.post_save.connect(post_post_save, sender=Post) #signals.post_save.connect(post_post_sace,sender=Book)可以监听多个类
只要model中有object的保存操作,都将执行post_post_save函数,故可以在这个接受函数中实现通知好友等功能。
前面说到django在保存一个object的时候会发出一系列signals,在这里我们所监听的是signals.post_save这个signal,这个signal是在django保存完一个对象后发出的,django中已定义好得一些signal, 在django/db/models/signal.py中可以查看,同时也可以自定义信号。
利用connect这个函数来注册监听器, connect原型为:
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
第一个参数是要执行的函数,第二个参数是指定发送信号的Class,这里指定为Post这个Model,对其他Model所发出的signal并不会执行注册的函数。
instance这个参数,即刚刚保存完的Model对象实例。创建事件的时候看到可以将post这个instance直接赋给generic.GenericForeignKey类型的字段,从而event实例就可以通过它来获取事件的真正信息了。
最后有一点需要的注意的是,Post的Model定义里现在多了一个字段:
content_object= GenericRelation(‘Event’)
通过这个字段可以得到与某篇post相关联的所有事件,最重要的一点是如果没有这个字段,那么当删除一篇post的时候,与该post关联的事件是不会自动删除的。反之有这个字段就会进行自动的级联删除
ContentType其他案例总结
案例一、调查问卷表设计
例如:设计如下类型的调查问卷表:问卷类型包括(打分,建议,选项),先来看看一个简单的问答,
- 您最喜欢吃什么水果?
- A.苹果 B.香蕉 C.梨子 D.橘子
对于上面一个类型的问答,我们可以知道,一个问卷系统主要包括:问卷,问卷中每个题目,每个题目的答案,以及生成问卷记录。常规设计表如下
from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType class Survery(models.Model): """ 问卷 ID name by_class creator 1 第一次班级调查 三年级五班 李老师 """ name = models.CharField(verbose_name="调查问卷名称", max_length=128, unique=True) by_class = models.ForeignKey(verbose_name="问卷调查班级", to="ClassList") date = models.DateTimeField(verbose_name="问卷创建日期", auto_now_add=True) creator = models.ForeignKey(verbose_name="创建者", to="UserInfo") class SurveryItem(models.Model): """ 问卷题目 ID survery name date answer_type 1 1(代表上面创建的第一次班级调查) 您最喜欢吃什么水果? xxx-xxx-xx 1 1 1(代表上面创建的第一次班级调查) 您最喜欢什么玩具? xxx-xxx-xx 2 1 1(代表上面创建的第一次班级调查) 您最喜欢什么英雄人物? xxx-xxx-xx 3 """ survery = models.ForeignKey(verbose_name='问卷', to='Survery') name = models.CharField(verbose_name="调查问题", max_length=255) date = models.DateField(auto_now_add=True) answer_type_choices = ( (1, "打分(1~10分)"), (2, "单选"), (3, "建议"), ) answer_type = models.IntegerField(verbose_name="问题类型", choices=answer_type_choices, default=1) class SurveryChoices(models.Model): """ 问卷选项答案(针对选项类型) ID item content points 1 2 A 10分 1 2 B 9分 1 2 C 8分 1 2 D 7分 """ item = models.ForeignKey(verbose_name='问题', to='SurveryItem') content = models.CharField(verbose_name='内容', max_length=256) points = models.IntegerField(verbose_name='分值') class SurveryRecord(models.Model): """ 问卷记录 ID survery student_name survery_item score single suggestion date 1 1 1 1 10分 null null xxxxx 1 1 1 2 null A null xxxxx 1 1 1 3 null null XXXXX xxxxx """ survery = models.ForeignKey(Survery, verbose_name="问卷") student_name = models.ForeignKey(verbose_name="学员姓名", to="Student") survery_item = models.ForeignKey(verbose_name="调查项", to='SurveryItem') score = models.IntegerField(verbose_name="评分", blank=True, null=True) single = models.ForeignKey(verbose_name='单选', to='SurveryChoices', blank=True, null=True) suggestion = models.TextField(verbose_name="建议", max_length=1024, blank=True, null=True) date = models.DateTimeField(verbose_name="答题日期", auto_now_add=True)
但是,如果我有另外一个需求,也需要与SurveryRecord建立外键关系,那么此时应该怎么做呢?是再给上面的表增加一个外键,然后重新修改数据库么?显然是不能,一旦数据库被创建了,我们几乎很少再去修改数据,如果再给其添加额外字段,无疑会带来不必要的麻烦。为此,我们可以利用Django自带的ContentType类,来做这件事情。
下面来看看经过修改以后的model
from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType class Survery(models.Model): """ 问卷 ID name by_class creator 1 第一次班级调查 三年级五班 李老师 """ name = models.CharField(verbose_name="调查问卷名称", max_length=128, unique=True) by_class = models.ForeignKey(verbose_name="问卷调查班级", to="ClassList") date = models.DateTimeField(verbose_name="问卷创建日期", auto_now_add=True) creator = models.ForeignKey(verbose_name="创建者", to="UserInfo") class SurveryItem(models.Model): """ 问卷题目 ID survery name date answer_type 1 1(代表上面创建的第一次班级调查) 您最喜欢吃什么水果? xxx-xxx-xx 1 1 1(代表上面创建的第一次班级调查) 您最喜欢什么玩具? xxx-xxx-xx 2 1 1(代表上面创建的第一次班级调查) 您最喜欢什么英雄人物? xxx-xxx-xx 3 """ survery = models.ForeignKey(verbose_name='问卷', to='Survery') name = models.CharField(verbose_name="调查问题", max_length=255) date = models.DateField(auto_now_add=True) answer_type_choices = ( (1, "打分(1~10分)"), (2, "单选"), (3, "建议"), ) answer_type = models.IntegerField(verbose_name="问题类型", choices=answer_type_choices, default=1) class SurveryChoices(models.Model): """ 问卷选项答案(针对选项类型) ID item content points 1 2 A 10分 1 2 B 9分 1 2 C 8分 1 2 D 7分 """ item = models.ForeignKey(verbose_name='问题', to='SurveryItem') content = models.CharField(verbose_name='内容', max_length=256) points = models.IntegerField(verbose_name='分值') surveryrecord = GenericRelation("SurveryRecord") class Score(models.Model): item = models.ForeignKey(verbose_name='问题', to='SurveryItem') points = models.IntegerField(verbose_name='分值') surveryrecord = GenericRelation("SurveryRecord") class Suggestion(models.Model): item = models.ForeignKey(verbose_name='问题', to='SurveryItem') suggests = content = models.CharField(verbose_name='内容', max_length=256) surveryrecord = GenericRelation("SurveryRecord") class SurveryRecord(models.Model): """ 问卷记录 ID survery student_name survery_item content_type object_id 1 1 1 1 1 1 1 1 1 2 1 2 1 1 1 3 1 3 """ survery = models.ForeignKey(Survery, verbose_name="问卷") student_name = models.ForeignKey(verbose_name="学员姓名", to="Student") survery_item = models.ForeignKey(verbose_name="调查项", to='SurveryItem') content_type = models.ForeignKey(ContentType, blank=True, null=True) object_id = models.PositiveIntegerField(blank=True, null=True) content_object = GenericForeignKey('content_type', 'object_id') # 這个字段不会再数据库中存在,只是在查询时有用 date = models.DateTimeField(verbose_name="答题日期", auto_now_add=True)
案例二、优惠券系统设计
应用场景:某一在线教育网,需要为每位积极客户发一些观看视频的优惠券,但是,对于不同类型的视频,优惠券是不同。比如:有一个普通课程,需要发一些满200减30的优惠券,而又有精品课程,需要发满100键70的优惠券。根据以上需求,我们很快就知道,需要三张表,学位课程表,课程表以及优惠券表,那么,这三张表又是如何关联的呢?
我们先来看看下面這个
#A 学位课程表结构 # ID 名称 # 1 学位课1 # 2 学位课2 #B普通课程表 #ID 名称 #1 普通课1 #2 普通课2 #优惠券表 #ID 优惠券名称 A(FK) B(FK) #1 通用优惠券 null null # 两个都为空,说明全场都可以使用 #2 满100-10 1 null # 给学位课程创建优惠券 #3 满200-30 null 1 # 给普通课程创建优惠券
还是与上面一样,如果再来一种课程,上面的优惠券表还需要额外新增一列,为了解决這个问题,我们可以使用ContentType类来实现上述需求
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 DegreeCourse(models.Model): """学位课程 ID 名称 1 学位课1 2 学位课2 """ name = models.CharField(max_length=128, unique=True) x1 = GenericRelation("Coupon") class Course(models.Model): """课程 ID 名称 1 普通课1 2 普通课2 """ name = models.CharField(max_length=128, unique=True) class Coupon(models.Model): """优惠券生成规则 ID 优惠券名称 A FK B.FK c.FK 1 通用 null null 2 满100-10 8 1 3 满200-30 8 2 4 满200-30 9 1 ID 优惠券名称 content_type_id(表) object_id(表中数据ID) 1 通用 null null 2 满100-10 8 1 3 满200-30 8 2 4 满200-30 9 1 总结: """ name = models.CharField(max_length=64, verbose_name="活动名称") brief = models.TextField(blank=True, null=True, verbose_name="优惠券介绍") # 那个表? content_type = models.ForeignKey(ContentType, blank=True, null=True) # 对象ID object_id = models.PositiveIntegerField("绑定课程", blank=True, null=True, help_text="可以把优惠券跟课程绑定") content_object = GenericForeignKey('content_type', 'object_id') # # # # # class Homework(models.Model): # """ # ID User Name score # 1 吴昊 第一模块 30 # 2 吴昊 第二模块 80 # 3 吴昊 第三模块 100 # # """ # name = models.CharField(max_length=32) # # score_choices = ( # (100,'A'), # (80,'B'), # (60,'C'), # (30,'D'), # ) # score = models.IntegerField(choices=score_choices) # # user = models.ForeignKey('User') # # # class Record(models.Model): # """ # ID User Name score # 1 吴昊 第一模块 10 5 # 2 吴昊 第二模块 8 10 # """ # name = models.CharField(max_length=32) # score_choices = ( # (100, 'A'), # (80, 'B') # ) # score = models.IntegerField(choices=score_choices) # # class ScoreRecord(models.Model): # """ # ID Name 表 对象 # 1 作业太差 1 # 2 作业太好 1 # 5 看的太快 null 1 # """ # name = models.CharField(max_length=32) # content_type = models.ForeignKey(ContentType, blank=True, null=True) # # 对象ID # object_id = models.PositiveIntegerField("绑定课程", blank=True, null=True, help_text="可以把优惠券跟课程绑定") # #
根据ContentType字段查询关联字段操作
from django.shortcuts import render,HttpResponse from django.contrib.contenttypes.models import ContentType from . import models def test(request): # models.UserInfo.objects.filter() # content = ContentType.objects.get(app_label='app01',model='userinfo') # model_class = content.model_class() # print(model_class.objects.all()) # 给学位课1或普通课创建优惠券 d1 = models.DegreeCourse.objects.get(id=1) models.Coupon.objects.create(name='优惠券', brief='2000-30', content_object=d1) # d1 = models.Course.objects.get(id=1) # models.Coupon.objects.create(name='优惠券', brief='100-90', content_object=d1) # 当前优惠券,绑定的课程? obj = models.Coupon.objects.get(id=1) # print(obj.content_object) # 当前课程,都有哪些优惠券? # obj = models.DegreeCourse.objects.get(id=1) # print(obj.x1.all()) # v = models.DegreeCourse.objects.values('name','x1__brief') # print(v) return HttpResponse('...')
总之,如果一个表与其他表有多个外键关系,我们可以通过ContentType来解决这种关联
源码分析
ContentType模型
class ContentType,来看下它的源码, @python_2_unicode_compatible 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 @property def name(self): model = self.model_class() if not model: return self.model return force_text(model._meta.verbose_name) def model_class(self): "Returns the Python model class for this type of content." try: return apps.get_model(self.app_label, self.model) except LookupError: return None def get_object_for_this_type(self, **kwargs): """ Returns an object of this type for the keyword arguments given. Basically, this is a proxy around this object_type's get_object() model method. The ObjectNotExist exception, if thrown, will not be caught, so code that calls this method should catch it. """ return self.model_class()._base_manager.using(self._state.db).get(**kwargs) def get_all_objects_for_this_type(self, **kwargs): """ Returns all objects of this type for the keyword arguments given. """ return self.model_class()._base_manager.using(self._state.db).filter(**kwargs) def natural_key(self): return (self.app_label, self.model)
可以看到ContentType就是一个简单的django model,而且它在数据库中表的名字为django_content_type。django_content_type记录了当前Django项目中所有model所属的app(即app_label属性)以及model的名字(即model属性)。contenttypes是对model的一次封装,因此,可以通过contenttypes动态地访问model类型,而不需要每次import具体的model类型。
每个ContentType实例有两个字段,共同地来唯一描述一个已经安装的模型。
app_label:模型所在的应用的名称。这取自模型的app_label属性,并且只包括应用的Python导入路径的最后的部分。例如,"django.contrib.contenttypes"的app_label是"contenttypes"。
model:模型的类的名称。
此外,下面的属性是可用的:
name:ContentType的人类可读的名称。它取自模型的verbose_name属性
Model Meta options: Options.verbose_name: A human-readable name for the object. If this isn't given, Django will use a munged version of the class name: CamelCase becomes camel case.
让我们来看一个例子,看看它是如何工作的。如果你已经安装了contenttypes应用,那么添加sites应用到你的INSTALLED_APPS设置中,并运行manage.py migrate来安装它,模型django.contrib.sites.models.Site将安装到你的数据库中。同时,一个ContentType的新实例将会被创建,并具有下面的值:
a. app_label将设置为'sites'(Python路径"django.contrib.sites"的最后部分);
b. model将设置为'site'。
像下面这样,
mysql> select * from django_content_type;
+----+--------------+----------------+
| id | app_label | model |
+----+--------------+----------------+
| 1 | auth | group |
| 2 | auth | permission |
| 16 | blog | blog |
| 19 | blog | blogcomment |
| 21 | blog | category |
| 18 | blog | tag |
| 20 | blog | theme |
| 17 | blog | topic |
| 22 | box | box |
ContentType实例的方法
每一个ContentType实例都有一些方法,允许你得到ContentType实例所代表的模型,或者在模型中查询对象。
ContentType.get_object_for_this_type(**kwargs)
接收一系列有效的查询参数,在ContentType所代表的模型中做一次get()查询操作,返回相应的对象。
ContentType.model_class()
返回此ContentType实例所表示的模型类。
例如,我们可以查找User模型的ContentType:
>>from django.contrib.contenttypes.models import ContentType >>ContentType.objects.get(app_label="auth",model="user") <ContentType:user>
然后,使用它来查询一个特定的User,或者访问User模型类
>>> user_type.model_class() <class 'django.contrib.auth.models.User'> >>> user_type.get_object_for_this_type(username='Guido') <User: Guido>
get_object_for_this_type()和model_class()一起使用,可以实现两个及其重要的功能。
a.使用这些方法,你可以编写高级别的通用代码:在任何已安装的模型执行查询---而不是导入和使用单一特定模型的类。你可以在运行时传入app_label和model到ContentType进行查找,然后使用这个模型类或从它获取对象。
b.你可以关联另一个模型到ContentType,作为一种绑定它的实例到特定模型类的方式,然后使用这些方法来获取对那些模型类的访问。
几个Django捆绑的应用利用了后者的技术。比如,Django认证框架(auth)中的权限系统使用的Permission模型具有一个外键到ContentType。这允许Permission表示"可以添加博客条目"或"可以删除新闻故事"的概念。
可以看下Permission的model源码,
@python_2_unicode_compatible class Permission(models.Model): """ The permissions system provides a way to assign permissions to specific users and groups of users. The permission system is used by the Django admin site, but may also be useful in your own code. The Django admin site uses permissions as follows: - The "add" permission limits the user's ability to view the "add" form and add an object. - The "change" permission limits a user's ability to view the change list, view the "change" form and change an object. - The "delete" permission limits the ability to delete an object. Permissions are set globally per type of object, not per specific object instance. It is possible to say "Mary may change news stories," but it's not currently possible to say "Mary may change news stories, but only the ones she created herself" or "Mary may only change news stories that have a certain status or publication date." Three basic permissions -- add, change and delete -- are automatically created for each Django model. """ name = models.CharField(_('name'), max_length=255) content_type = models.ForeignKey( ContentType, models.CASCADE, verbose_name=_('content type'), ) codename = models.CharField(_('codename'), max_length=100) objects = PermissionManager() class Meta: verbose_name = _('permission') verbose_name_plural = _('permissions') unique_together = (('content_type', 'codename'),) ordering = ('content_type__app_label', 'content_type__model', 'codename') def __str__(self): return "%s | %s | %s" % ( six.text_type(self.content_type.app_label), six.text_type(self.content_type), six.text_type(self.name)) def natural_key(self): return (self.codename,) + self.content_type.natural_key() natural_key.dependencies = ['contenttypes.contenttype']
可以看到Permission模型中设置了一个对ContentType的外键,这意味着每一个Permission的实例都有一个ContentType的id作为外键,而ContentType的id恰恰代表着一个Model。
回想Permission模型在初始化的时候发生了什么,它为每个模型设置了三个权限,分别是add,change以及delete,那么它是如何跟每个模型联系起来的呢?就是通过一个到ContentType的外键。如此,Permission模型借助ContentType表达了对一个model的权限操作。
mysql> select * from auth_permission;
+-----+-------------------------------------+-----------------+-----------------------+
| id | name | content_type_id | codename |
+-----+-------------------------------------+-----------------+-----------------------+
| 1 | Can add group | 1 | add_group |
| 2 | Can change group | 1 | change_group |
| 3 | Can delete group | 1 | delete_group |
| 4 | Can add permission | 2 | add_permission |
| 5 | Can change permission | 2 | change_permission |
| 6 | Can delete permission | 2 | delete_permission |
| 7 | Can view group | 1 | view_group |
| 8 | Can view permission | 2 | view_permission |
| 9 | Can add content type | 3 | add_contenttype |
| 10 | Can change content type | 3 | change_contenttype |
| 11 | Can delete content type | 3 | delete_contenttype |
| 12 | Can view content type | 3 | view_contenttype |
| 13 | Can add site | 4 | add_site |
| 14 | Can change site | 4 | change_site |
| 15 | Can delete site | 4 | delete_site
...
ContentTypeManager
class ContentTypeManager,源代码如下,
class ContentTypeManager(models.Manager): use_in_migrations = True def __init__(self, *args, **kwargs): super(ContentTypeManager, self).__init__(*args, **kwargs) # Cache shared by all the get_for_* methods to speed up # ContentType retrieval. self._cache = {} def get_by_natural_key(self, app_label, model): try: ct = self._cache[self.db][(app_label, model)] except KeyError: ct = self.get(app_label=app_label, model=model) self._add_to_cache(self.db, ct) return ct def _get_opts(self, model, for_concrete_model): if for_concrete_model: model = model._meta.concrete_model return model._meta def _get_from_cache(self, opts): key = (opts.app_label, opts.model_name) return self._cache[self.db][key] def get_for_model(self, model, for_concrete_model=True): """ Returns the ContentType object for a given model, creating the ContentType if necessary. Lookups are cached so that subsequent lookups for the same model don't hit the database. """ opts = self._get_opts(model, for_concrete_model) try: return self._get_from_cache(opts) except KeyError: pass # The ContentType entry was not found in the cache, therefore we # proceed to load or create it. try: # Start with get() and not get_or_create() in order to use # the db_for_read (see #20401). ct = self.get(app_label=opts.app_label, model=opts.model_name) except self.model.DoesNotExist: # Not found in the database; we proceed to create it. This time # use get_or_create to take care of any race conditions. ct, created = self.get_or_create( app_label=opts.app_label, model=opts.model_name, ) self._add_to_cache(self.db, ct) return ct def get_for_models(self, *models, **kwargs): """ Given *models, returns a dictionary mapping {model: content_type}. """ for_concrete_models = kwargs.pop('for_concrete_models', True) results = {} # Models that aren't already in the cache. needed_app_labels = set() needed_models = set() # Mapping of opts to the list of models requiring it. needed_opts = defaultdict(list) for model in models: opts = self._get_opts(model, for_concrete_models) try: ct = self._get_from_cache(opts) except KeyError: needed_app_labels.add(opts.app_label) needed_models.add(opts.model_name) needed_opts[opts].append(model) else: results[model] = ct if needed_opts: # Lookup required content types from the DB. cts = self.filter( app_label__in=needed_app_labels, model__in=needed_models ) for ct in cts: model = ct.model_class() opts_models = needed_opts.pop(ct.model_class()._meta, []) for model in opts_models: results[model] = ct self._add_to_cache(self.db, ct) # Create content types that weren't in the cache or DB. for opts, opts_models in needed_opts.items(): ct = self.create( app_label=opts.app_label, model=opts.model_name, ) self._add_to_cache(self.db, ct) for model in opts_models: results[model] = ct return results def get_for_id(self, id): """ Lookup a ContentType by ID. Uses the same shared cache as get_for_model (though ContentTypes are obviously not created on-the-fly by get_by_id). """ try: ct = self._cache[self.db][id] except KeyError: # This could raise a DoesNotExist; that's correct behavior and will # make sure that only correct ctypes get stored in the cache dict. ct = self.get(pk=id) self._add_to_cache(self.db, ct) return ct def clear_cache(self): """ Clear out the content-type cache. """ self._cache.clear() def _add_to_cache(self, using, ct): """Insert a ContentType into the cache.""" # Note it's possible for ContentType objects to be stale; model_class() will return None. # Hence, there is no reliance on model._meta.app_label here, just using the model fields instead. key = (ct.app_label, ct.model) self._cache.setdefault(using, {})[key] = ct self._cache.setdefault(using, {})[ct.id] = ct
ContentType还有一个自定义的管理器ContentTypeManager,它增加了下列方法:
clear_cache()
清除那些ContentType使用的用于跟踪模型(已为其创建ContentType实例)的内部缓存。你可能不需要自己调用此方法,Django将在它需要的时候自动调用。
get_for_id(id)
通过id查找ContentType。由于此方法使用与get_for_model()相同的共享缓存,建议使用这个方法而不是通常的ContentType.objects.get(pk=id)。
get_for_model(model,for_concrete_model=True)
接收一个模型类或模型类的实例,返回表示该模型的ContentType实例。for_concrete_model=False允许获取代理模型的ContentType。
get_for_models(*models,for_concrete_models=True)
接收可变数目的模型类,返回一个字典:映射模型类到表示该模型的ContentType实例。for_concrete_model=False允许获取代理模型的ContentType。
get_by_natural_key(app_label,model)
返回由给定的应用标签和模型名称唯一标示的ContentType实例。这种方法的主要目的是为允许在反序列化期间通过自然键来引用ContentType实例对象。
get_for_model()方法特别有用,当你知道你需要与ContentType交互,但不想要执行手动查询去获取模型元数据带来的麻烦。
>>>from django.contrib.auth.models import User >>>ContentType.object.get_for_model(User) <ContentType: user>
Generic relations
在你自己的模型中添加一个外键到ContentType,这将允许你的模型更有效地绑定自身到其他的模型类,就像上述的Permisssion model一样。但是非常有可能进一步,利用ContentType来实现真正的模型之间的generic relationships(有时称作"polymorphic")。
看一个例子,假设我们要写一个Blog,要定义的模型会有Post和URL,如下,
from django.db import models class Post(models.Model): title = models.CharField(max_lenght=100) pub_date = models.DateTimeField(auto_now_add=True) content = models.TextField() class Url(models.Model): title = models.CharField(max_length=100) pub_date = models.DateTimeField(auto_now_add=True) url = models.URLField(blank=True, verify_exists=True)
这个时候,我想再写一个Comment的Model,因为不管是Post或者URL都可以允许被评论。如果之前没有接触过Generic Relations,有可能会写两个模型,一个Post_comments和一个URL_comments,或者是在Comment的model里面加入两组Foreign Key。
class Comment(models.Model): title = models.CharField(max_length=100) post = models.ForeignKey(Post, blank=True, null=True) url = models.ForeignKey(Url, blank=True, null=True)
估计没有人想用这样的模型。
好,引入正题,Generic Relation,我们希望创建一个Comment的模型适用于所有内容类型,不管是Post还是Url。Generic Relation能够帮助我们实现这样的模型
from django.db import models from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType class Comment(models.Model): content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type','object_id') content = models.CharField(max_length=1000)
在Django中,有一个记录了项目中所有model元数据的表,就是ContentType。表中一条记录,对应着一个存在的model,那么我们只要通过一个元数据表的id和一个具体数据表中的id,便可以找到任何model中的任何记录。
Comment中使用GenericForeignKey()来指向其它的Model实例。为了使用它,还需要在Model中定义content_type和object_id才可以。其中,content_type来自ContentType这个Model,记录Comment所指向的其他Model的名字。object_id则是表示所指向的Model实例的id。想一想,在django中,如何定位一条记录?一般要三个值:appname,modelname和object_id。在ContentType中保存了appname和modelname,因为GenericForeignKey只要两个值就可以了content_type和object_id。
实际上根据上面的解释,只要有content_type和object_id两个字段就够了,不过我们总是需要亲自指定这两个字段的值。而GenericForeignKey就是要把这个过程给自动化了,只要给content_object赋一个对象,就会自动的根据这个对象的元数据,给content_type和object_id赋值了。
GenericForeignKey的构造函数接受两个可选参数:
@python_2_unicode_compatible class GenericForeignKey(object): """ Provide a generic many-to-one relation through the ``content_type`` and ``object_id`` fields. This class also doubles as an accessor to the related object (similar to ForwardManyToOneDescriptor) by adding itself as a model attribute. """ # Field flags auto_created = False concrete = False editable = False hidden = False is_relation = True many_to_many = False many_to_one = True one_to_many = False one_to_one = False related_model = None remote_field = None def __init__(self, ct_field='content_type', fk_field='object_id', for_concrete_model=True): self.ct_field = ct_field self.fk_field = fk_field self.for_concrete_model = for_concrete_model self.editable = False self.rel = None self.column = None
你可以在构造GenericForeignKey时,指定另外的字段名称。
a = Post(title='post1') a.save() b = Url(title='url1') b.save() c = Comment(content_object=a, content='test1') c.save() c.content_object d = Comment(content_object=b, content='test2') d.save() d.content_object Comment.objects.all() #Output [<Comment: test1>, <Comment: test2>]
由于GenericForeignKey()不是普通的外键,如果我们想要查找一个Post的所有评论,没法用下面的这样方式。
# This will fail Comment.objects.filter(content_object=a) # This will also fail Comment.objects.get(content_object=a)
需要绕一步,略微复杂,
a_type = ContentType.objects.get_for_model(a) Comment.objects.filter(content_type__pk=a_type.id, object_id=a.id)
其实是有办法让这个很正常的查询变得简单一些,Django 提供了 Reverse generic relations 的机制。重新改一下Post这个Model,
class Post(models.Model): title = models.CharField(max_length=100) pub_date = model.DateTimeField(auto_now_add=True) content = models.TextField() comments = generic.GenericRelation(Comment)
这样我们就给Post这个Model添加了一个"逆向"的 generic relationship。每个Post的实例都有了个 comments的属性,用于检索与之有关的comments
a = Post(title='test2') a.save() c1 = Comment(content_object=a, content='comment1') c1.save() c2= Comment(content_object=a, content='comment2') c2.save() a.comments.all() #outputs [<Comment: comment1>, <Comment: comment2>]
这里有一段总结,
A generic relationship is defined by two elements:a foreign key to the ContentType table, to determine the type of the related object,and an ID field,to identify the specific object to link to. Django uses these two elements to provide a content_object pseudo-field which,to the user, works similary to a real ForeignKey field.And,again just like a FroeignKey,Django can helpfully provide a reverse relationship from the linked model back to the generic one,although you do need to explicitly defins this using generic.GenericRelation to make Django aware of it.
一个简单的例子就是标签系统,它可能看起来像是这样:
from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType class TaggedItem(models.Model): tag = models.SlugField() content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField(0 content_object = GenericForeignKey('content_type', 'object_id') def __str__(self): #__unicode__ on Python 2 return self.tag
一个普通的ForeignKey只能指向一个其他的模型,这是说如果TaggedItem模型使用了ForeignKey,它不得不选择且只能一个模型来为其存储标签。contenttypes应用提供一个特殊的字段类型GenericForeignKey,避免了这个问题并且允许你和任何一个model建立关联关系。
class GenericForeignKey
建立GenericForeignKey有三部分:
1.给你的model设置一个ForeignKey字段到ContentType。一般命名为"content_type"。
2.给你的model设置一个字段,用来储存你想要关联的model主键值。对于大多数model,这是一个PositiveIntegerField字段。并且通常命名为"object_id"。
3.给你的model一个GenericForeignKey字段,把上面提到的那两个字段的名称传给它。如果这两个字段名字分别为"content_type"和"object_id",你就可以省略它们---GenericForeignKey默认的会自动去查找这两个字段名称。
for_concrete_model:如果为False,这个字段可以引用代理模型。默认是True。
提供一个类似常用ForeignKey的API,每一个TaggedItem都将有一个content_object字段,返回它相关联的对象,你也可以指定这个字段或者用它来创建一个TaggedItem。
from django.contrib.auth.models import User guido = User.objects.get(username='Guido') t = TaggedItem(content_object=guido, tag='bdfl') t.save() t.content_object #Output <User:Guido>
如果相关联的对象被删除了,那么content_type和object_id字段会保持原值,GenericForeignKey会返回None:
>>> guido.delete() >>> t.content_object # returns None
由于GenericForeignKey的实现方式,没有办法用这个字段直接执行数据库API(比如filter()或者exclude()等等)。因为GenericForeignKey毕竟不是一个普通的字段,下面的例子将不会工作:
# This will fail >>> TaggedItem.objects.filter(content_object=guido) # This will also fail >>> TaggedItem.objects.get(content_object=guido)
同样的,GenericForeignKey是不会出现在ModelForms里的。
Reverse Generic relations反向通用关系
class GenericRelation
related_query_name
默认情况下,从相关联对象返回到对象的关系是不存在的,设置related_query_name来创建一个关系:从相关联对象返回到对象自身。这允许从相关联的对象进行查询和筛选。
如果你知道最经常使用哪个模型,你可以添加一个"反向"的通用关系,以激活一个附加的API。例如,
from django.db import models from django.contrib.contenttypes.fields import GenericRelation class Bookmark(models.Model): url = models.URLField() tags = GenericRelation(TaggedItem)
Bookmark的每个实例都会有一个tags属性,可以用来获取相关的TaggedItems。
b = Bookmark(url='https://www.djangoproject.com/') b.save() t1 = TaggedItem(content_object=b, tag='django') ta.save() t2 = TaggedItem(content_object=b, tag='python') t2.save() b.tags.all() <QuerySet [<TaggedItem: django>, <TaggedItem: python>]>
定义一个设置了related_query_name的GenericRelation,允许你从相关联的对象进行查询。
tags = GenericRelation(TaggedItem, related_query_name='bookmarks')
这将允许你从TaggedItem执行过滤筛选,排序和其它的查询,来操作Bookmark:
>>> # Get all tags belonging to bookmarks containing `django` in the url >>> TaggedItem.objects.filter(bookmarks__url__contains='django') <QuerySet [<TaggedItem: django>, <TaggedItem: python>]>
当然,如果你没有添加一个反向的关系,你可以手动做相同类型的查找:
>>> b = Bookmark.objects.get(url='https://www.djangoproject.com/') >>> bookmark_type = ContentType.objects.get_for_model(b) >>> TaggedItem.objects.filter(content_type__pk=bookmark_type.id, object_id=b.id) <QuerySet [<TaggedItem: django>, <TaggedItem: python>]>
就像GenericForeignKey接受content-type和object-ID字段做为参数,GenericRelation也是一样的。如果一个model的generic foreignkey字段使用的不是默认的命名,当你创建一个GenericRelation时,一定要显示的传递这个字段的命名给它。例如,如果TaggedItem模型使用字段名为content-type-fk和object_primary_key 来创建一个generic foreign key关联到上述模型,那么返回自身的GenericRelation需要像下面来定义,
tags = GenericRelation( TaggedItem, content_type_field='content_type_fk', object_id_field='object_primary_key', )
注意,如果删除一个包含GenericRelation的对象,任何一个通过GenericForeignKey指向它的对象同样会被删除。在上面的例子中,这意味着,如果Bookmark对象被删除了,任何指向它的TaggedItem对象在同一时间会被删除。
不像ForeignKey,GenericForeignKey不接受on_delete参数去定制化这个行为。如果渴望,你可以不使用GenericRelation来避免级联删除,并且这种行为也可以通过信号pre_delete来提供。
Generic relations and aggregation通用关系和聚合
Django数据库聚合API可以与GenericRelation配合使用。例如,您可以找出所有的书签有多少标签tags:
Bookmark.objects.aggregate(Count('tags')) {'tags__count': 3}
Generic relation in forms
django.contrib.contenttypes.forms模块提供
a.BaseGenericInlineFormSet
b.表单集工厂,generic_inlineformset_factory(),和GenericForeignKey一起使用。
class BaseGenericInlineFormSet,源码如下,
class BaseGenericInlineFormSet(BaseModelFormSet): """ A formset for generic inline objects to a parent. """ def __init__(self, data=None, files=None, instance=None, save_as_new=None, prefix=None, queryset=None, **kwargs): opts = self.model._meta self.instance = instance self.rel_name = '-'.join(( opts.app_label, opts.model_name, self.ct_field.name, self.ct_fk_field.name, )) if self.instance is None or self.instance.pk is None: qs = self.model._default_manager.none() else: if queryset is None: queryset = self.model._default_manager qs = queryset.filter(**{ self.ct_field.name: ContentType.objects.get_for_model( self.instance, for_concrete_model=self.for_concrete_model), self.ct_fk_field.name: self.instance.pk, }) super(BaseGenericInlineFormSet, self).__init__( queryset=qs, data=data, files=files, prefix=prefix, **kwargs ) @classmethod def get_default_prefix(cls): opts = cls.model._meta return '-'.join((opts.app_label, opts.model_name, cls.ct_field.name, cls.ct_fk_field.name)) def save_new(self, form, commit=True): setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk) setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk) return form.save(commit=commit)
def generic_inlineformset_factory,源码如下,
def generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True, min_num=None, validate_min=False): """ Returns a ``GenericInlineFormSet`` for the given kwargs. You must provide ``ct_field`` and ``fk_field`` if they are different from the defaults ``content_type`` and ``object_id`` respectively. """
使用modelformset_factory()返回一个GenericInlineFormSet
如果和默认的content_type和object_id不同,则必须提供ct_field和fk_field。其他参数与modelformset_factory()和inlineformset_factory()中记类似。
Generic relations in admin
django.contrib.contenttypes.admin模块提供了GenericTabularInline和GenericStackedInline(GenericInlineModelAdmin的子类)。
这些类和函数确保了generic relations在表单forms和admin中可以使用。
class GenericInlineModelAdmin
GenericInlineModelAdmin类继承了InlineModelAdmin类的所有属性。然而,它添加了一组自己的属性,为了和generic relation交互。
ct_field:模型中ContentType外键字段的名字。默认是content_type。
ct_fk_field:代表了相关联对象的ID的整数字段的名字。默认是object_id。
class GenericTabularInline & class GenericStackedInline:GenericInlineModelAdmin的子类,分别提供stacked和tabular布局。
参考
1.https://docs.djangoproject.com/en/1.10/ref/contrib/contenttypes/
2.http://python.usyiyi.cn/translate/django_182/ref/contrib/contenttypes.html
3.http://blog.chedushi.com/archives/6048
4.Aovid Django's GenericForeignKey
Django组件(五) Django之ContentType组件的更多相关文章
- Django框架(五) Django之模板语法
什么是模板 只要是在html里面有模板语法就不是html文件了,这样的文件就叫做模板 模板语法分类 模板语法之变量:语法为 {{ 变量名 }}: 在 Django 模板中遍历复杂数据结构的关键是句点字 ...
- vue创建路由,axios前后台交互,element-ui配置使用,django contentType组件
vue中创建路由 每一个vue组件都有三部分组成 template:放html代码 script:放js相关 style:放css相关 vue中创建路由 1.先创建组件 Course.vue 2.ro ...
- Django之ContentType组件
一.理想表结构设计 1.初始构建 1. 场景刚过去的双12,很多电商平台都会对他们的商品进行打折促销活动的,那么我们如果要实现这样的一个场景,改如何设计我们的表? 2. 初始表设计 注释很重要,看看吧 ...
- Django ContentType组件
ContentType组件 引入 现在我们有这样一个需求~我们的商城里有很多的商品~~节日要来了~我们要搞活动~~ 那么我们就要设计优惠券~~优惠券都有什么类型呢~~满减的~折扣的~立减的~~ 我们对 ...
- python 全栈开发,Day98(路飞学城背景,django ContentType组件,表结构讲解)
昨日内容回顾 1. 为什么要做前后端分离? - 前后端交给不同的人来编写,职责划分明确. - API (IOS,安卓,PC,微信小程序...) - vue.js等框架编写前端时,会比之前写jQuery ...
- ContentType组件,Django缓存机制,跨域请求
ContentType组件 解决什么问题:表的id和数据id,来唯一确定一条数据 用: 插入数据: models:content_obj = GenericForeignKey('table_id', ...
- contenttype组件、Django缓存机制以及跨域请求
1 昨日回顾 版本控制 *** (1)url=127.0.0.1/course/?version=v100000 1 versioning_class=QueryParameterVersioning ...
- Django day31 contentType组件,Django的缓存
一.contentType组件 1.干什么用的? 是一个django内置的一个组件,方便我们快速的连表操作 2.这两个字段都不会在数据库中生成,他只是用来查询,插入的 -在course表中: poli ...
- 【Django】ContentType组件
目录 理解 表结构 使用 @ 好,现在我们有这样一个需求,我们的商城里有很多的商品,然而节日要来了,我们要搞活动. 那么,我们就要设计优惠券,优惠券都有什么类型呢?满减的.折扣的.立减的.等等等... ...
随机推荐
- django admin 处理文本换行空格
使用 linebreaks filter <p>{{ blogpassage.content|linebreaksbr }}<p>
- Day6 模块及Python常用模块
模块概述 定义:模块,用一砣代码实现了某类功能的代码集合. 为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,提供了代码的重用性.在Python中,一个.py文件就称之为一个模块(Mod ...
- sublime text3 增加代码片段(snipper)
有时候编写代码时有些代码片段经常会用到,如果能将这些代码片段整理,再在需要的时候通过某些条件触发,那无疑是非常方便的! 创建方法:工具->插件开发->新建代码片段 默认产生的内容是: &l ...
- .net web site 和 web application 的区别
web application 会把所有的代码编译打包成单一的库文件(.dll). web site 不会对整个的代码进行编译,在运行时须要哪一段代码就编译哪段代码.这导致web site 上线后,如 ...
- 用python进行wifi密码生成
随着无线网络的不断发展,几乎所有场合都会覆盖WIFI信号,无论是公共地点还是家庭之中.众所周知,目前WIFI普遍的认证方式为wpa2,这种认证方式安全性相当不错,但由于人们设置密码时的随意性和固有思维 ...
- Log4j基础知识
Log4J是Apache的一个开放源代码的项目.通过使用Log4J,程序员可以控制日志信息输送的目的地,包括控制台,文件,GUI组件和NT事件记录器,也可以控制每一条日志的输出格式,或通过定义每一条日 ...
- 输入一串字符,检查是否可以组成friend
"""输入一串字符,检查是否可以组成friend""" from collections import Counter def foo(nu ...
- 35 个最好用的 Vue 开源库
35 个最好用的 Vue 开源库 Vue.js 是一个非常易用的渐进式 JavaScript 框架,用于构建用户界面. 1.Vue Dark Mode Vue.js 的一个极简主义的深色设计系统.它提 ...
- django -- 修改admin 密码问题
1.python manage.py shell 2.from django.contrib.auth.models import User 3.user=User.objects.get(usern ...
- node.js读取到的文件列表
var fs = require('fs'); var path = require('path'); //解析需要遍历的文件夹,我这以E盘根目录为例 var filePath = path.reso ...