Skip to content

Commit

Permalink
Merge branch 'develop' into feature/update-questions-based-on-feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
juuso-j committed Feb 8, 2024
2 parents 9bc3827 + 550e63e commit 8444c7d
Show file tree
Hide file tree
Showing 17 changed files with 248 additions and 34 deletions.
2 changes: 0 additions & 2 deletions deploy/docker_uwsgi.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
chdir = /mpbackend
# Django's wsgi file
module = mpbackend.wsgi
# full path to python virtual env
home = /mpbackend/env
uid = appuser
gid = root
# enable uwsgi master process
Expand Down
2 changes: 1 addition & 1 deletion mpbackend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@
"Authorization",
]
CORS_ALLOW_CREDENTIALS = True

CORS_ORIGIN_WHITELIST = env("CORS_ORIGIN_WHITELIST")

CSRF_TRUSTED_ORIGINS = ["http://localhost:8080"]

SECURE_CROSS_ORIGIN_OPENER_POLICY = None

REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
Expand Down
10 changes: 6 additions & 4 deletions mpbackend/urls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# from django.conf import settings
from django.contrib import admin
from django.urls import include, path, re_path
from drf_spectacular.views import (
Expand All @@ -8,13 +9,16 @@

import account.api.urls
import profiles.api.urls
from profiles.views import get_csrf

urlpatterns = [
re_path("^admin/", admin.site.urls),
# re_path(r"^api/v1/", include(router.urls)),
re_path(r"^api/account/", include(account.api.urls), name="account"),
re_path(r"^api/v1/", include(profiles.api.urls), name="profiles"),
# path("api-auth/", include("rest_framework.urls")),
]

# NOTE, consider disabling in production. if settings.DEBUG:
urlpatterns += [
path("api/v1/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/v1/schema/swagger-ui/",
Expand All @@ -26,6 +30,4 @@
SpectacularRedocView.as_view(url_name="schema"),
name="redoc",
),
path("csrf/", get_csrf)
# path("api-auth/", include("rest_framework.urls")),
]
41 changes: 40 additions & 1 deletion profiles/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from profiles.models import (
Answer,
AnswerOther,
Option,
PostalCode,
PostalCodeResult,
Expand Down Expand Up @@ -58,7 +59,46 @@ class Meta:
model = Option


@admin.register(Answer)
class AnswerAdmin(DisableDeleteAdminMixin, ReadOnlyFieldsAdminMixin, admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.filter(option__is_other=False)
return qs

class Meta:
model = Answer


@admin.register(AnswerOther)
class AnswerOtherAdmin(
DisableDeleteAdminMixin, ReadOnlyFieldsAdminMixin, admin.ModelAdmin
):
list_display = (
"question_description",
"sub_question_description",
"other",
)

def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.filter(option__is_other=True)
return qs

def other(self, obj):
return obj.option.other

def question_description(self, obj):
if obj.question:
return obj.question.question_en
else:
return obj.sub_question.question.question_en

def sub_question_description(self, obj):
if obj.sub_question:
return obj.sub_question.description_en
return None

class Meta:
model = Answer

Expand Down Expand Up @@ -90,7 +130,6 @@ class Meta:
admin.site.register(SubQuestionCondition, SubQuestionConditionAdmin)
admin.site.register(Option, OptionAdmin)
admin.site.register(Result, ResultAdmin)
admin.site.register(Answer, AnswerAdmin)
admin.site.register(PostalCode, PostalCodeAdmin)
admin.site.register(PostalCodeType, PostalCodeTypeAdmin)
admin.site.register(PostalCodeResult, PostalCodeResultAdmin)
1 change: 1 addition & 0 deletions profiles/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class SubQuestionRequestSerializer(serializers.Serializer):
class AnswerRequestSerializer(QuestionRequestSerializer):
option = serializers.IntegerField()
sub_question = serializers.IntegerField(required=False)
other = serializers.CharField(required=False)


class InConditionResponseSerializer(serializers.Serializer):
Expand Down
50 changes: 45 additions & 5 deletions profiles/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ def get_questions_with_conditions(self, request):
return self.get_paginated_response(serializer.data)

@extend_schema(
description="Returns current state of condition for all questions that have a condition."
"If true, the condition has been met and can be displayed for the user.",
description="Returns the current state of conditions for all questions that have a condition."
"If true, the condition has been met and the question can be displayed for the user.",
parameters=[],
examples=None,
responses={
Expand Down Expand Up @@ -247,6 +247,38 @@ def get_questions_conditions_states(self, request):
else:
return Response(serializer.errors, status=400)

@extend_schema(
description="Returns the current state of conditions for all Sub questions that have a condition."
"If true, the condition has been met and the sub question can be displayed for the user.",
parameters=[],
examples=None,
responses={
200: OpenApiResponse(
description="List of states, containing the sub question ID and the state."
)
},
)
@action(detail=False, methods=["GET"], permission_classes=[IsAuthenticated])
def get_sub_questions_conditions_states(self, request):
sub_questions_with_cond_qs = SubQuestion.objects.filter(
sub_question_conditions__isnull=False
)
user = request.user
states = []
for sub_question in sub_questions_with_cond_qs:
state = {"id": sub_question.id}
sub_question_condition = SubQuestionCondition.objects.filter(
sub_question=sub_question
).first()
state["state"] = sub_question_condition_met(sub_question_condition, user)
states.append(state)
serializer = QuestionsConditionsStatesSerializer(data=states, many=True)
if serializer.is_valid():
validated_data = serializer.validated_data
return Response(validated_data)
else:
return Response(serializer.errors, status=400)

@action(
detail=False,
methods=["GET"],
Expand Down Expand Up @@ -474,10 +506,11 @@ def get_permissions(self):
responses={
201: OpenApiResponse(description="created"),
400: OpenApiResponse(
description="'option' or 'question' argument not given"
description="'option' or 'question' not found in body or for if 'is_other' is true for option"
" 'other' field is missing in body"
),
404: OpenApiResponse(
description="'option', 'question' or 'sub_question' not found"
description="'option', 'question' or 'sub_question' not found"
),
405: OpenApiResponse(
description="Question or sub question condition not met,"
Expand Down Expand Up @@ -534,7 +567,6 @@ def create(self, request, *args, **kwargs):
f"Option {option_id} not found or wrong related question.",
status=status.HTTP_404_NOT_FOUND,
)

question_condition_qs = QuestionCondition.objects.filter(question=question)
if question_condition_qs.count() > 0:
if not question_condition_met(question_condition_qs, user):
Expand All @@ -554,6 +586,14 @@ def create(self, request, *args, **kwargs):
)
if user:
filter = {"user": user, "question": question, "sub_question": sub_question}
if option.is_other:
other = request.data.get("other", None)
if not other:
return Response(
"'other' not found in body, required if is_other field is true for option.",
status=status.HTTP_400_BAD_REQUEST,
)
filter["other"] = other
queryset = Answer.objects.filter(**filter)
if queryset.count() == 0:
filter["option"] = option
Expand Down
3 changes: 1 addition & 2 deletions profiles/management/commands/import_questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def create_sub_question_condition(row_data: str, sub_question: SubQuestion):
question_number, option_order_number = row_data.split(".")
question = Question.objects.get(number=question_number)
option = Option.objects.get(question=question, order_number=option_order_number)
SubQuestionCondition.objects.create(sub_question=sub_question, option=option)
SubQuestionCondition.objects.get_or_create(sub_question=sub_question, option=option)


@db.transaction.atomic
Expand Down Expand Up @@ -286,7 +286,6 @@ def handle(self, *args, **options):
# Question.objects.all().delete()
# QuestionCondition.objects.all().delete()
# Result.objects.all().delete()

file_path = f"{get_root_dir()}/media/{FILENAME}"
excel_data = pd.read_excel(file_path, sheet_name="Yhdistetty")
excel_data = excel_data.fillna("").replace([""], [None])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.1.10 on 2024-02-05 14:45

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("profiles", "0009_subquestioncondition"),
]

operations = [
migrations.RemoveConstraint(
model_name="answer",
name="unique_user_and_option",
),
migrations.AlterField(
model_name="answer",
name="option",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="answers",
to="profiles.option",
),
),
]
18 changes: 18 additions & 0 deletions profiles/migrations/0011_answer_other.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.1.10 on 2024-02-05 14:51

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("profiles", "0010_remove_answer_unique_user_and_option_and_more"),
]

operations = [
migrations.AddField(
model_name="answer",
name="other",
field=models.TextField(blank=True, null=True),
),
]
18 changes: 18 additions & 0 deletions profiles/migrations/0012_option_is_other.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.1.10 on 2024-02-06 11:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("profiles", "0011_answer_other"),
]

operations = [
migrations.AddField(
model_name="option",
name="is_other",
field=models.BooleanField(default=False, verbose_name="is other textfield"),
),
]
23 changes: 23 additions & 0 deletions profiles/migrations/0013_answerother.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.1.10 on 2024-02-07 07:14

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("profiles", "0012_option_is_other"),
]

operations = [
migrations.CreateModel(
name="AnswerOther",
fields=[],
options={
"proxy": True,
"indexes": [],
"constraints": [],
},
bases=("profiles.answer",),
),
]
17 changes: 9 additions & 8 deletions profiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Option(models.Model):
)
results = models.ManyToManyField("Result", related_name="options")
order_number = models.PositiveSmallIntegerField(null=True)
is_other = models.BooleanField(default=False, verbose_name="is other textfield")

