2017-07-07 130 views
0

我试图建立两个抽象类称为SurveyQuestionBaseSurveyResponseBase,将作为模板来快速确定新的具体型号我们的网站上实施具体的调查。我遇到的问题是强制执行SurveyResponseBase模型,具体时应将ForeignKey定义为SurveyQuestionBase的具体模型。定义有一个ForeignKey的抽象模型到另一个抽象模型

Django不允许我们定义ForeignKeys抽象类,所以我没有,比如,这样做可以: question = models.ForeignKey(SurveyQuestionBase) 无论是我可以把它作为Noneapp_label.ModelName出于同样的原因。

一个诡异的问题是创建一个新的具体模型SurveyQuestionConcrete并使ForeignKey指向这个:question = models.ForeignKey(concrete_model),并结合验证以确保此模型被替换。

有没有更简单的方法来实现同样的目标? 所有我需要做的是确保当有人从SurveyResponseBase定义了一个具体的模型,他们包括ForeignKeySurveyQuestionBase

这里定义的具体模型的完整代码:

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 

回答

1

两个解决方案(两种工作)这个问题:

第一个解决方案包括使用GenericForeignKey。第二个更有趣,涉及动态生成SurveyResponseBase

解决方法1:使用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 

解决方案2:动态基类代

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选择。