2017-07-07 77 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選擇。