class Meta:
ordering = ["question__number", "sub_question__question__number"]
Expand All @@ -72,8 +73,10 @@ class Answer(models.Model):
"account.User", related_name="answers", on_delete=models.CASCADE
)
option = models.ForeignKey(
"Option", related_name="answers", on_delete=models.CASCADE
"Option", related_name="answers", null=True, on_delete=models.CASCADE
)
other = models.TextField(null=True, blank=True)

question = models.ForeignKey(
"Question", related_name="answers", null=True, on_delete=models.CASCADE
)
Expand All @@ -84,15 +87,13 @@ class Answer(models.Model):
created = models.DateTimeField(auto_now_add=True)

class Meta:
constraints = [
models.UniqueConstraint(
fields=["user", "option"], name="unique_user_and_option"
)
]
ordering = ["id"]

def __str__(self):
return f"{self.option.value}"

class AnswerOther(Answer):
# Proxy model that allows registerin Answer model twice to the Admin
class Meta:
proxy = True


class QuestionCondition(models.Model):
Expand Down
25 changes: 25 additions & 0 deletions profiles/tests/api/test_answer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ def test_post_answer(api_client_authenticated, users, questions, options):
assert user.result.value == "negative result"


@pytest.mark.django_db
def test_post_answer_with_other_option(api_client, users, answers, questions, options):
user = users.get(username="no answers user")
token = Token.objects.create(user=user)
api_client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
option = options.get(value="other")
question3 = questions.get(number="3")
answer_url = reverse("profiles:answer-list")
response = api_client.post(
answer_url,
{"option": option.id, "question": question3.id, "other": "test data"},
)
assert response.status_code == 201
answers_qs = Answer.objects.filter(user=user)
assert answers_qs.count() == 1
assert answers_qs.first().other == "test data"
# Test posting without 'other' field to a option where is_other is True
response = api_client.post(
answer_url, {"option": option.id, "question": question3.id}
)
assert response.status_code == 400
answers_qs = Answer.objects.filter(user=user)
assert answers_qs.count() == 1


@pytest.mark.django_db
def test_post_answer_answer_is_updated(
api_client_authenticated, users, answers, questions, options
Expand Down
Loading

0 comments on commit 8444c7d

Please sign in to comment.