Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add create responses and list/retrieve questionnaires #8

Merged
merged 9 commits into from
Mar 13, 2023
Merged
35 changes: 34 additions & 1 deletion b2b/feedback/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
from django.contrib import admin
from nested_admin.nested import NestedModelAdmin, NestedStackedInline

from .models import Client, Question, QuestionChoice, Questionnaire
from .models import (
Answer,
AnswerChoice,
Client,
Question,
QuestionChoice,
Questionnaire,
Response,
)


@admin.register(Client)
Expand Down Expand Up @@ -41,3 +49,28 @@ class QuestionnaireAdmin(NestedModelAdmin):
list_editable = ["due_at"]
list_select_related = ["client_rep"]
search_fields = ["title__icontains", "description__icontains"]


class AnswerChoiceInline(NestedStackedInline):
"""Inline class for answer choices."""

model = AnswerChoice
extra = 0


class AnswerInline(NestedStackedInline):
"""Inline class for response answers."""

model = Answer
inlines = [AnswerChoiceInline]
extra = 0


@admin.register(Response)
class ResponseAdmin(NestedModelAdmin):
"""Define admin configuration for the Questionnaire model."""

autocomplete_fields = ["questionnaire", "respondent"]
inlines = [AnswerInline]
list_select_related = ["questionnaire", "respondent"]
search_fields = ["questionnaire__title", "respondent__email"]
17 changes: 17 additions & 0 deletions b2b/feedback/migrations/0004_alter_answer_answer_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.1.7 on 2023-03-12 23:23

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("feedback", "0003_alter_client_email"),
]

operations = [
migrations.AlterField(
model_name="answer",
name="answer_text",
field=models.TextField(blank=True, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 4.1.7 on 2023-03-13 15:27

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


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("feedback", "0004_alter_answer_answer_text"),
]

operations = [
migrations.AddField(
model_name="questionnaire",
name="author",
field=models.ForeignKey(
blank=True,
limit_choices_to={"groups__name": "Sales Managers"},
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="sales_manager_questionnaires",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="questionnaire",
name="client_rep",
field=models.ForeignKey(
blank=True,
limit_choices_to={"groups__name": "Corporate Client Representatives"},
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="client_rep_questionnaires",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="response",
name="questionnaire",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="questionnaire_responses",
to="feedback.questionnaire",
),
),
migrations.AlterField(
model_name="response",
name="respondent",
field=models.ForeignKey(
limit_choices_to={"groups__name": "Corporate Client Representatives"},
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="client_rep_responses",
to=settings.AUTH_USER_MODEL,
),
),
]
20 changes: 15 additions & 5 deletions b2b/feedback/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,18 @@ def __str__(self):
class Questionnaire(models.Model):
"""The Questionnaire model."""

author = models.ForeignKey(
User,
blank=True,
null=True,
limit_choices_to={"groups__name": SALES_MANAGER_GROUP},
related_name="sales_manager_questionnaires",
on_delete=models.SET_NULL,
)
client_rep = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name="questionnaires",
related_name="client_rep_questionnaires",
blank=True,
null=True,
limit_choices_to={"groups__name": CLIENT_REP_GROUP},
Expand Down Expand Up @@ -101,11 +109,13 @@ class Response(models.Model):
respondent = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name="responses",
related_name="client_rep_responses",
limit_choices_to={"groups__name": CLIENT_REP_GROUP},
null=True,
)
questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE)
questionnaire = models.ForeignKey(
Questionnaire, on_delete=models.CASCADE, related_name="questionnaire_responses"
)
submitted_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
Expand All @@ -120,11 +130,11 @@ class Answer(models.Model):
Response, on_delete=models.CASCADE, related_name="answers"
)
question = models.ForeignKey(Question, on_delete=models.CASCADE)
answer_text = models.TextField()
answer_text = models.TextField(null=True, blank=True)

def __str__(self):
"""Return the answer_text."""
return self.answer_text
return f"Answer to {self.question}"


