Skip to content

Commit

Permalink
Merge pull request #177 from PROCOLLAB-github/feature/incorrect_answer
Browse files Browse the repository at this point in the history
Questions answer tryes
  • Loading branch information
pavuchara authored Dec 27, 2024
2 parents d268955 + d790637 commit d9148dc
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 18 deletions.
1 change: 1 addition & 0 deletions apps/courses/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
class TaskResultData:
points_gained: int
quantity_done_correct: int
quantity_done: int
quantity_all: int
level: int
progress: int
Expand Down
12 changes: 10 additions & 2 deletions apps/courses/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ def get(self, request, *args, **kwargs) -> Response:

data = TaskResultData(
points_gained=task.points_gained if task.points_gained else 0,
quantity_done_correct=task.total_answers,
quantity_done_correct=task.total_correct_answers,
quantity_done=task.total_answers,
quantity_all=task.total_questions,
level=task.level,
progress=progress,
Expand Down Expand Up @@ -262,10 +263,17 @@ def get_object(self, task_id: int) -> tuple[Task, int]:
filter=Q(task_objects__user_results__user_profile_id=self.profile_id),
distinct=True,
),
total_correct_answers=Count( # Всего ответов пользователя в задании.
"task_objects__user_results",
filter=(
Q(task_objects__user_results__user_profile_id=self.profile_id)
& Q(task_objects__user_results__correct_answer=True)
),
distinct=True,
),
points_gained=Sum( # Кол-во полученных поинтов юзером в рамках задания.
"task_objects__user_results__points_gained",
filter=Q(task_objects__user_results__user_profile_id=self.profile_id),
distinct=True,
),
next_task_id=Subquery( # ID следующего задания.
Task.available.only_awailable_weeks(available_week, self.request.user)
Expand Down
4 changes: 3 additions & 1 deletion apps/progress/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ def create_user_result(
user_profile_id: int,
type_task_obj: TaskObjs,
text: str = "",
correct_answer: bool = True,
):
try:
self.get_queryset().create(
task_object_id=task_obj_id,
user_profile_id=user_profile_id,
points_gained=type_task_obj.value,
text=text
text=text,
correct_answer=correct_answer,
)
except IntegrityError as e:
if "unique constraint" in str(e.args).lower():
Expand Down
20 changes: 20 additions & 0 deletions apps/progress/migrations/0020_taskobjuserresult_correct_answer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.0.3 on 2024-12-26 09:13

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("progress", "0019_useranswersattemptcounter"),
]

operations = [
migrations.AddField(
model_name="taskobjuserresult",
name="correct_answer",
field=models.BooleanField(
default=True, help_text="Дан верный/не верный ответ на задание", verbose_name="Правильный ответ"
),
),
]
7 changes: 7 additions & 0 deletions apps/progress/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ class TaskObjUserResult(AbstractDateTimeCreated):
null=False,
help_text="Для ответов юзера, которые связаны с вопросами по вводу ответа",
)
correct_answer = models.BooleanField(
default=True,
null=False,
blank=False,
verbose_name="Правильный ответ",
help_text="Дан верный/не верный ответ на задание",
)
points_gained = models.PositiveIntegerField(verbose_name="Набранные баллы")
datetime_created = models.DateTimeField(
verbose_name="Дата создания", null=False, default=timezone.now
Expand Down
7 changes: 6 additions & 1 deletion apps/questions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ class QuestionConnectAdmin(AbstractQuestionShowcase, SummernoteModelAdmin):
},
),
(
"Подсказка (если не требуется, необходимо `Attempts before hint` оставить пустым)",
(
"Подсказка: 1) Без подсказки - оставить все пустым; "
"2) Без подсказки, но с попытками к ответу: `Попытки до подсказки`; "
"3) С подсказкой в конце, но без попыток после подсказки: оставить пустым `Попытки после подсказки`;."

),
{
"fields": (
"hint_text",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated by Django 5.0.3 on 2024-12-26 09:34

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("questions", "0013_rename_title_infoslide_text"),
]

operations = [
migrations.AlterField(
model_name="questionconnect",
name="attempts_after_hint",
field=models.SmallIntegerField(
blank=True,
null=True,
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Попытки после подсказки",
),
),
migrations.AlterField(
model_name="questionconnect",
name="attempts_before_hint",
field=models.SmallIntegerField(
blank=True,
null=True,
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Попытки до подсказки",
),
),
migrations.AlterField(
model_name="questionsingleanswer",
name="attempts_after_hint",
field=models.SmallIntegerField(
blank=True,
null=True,
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Попытки после подсказки",
),
),
migrations.AlterField(
model_name="questionsingleanswer",
name="attempts_before_hint",
field=models.SmallIntegerField(
blank=True,
null=True,
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Попытки до подсказки",
),
),
]
4 changes: 2 additions & 2 deletions apps/questions/models/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ class AbstractHint(models.Model):
blank=True,
null=True,
validators=[MinValueValidator(1)],
help_text="Количество попыток до показа подсказки",
verbose_name="Попытки до подсказки",
)
attempts_after_hint = models.SmallIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(1)],
help_text="Количество попыток после подсказки",
verbose_name="Попытки после подсказки",
)

