Search code examples
pythondjangomultiple-inheritance

Django Model inheritance for efficient code


I have a Django app that uses an Abstract Base Class ('Answer') and creates different Answers depending on the answer_type required by the Question objects. (This project started life as the Polls tutorial). Question is now:

class Question(models.Model):
    ANSWER_TYPE_CHOICES = (
    ('CH', 'Choice'),
    ('SA', 'Short Answer'),
    ('LA', 'Long Answer'),
    ('E3', 'Expert Judgement of Probabilities'),
    ('E4', 'Expert Judgment of Values'),
    ('BS', 'Brainstorms'),
    ('FB', 'Feedback'),
    )
    answer_type = models.CharField(max_length=2,
                               choices=ANSWER_TYPE_CHOICES,
                               default='SA')
    question_text = models.CharField(max_length=200, default="enter a question here")

And Answer is:

class Answer(models.Model):
"""
Answer is an abstract base class which ensures that question and user are
always defined for every answer
"""
question = models.ForeignKey(Question, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
class Meta:
    abstract = True
    ordering = ['user']

At the moment, I have a single method in Answer (overwriting get_or_update_answer()) with type-specific instructions to look in the right table and collect or create the right type of object.

    @classmethod
def get_or_update_answer(self, user, question, submitted_value={}, pk_ans=None):
    """
    this replaces get_or_update_answer with appropriate handling for all
    different Answer types. This allows the views answer and page_view to get
    or create answer objects for every question type calling this function.
    """
    if question.answer_type == 'CH':
        if not submitted_value:
            # by default, select the top of a set of radio buttons
            selected_choice = question.choice_set.first()
            answer, _created = Vote.objects.get_or_create(
                user=user,
                question=question,
                defaults={'choice': selected_choice})
        else:
            selected_choice = question.choice_set.get(pk=submitted_value)
            answer = Vote.objects.get(user=user, question=question)
            answer.choice = selected_choice

    elif question.answer_type == 'SA':
        if not submitted_value:
            submitted_value = ""
            answer, _created = Short_Answer.objects.get_or_create(
                user=user,
                question=question,
                defaults={'short_answer': submitted_value})
        else:
            answer = Short_Answer.objects.get(
                user=user,
                question=question)
            answer.short_answer = hashtag_cleaner(submitted_value['short_answer'])
 etc... etc... (similar handling for five more types)

By putting all this logic in 'models.py', I can load user answers for a page_view for any number of questions with:

    for question in page_question_list:
        answers[question] = Answer.get_or_update_answer(user, question, submitted_value, pk_ans)

I believe there is a more Pythonic way to design this code - something that I haven't learned to use, but I'm not sure what. Something like interfaces, so that each object type can implement its own version of Answer.get_or_update_answer(), and Python will use the version appropriate for the object. This would make extending 'models.py' a lot neater.


Solution

  • Based on what you've shown, you're most of the way to reimplementing the Visitor pattern, which is a pretty standard way of handling this sort of situation (you have a bunch of related subclasses, each needing its own handling logic, and want to iterate over instances of them and do something with each).

    I'd suggest taking a look at how that pattern works, and perhaps implementing it more explicitly.