class AnswerChoice(models.Model):
Expand Down
12 changes: 12 additions & 0 deletions b2b/feedback/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@ def has_permission(self, request, view):
"""Return true if current user is in the Client Representatives group."""
client_rep_group, _ = Group.objects.get_or_create(name=CLIENT_REP_GROUP)
return client_rep_group in request.user.groups.all()


class IsSalesManagerOrClientRep(BasePermission):
"""Sales Managers or Client Representatives permission class."""

def has_permission(self, request, view):
"""Return true if current user is in either group."""
sales_manager_group, _ = Group.objects.get_or_create(name=SALES_MANAGER_GROUP)
client_rep_group, _ = Group.objects.get_or_create(name=CLIENT_REP_GROUP)

user_groups = request.user.groups.all()
return sales_manager_group in user_groups or client_rep_group in user_groups
113 changes: 106 additions & 7 deletions b2b/feedback/serializers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
"""Serializers for the feedback app."""
from django.db import transaction
from django.http import Http404
from django.shortcuts import get_object_or_404
from rest_framework import serializers

from .models import Client, Question, QuestionChoice, Questionnaire
from .models import (
Answer,
AnswerChoice,
Client,
Question,
QuestionChoice,
Questionnaire,
Response,
)


class ClientSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -56,22 +66,33 @@ class Meta:
]


class QuestionnaireSerializer(serializers.ModelSerializer):
class QuestionnaireListSerializer(serializers.ModelSerializer):
"""The questionnaire list serializer."""

class Meta:
"""Questionnaire list serializer meta class."""

model = Questionnaire
fields = [
"id",
"title",
"due_at",
]


class QuestionnaireSerializer(QuestionnaireListSerializer):
"""The questionnaire serializer."""

questions = QuestionSerializer(many=True, required=True)

class Meta:
class Meta(QuestionnaireListSerializer.Meta):
"""Questionnaire serializer meta class."""

model = Questionnaire
fields = [
"id",
fields = QuestionnaireListSerializer.Meta.fields + [
"client_rep",
"title",
"description",
"is_active",
"due_at",
"created_at",
"questions",
]
Expand Down Expand Up @@ -99,3 +120,81 @@ def validate_questions(self, value):
if not value:
raise serializers.ValidationError("At least one question is required.")
return value


class AnswerChoiceSerializer(serializers.ModelSerializer):
"""The answer choice serializer."""

question_choice_id = serializers.IntegerField()

class Meta:
"""Answer choice serializer meta class."""

model = AnswerChoice
fields = [
"id",
"question_choice_id",
]


class AnswerSerializer(serializers.ModelSerializer):
"""The answer serializer."""

choices = AnswerChoiceSerializer(many=True, required=False)
question_id = serializers.IntegerField()

class Meta:
"""Answer serializer meta class."""

model = Answer
fields = [
"id",
"answer_text",
"question_id",
"choices",
]


class ResponseSerializer(serializers.ModelSerializer):
"""The response serializer."""

answers = AnswerSerializer(many=True, required=True)

class Meta:
"""Response serializer meta class."""

model = Response
fields = [
"id",
"respondent",
"submitted_at",
"answers",
]
read_only_fields = [
"respondent",
]

def create(self, validated_data):
"""Create a response."""
with transaction.atomic():
answers = validated_data.pop("answers", [])
response = Response.objects.create(**validated_data)

for answer in answers:
choices = answer.pop("choices", [])
answer = Answer.objects.create(**answer, response=response)
choice_objs = [
AnswerChoice(**choice, answer=answer) for choice in choices
]
AnswerChoice.objects.bulk_create(choice_objs)

return response

def validate(self, attrs):
"""Validate that current user is assigned to the questionnaire."""
questionnaire = get_object_or_404(
Questionnaire, pk=self.context["questionnaire_id"]
)
if questionnaire.client_rep != self.context["user"]:
raise Http404()
return super().validate(attrs)
Loading