class Meta:
Expand Down
42 changes: 30 additions & 12 deletions apps/questions/services/check_questions_answers.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _check_correct_answer(self) -> tuple[dict[str, Any], int]:
raise NotImplementedError

@abstractmethod
def _create_correct_answer_response_body(self) -> dict[str, Any]:
def _create_answer_response_body(self) -> dict[str, Any]:
raise NotImplementedError

def _process_answer_attempt_counter(self, question_answers: Any):
Expand Down Expand Up @@ -104,17 +104,29 @@ def _process_answer_attempt_counter(self, question_answers: Any):
# Проверка после подсказки, если counter `after` max, то сохранение без баллов:
if counter.is_take_hint and counter.attempts_made_after >= self.request_question.attempts_after_hint:
counter.delete()
self._create_tast_obj_result(TypeQuestionPoints.QUESTION_WO_POINTS)
question_answer_body = self._create_correct_answer_response_body(question_answers)
self._create_tast_obj_result(TypeQuestionPoints.QUESTION_WO_POINTS, correct_answer=False)
question_answer_body = self._create_answer_response_body(question_answers)
question_answer_body["hint"] = self.request_question.hint_text
return question_answer_body, status.HTTP_201_CREATED

# Проверка до подсказки, если counter `before` max, то в response будет подсказка:
# Проверка до подсказки:
# - если counter `before` max, то в response будет подсказка.
# - если `after` нет, то засчитывается неверный ответ.
if counter.attempts_made_before >= self.request_question.attempts_before_hint:
counter.is_take_hint = True
counter.save()
response_body = deepcopy(self._UNSUCCESS_RESPONSE_BODY)
response_body["hint"] = self.request_question.hint_text
return response_body, status.HTTP_400_BAD_REQUEST

if self.request_question.attempts_after_hint and self.request_question.hint_text:
response_body["hint"] = self.request_question.hint_text
return response_body, status.HTTP_400_BAD_REQUEST
else:
counter.delete()
self._create_tast_obj_result(TypeQuestionPoints.QUESTION_WO_POINTS, correct_answer=False)
question_answer_body = self._create_answer_response_body(question_answers)
if self.request_question.hint_text:
question_answer_body["hint"] = self.request_question.hint_text
return question_answer_body, status.HTTP_201_CREATED

