将具有外键的抽象模型定义为另一个抽象模型
Defining an Abstract model with a ForeignKey to another Abstract model
我正在尝试构建两个名为 SurveyQuestionBase
和 SurveyResponseBase
的抽象 类,它们将用作模板以快速定义新的具体模型,以便在我们的网站上实施特定调查。我遇到的问题是强制执行 SurveyResponseBase
模型,当具体化时,应该将 ForeignKey
定义为 SurveyQuestionBase
.
的具体模型
Django 不允许我们定义 ForeignKeys
来抽象 类 所以我不能,例如,这样做:
question = models.ForeignKey(SurveyQuestionBase)
出于类似的原因,我也不能将其设为 None
或 app_label.ModelName
。
一个 hacky 修复是创建一个新的具体模型 SurveyQuestionConcrete
并使 ForeignKey
指向此:question = models.ForeignKey(concrete_model)
,结合验证以确保替换此模型。
有没有更简洁的方法来实现同样的事情? 我需要做的就是确保当有人从 SurveyResponseBase
定义具体模型时,他们将 ForeignKey
包含到从 SurveyQuestionBase
[ 定义的具体模型中=26=]
完整代码如下:
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
# Implementation borrows from: https://github.com/jessykate/django-survey/
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
ANSWER_TYPE_CHOICES = (
(INTEGER, 'Integer',),
(TEXT, 'Text',),
(RADIO, 'Radio',),
(SELECT, 'Select',),
(MULTI_SELECT, 'Multi-Select',),
)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=ANSWER_TYPE_CHOICES, max_length=20)
class Meta:
abstract = True
class SurveyResponseBase(models.Model):
"""
concrete_question_model: 'app_label.Model' - Define the concrete model this question belongs to
"""
concrete_model = 'SurveyQuestionBase'
question = models.ForeignKey(concrete_model)
response = models.TextField()
class Meta:
abstract = True
我相信你不能那样做,因为 ForeignKey 不知道要指向哪个实际模型。
您可能正在寻找 GenericForeignKey (https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations)。它允许您正确定义该关系。
这个问题的两个解决方案(都有效):
第一个解决方案涉及使用 GenericForeignKey
。第二个更有趣,涉及动态生成 SurveyResponseBase
。
Solution 1: Using GenericForeignKey
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
ANSWER_TYPE_CHOICES = (
(INTEGER, 'Integer',),
(TEXT, 'Text',),
(RADIO, 'Radio',),
(SELECT, 'Select',),
(MULTI_SELECT, 'Multi-Select',),
)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=ANSWER_TYPE_CHOICES, max_length=20)
class Meta:
abstract = True
@classmethod
def get_subclasses(cls, *args, **kwargs):
for app_config in apps.get_app_configs():
for app_model in app_config.get_models():
model_classes = [c.__name__ for c in inspect.getmro(app_model)]
if cls.__name__ in model_classes:
yield app_model
class SurveyResponseBase(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to=get_content_choices)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
response = models.TextField()
class Meta:
abstract = True
def get_content_choices():
query_filter = None
for cls in SurveyQuestionBase.get_subclasses():
app_label, model = cls._meta.label_lower.split('.')
current_filter = models.Q(app_label=app_label, model=model)
if query_filter is None:
query_filter = current_filter
else:
query_filter |= current_filter
return query_filter
Solution 2: Dynamic base class generation
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
RATING = 'rating'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
QUESTION_TYPES = (
(INTEGER, 'Integer'),
(TEXT, 'Text'),
(RADIO, 'Radio'),
(RATING, 'Rating'),
(SELECT, 'Select'),
(MULTI_SELECT, 'Multi-Select'),
)
CHOICE_TYPES = (RADIO, RATING, SELECT, MULTI_SELECT)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=QUESTION_TYPES, max_length=20)
choices = models.TextField(blank=True, null=True)
choices.help_text = """
If the question type is "Radio," "Select," or "Multi-Select",
provide a comma-separated list of options for this question
"""
class Meta:
abstract = True
Meta = type('Meta', (object,), {'abstract': True})
def get_response_base_class(concrete_question_model):
"""
Builder method that returns the SurveyResponseBase base class
Args:
concrete_question_model: Concrete Model for SurveyQuestionBase
Returns: SurveyResponseBase Class
"""
try:
assert SurveyQuestionBase in concrete_question_model.__bases__
except AssertionError:
raise ValidationError('{} is not a subclass of SurveyQuestionBase'.format(concrete_question_model))
attrs = {
'question': models.ForeignKey(concrete_question_model, related_name='responses'),
'response': models.TextField(),
'__module__': 'survey_builder.models',
'Meta': Meta(),
}
return type('SurveyResponseBase', (models.Model,), attrs)
我们决定继续使用解决方案 2,因为 GenericForeignKeys 方法需要额外的 ContentType 选择。
我正在尝试构建两个名为 SurveyQuestionBase
和 SurveyResponseBase
的抽象 类,它们将用作模板以快速定义新的具体模型,以便在我们的网站上实施特定调查。我遇到的问题是强制执行 SurveyResponseBase
模型,当具体化时,应该将 ForeignKey
定义为 SurveyQuestionBase
.
Django 不允许我们定义 ForeignKeys
来抽象 类 所以我不能,例如,这样做:
question = models.ForeignKey(SurveyQuestionBase)
出于类似的原因,我也不能将其设为 None
或 app_label.ModelName
。
一个 hacky 修复是创建一个新的具体模型 SurveyQuestionConcrete
并使 ForeignKey
指向此:question = models.ForeignKey(concrete_model)
,结合验证以确保替换此模型。
有没有更简洁的方法来实现同样的事情? 我需要做的就是确保当有人从 SurveyResponseBase
定义具体模型时,他们将 ForeignKey
包含到从 SurveyQuestionBase
[ 定义的具体模型中=26=]
完整代码如下:
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
# Implementation borrows from: https://github.com/jessykate/django-survey/
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
ANSWER_TYPE_CHOICES = (
(INTEGER, 'Integer',),
(TEXT, 'Text',),
(RADIO, 'Radio',),
(SELECT, 'Select',),
(MULTI_SELECT, 'Multi-Select',),
)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=ANSWER_TYPE_CHOICES, max_length=20)
class Meta:
abstract = True
class SurveyResponseBase(models.Model):
"""
concrete_question_model: 'app_label.Model' - Define the concrete model this question belongs to
"""
concrete_model = 'SurveyQuestionBase'
question = models.ForeignKey(concrete_model)
response = models.TextField()
class Meta:
abstract = True
我相信你不能那样做,因为 ForeignKey 不知道要指向哪个实际模型。
您可能正在寻找 GenericForeignKey (https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations)。它允许您正确定义该关系。
这个问题的两个解决方案(都有效):
第一个解决方案涉及使用 GenericForeignKey
。第二个更有趣,涉及动态生成 SurveyResponseBase
。
Solution 1: Using GenericForeignKey
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
ANSWER_TYPE_CHOICES = (
(INTEGER, 'Integer',),
(TEXT, 'Text',),
(RADIO, 'Radio',),
(SELECT, 'Select',),
(MULTI_SELECT, 'Multi-Select',),
)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=ANSWER_TYPE_CHOICES, max_length=20)
class Meta:
abstract = True
@classmethod
def get_subclasses(cls, *args, **kwargs):
for app_config in apps.get_app_configs():
for app_model in app_config.get_models():
model_classes = [c.__name__ for c in inspect.getmro(app_model)]
if cls.__name__ in model_classes:
yield app_model
class SurveyResponseBase(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to=get_content_choices)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
response = models.TextField()
class Meta:
abstract = True
def get_content_choices():
query_filter = None
for cls in SurveyQuestionBase.get_subclasses():
app_label, model = cls._meta.label_lower.split('.')
current_filter = models.Q(app_label=app_label, model=model)
if query_filter is None:
query_filter = current_filter
else:
query_filter |= current_filter
return query_filter
Solution 2: Dynamic base class generation
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
RATING = 'rating'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
QUESTION_TYPES = (
(INTEGER, 'Integer'),
(TEXT, 'Text'),
(RADIO, 'Radio'),
(RATING, 'Rating'),
(SELECT, 'Select'),
(MULTI_SELECT, 'Multi-Select'),
)
CHOICE_TYPES = (RADIO, RATING, SELECT, MULTI_SELECT)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=QUESTION_TYPES, max_length=20)
choices = models.TextField(blank=True, null=True)
choices.help_text = """
If the question type is "Radio," "Select," or "Multi-Select",
provide a comma-separated list of options for this question
"""
class Meta:
abstract = True
Meta = type('Meta', (object,), {'abstract': True})
def get_response_base_class(concrete_question_model):
"""
Builder method that returns the SurveyResponseBase base class
Args:
concrete_question_model: Concrete Model for SurveyQuestionBase
Returns: SurveyResponseBase Class
"""
try:
assert SurveyQuestionBase in concrete_question_model.__bases__
except AssertionError:
raise ValidationError('{} is not a subclass of SurveyQuestionBase'.format(concrete_question_model))
attrs = {
'question': models.ForeignKey(concrete_question_model, related_name='responses'),
'response': models.TextField(),
'__module__': 'survey_builder.models',
'Meta': Meta(),
}
return type('SurveyResponseBase', (models.Model,), attrs)
我们决定继续使用解决方案 2,因为 GenericForeignKeys 方法需要额外的 ContentType 选择。