Atcrank
Atcrank

Reputation: 479

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.

Upvotes: 1

Views: 142

Answers (2)

Atcrank
Atcrank

Reputation: 479

I've revisited this problem recently, replaced one or two hundred lines of code with five or ten, and thought it might one day be useful to someone to find what I did here.

There are several elements to the problem I had - first, many answer types to be created, saved and retrieved when required; second, the GET vs POST dichotomy (and my idiosyncratic solution of always creating an answer, sending it to a form); third, some of the types have different logic (the Brainstorm can have multiple answers per user, the FeedBack does not even need a response - if it is created for a user, it has been presented.) These elements probably obscured some opportunity to remove repetition, which make the visitor pattern quite appropriate.

Solution for elements 1 & 2

A dictionary of question.answer_type codes that map to the relevant Answer sub-class, is created in views.py (because its hard to place it in models.py and resolve dependencies):

# views.py: 
ANSWER_CLASS_DICT = {
'CH': Vote,
'SA': Short_Answer,
'LA': Long_Answer,
'E3': EJ_three_field,
'E4': EJ_four_field,
'BS': Brainstorm,
'FB': FB,}

Then I can get the class of Answer that I want 'get_or_created' for any question with:

ANSWER_CLASS_DICT[question.answer_type]

I pass it as a parameter to the class method:

# models.py:
def get_or_update_answer(self, user, question, Cls, submitted_value=None, pk_ans=None):            
    if not submitted_value:
            answer, _created = Cls.objects.get_or_create(user=user, question=question)
    elif isinstance(submitted_value, dict):
            answer, _created = Cls.objects.get_or_create(user=user, question=question)
        for key, value in submitted_value.items():
                setattr(answer, key, value)
    else:
        pass

So the same six lines of code handles get_or_creating any Answer when submitted_value=None (GET) or not (submitted_value).

Solution for element 3

The solution for element three has been to extend the model to separate at least three types of handling for users revisiting the same question: 'S' - single, which allows them to record only one answer, revisit and amend the answer, but never to give two different answers. 'T' - tracked, which allows them to update their answer every time, but makes the history of what their answer was available (e.g. to researchers.) 'M' - multiple, which allows many answers to be submitted to a question.

Still bug-fixing after all these changes, so I won't post code. Next feature: compound questions and question templates, so people can use the admin to screen to make their own answer types.

Upvotes: 1

James Bennett
James Bennett

Reputation: 11163

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.

Upvotes: 0

Related Questions