counter.save()
return self._UNSUCCESS_RESPONSE_BODY, status.HTTP_400_BAD_REQUEST
Expand All @@ -130,7 +142,12 @@ def _handle_no_question_validation(
self._create_tast_obj_result(point_type)
return self._SUCCESS_RESPONSE_BODY, status.HTTP_201_CREATED

def _create_tast_obj_result(self, point_type: TypeQuestionPoints, text: str = ""):
def _create_tast_obj_result(
self,
point_type: TypeQuestionPoints,
text: str = "",
correct_answer: bool = True,
):
"""
Формирование результата.
Если навык Task бесплатный, то без поинтов.
Expand All @@ -143,6 +160,7 @@ def _create_tast_obj_result(self, point_type: TypeQuestionPoints, text: str = ""
self.request_profile_id,
point_type,
text=text,
correct_answer=correct_answer,
)

def _delete_self_counter(self):
Expand Down Expand Up @@ -178,7 +196,7 @@ def _check_correct_answer(self) -> tuple[dict[str, Any], int]:
return self._process_answer_attempt_counter(question_answer)
return self._UNSUCCESS_RESPONSE_BODY, status.HTTP_400_BAD_REQUEST

def _create_correct_answer_response_body(self, question_answer: AnswerSingle):
def _create_answer_response_body(self, question_answer: AnswerSingle):
response_body = deepcopy(self._UNSUCCESS_RESPONSE_BODY)
response_body["answer_ids"] = question_answer.id
return response_body
Expand All @@ -205,7 +223,7 @@ def _check_correct_answer(self) -> tuple[dict[str, Any], int]:

return self._UNSUCCESS_RESPONSE_BODY, status.HTTP_400_BAD_REQUEST

def _create_correct_answer_response_body(self, question_answer: AnswerSingle):
def _create_answer_response_body(self, question_answer: AnswerSingle):
response_body = deepcopy(self._UNSUCCESS_RESPONSE_BODY)
response_body["answer_ids"] = list(question_answer)
return response_body
Expand Down Expand Up @@ -239,7 +257,7 @@ def _check_correct_answer(self) -> tuple[dict[str, Any], int]:

return self._UNSUCCESS_RESPONSE_BODY, status.HTTP_400_BAD_REQUEST

def _create_correct_answer_response_body(self, question_answer: AnswerSingle):
def _create_answer_response_body(self, question_answer: AnswerSingle):
response_body = deepcopy(self._UNSUCCESS_RESPONSE_BODY)
response_body["answer_ids"] = [
{
Expand All @@ -265,7 +283,7 @@ def create_answer(self) -> tuple[dict[str, Any], int]:
def _check_correct_answer(self) -> tuple[dict[str, Any], int]:
raise NotImplementedError

def _create_correct_answer_response_body(self) -> dict[str, Any]:
def _create_answer_response_body(self) -> dict[str, Any]:
raise NotImplementedError


Expand All @@ -280,5 +298,5 @@ def create_answer(self) -> tuple[dict[str, Any], int]:
def _check_correct_answer(self) -> tuple[dict[str, Any], int]:
raise NotImplementedError

def _create_correct_answer_response_body(self) -> dict[str, Any]:
def _create_answer_response_body(self) -> dict[str, Any]:
raise NotImplementedError
2 changes: 2 additions & 0 deletions apps/tests/courses_tests/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"quantity_all": 2,
"level": 1,
"progress": 0,
"quantity_done": 0,
"skill_name": "Навык 1",
"next_task_id": None
}
Expand All @@ -161,6 +162,7 @@
"quantity_all": 2,
"level": 1,
"progress": 0,
"quantity_done": 0,
"skill_name": "Навык 1",
"next_task_id": 2
}
Expand Down
37 changes: 37 additions & 0 deletions apps/tests/courses_tests/test_courses_response.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import json
import pytest
from django.test import override_settings

from rest_framework.test import APIClient

from . import constants
from tests.questions_tests import constants as questions_constants


class TestTaskListPathResponse:
Expand Down Expand Up @@ -298,6 +300,41 @@ def test_free_task_result_auth_user_wo_sub_after_answer(self, api_auth_without_s
assert respose_dct["quantity_done_correct"] == self.DONE_CORRECT, "(Бесплатно)Не засчитало выполненное задание"
assert respose_dct["progress"] == self.OLD_SUB_PERCENT_PROGRESS, "(Бесплатно)Не засчитало прогресс пользователю"

@pytest.mark.usefixtures("single_question_data_with_tryes")
@override_settings(task_always_eager=True)
def test_task_result_user_with_sub_after_wrong_answer(self, api_auth_with_sub_client: APIClient):
"""
Делается 2 запроса (2 попытки к ответу) с проверкой результата.
Оба запроса совершаются с неправильным ответом (для проверки посчета).
"""
data = {"answer_id": 2}

api_auth_with_sub_client.post(
questions_constants.SINGLE_CORRECT_POST,
data=json.dumps(data),
content_type="application/json",
)
response = api_auth_with_sub_client.get(constants.TASK_RESULT)
respose_dct = response.json()

assert respose_dct["points_gained"] == 0, "Должно быть 0 баллов (неверный ответ)"
assert respose_dct["quantity_done"] == 0, "Должно быть 0 отвеченных (неверный ответ)"
assert respose_dct["quantity_done_correct"] == 0, "Должно быть 0 отвеченных верно (неверный ответ)"
assert respose_dct["progress"] == 0, "Должно быть 0, еще 1 попытка для ответа"

api_auth_with_sub_client.post(
questions_constants.SINGLE_CORRECT_POST,
data=json.dumps(data),
content_type="application/json",
)
response = api_auth_with_sub_client.get(constants.TASK_RESULT)
respose_dct = response.json()

assert respose_dct["points_gained"] == 0, "Должно быть 0 баллов (неверный ответ)"
assert respose_dct["quantity_done"] == 1, "1 ответ должен быть засчитан"
assert respose_dct["quantity_done_correct"] == 0, "Должно быть 0 отвеченных верно (неверный ответ)"
assert respose_dct["progress"] == 100, "Должно быть 0, еще 1 попытка для ответа"


class TestTaskOfSkillPathResponse:
"""
Expand Down
27 changes: 27 additions & 0 deletions apps/tests/fixtures/questions/fixtures_single.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,33 @@ def single_question_data_with_hint(task_wo_questions) -> TaskObject:
return task_obj


@pytest.fixture
def single_question_data_with_tryes(task_wo_questions) -> TaskObject:
"""Вопрос из ПЛАТНОГО навыка с ПОПЫТКАМИ к ответу."""
question = QuestionSingleAnswer(
text="123",
attempts_before_hint=2,
)
question.save()

answer = AnswerSingle(text="asd", is_correct=True, question=question)
answer.save()

answer1 = AnswerSingle(text="asd2", is_correct=False, question=question)
answer1.save()

answer2 = AnswerSingle(text="asd1", is_correct=False, question=question)
answer2.save()

task_obj = TaskObject(
task=task_wo_questions,
content_type=ContentType.objects.get_for_model(QuestionSingleAnswer),
object_id=1,
)
task_obj.save()
return task_obj


@pytest.fixture
@override_settings(task_always_eager=True)
def question_data_answered(question_data, user):
Expand Down
Loading

0 comments on commit d9148dc

Please sign in to comment.