Defining an Abstract model with a ForeignKey to another Abstract model

ぃ、小莉子 提交于 2020-07-07 07:43:20

问题


I'm trying to build two abstract classes called SurveyQuestionBase and SurveyResponseBase that will serve as templates to quickly define new concrete Models for implementing specific surveys on our website. The issue I am having is in enforcing that the SurveyResponseBase model, when made concrete, should define a ForeignKey to a concrete model of SurveyQuestionBase.

Django does not allow us to define ForeignKeys to abstract classes so I cannot, for instance, do this: question = models.ForeignKey(SurveyQuestionBase) Neither can I have it as None or app_label.ModelName for similar reasons.

One hacky fix is to create a new concrete model SurveyQuestionConcrete and make the ForeignKey point to this: question = models.ForeignKey(concrete_model), combined with validation to ensure this model is replaced.

Is there a cleaner way to achieve the same thing? All I need to do is ensure that when someone defines a concrete model from SurveyResponseBase they include a ForeignKey to a concrete model defined from SurveyQuestionBase

Here's the full code:

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:


Two solutions (both working) to this problem:

The first solution involves using GenericForeignKey. The second is more interesting and involves generating the SurveyResponseBase dynamically.

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)

We decided to go ahead with Solution 2 since the GenericForeignKeys approach requires an additional ContentType selection.




回答2:


I believe you can't do that because the ForeignKey doesn't know what actual model to point to.

You may be looking for GenericForeignKey (https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations). It allows you to define that relationship properly.



来源:https://stackoverflow.com/questions/44965172/defining-an-abstract-model-with-a-foreignkey-to-another-abstract-model

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!