From 10854c2f067d56a3a64767c994272795e63543a3 Mon Sep 17 00:00:00 2001 From: David Vogt Date: Fri, 1 Jul 2022 14:40:22 +0200 Subject: [PATCH] feat(form): multiple files in file questions - graphql schema changes The GraphQL schema is updated to allow saving and updating multiple files on an answer. The semantics are as follows: The answer's value is a list of dicts, each containing a file name, and optionally an ID. When you're creating new files, add an entry without ID to the value. Caluma will then save the file, and return it back WITH and ID. --- caluma/caluma_form/factories.py | 46 +++++++++++++++------ caluma/caluma_form/historical_schema.py | 14 +++---- caluma/caluma_form/schema.py | 45 +++++++++++--------- caluma/schema.py | 6 +-- caluma/tests/__snapshots__/test_schema.ambr | 39 +++++++++-------- 5 files changed, 89 insertions(+), 61 deletions(-) diff --git a/caluma/caluma_form/factories.py b/caluma/caluma_form/factories.py index cc604ae9a..941ef5a9e 100644 --- a/caluma/caluma_form/factories.py +++ b/caluma/caluma_form/factories.py @@ -1,5 +1,14 @@ import faker -from factory import Faker, LazyAttribute, Maybe, SubFactory, lazy_attribute +from django.db.models.signals import post_save +from factory import ( + Faker, + LazyAttribute, + Maybe, + SubFactory, + django, + lazy_attribute, + post_generation, +) from ..caluma_core.factories import DjangoModelFactory from . import models @@ -141,13 +150,6 @@ class Meta: model = models.Document -class FileFactory(DjangoModelFactory): - name = Faker("file_name") - - class Meta: - model = models.File - - class AnswerFactory(DjangoModelFactory): question = SubFactory(QuestionFactory) document = SubFactory(DocumentFactory) @@ -166,26 +168,44 @@ def value(self): return faker.Faker().pyint() elif self.question.type not in [ models.Question.TYPE_TABLE, - models.Question.TYPE_FILE, + models.Question.TYPE_FILES, models.Question.TYPE_DATE, ]: return faker.Faker().name() return None - file = Maybe( - "is_file", yes_declaration=SubFactory(FileFactory), no_declaration=None - ) date = Maybe("is_date", yes_declaration=Faker("date"), no_declaration=None) + @post_generation + @django.mute_signals(post_save) + def files(self, create, extracted, **kwargs): + if not create: + return + if self.question.type == models.Question.TYPE_FILES: + if extracted: + self.files.set(extracted) + else: + num_files = kwargs.pop("count", 1) + self.files.set( + FileFactory.create_batch(num_files, **kwargs, answer=self) + ) + class Meta: model = models.Answer class Params: - is_file = LazyAttribute(lambda a: a.question.type == models.Question.TYPE_FILE) is_date = LazyAttribute(lambda a: a.question.type == models.Question.TYPE_DATE) +class FileFactory(DjangoModelFactory): + name = Faker("file_name") + answer = SubFactory(AnswerFactory) + + class Meta: + model = models.File + + class AnswerDocumentFactory(DjangoModelFactory): answer = SubFactory(AnswerFactory) document = SubFactory(DocumentFactory) diff --git a/caluma/caluma_form/historical_schema.py b/caluma/caluma_form/historical_schema.py index c732f0c80..f40cbcae9 100644 --- a/caluma/caluma_form/historical_schema.py +++ b/caluma/caluma_form/historical_schema.py @@ -10,7 +10,7 @@ QUESTION_ANSWER_TYPES, Answer, DateAnswer, - FileAnswer, + FilesAnswer, FloatAnswer, FormDjangoObjectType, IntegerAnswer, @@ -99,7 +99,7 @@ class HistoricalFile(ObjectType): download_url = graphene.String() metadata = generic.GenericScalar() historical_answer = graphene.Field( - "caluma.caluma_form.historical_schema.HistoricalFileAnswer" + "caluma.caluma_form.historical_schema.HistoricalFilesAnswer" ) history_date = graphene.types.datetime.DateTime(required=True) history_user_id = graphene.String() @@ -118,8 +118,8 @@ class Meta: interfaces = (relay.Node,) -class HistoricalFileAnswer(FileAnswer): - value = graphene.Field( +class HistoricalFilesAnswer(FilesAnswer): + value = graphene.List( HistoricalFile, required=False, as_of=graphene.types.datetime.DateTime(required=True), @@ -127,13 +127,11 @@ class HistoricalFileAnswer(FileAnswer): def resolve_value(self, info, as_of, **args): # we need to use the HistoricalFile of the correct revision - return models.File.history.filter( - id=self.file_id, history_date__lte=as_of - ).first() + return models.File.history.filter(answer_id=self.id, history_date__lte=as_of) class Meta: model = models.Answer.history.model - exclude = ("document", "date") + exclude = ("document", "date", "files") use_connection = False interfaces = (HistoricalAnswer, graphene.Node) diff --git a/caluma/caluma_form/schema.py b/caluma/caluma_form/schema.py index c14e0710e..2d8800408 100644 --- a/caluma/caluma_form/schema.py +++ b/caluma/caluma_form/schema.py @@ -495,7 +495,7 @@ class Meta: interfaces = (Question, graphene.Node) -class FileQuestion(QuestionQuerysetMixin, FormDjangoObjectType): +class FilesQuestion(QuestionQuerysetMixin, FormDjangoObjectType): hint_text = graphene.String() class Meta: @@ -735,7 +735,7 @@ class Meta: return_field_type = Question -class SaveFileQuestion(SaveQuestion): +class SaveFilesQuestion(SaveQuestion): class Meta: serializer_class = serializers.SaveFileQuestionSerializer return_field_type = Question @@ -799,7 +799,7 @@ class IntegerAnswer(AnswerQuerysetMixin, FormDjangoObjectType): class Meta: model = models.Answer - exclude = ("document", "documents", "file", "date") + exclude = ("document", "documents", "files", "date") use_connection = False interfaces = (Answer, graphene.Node) @@ -809,7 +809,7 @@ class FloatAnswer(AnswerQuerysetMixin, FormDjangoObjectType): class Meta: model = models.Answer - exclude = ("document", "documents", "file", "date") + exclude = ("document", "documents", "files", "date") use_connection = False interfaces = (Answer, graphene.Node) @@ -822,7 +822,7 @@ def resolve_value(self, info, **args): class Meta: model = models.Answer - exclude = ("document", "documents", "file") + exclude = ("document", "documents", "files") use_connection = False interfaces = (Answer, graphene.Node) @@ -847,7 +847,7 @@ def resolve_selected_option(self, info, **args): class Meta: model = models.Answer - exclude = ("document", "documents", "file", "date") + exclude = ("document", "documents", "files", "date") use_connection = False interfaces = (Answer, graphene.Node) @@ -858,7 +858,7 @@ class ListAnswer(AnswerQuerysetMixin, FormDjangoObjectType): class Meta: model = models.Answer - exclude = ("document", "documents", "file", "date") + exclude = ("document", "documents", "files", "date") use_connection = False interfaces = (Answer, graphene.Node) @@ -896,7 +896,7 @@ def resolve_value(self, info, **args): class Meta: model = models.Answer - exclude = ("documents", "file", "date") + exclude = ("documents", "files", "date") use_connection = False interfaces = (Answer, graphene.Node) @@ -906,7 +906,6 @@ class File(FormDjangoObjectType): upload_url = graphene.String() download_url = graphene.String() metadata = generic.GenericScalar() - answer = graphene.Field("caluma.caluma_form.schema.FileAnswer") class Meta: model = models.File @@ -914,15 +913,15 @@ class Meta: fields = "__all__" -class FileAnswer(AnswerQuerysetMixin, FormDjangoObjectType): - value = graphene.Field(File, required=True) +class FilesAnswer(AnswerQuerysetMixin, FormDjangoObjectType): + value = graphene.List(File, required=True) def resolve_value(self, info, **args): - return self.file + return self.files.all() class Meta: model = models.Answer - exclude = ("document", "documents", "date") + exclude = ("document", "documents", "date", "files") use_connection = False interfaces = (Answer, graphene.Node) @@ -992,9 +991,17 @@ class Meta: return_field_type = Answer -class SaveDocumentFileAnswer(SaveDocumentAnswer): +class SaveFile(graphene.InputObjectType): + id = graphene.String() + name = graphene.String() + + +class SaveDocumentFilesAnswer(SaveDocumentAnswer): + class Input: + value = graphene.List(SaveFile, required=False) + class Meta: - serializer_class = serializers.SaveDocumentFileAnswerSerializer + serializer_class = serializers.SaveDocumentFilesAnswerSerializer return_field_type = Answer @@ -1087,7 +1094,7 @@ class Mutation(object): save_integer_question = SaveIntegerQuestion().Field() save_table_question = SaveTableQuestion().Field() save_form_question = SaveFormQuestion().Field() - save_file_question = SaveFileQuestion().Field() + save_files_question = SaveFilesQuestion().Field() save_static_question = SaveStaticQuestion().Field() save_calculated_float_question = SaveCalculatedFloatQuestion().Field() save_action_button_question = SaveActionButtonQuestion().Field() @@ -1100,7 +1107,7 @@ class Mutation(object): save_document_date_answer = SaveDocumentDateAnswer().Field() save_document_list_answer = SaveDocumentListAnswer().Field() save_document_table_answer = SaveDocumentTableAnswer().Field() - save_document_file_answer = SaveDocumentFileAnswer().Field() + save_document_files_answer = SaveDocumentFilesAnswer().Field() save_default_string_answer = SaveDefaultStringAnswer().Field() save_default_integer_answer = SaveDefaultIntegerAnswer().Field() @@ -1200,7 +1207,7 @@ def resolve_document_validity(self, info, id, **kwargs): models.Question.TYPE_TEXTAREA: StringAnswer, models.Question.TYPE_TEXT: StringAnswer, models.Question.TYPE_TABLE: TableAnswer, - models.Question.TYPE_FILE: FileAnswer, + models.Question.TYPE_FILES: FilesAnswer, models.Question.TYPE_DYNAMIC_CHOICE: StringAnswer, models.Question.TYPE_DYNAMIC_MULTIPLE_CHOICE: ListAnswer, models.Question.TYPE_CALCULATED_FLOAT: FloatAnswer, @@ -1218,7 +1225,7 @@ def resolve_document_validity(self, info, id, **kwargs): models.Question.TYPE_DATE: DateQuestion, models.Question.TYPE_TABLE: TableQuestion, models.Question.TYPE_FORM: FormQuestion, - models.Question.TYPE_FILE: FileQuestion, + models.Question.TYPE_FILES: FilesQuestion, models.Question.TYPE_STATIC: StaticQuestion, models.Question.TYPE_CALCULATED_FLOAT: CalculatedFloatQuestion, models.Question.TYPE_ACTION_BUTTON: ActionButtonQuestion, diff --git a/caluma/schema.py b/caluma/schema.py index 68306f47d..0fb6becd2 100644 --- a/caluma/schema.py +++ b/caluma/schema.py @@ -48,7 +48,7 @@ form_schema.DateQuestion, form_schema.TableQuestion, form_schema.FormQuestion, - form_schema.FileQuestion, + form_schema.FilesQuestion, form_schema.StaticQuestion, form_schema.StringAnswer, form_schema.ListAnswer, @@ -56,7 +56,7 @@ form_schema.FloatAnswer, form_schema.DateAnswer, form_schema.TableAnswer, - form_schema.FileAnswer, + form_schema.FilesAnswer, form_schema.CalculatedFloatQuestion, form_schema.ActionButtonQuestion, workflow_schema.SimpleTask, @@ -73,7 +73,7 @@ form_historical_schema.HistoricalFloatAnswer, form_historical_schema.HistoricalDateAnswer, form_historical_schema.HistoricalTableAnswer, - form_historical_schema.HistoricalFileAnswer, + form_historical_schema.HistoricalFilesAnswer, ] if settings.ENABLE_HISTORICAL_API: diff --git a/caluma/tests/__snapshots__/test_schema.ambr b/caluma/tests/__snapshots__/test_schema.ambr index a95f1a2bd..6864c46e1 100644 --- a/caluma/tests/__snapshots__/test_schema.ambr +++ b/caluma/tests/__snapshots__/test_schema.ambr @@ -409,8 +409,8 @@ """form""" FORM - """file""" - FILE + """files""" + FILES """dynamic_choice""" DYNAMIC_CHOICE @@ -1065,13 +1065,13 @@ """The ID of the object""" id: ID! name: String! - answer: FileAnswer + answer: FilesAnswer uploadUrl: String downloadUrl: String metadata: GenericScalar } - type FileAnswer implements Answer & Node { + type FilesAnswer implements Answer & Node { createdAt: DateTime! modifiedAt: DateTime! createdByUser: String @@ -1082,12 +1082,11 @@ """The ID of the object""" id: ID! question: Question! - value: File! + value: [File]! meta: GenericScalar! - file: File } - type FileQuestion implements Question & Node { + type FilesQuestion implements Question & Node { createdAt: DateTime! modifiedAt: DateTime! createdByUser: String @@ -1446,13 +1445,13 @@ name: String! downloadUrl: String metadata: GenericScalar - historicalAnswer: HistoricalFileAnswer + historicalAnswer: HistoricalFilesAnswer historyDate: DateTime! historyUserId: String historyType: String } - type HistoricalFileAnswer implements HistoricalAnswer & Node { + type HistoricalFilesAnswer implements HistoricalAnswer & Node { createdAt: DateTime! modifiedAt: DateTime! createdByUser: String @@ -1463,7 +1462,7 @@ """The ID of the object""" id: ID! - value(asOf: DateTime!): HistoricalFile + value(asOf: DateTime!): [HistoricalFile] meta: GenericScalar! historyUserId: String question: Question! @@ -1471,7 +1470,6 @@ historyDate: DateTime! historyChangeReason: String historyType: String - file: File } type HistoricalFloatAnswer implements HistoricalAnswer & Node { @@ -1740,7 +1738,7 @@ saveIntegerQuestion(input: SaveIntegerQuestionInput!): SaveIntegerQuestionPayload saveTableQuestion(input: SaveTableQuestionInput!): SaveTableQuestionPayload saveFormQuestion(input: SaveFormQuestionInput!): SaveFormQuestionPayload - saveFileQuestion(input: SaveFileQuestionInput!): SaveFileQuestionPayload + saveFilesQuestion(input: SaveFilesQuestionInput!): SaveFilesQuestionPayload saveStaticQuestion(input: SaveStaticQuestionInput!): SaveStaticQuestionPayload saveCalculatedFloatQuestion(input: SaveCalculatedFloatQuestionInput!): SaveCalculatedFloatQuestionPayload saveActionButtonQuestion(input: SaveActionButtonQuestionInput!): SaveActionButtonQuestionPayload @@ -1752,7 +1750,7 @@ saveDocumentDateAnswer(input: SaveDocumentDateAnswerInput!): SaveDocumentDateAnswerPayload saveDocumentListAnswer(input: SaveDocumentListAnswerInput!): SaveDocumentListAnswerPayload saveDocumentTableAnswer(input: SaveDocumentTableAnswerInput!): SaveDocumentTableAnswerPayload - saveDocumentFileAnswer(input: SaveDocumentFileAnswerInput!): SaveDocumentFileAnswerPayload + saveDocumentFilesAnswer(input: SaveDocumentFilesAnswerInput!): SaveDocumentFilesAnswerPayload saveDefaultStringAnswer(input: SaveDefaultStringAnswerInput!): SaveDefaultStringAnswerPayload saveDefaultIntegerAnswer(input: SaveDefaultIntegerAnswerInput!): SaveDefaultIntegerAnswerPayload saveDefaultFloatAnswer(input: SaveDefaultFloatAnswerInput!): SaveDefaultFloatAnswerPayload @@ -2381,15 +2379,15 @@ clientMutationId: String } - input SaveDocumentFileAnswerInput { + input SaveDocumentFilesAnswerInput { + value: [SaveFile] question: ID! document: ID! meta: JSONString - value: String clientMutationId: String } - type SaveDocumentFileAnswerPayload { + type SaveDocumentFilesAnswerPayload { answer: Answer clientMutationId: String } @@ -2509,7 +2507,12 @@ clientMutationId: String } - input SaveFileQuestionInput { + input SaveFile { + id: String + name: String + } + + input SaveFilesQuestionInput { slug: String! label: String! infoText: String @@ -2521,7 +2524,7 @@ clientMutationId: String } - type SaveFileQuestionPayload { + type SaveFilesQuestionPayload { question: Question clientMutationId: String }