post_save signal on Question and Choice example

To learn about signals and particularly the Question and Choice models in the Django beginner Tutorial.

Here is the models.py

import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
    jsonDefault = {'json':'sample'}

    id = models.BigAutoField(primary_key = True)
    question_text = models.CharField( null=True, max_length=200, blank=True)
    request_details = models.TextField(null=True, max_length = 250, blank=True)
    json_field = models.JSONField( null=True, blank=True,  default = dict)
    approve_and_train = models.BooleanField(null=True, default=False)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text

    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now
    
    was_published_recently.admin_order_field = 'pub_date'
    was_published_recently.boolean = True
    was_published_recently.short_description = 'Published recently?'


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text

Here is the signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Question, Choice


@receiver(post_save, sender=Question)
def post_save_create(sender, instance, created, **kwargs):
    #obj = Choice.objects.get_or_create(Question=instance)
    obj = Choice
    print('post_save')
    #1
    obj.question = Question.id
    obj.choice_text = 'Yes, confirm.'
    obj.votes = 0
    obj.save()
    # #2
    # obj.choice_text = 'No, Reject.'
    # obj.votes = 0
    # obj.save()
    # #3
    # obj.choice_text = 'Investigate.'
    # obj.votes = 0
    # obj.save()

On saving a Question with no choices entered. I want to make three choices default on all questions.

The Error is this.


Got a `TypeError` when calling `Question.objects.create()`. This may be because you have a writable field on the serializer class that is not a valid argument to `Question.objects.create()`. You may need to make the field read-only, or override the QuestionListSerializer.create() method to handle this correctly.
Original exception was:
 Traceback (most recent call last):
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\rest_framework\serializers.py", line 962, in create
    instance = ModelClass._default_manager.create(**validated_data)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\query.py", line 453, in create
    obj.save(force_insert=True, using=self.db)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\base.py", line 740, in save
    force_update=force_update, update_fields=update_fields)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\base.py", line 789, in save_base
    update_fields=update_fields, raw=raw, using=using,
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\dispatch\dispatcher.py", line 182, in send
    for receiver in self._live_receivers(sender)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\dispatch\dispatcher.py", line 182, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\reviews\signals.py", line 16, in post_save_create
    obj.save()
TypeError: save() missing 1 required positional argument: 'self'

You’re defining obj as a reference to the class and not as an instance.

You’re going to want to create three different instances of Choice - one for each default choice you wish to add.

Review the exercises from the tutorial in part 2 at Playing with the API

ok makes sense will give that a try

Ok almost there I think.

revised signals.py

#/reviews/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Question, Choice


@receiver(post_save, sender=Question)
def post_save_create(sender, instance, created, **kwargs):
    print('post_save')
    #obj = Choice.objects.get_or_create(Question=instance)
    print('post_save, instance.id = ', instance.id)
    q = Question.objects.get(pk=instance.id)

    # Create three choices.
    q.choice_set.create(choice_text='Yes, Confirm!', votes=0)
    q.choice_set.create(choice_text='No, Reject!', votes=0)
    q.choice_set.create(choice_text='Investigate', votes=0)
    post_save.disconnect(post_save_create, sender=sender)
    q.save()
    post_save.connect(post_save_create, sender=sender)

Question saves and no endless loop, but the choices didn’t make it.

The log shows the print statements in the signal handler but no choices saved.

post_save
post_save, instance.id = 30

Couple different things here:

This is unnecessary because instance already is the instance of Question you want to use in your relationship. (See post_save)

This also is not necessary. You’re not changing the Question instance with these actions. You’re creating instances of Choice that relate to a Question.

(See the docs for create.)

Now, I don’t see anything really wrong with this code, so I’d probably need to see the view that is causing this to execute along with a description of how you’re verifying that these entries are not being saved.

