0

I'm using django 4.1.2 with python 3.10.8. I have three models one for user management, one for questions and another for answers. They are described below:

class User(AbstractUser):
    phone_number = models.CharField(max_length=14, unique=True)
    first_name = models.CharField(max_length=40)
    father_name = models.CharField(max_length=40)
    email = models.EmailField(unique=True, required=True)
    age = models.CharField(max_length=3)
    username = models.CharField(max_length=8, required=True)

class Question(models.Model):
    question = models.CharField(
        max_length=500,
        null=False,
        unique=True
    )
    creating_staff = models.ForeignKey(
        User,
        null=False,
        on_delete=models.PROTECT,
        to_field="phone_number",
    )
    options = models.JSONField(null=False)
    correct_option = models.CharField(max_length=250, null=False)
    question_ts = models.DateTimeField(auto_now_add=True, null=False)

    class Meta:
        verbose_name = "Question"

    def __str__(self) -> str:
        return f"{self.question}"


class Answer(models.Model):
    answer = models.CharField(max_length=500, null=False)
    question_answered = models.ForeignKey(
        Question,
        null=False,
        on_delete=models.PROTECT,
        related_name="question_answered"
    )
    answering_user = models.ForeignKey(
        User,
        null=False,
        on_delete=models.PROTECT,
        to_field="phone_number",
        related_name="answerer"
    )
    status = models.BooleanField(null=False)
    answer_ts = models.DateTimeField(auto_now_add=True, null=False)

    class Meta:
        verbose_name = "Answer"

    def __str__(self) -> str:
        return f"{self.answer} -- {self.answering_user}"

This is the urls.py file:

from django.urls import path

from .views import (AnswerView)

app_name = "commons"

urlpatterns = [
    path("play/", AnswerView.as_view(), name="play"),
]

What I'm trying to do is whenever a user has logged in a wants to answer a set of questions by going to /commons/play/, on the GET request I want to parse out all the previous questions that user has answered and always display new questions by randomly selecting 10 out of the unanswered questions.

What I've done thus far is:

import random
from django.shortcuts import (redirect, render)
from django.views import View
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin

from .models import (Question, Answer, User)

class AnswerView(LoginRequiredMixin, View):

    def get(self, request):
            answerer = request.user
            total_answers_by_user = Answer.objects.filter(answering_user=answerer)
            questions = Question.objects.all()
            question_list = list()
            for ans in total_answers_by_user:
                for q in questions:
                    if not ans.question_answered == q:
                        question_list.append(q)
            questions_count = question.count()
            try:
                rand_sample = random.sample(range(questions_count), 10)
            except (ValueError, Exception) as e:
                print(f"{e} population for random sample < 10, perhaps new player")
                total_questions = Question.objects.all().count()
                rand_sample = random.sample(range(total_questions), 10)
                questions_to_ask = Question.objects.filter(id__in=rand_sample)
            else:
                questions_to_ask = Question.objects.filter(id__in=rand_sample)

            return render(request, "commons/answer.html", {"questions": questions_to_ask})

But I'm very doubtful that this is an efficient way of retrieving unanswered or new questions, especially when it comes to multiple users. Is there a better way to retrieve all the questions that the user has previously answered and only display new or unanswered questions?

My gratitude before hand for your response.

NegassaB
  • 379
  • 7
  • 24

1 Answers1

1

To break down what you need to do, first you want a list of IDs of questions answered by the user

answered_qs = (Answer.objects
    .filter(answering_user=answerer)
    .values_list('question_answered_id', flat=True)
    )

Then you want to exclude that list of questions from the set of all questions

question_list = Question.objects.exclude(pk__in=answered_qs)

Using related_names, it is possible to do this all in one call. Here we follow questions_answered to the set of answers where the field answering_user is our requester, and exclude that set.

questions_list = Question.objects.exclude(question_answered__answering_user=answerer)

With a slight performance hit if there are a lot of questions, you can even get your random set of 10 in the same database call, eg,

questions_list = (
    Question.objects
    .exclude(question_answered__answering_user=answerer)
    .order_by('?')[:10]
    )
SamSparx
  • 4,629
  • 1
  • 4
  • 16
  • At first I removed the questions_list list and modified the for loop to look like this: ```python for ans in total_qs_answered_by_user: if questions.contains(ans.question_answered): questions = questions.exclude(id=ans.question_answered.id) ``` but it seems you first answer will do the same trick efficiently. I thank you for you help – NegassaB Dec 07 '22 at 08:42
  • I dont know about the `order_by` though. Will not reorder the pks inside the db table? – NegassaB Dec 07 '22 at 08:47
  • for the randomizing what I currently have is: ```questions_to_ask = random.sample(list(questions), 10) return render(request, "commons/answer.html", {"questions": questions_to_ask)``` – NegassaB Dec 07 '22 at 09:01
  • 1
    order_by('?') just uses sql to return the recordset randomly. It doesn't affect the order of ids within the table, but has an overhead for large results: https://docs.djangoproject.com/en/4.1/ref/models/querysets/#order-by. You can use random sample just as well if you are getting your sampling set efficiently – SamSparx Dec 07 '22 at 09:02
  • decided to stick with `random.sample(list(question), 10)`. As I understand it this will only execute the query once thus saving overhead. – NegassaB Dec 07 '22 at 09:18
  • 1
    Turns out I had misused contains in my one line attempt so I updated answer. Based on this question: https://stackoverflow.com/questions/67692881/understanding-django-exclude-queryset-on-foreignkey-relationship you might be able to do a 1 liner in an even simpler fashion – SamSparx Dec 08 '22 at 09:51
  • the `contains` doesn't work. It raises this errror: `django.core.exceptions.FieldError: Cannot resolve keyword 'answerer' into field. Choices are: correct_option, creating_staff, creating_staff_id, id, options, question, question_answered, question_ts`. It needs to be `question_answered` but even that throws an error specifically `ValueError: Cannot query "+111111111": Must be "Answer" instance.` – NegassaB Dec 09 '22 at 11:02
  • it does work like this though `questions_list = Question.objects.exclude(question_answered__answering_user=answerer)` without `contains` – NegassaB Dec 09 '22 at 11:09
  • 1
    Sorry about `contains` - entirely the wrong operator (what was I thinking?). I think the above is the best approach and have update the answer for posterity. – SamSparx Dec 09 '22 at 14:01
  • no need for the apology, I very much appreciate your help. I was going crazy over this, thank you so much – NegassaB Dec 09 '22 at 15:54