Migrate form data to json field

This commit is contained in:
Christian Cueni 2023-02-06 15:16:08 +01:00
parent f5c7ab77e1
commit aa5b744285
12 changed files with 292 additions and 169 deletions

View File

@ -157,12 +157,9 @@ const MAX_STEPS = 12;
const sendFeedbackMutation = graphql(`
mutation SendFeedbackMutation($input: SendFeedbackInput!) {
sendFeedback(input: $input) {
feedbackResponse {
id
satisfaction
goalAttainment
proficiency
receivedMaterials
materialsRating
}
errors {
field
messages
@ -205,17 +202,19 @@ const sendFeedback = () => {
return;
}
const input: SendFeedbackInput = reactive({
materialsRating,
courseNegativeFeedback,
coursePositiveFeedback,
goalAttainment,
instructorCompetence,
instructorRespect,
instructorOpenFeedback,
data: {
materials_rating: materialsRating,
course_negative_feedback: courseNegativeFeedback,
course_positive_feedback: coursePositiveFeedback,
goald_attainment: goalAttainment,
instructor_competence: instructorCompetence,
instructor_respect: instructorRespect,
instructor_open_feedback: instructorOpenFeedback,
satisfaction,
proficiency,
receivedMaterials,
wouldRecommend,
received_materials: receivedMaterials,
would_recommend: wouldRecommend,
},
page: props.page.translation_key,
courseSession: courseSession.id,
});

View File

@ -3,13 +3,13 @@ import type { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-
import * as types from "./graphql";
const documents = {
"\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n":
"\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n":
types.SendFeedbackMutationDocument,
};
export function graphql(
source: "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n"
): typeof documents["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n"];
source: "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"
): typeof documents["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"];
export function graphql(source: string): unknown;
export function graphql(source: string) {

View File

@ -17,6 +17,7 @@ export type Scalars = {
Int: number;
Float: number;
DateTime: any;
GenericScalar: any;
JSONString: any;
PositiveInt: any;
UUID: any;
@ -152,6 +153,11 @@ export type CircleSiblingsArgs = {
searchQuery?: InputMaybe<Scalars["String"]>;
};
export type CircleDocument = {
__typename?: "CircleDocument";
id?: Maybe<Scalars["ID"]>;
};
export type CollectionObjectType = {
__typename?: "CollectionObjectType";
ancestors: Array<Maybe<CollectionObjectType>>;
@ -520,6 +526,14 @@ export type ErrorType = {
messages: Array<Scalars["String"]>;
};
export type FeedbackResponse = Node & {
__typename?: "FeedbackResponse";
circle: Circle;
courseSession: CourseSession;
data?: Maybe<Scalars["GenericScalar"]>;
id: Scalars["ID"];
};
export type FloatBlock = StreamFieldInterface & {
__typename?: "FloatBlock";
blockType: Scalars["String"];
@ -1170,6 +1184,10 @@ export type MutationSendFeedbackArgs = {
input: SendFeedbackInput;
};
export type Node = {
id: Scalars["ID"];
};
export type Page = PageInterface & {
__typename?: "Page";
aliasOf?: Maybe<Page>;
@ -1581,6 +1599,7 @@ export type RichTextBlock = StreamFieldInterface & {
export type Search =
| Circle
| CircleDocument
| CompetencePage
| CompetenceProfilePage
| Course
@ -1609,37 +1628,16 @@ export type SecurityRequestResponseLog = {
export type SendFeedbackInput = {
clientMutationId?: InputMaybe<Scalars["String"]>;
courseNegativeFeedback?: InputMaybe<Scalars["String"]>;
coursePositiveFeedback?: InputMaybe<Scalars["String"]>;
goalAttainment?: InputMaybe<Scalars["Int"]>;
id?: InputMaybe<Scalars["Int"]>;
instructorCompetence?: InputMaybe<Scalars["Int"]>;
instructorOpenFeedback?: InputMaybe<Scalars["String"]>;
instructorRespect?: InputMaybe<Scalars["Int"]>;
materialsRating?: InputMaybe<Scalars["Int"]>;
courseSession: Scalars["Int"];
data?: InputMaybe<Scalars["GenericScalar"]>;
page: Scalars["String"];
proficiency?: InputMaybe<Scalars["Int"]>;
receivedMaterials?: InputMaybe<Scalars["Boolean"]>;
satisfaction?: InputMaybe<Scalars["Int"]>;
wouldRecommend?: InputMaybe<Scalars["Boolean"]>;
};
export type SendFeedbackPayload = {
__typename?: "SendFeedbackPayload";
clientMutationId?: Maybe<Scalars["String"]>;
courseNegativeFeedback?: Maybe<Scalars["String"]>;
coursePositiveFeedback?: Maybe<Scalars["String"]>;
errors?: Maybe<Array<Maybe<ErrorType>>>;
goalAttainment?: Maybe<Scalars["Int"]>;
id?: Maybe<Scalars["Int"]>;
instructorCompetence?: Maybe<Scalars["Int"]>;
instructorOpenFeedback?: Maybe<Scalars["String"]>;
instructorRespect?: Maybe<Scalars["Int"]>;
materialsRating?: Maybe<Scalars["Int"]>;
proficiency?: Maybe<Scalars["Int"]>;
receivedMaterials?: Maybe<Scalars["Boolean"]>;
satisfaction?: Maybe<Scalars["Int"]>;
wouldRecommend?: Maybe<Scalars["Boolean"]>;
feedbackResponse?: Maybe<FeedbackResponse>;
};
export type SiteObjectType = {
@ -1851,12 +1849,7 @@ export type SendFeedbackMutationMutation = {
__typename?: "Mutation";
sendFeedback?: {
__typename?: "SendFeedbackPayload";
id?: number | null;
satisfaction?: number | null;
goalAttainment?: number | null;
proficiency?: number | null;
receivedMaterials?: boolean | null;
materialsRating?: number | null;
feedbackResponse?: { __typename?: "FeedbackResponse"; id: string } | null;
errors?: Array<{
__typename?: "ErrorType";
field: string;
@ -1898,15 +1891,19 @@ export const SendFeedbackMutationDocument = {
value: { kind: "Variable", name: { kind: "Name", value: "input" } },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "feedbackResponse" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "satisfaction" } },
{ kind: "Field", name: { kind: "Name", value: "goalAttainment" } },
{ kind: "Field", name: { kind: "Name", value: "proficiency" } },
{ kind: "Field", name: { kind: "Name", value: "receivedMaterials" } },
{ kind: "Field", name: { kind: "Name", value: "materialsRating" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "errors" },

View File

@ -79,6 +79,10 @@ type Circle implements PageInterface {
ancestors(limit: PositiveInt, offset: PositiveInt, order: String, searchQuery: String, id: ID): [PageInterface!]!
}
type CircleDocument {
id: ID
}
type CollectionObjectType {
id: ID!
path: String!
@ -281,6 +285,13 @@ type ErrorType {
messages: [String!]!
}
type FeedbackResponse implements Node {
id: ID!
data: GenericScalar
circle: Circle!
courseSession: CourseSession!
}
type FloatBlock implements StreamFieldInterface {
id: String
blockType: String!
@ -289,6 +300,8 @@ type FloatBlock implements StreamFieldInterface {
value: Float!
}
scalar GenericScalar
type ImageChooserBlock implements StreamFieldInterface {
id: String
blockType: String!
@ -608,6 +621,10 @@ type Mutation {
sendFeedback(input: SendFeedbackInput!): SendFeedbackPayload
}
interface Node {
id: ID!
}
type Page implements PageInterface {
id: ID
path: String!
@ -783,42 +800,21 @@ type RichTextBlock implements StreamFieldInterface {
value: String!
}
union Search = CoursePage | LearningPath | Topic | Circle | LearningSequence | LearningUnit | LearningContent | CompetenceProfilePage | CompetencePage | PerformanceCriteria | MediaLibraryPage | MediaCategoryPage | Page | LibraryDocument | User | SecurityRequestResponseLog | Course | CourseCategory | CourseCompletion | CourseSession | CourseSessionUser
union Search = CoursePage | LearningPath | Topic | Circle | LearningSequence | LearningUnit | LearningContent | CompetenceProfilePage | CompetencePage | PerformanceCriteria | MediaLibraryPage | MediaCategoryPage | Page | LibraryDocument | User | SecurityRequestResponseLog | Course | CourseCategory | CourseCompletion | CourseSession | CourseSessionUser | CircleDocument
type SecurityRequestResponseLog {
id: ID
}
input SendFeedbackInput {
id: Int
page: String!
satisfaction: Int
goalAttainment: Int
proficiency: Int
receivedMaterials: Boolean
materialsRating: Int
instructorCompetence: Int
instructorRespect: Int
instructorOpenFeedback: String
wouldRecommend: Boolean
coursePositiveFeedback: String
courseNegativeFeedback: String
courseSession: Int!
data: GenericScalar
clientMutationId: String
}
type SendFeedbackPayload {
id: Int
satisfaction: Int
goalAttainment: Int
proficiency: Int
receivedMaterials: Boolean
materialsRating: Int
instructorCompetence: Int
instructorRespect: Int
instructorOpenFeedback: String
wouldRecommend: Boolean
coursePositiveFeedback: String
courseNegativeFeedback: String
feedbackResponse: FeedbackResponse
errors: [ErrorType]
clientMutationId: String
}

View File

@ -1,3 +1,4 @@
from factory import Dict
from factory.django import DjangoModelFactory
from factory.fuzzy import FuzzyChoice, FuzzyInteger
@ -5,33 +6,37 @@ from vbv_lernwelt.feedback.models import FeedbackResponse
class FeedbackFactory(DjangoModelFactory):
class Meta:
model = FeedbackResponse
satisfaction = FuzzyInteger(2, 4)
goal_attainment = FuzzyInteger(3, 4)
proficiency = FuzzyChoice([20, 40, 60, 80])
received_materials = FuzzyChoice([True, False])
materials_rating = FuzzyInteger(2, 4)
instructor_competence = FuzzyInteger(3, 4)
instructor_respect = FuzzyInteger(3, 4)
instructor_open_feedback = FuzzyChoice(
data = Dict(
{
"satisfaction": FuzzyInteger(2, 4),
"goal_attainment": FuzzyInteger(3, 4),
"proficiency": FuzzyChoice([20, 40, 60, 80]),
"received_materials": FuzzyChoice([True, False]),
"materials_rating": FuzzyInteger(2, 4),
"instructor_competence": FuzzyInteger(3, 4),
"instructor_respect": FuzzyInteger(3, 4),
"instructor_open_feedback": FuzzyChoice(
[
"Alles gut, manchmal etwas langfädig",
"Super, bin begeistert",
"Ok, enspricht den Erwartungen",
]
)
would_recommend = FuzzyChoice([True, False])
course_positive_feedback = FuzzyChoice(
),
"would_recommend": FuzzyChoice([True, False]),
"course_positive_feedback": FuzzyChoice(
[
"Die Präsentation war super",
"Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
]
)
course_negative_feedback = FuzzyChoice(
),
"course_negative_feedback": FuzzyChoice(
[
"Es wäre praktisch, Zugang zu einer FAQ zu haben.",
"Es wäre schön, mehr Videos hinzuzufügen.",
]
),
}
)
class Meta:
model = FeedbackResponse

View File

@ -1,13 +1,56 @@
import graphene
from graphene_django.rest_framework.mutation import SerializerMutation
import structlog
from graphene import ClientIDMutation, Field, Int, List, String
from graphene.types.generic import GenericScalar
from graphene_django.types import ErrorType
from vbv_lernwelt.feedback.serializers import FeedbackResponseSerializer
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.feedback.graphql.types import FeedbackResponse as FeedbackResponseType
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer
from wagtail.models import Page
logger = structlog.get_logger(__name__)
class SendFeedback(SerializerMutation):
class Meta:
serializer_class = FeedbackResponseSerializer
model_operations = ["create"]
# https://medium.com/open-graphql/jsonfield-models-in-graphene-django-308ae43d14ee
class SendFeedback(ClientIDMutation):
feedback_response = Field(FeedbackResponseType)
errors = List(
ErrorType, description="May contain more than one error for same field."
)
class Input:
page = String(required=True)
course_session = Int(required=True)
data = GenericScalar()
@classmethod
def mutate_and_get_payload(cls, _, info, **input):
page_key = input["page"]
course_session_id = input["course_session"]
logger.info("creating feedback")
learning_content = Page.objects.get(
translation_key=page_key, locale__language_code="de-CH"
)
circle = learning_content.get_parent().specific
course_session = CourseSession.objects.get(id=course_session_id)
data = input.get("data", {})
serializer = CourseFeedbackSerializer(data=data)
if not serializer.is_valid():
logger.error(serializer.errors)
return SendFeedback(errors=serializer.errors)
feedback_response = FeedbackResponse.objects.create(
circle=circle,
course_session=course_session,
data=serializer.validated_data,
)
logger.info(feedback_response)
return SendFeedback(feedback_response=feedback_response)
class Mutation(object):

View File

@ -1,8 +1,13 @@
from graphene.relay import Node
from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType
from vbv_lernwelt.feedback.models import Feedback
from vbv_lernwelt.feedback.models import FeedbackResponse as FeedbackResponseModel
# class FeedbackType(DjangoObjectType):
# class Meta:
# model = Feedback
class FeedbackResponse(DjangoObjectType):
data = GenericScalar()
class Meta:
model = FeedbackResponseModel
interfaces = (Node,)

View File

@ -0,0 +1,66 @@
# Generated by Django 3.2.13 on 2023-02-06 10:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("feedback", "0002_auto_20230111_1044"),
]
operations = [
migrations.RemoveField(
model_name="feedbackresponse",
name="course_negative_feedback",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="course_positive_feedback",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="goal_attainment",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="instructor_competence",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="instructor_open_feedback",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="instructor_respect",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="materials_rating",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="proficiency",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="received_materials",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="satisfaction",
),
migrations.RemoveField(
model_name="feedbackresponse",
name="would_recommend",
),
migrations.AddField(
model_name="feedbackresponse",
name="data",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="feedbackresponse",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -37,17 +37,20 @@ class FeedbackResponse(models.Model):
EIGHTY = 80, "80%"
HUNDRED = 100, "100%"
satisfaction = FeedbackIntegerField()
goal_attainment = FeedbackIntegerField()
proficiency = models.IntegerField(null=True)
received_materials = models.BooleanField(null=True)
materials_rating = FeedbackIntegerField()
instructor_competence = FeedbackIntegerField()
instructor_respect = FeedbackIntegerField()
instructor_open_feedback = models.TextField(blank=True)
would_recommend = models.BooleanField(null=True)
course_positive_feedback = models.TextField(blank=True)
course_negative_feedback = models.TextField(blank=True)
# satisfaction = FeedbackIntegerField()
# goal_attainment = FeedbackIntegerField()
# proficiency = models.IntegerField(null=True)
# received_materials = models.BooleanField(null=True)
# materials_rating = FeedbackIntegerField()
# instructor_competence = FeedbackIntegerField()
# instructor_respect = FeedbackIntegerField()
# instructor_open_feedback = models.TextField(blank=True)
# would_recommend = models.BooleanField(null=True)
# course_positive_feedback = models.TextField(blank=True)
# course_negative_feedback = models.TextField(blank=True)
data = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
circle = models.ForeignKey("learnpath.Circle", models.PROTECT)
course_session = models.ForeignKey("course.CourseSession", models.PROTECT)

View File

@ -1,32 +1,31 @@
import structlog
from rest_framework import serializers
from wagtail.models import Page
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.feedback.models import FeedbackResponse
logger = structlog.get_logger(__name__)
class FeedbackResponseSerializer(serializers.ModelSerializer):
page = serializers.CharField(write_only=True)
course_session = serializers.CharField(write_only=True)
class Meta:
model = FeedbackResponse
exclude = ["circle"]
# extra_kwargs = {"course", {"read_only": True}}
def create(self, validated_data):
logger.info("creating feedback")
page_key = validated_data.pop("page")
course_session_id = validated_data.pop("course_session")
learning_content = Page.objects.get(
translation_key=page_key, locale__language_code="de-CH"
class FeedbackIntegerField(serializers.IntegerField):
def __init__(self, **kwargs):
super().__init__(
required=False, allow_null=True, min_value=1, max_value=5, **kwargs
)
circle = learning_content.get_parent().specific
course_session = CourseSession.objects.get(id=course_session_id)
return FeedbackResponse.objects.create(
**validated_data, circle=circle, course_session=course_session
class CourseFeedbackSerializer(serializers.Serializer):
satisfaction = FeedbackIntegerField()
goal_attainment = FeedbackIntegerField()
proficiency = serializers.IntegerField(required=False, allow_null=True)
received_materials = serializers.BooleanField(required=False, allow_null=True)
materials_rating = FeedbackIntegerField()
instructor_competence = FeedbackIntegerField()
instructor_respect = FeedbackIntegerField()
instructor_open_feedback = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
would_recommend = serializers.BooleanField(required=False, allow_null=True)
course_positive_feedback = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
course_negative_feedback = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)

View File

@ -159,17 +159,25 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
FeedbackFactory(
circle=circle,
course_session=csu.course_session,
satisfaction=feedback_data["satisfaction"][i],
goal_attainment=feedback_data["goal_attainment"][i],
proficiency=feedback_data["proficiency"][i],
received_materials=feedback_data["received_materials"][i],
materials_rating=feedback_data["materials_rating"][i],
instructor_competence=feedback_data["instructor_competence"][i],
instructor_open_feedback=feedback_data["instructor_open_feedback"][i],
instructor_respect=feedback_data["instructor_respect"][i],
would_recommend=feedback_data["would_recommend"][i],
course_positive_feedback=feedback_data["course_positive_feedback"][i],
course_negative_feedback=feedback_data["course_negative_feedback"][i],
data={
"satisfaction": feedback_data["satisfaction"][i],
"goal_attainment": feedback_data["goal_attainment"][i],
"proficiency": feedback_data["proficiency"][i],
"received_materials": feedback_data["received_materials"][i],
"materials_rating": feedback_data["materials_rating"][i],
"instructor_competence": feedback_data["instructor_competence"][i],
"instructor_open_feedback": feedback_data[
"instructor_open_feedback"
][i],
"instructor_respect": feedback_data["instructor_respect"][i],
"would_recommend": feedback_data["would_recommend"][i],
"course_positive_feedback": feedback_data[
"course_positive_feedback"
][i],
"course_negative_feedback": feedback_data[
"course_negative_feedback"
][i],
},
).save()
response = self.client.get(

View File

@ -49,7 +49,7 @@ def get_feedback_for_circle(request, course_id, circle_id):
course_session__course_id=course_id,
circle__expert__user=request.user,
circle_id=circle_id,
)
).order_by("created_at")
# I guess this is ok for the üK case
feedback_data = {"amount": len(feedbacks), "questions": {}}
@ -62,6 +62,8 @@ def get_feedback_for_circle(request, course_id, circle_id):
for feedback in feedbacks:
for field in FEEDBACK_FIELDS:
feedback_data["questions"][field].append(getattr(feedback, field))
data = feedback.data.get(field, None)
if data is not None:
feedback_data["questions"][field].append(data)
return Response(status=200, data=feedback_data)