ok

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils import timezone
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'reviews/questionindex.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """
        Return the last five published questions (not including those set to be
        published in the future).
        """
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'reviews/questiondetail.html'

    def get_queryset(self):
        """
        Excludes any questions that aren't published yet.
        """
        return Question.objects.filter(pub_date__lte=timezone.now())


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'reviews/questionresults.html'


def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'reviews/questiondetail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('reviews:questionresults', args=(question.id,)))

revised signal handler

#/reviews/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Question, Choice


@receiver(post_save, sender=Question)
def post_save_create(sender, instance, created, **kwargs):
    print('post_save')
    #obj = Choice.objects.get_or_create(Question=instance)
    print('post_save, instance.id = ', instance.id)
    q = instance

    # Create three choices.
    
    q.choice_set.create(choice_text='Yes, Confirm!', votes=0)
    
    q.choice_set.create(choice_text='No, Reject!', votes=0)
    
    q.choice_set.create(choice_text='Investigate', votes=0)
    

    post_save.connect(post_save_create, sender=sender)

    #  q = Question.objects.get(pk=instance.id)

    # # Create three choices.
    # q.choice_set.create(choice_text='Yes, Confirm!', votes=0)
    # q.choice_set.create(choice_text='No, Reject!', votes=0)
    # q.choice_set.create(choice_text='Investigate', votes=0)
    # post_save.disconnect(post_save_create, sender=sender)
    # q.save()
    # post_save.connect(post_save_create, sender=sender)

I tried

q = instance

    #1
    c = Choice(
        Question=q,
        choice_text='Yes, Confirm!',
        votes=0)
    c.save(force_insert=True)

but that gives

TypeError at /api/questions/
Got a `TypeError` when calling `Question.objects.create()`. This may be because you have a writable field on the serializer class that is not a valid argument to `Question.objects.create()`. You may need to make the field read-only, or override the QuestionListSerializer.create() method to handle this correctly.
Original exception was:
 Traceback (most recent call last):
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\rest_framework\serializers.py", line 962, in create
    instance = ModelClass._default_manager.create(**validated_data)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\query.py", line 453, in create
    obj.save(force_insert=True, using=self.db)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\base.py", line 740, in save
    force_update=force_update, update_fields=update_fields)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\base.py", line 789, in save_base
    update_fields=update_fields, raw=raw, using=using,
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\dispatch\dispatcher.py", line 182, in send
    for receiver in self._live_receivers(sender)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\dispatch\dispatcher.py", line 182, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\reviews\signals.py", line 18, in post_save_create
    votes=0)
  File "C:\Users\MikeOliver\GitHub\Correlator-2\venv\lib\site-packages\django\db\models\base.py", line 503, in __init__
    raise TypeError("%s() got an unexpected keyword argument '%s'" % (cls.__name__, kwarg))
TypeError: Choice() got an unexpected keyword argument 'Question'

I tried the Playing with the API and was specifically hopeful that it would provide the solution.

I used the shell in my project as follows:


python manage.py shell
Python 3.6.4 (v3.6.4:d48eceb, Dec 19 2017, 06:54:40) [MSC v.1900 64 bit (AMD64)] on win32     
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from polls.models import Choice, Question
>>> q = Question.objects.get(pk=38)
>>> q.choice_set.all()
<QuerySet []>
>>> q.choice_set.create(choice_text='Yes, Confirm', votes=0)
<Choice: Yes, Confirm>
>>> q.choice_set.create(choice_text='No, reject', votes=0)
<Choice: No, reject>
>>> c = q.choice_set.create(choice_text='Investigate', votes=0)
>>> c.question
<Question: Restarting after 900+ choices>
>>> q.choice_set.all()
<QuerySet [<Choice: Yes, Confirm>, <Choice: No, reject>, <Choice: Investigate>]>
>>> a.choice_set.count()
Traceback (most recent call last):
  File "<console>", line 1, in <module>        
NameError: name 'a' is not defined
>>> q.choice_set.count()
3
>>>

Yet when I go to http://localhost:8000/api/questions/

I only see the Question and not the choices. However, when I go to the admin pages I see the question and the choices.

What view handles the url api/questions, and what is the template that it renders?

/api/views.py


#/api/views.py
from django.shortcuts import render
from django.shortcuts import get_object_or_404

# Create your views here.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.reverse import reverse
from rest_framework import generics
from correlator.models import Instruction
from parameters.models import Parameter
from reviews.models import Question
from reviews.models import Choice

from rest_framework.generics import ListCreateAPIView
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action

from .serializers import QuestionListPageSerializer, QuestionDetailPageSerializer, QuestionChoiceSerializer, VoteSerializer, QuestionResultPageSerializer, ChoiceSerializer



import api.serializers as serializers

class InstructionsListAPIView(generics.ListCreateAPIView):
    queryset = Instruction.objects.all()
    serializer_class = serializers.InstructionListSerializer

class InstructionsAPIView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Instruction.objects.all()
    serializer_class = serializers.InstructionSerializer

class ParametersListAPIView(generics.ListCreateAPIView):
    queryset = Parameter.objects.all()
    serializer_class = serializers.ParametersListSerializer

class ParameterAPIView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Parameter.objects.all()
    serializer_class = serializers.ParameterSerializer

#reviews 

class QuestionsListAPIView(generics.ListCreateAPIView):
    queryset = Question.objects.all()
    serializer_class = serializers.QuestionListSerializer

class QuesttionsAPIView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Question.objects.all()
    serializer_class = serializers.QuestionSerializer

class QuestionsAPIView(generics.ListCreateAPIView):
    queryset = Question.objects.all()
    serializer_class = serializers.ParameterSerializer

class ChoicesView(ListCreateAPIView):
    serializer_class = ChoiceSerializer
    queryset = Choice.objects.all()


class QuestionsViewSet(ModelViewSet):
    print()
    print('QuestionsViewSet')
    print()
    """
    @api {get} /questions/ Questions list
    @apiName Questions list
    @apiDescription This returns a list of questions.
    @apiGroup Questions
    @apiVersion 1.0.0

    @apiSuccess {Integer} id Unique identifier
    @apiSuccess {String} pub_date Published date
    @apiSuccess {String} question_text Question text
    @apiSuccess {String} was_published_recently Was published recently?

    @apiSuccessExample Response
        HTTP/1.1 200 OK
        [
            {
                "id": 1,
                "was_published_recently": false,
                "question_text": "Who is the most likable character in GOT?"
                "pub_date": "2019-05-09T00:00:00Z",
            },
            {
                "id": 46,
                "was_published_recently": false,
                "question_text": "Who is your favorite fictional character?",
                "pub_date": "2019-05-10T00:00:00Z"
            }
        ]
    """
    """
    @api {post} /questions/ Create a question
    @apiName Question create
    @apiGroup Questions
    @apiVersion 1.0.0

    @apiParam (Payload) {String} pub_date Published date
    @apiParam (Payload) {String} question_text Question text
    @apiParam (Payload) {Dictionary[]} choice_set List of choices

    @apiParamExample {json} Payload-example
        {
            "pub_date": "2019-05-05T00:00",
            "question_text": "Is Samsung more reliable than iPhone?",
            "choice_set": [
                {
                    "choice_text": "No"
                }
            ]
        }

    @apiSuccess {Integer} id Unique identifier
    @apiSuccess {Dictionary[]} choice_set List of choices
    @apiSuccess {String} pub_date Published date
    @apiSuccess {String} question_text Question text
    @apiSuccess {String} was_published_recently Was published recently?

    @apiSuccessExample Response
        HTTP/1.1 201 Created
        {
            "id": 50,
            "was_published_recently": false,
            "choice_set": [
                {
                    "id": 50,
                    "choice_text": "No"
                }
            ],
            "question_text": "Is Samsung more reliable than iPhone?",
            "pub_date": "2019-05-05T00:00:00Z"
        }
    """
    queryset = Question.objects.all()
    lookup_url_kwarg = 'question_id'

    def get_serializer_class(self):
        # Handle .create() requests
        if self.request.method == 'POST':
            return QuestionDetailPageSerializer
        # Handle .result() requests
        elif self.detail is True and self.request.method == 'GET' and self.name == 'Result':
            return QuestionResultPageSerializer
        # Handle .retrieve() requests
        elif self.detail is True and self.request.method == 'GET':
            return QuestionDetailPageSerializer
        return QuestionListPageSerializer

    @action(detail=True)
    def result(self, request, *args, **kwargs):
        return self.retrieve(self, request, *args, **kwargs)

    @action(methods=['GET', 'POST'], detail=True)
    def choices(self, request, *args, **kwargs):
        question = self.get_object()
        if request.method == 'GET':
            choices = question.choice_set.all()
            serializer = QuestionChoiceSerializer(choices, many=True)
            return Response(serializer.data)
        else:
            serializer = QuestionChoiceSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save(question=question)
                return Response(serializer.data)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    @action(methods=['patch'], detail=True)
    def vote(self, request, *args, **kwargs):
        question = self.get_object()
        serializer = VoteSerializer(data=request.data)
        if serializer.is_valid():
            choice = get_object_or_404(Choice, pk=serializer.validated_data['choice_id'], question=question)
            choice.votes += 1
            choice.save()
            return Response("Voted")
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(('GET',))
def api_root(request, format=None):
    return Response({
        'instructions': reverse('instructions', request=request, format=format),
        'parameters': reverse('parameters', request=request, format=format),
        'questions' : reverse('questions', request=request, format=format),
        })

and

#/api/serializers.py
from django.forms import widgets
from rest_framework import serializers
from correlator.models import Instruction
from reviews.models import Question
from parameters.models import Parameter
from reviews.models import Question
from reviews.models import Choice

class ParametersListSerializer(serializers.ModelSerializer):
    class Meta:
        model = Parameter
        fields = ('id', 'matchingKey', 'sequence', 'param_kwargs_json', 'param1', 'param2', 'param3', 'param4', 'param5', 
        'param6', 'param7', 'param8', 'param9', 'param10', 'description')

class ParameterSerializer(serializers.ModelSerializer):
    class Meta:
        model = Parameter
        fields = ('id', 'matchingKey', 'sequence', 'param_kwargs_json', 'param1', 'param2', 'param3', 'param4', 'param5', 
        'param6', 'param7', 'param8', 'param9', 'param10', 'description')

class InstructionListSerializer(serializers.ModelSerializer):
    class Meta:
        model = Instruction
        fields = '__all__'

class InstructionSerializer(serializers.ModelSerializer):
 
    class Meta:
        model = Instruction
        fields = '__all__'
#Polls

class QuestionListSerializer(serializers.ModelSerializer):
    class Meta:
        model = Question
        fields = '__all__'

class QuestionSerializer(serializers.ModelSerializer):
 
    class Meta:
        model = Question
        fields = '__all__'

class QuestionChoiceSerializer(serializers.ModelSerializer):

    class Meta:
        model = Choice
        fields = ('id', 'choice_text')

class ChoiceSerializer(serializers.ModelSerializer):
    question_text = serializers.CharField(read_only=True, source='question.question_text')

    class Meta:
        model = Choice
        fields = ('id', 'choice_text', 'question', 'question_text')
        extra_kwargs = {
            'question': {'write_only': True}
        }


class QuestionChoiceSerializerWithVotes(QuestionChoiceSerializer):

    class Meta(QuestionChoiceSerializer.Meta):
        fields = QuestionChoiceSerializer.Meta.fields + ('votes',)


class QuestionListPageSerializer(serializers.ModelSerializer):

    # was_published_recently = serializers.BooleanField(read_only=True)

    class Meta:
        model = Question
        # fields = '__all__'
        fields = ('id', 'question_text')

class QuestionDetailPageSerializer(QuestionListPageSerializer):
    choice_set = QuestionChoiceSerializer(many=True)

    def create(Question, validated_data):
        choice_validated_data = validated_data.pop('choice_set')
        question = Question.objects.create(**validated_data)
        choice_set_serializer = Question.fields['choice_set']
        for each in choice_validated_data:
            each['question'] = question
        choices = choice_set_serializer.create(choice_validated_data)
        return question


class QuestionResultPageSerializer(QuestionListPageSerializer):
    choices = QuestionChoiceSerializerWithVotes(many=True, read_only=True)

class VoteSerializer(serializers.Serializer):
    choice_id = serializers.IntegerField()

Like the other two DRF APIs I didn’t specify a template.

Which view handles the specific url we’re looking at here? (/api/questions/)

I took another approach and the problem went away.