diff --git a/caluma/caluma_form/domain_logic.py b/caluma/caluma_form/domain_logic.py index d1a38e7ea..7658bb183 100644 --- a/caluma/caluma_form/domain_logic.py +++ b/caluma/caluma_form/domain_logic.py @@ -42,13 +42,51 @@ def get_new_answer(cls, data, user, answer): cls.validate_for_save(data, user, answer=answer, origin=True) ) + files = validated_data.pop("files", None) if answer is None: answer = cls.create(validated_data, user) else: answer = cls.update(answer, validated_data, user) + # FILES type needs a bit more work due to the rverse + # relation of file -> answer + if answer.question.type == models.Question.TYPE_FILES: + if files is None: + raise ValidationError("Files input must be a list") + cls.update_answer_files(answer, files) + return cls.post_save(answer) + @classmethod + def update_answer_files(cls, answer: models.Answer, files: list): + """Update the files of a "FILES" answer. + + The files parameter is expected to be a list of dicts, where + each entry has a "name" (the file name), and optionally an "id" + for the case when the given file already exists. + """ + if not files: + files = [] + + to_update = { + str(ex_f.pk): ex_f + for ex_f in answer.files.filter( + pk__in=[f["id"] for f in files if "id" in f] + ) + } + for file_ in files: + file_id = str(file_.get("id")) + file_name = file_["name"] + if file_id in to_update: + if to_update[file_id].name != file_name: + to_update[file_id].rename(file_name) + to_update[file_id].save() + # else: already there, no change required + else: + # need to be created, regardless whether the file_id + # exists or not + answer.files.create(name=file_name) + @staticmethod def validate_for_save( data: dict, user: BaseUser, answer: models.Answer = None, origin: bool = False @@ -68,8 +106,8 @@ def validate_for_save( else models.Document.objects.none() ) del data["value"] - elif question.type == models.Question.TYPE_FILE and not data.get("file"): - data["file"] = data["value"] + elif question.type == models.Question.TYPE_FILES and not data.get("files"): + data["files"] = data["value"] del data["value"] elif question.type == models.Question.TYPE_DATE and not data.get("date"): data["date"] = data["value"] @@ -91,35 +129,35 @@ def post_save(answer: models.Answer) -> models.Answer: # TODO emit events return answer - @staticmethod + @classmethod @transaction.atomic - def create(validated_data: dict, user: Optional[BaseUser] = None) -> models.Answer: - if validated_data["question"].type == models.Question.TYPE_FILE: - validated_data = __class__.set_file(validated_data) + def create( + cls, validated_data: dict, user: Optional[BaseUser] = None + ) -> models.Answer: if validated_data["question"].type == models.Question.TYPE_TABLE: documents = validated_data.pop("documents") + files = validated_data.pop("files", None) answer = BaseLogic.create(models.Answer, validated_data, user) + if validated_data["question"].type == models.Question.TYPE_FILES: + cls.update_answer_files(answer, files) + if answer.question.type == models.Question.TYPE_TABLE: answer.create_answer_documents(documents) return answer - @staticmethod + @classmethod @transaction.atomic - def update(answer, validated_data, user: Optional[BaseUser] = None): + def update(cls, answer, validated_data, user: Optional[BaseUser] = None): if answer.question.type == models.Question.TYPE_TABLE: documents = validated_data.pop("documents") answer.unlink_unused_rows(docs_to_keep=documents) - if ( - answer.question.type == models.Question.TYPE_FILE - and answer.file.name is not validated_data["file"] - ): - answer.file.delete() - validated_data = __class__.set_file(validated_data) + if answer.question.type == models.Question.TYPE_FILES: + cls.update_answer_files(answer, validated_data.pop("files", None)) BaseLogic.update(answer, validated_data, user) @@ -129,13 +167,6 @@ def update(answer, validated_data, user: Optional[BaseUser] = None): answer.refresh_from_db() return answer - @staticmethod - def set_file(validated_data): - file_name = validated_data.get("file") - file = models.File.objects.create(name=file_name) - validated_data["file"] = file - return validated_data - class SaveDefaultAnswerLogic(SaveAnswerLogic): @staticmethod @@ -143,7 +174,7 @@ def validate_for_save( data: dict, user: BaseUser, answer: models.Answer = None, origin: bool = False ) -> dict: if data["question"].type in [ - models.Question.TYPE_FILE, + models.Question.TYPE_FILES, models.Question.TYPE_STATIC, models.Question.TYPE_DYNAMIC_CHOICE, models.Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, diff --git a/caluma/caluma_form/filters.py b/caluma/caluma_form/filters.py index 70a8524a2..0d9c7501f 100644 --- a/caluma/caluma_form/filters.py +++ b/caluma/caluma_form/filters.py @@ -449,7 +449,7 @@ class DocumentFilterSet(MetaFilterSet): "form__name", "form__description", "answers__value", - "answers__file__name", + "answers__files__name", ) ) root_document = GlobalIDFilter(field_name="family") diff --git a/caluma/caluma_form/ordering.py b/caluma/caluma_form/ordering.py index 406330a20..ddb8516d7 100644 --- a/caluma/caluma_form/ordering.py +++ b/caluma/caluma_form/ordering.py @@ -39,7 +39,7 @@ def get_ordering_value( Question.TYPE_CHOICE: "value", Question.TYPE_TEXTAREA: "value", Question.TYPE_TEXT: "value", - Question.TYPE_FILE: "file__name", + Question.TYPE_FILES: "file__name", Question.TYPE_DYNAMIC_CHOICE: "value", } diff --git a/caluma/caluma_form/serializers.py b/caluma/caluma_form/serializers.py index bfebcfa4b..9e4dee2bb 100644 --- a/caluma/caluma_form/serializers.py +++ b/caluma/caluma_form/serializers.py @@ -1,3 +1,4 @@ +import graphene from django.db import transaction from rest_framework import exceptions from rest_framework.serializers import ( @@ -8,6 +9,7 @@ IntegerField, ListField, PrimaryKeyRelatedField, + Serializer, ) from ..caluma_core import serializers @@ -423,7 +425,7 @@ class Meta(SaveQuestionSerializer.Meta): class SaveFileQuestionSerializer(SaveQuestionSerializer): def validate(self, data): - data["type"] = models.Question.TYPE_FILE + data["type"] = models.Question.TYPE_FILES return super().validate(data) class Meta(SaveQuestionSerializer.Meta): @@ -593,14 +595,20 @@ class Meta(SaveAnswerSerializer.Meta): pass -class SaveDocumentFileAnswerSerializer(SaveAnswerSerializer): - value = CharField(write_only=True, source="file", required=False) - value_id = PrimaryKeyRelatedField(read_only=True, source="file", required=False) +class SaveFile(Serializer): + id = graphene.String() + name = graphene.String() + + +class SaveDocumentFilesAnswerSerializer(SaveAnswerSerializer): + value = ListField( + source="files", + required=False, + help_text="List of files for this answer", + ) class Meta(SaveAnswerSerializer.Meta): - fields = SaveAnswerSerializer.Meta.fields + [ - "value_id", - ] + fields = SaveAnswerSerializer.Meta.fields class RemoveAnswerSerializer(serializers.ModelSerializer): diff --git a/caluma/caluma_form/structure.py b/caluma/caluma_form/structure.py index ec7104c2f..50d4b1d55 100644 --- a/caluma/caluma_form/structure.py +++ b/caluma/caluma_form/structure.py @@ -91,8 +91,8 @@ def value(self): for row in self.children() ] - elif self.question.type == Question.TYPE_FILE: - return self.answer.file + elif self.question.type == Question.TYPE_FILES: + return "; ".join([f.name for f in self.answer.files.all()]) elif self.question.type == Question.TYPE_DATE: return self.answer.date diff --git a/caluma/caluma_form/tests/__snapshots__/test_answer.ambr b/caluma/caluma_form/tests/__snapshots__/test_answer.ambr index dea205571..2d816fef8 100644 --- a/caluma/caluma_form/tests/__snapshots__/test_answer.ambr +++ b/caluma/caluma_form/tests/__snapshots__/test_answer.ambr @@ -18,9 +18,6 @@ }), }) # --- -# name: test_save_calculated_dependency_default_answer[float-0.1] - Answer(document=None, question=Question(slug=environmental-ten, type=float), value=0.1) -# --- # name: test_save_default_answer_graphql[choice-option-slug-None-SaveDefaultStringAnswer-True-option-slug-False] dict({ 'saveDefaultStringAnswer': dict({ @@ -135,50 +132,6 @@ }), }) # --- -# name: test_save_default_answer_graphql[table-None-None-SaveDefaultTableAnswer-True-option-slug-False] - dict({ - 'saveDefaultTableAnswer': dict({ - 'answer': dict({ - '__typename': 'TableAnswer', - 'table_value': list([ - dict({ - 'form': dict({ - 'slug': 'eight-traditional', - }), - }), - dict({ - 'form': dict({ - 'slug': 'eight-traditional', - }), - }), - ]), - }), - 'clientMutationId': 'testid', - }), - }) -# --- -# name: test_save_default_answer_graphql[table-None-None-SaveDefaultTableAnswer-True-option-slug-True] - dict({ - 'saveDefaultTableAnswer': dict({ - 'answer': dict({ - '__typename': 'TableAnswer', - 'table_value': list([ - dict({ - 'form': dict({ - 'slug': 'eight-traditional', - }), - }), - dict({ - 'form': dict({ - 'slug': 'eight-traditional', - }), - }), - ]), - }), - 'clientMutationId': 'testid', - }), - }) -# --- # name: test_save_default_answer_graphql[text-Test-None-SaveDefaultStringAnswer-True-option-slug-False] dict({ 'saveDefaultStringAnswer': dict({ @@ -223,51 +176,3 @@ }), }) # --- -# name: test_save_default_answer_python_api[choice-option-slug-None-True-option-slug-False] - Answer(document=None, question=Question(slug=environmental-ten, type=choice), value='option-slug') -# --- -# name: test_save_default_answer_python_api[choice-option-slug-None-True-option-slug-True] - Answer(document=None, question=Question(slug=environmental-ten, type=choice), value='option-slug') -# --- -# name: test_save_default_answer_python_api[date-None-2019-02-22-True-option-slug-False] - Answer(document=None, question=Question(slug=environmental-ten, type=date), value=None) -# --- -# name: test_save_default_answer_python_api[date-None-2019-02-22-True-option-slug-True] - Answer(document=None, question=Question(slug=environmental-ten, type=date), value=None) -# --- -# name: test_save_default_answer_python_api[float-2.1-None-True-option-slug-False] - Answer(document=None, question=Question(slug=environmental-ten, type=float), value=2.1) -# --- -# name: test_save_default_answer_python_api[float-2.1-None-True-option-slug-True] - Answer(document=None, question=Question(slug=environmental-ten, type=float), value=2.1) -# --- -# name: test_save_default_answer_python_api[integer-1-None-True-option-slug-False] - Answer(document=None, question=Question(slug=environmental-ten, type=integer), value=1) -# --- -# name: test_save_default_answer_python_api[integer-1-None-True-option-slug-True] - Answer(document=None, question=Question(slug=environmental-ten, type=integer), value=1) -# --- -# name: test_save_default_answer_python_api[multiple_choice-answer__value5-None-True-option-slug-False] - Answer(document=None, question=Question(slug=environmental-ten, type=multiple_choice), value=['option-slug']) -# --- -# name: test_save_default_answer_python_api[multiple_choice-answer__value5-None-True-option-slug-True] - Answer(document=None, question=Question(slug=environmental-ten, type=multiple_choice), value=['option-slug']) -# --- -# name: test_save_default_answer_python_api[table-None-None-True-option-slug-False] - Answer(document=None, question=Question(slug=environmental-ten, type=table), value=None) -# --- -# name: test_save_default_answer_python_api[table-None-None-True-option-slug-True] - Answer(document=None, question=Question(slug=environmental-ten, type=table), value=None) -# --- -# name: test_save_default_answer_python_api[text-Test-None-True-option-slug-False] - Answer(document=None, question=Question(slug=environmental-ten, type=text), value='Test') -# --- -# name: test_save_default_answer_python_api[text-Test-None-True-option-slug-True] - Answer(document=None, question=Question(slug=environmental-ten, type=text), value='Test') -# --- -# name: test_save_default_answer_python_api[textarea-Test-None-True-option-slug-False] - Answer(document=None, question=Question(slug=environmental-ten, type=textarea), value='Test') -# --- -# name: test_save_default_answer_python_api[textarea-Test-None-True-option-slug-True] - Answer(document=None, question=Question(slug=environmental-ten, type=textarea), value='Test') -# --- diff --git a/caluma/caluma_form/tests/__snapshots__/test_document.ambr b/caluma/caluma_form/tests/__snapshots__/test_document.ambr index bb1949fa5..c2bb1daea 100644 --- a/caluma/caluma_form/tests/__snapshots__/test_document.ambr +++ b/caluma/caluma_form/tests/__snapshots__/test_document.ambr @@ -67,7 +67,7 @@ }), }) # --- -# name: test_query_all_documents[file-None-some-file.pdf-None] +# name: test_query_all_documents[files-None-answer__value6-None] dict({ 'allDocuments': dict({ 'edges': list([ @@ -77,31 +77,33 @@ 'edges': list([ dict({ 'node': dict({ - '__typename': 'FileAnswer', - 'fileValue': dict({ - 'downloadUrl': 'http://minio/download-url/09c697fb-fd0a-4345-bb9c-99df350b0cdb_some-file.pdf', - 'metadata': dict({ - 'bucket_name': 'caluma-media', - 'content_type': 'application/pdf', - 'etag': '5d41402abc4b2a76b9719d911017c592', - 'last_modified': '2021-03-05T15:24:33+00:00', + '__typename': 'FilesAnswer', + 'fileValue': list([ + dict({ + 'downloadUrl': "http://minio/download-url/09c697fb-fd0a-4345-bb9c-99df350b0cdb_[{'name': 'some-file.pdf'}]", 'metadata': dict({ - 'Accept-Ranges': 'bytes', - 'Content-Length': '5', - 'Content-Security-Policy': 'block-all-mixed-content', - 'Content-Type': 'binary/octet-stream', - 'Date': 'Fri, 05 Mar 2021 15:25:15 GMT', - 'ETag': '"5d41402abc4b2a76b9719d911017c592"', - 'Last-Modified': 'Fri, 05 Mar 2021 15:24:33 GMT', - 'Server': 'MinIO', - 'Vary': 'Origin', - 'X-Amz-Request-Id': '16697BAAD69D2214', - 'X-Xss-Protection': '1; mode=block', + 'bucket_name': 'caluma-media', + 'content_type': 'application/pdf', + 'etag': '5d41402abc4b2a76b9719d911017c592', + 'last_modified': '2021-03-05T15:24:33+00:00', + 'metadata': dict({ + 'Accept-Ranges': 'bytes', + 'Content-Length': '5', + 'Content-Security-Policy': 'block-all-mixed-content', + 'Content-Type': 'binary/octet-stream', + 'Date': 'Fri, 05 Mar 2021 15:25:15 GMT', + 'ETag': '"5d41402abc4b2a76b9719d911017c592"', + 'Last-Modified': 'Fri, 05 Mar 2021 15:24:33 GMT', + 'Server': 'MinIO', + 'Vary': 'Origin', + 'X-Amz-Request-Id': '16697BAAD69D2214', + 'X-Xss-Protection': '1; mode=block', + }), + 'size': 8200, }), - 'size': 8200, + 'name': "[{'name': 'some-file.pdf'}]", }), - 'name': 'some-file.pdf', - }), + ]), 'question': dict({ 'label': 'Taylor Williams', 'slug': 'suggest-adult-allow', @@ -119,7 +121,7 @@ }), }) # --- -# name: test_query_all_documents[file-None-some-other-file.pdf-None] +# name: test_query_all_documents[files-None-answer__value7-None] dict({ 'allDocuments': dict({ 'edges': list([ @@ -129,31 +131,33 @@ 'edges': list([ dict({ 'node': dict({ - '__typename': 'FileAnswer', - 'fileValue': dict({ - 'downloadUrl': 'http://minio/download-url/09c697fb-fd0a-4345-bb9c-99df350b0cdb_some-other-file.pdf', - 'metadata': dict({ - 'bucket_name': 'caluma-media', - 'content_type': 'application/pdf', - 'etag': '5d41402abc4b2a76b9719d911017c592', - 'last_modified': '2021-03-05T15:24:33+00:00', + '__typename': 'FilesAnswer', + 'fileValue': list([ + dict({ + 'downloadUrl': "http://minio/download-url/09c697fb-fd0a-4345-bb9c-99df350b0cdb_[{'name': 'some-other-file.pdf'}]", 'metadata': dict({ - 'Accept-Ranges': 'bytes', - 'Content-Length': '5', - 'Content-Security-Policy': 'block-all-mixed-content', - 'Content-Type': 'binary/octet-stream', - 'Date': 'Fri, 05 Mar 2021 15:25:15 GMT', - 'ETag': '"5d41402abc4b2a76b9719d911017c592"', - 'Last-Modified': 'Fri, 05 Mar 2021 15:24:33 GMT', - 'Server': 'MinIO', - 'Vary': 'Origin', - 'X-Amz-Request-Id': '16697BAAD69D2214', - 'X-Xss-Protection': '1; mode=block', + 'bucket_name': 'caluma-media', + 'content_type': 'application/pdf', + 'etag': '5d41402abc4b2a76b9719d911017c592', + 'last_modified': '2021-03-05T15:24:33+00:00', + 'metadata': dict({ + 'Accept-Ranges': 'bytes', + 'Content-Length': '5', + 'Content-Security-Policy': 'block-all-mixed-content', + 'Content-Type': 'binary/octet-stream', + 'Date': 'Fri, 05 Mar 2021 15:25:15 GMT', + 'ETag': '"5d41402abc4b2a76b9719d911017c592"', + 'Last-Modified': 'Fri, 05 Mar 2021 15:24:33 GMT', + 'Server': 'MinIO', + 'Vary': 'Origin', + 'X-Amz-Request-Id': '16697BAAD69D2214', + 'X-Xss-Protection': '1; mode=block', + }), + 'size': 8200, }), - 'size': 8200, + 'name': "[{'name': 'some-other-file.pdf'}]", }), - 'name': 'some-other-file.pdf', - }), + ]), 'question': dict({ 'label': 'Taylor Williams', 'slug': 'suggest-adult-allow', @@ -417,73 +421,81 @@ # name: test_save_document_answer[dynamic_multiple_choice-question__configuration19-MyDataSource-question__format_validators19-answer__value19-None-SaveDocumentListAnswer-True-option-slug-True-True] Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=dynamic_multiple_choice), value=['5.5', '1']) # --- -# name: test_save_document_answer[file-question__configuration10-None-question__format_validators10-not-exist.pdf-None-SaveDocumentFileAnswer-True-option-slug-False-False] +# name: test_save_document_answer[files-question__configuration10-None-question__format_validators10-answer__value10-None-SaveDocumentFilesAnswer-True-option-slug-False-False] dict({ - 'saveDocumentFileAnswer': dict({ + 'saveDocumentFilesAnswer': dict({ 'answer': dict({ - '__typename': 'FileAnswer', - 'fileValue': dict({ - 'name': 'not-exist.pdf', - 'uploadUrl': 'http://minio/upload-url', - }), + '__typename': 'FilesAnswer', + 'fileValue': list([ + dict({ + 'name': 'not-exist.pdf', + 'uploadUrl': 'http://minio/upload-url', + }), + ]), }), 'clientMutationId': 'testid', }), }) # --- -# name: test_save_document_answer[file-question__configuration10-None-question__format_validators10-not-exist.pdf-None-SaveDocumentFileAnswer-True-option-slug-False-True] - Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=file), value=None) +# name: test_save_document_answer[files-question__configuration10-None-question__format_validators10-answer__value10-None-SaveDocumentFilesAnswer-True-option-slug-False-True] + Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=files), value=None) # --- -# name: test_save_document_answer[file-question__configuration10-None-question__format_validators10-not-exist.pdf-None-SaveDocumentFileAnswer-True-option-slug-True-False] +# name: test_save_document_answer[files-question__configuration10-None-question__format_validators10-answer__value10-None-SaveDocumentFilesAnswer-True-option-slug-True-False] dict({ - 'saveDocumentFileAnswer': dict({ + 'saveDocumentFilesAnswer': dict({ 'answer': dict({ - '__typename': 'FileAnswer', - 'fileValue': dict({ - 'name': 'not-exist.pdf', - 'uploadUrl': 'http://minio/upload-url', - }), + '__typename': 'FilesAnswer', + 'fileValue': list([ + dict({ + 'name': 'not-exist.pdf', + 'uploadUrl': 'http://minio/upload-url', + }), + ]), }), 'clientMutationId': 'testid', }), }) # --- -# name: test_save_document_answer[file-question__configuration10-None-question__format_validators10-not-exist.pdf-None-SaveDocumentFileAnswer-True-option-slug-True-True] - Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=file), value=None) +# name: test_save_document_answer[files-question__configuration10-None-question__format_validators10-answer__value10-None-SaveDocumentFilesAnswer-True-option-slug-True-True] + Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=files), value=None) # --- -# name: test_save_document_answer[file-question__configuration9-None-question__format_validators9-some-file.pdf-None-SaveDocumentFileAnswer-True-option-slug-False-False] +# name: test_save_document_answer[files-question__configuration9-None-question__format_validators9-answer__value9-None-SaveDocumentFilesAnswer-True-option-slug-False-False] dict({ - 'saveDocumentFileAnswer': dict({ + 'saveDocumentFilesAnswer': dict({ 'answer': dict({ - '__typename': 'FileAnswer', - 'fileValue': dict({ - 'name': 'some-file.pdf', - 'uploadUrl': 'http://minio/upload-url', - }), + '__typename': 'FilesAnswer', + 'fileValue': list([ + dict({ + 'name': 'some-file.pdf', + 'uploadUrl': 'http://minio/upload-url', + }), + ]), }), 'clientMutationId': 'testid', }), }) # --- -# name: test_save_document_answer[file-question__configuration9-None-question__format_validators9-some-file.pdf-None-SaveDocumentFileAnswer-True-option-slug-False-True] - Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=file), value=None) +# name: test_save_document_answer[files-question__configuration9-None-question__format_validators9-answer__value9-None-SaveDocumentFilesAnswer-True-option-slug-False-True] + Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=files), value=None) # --- -# name: test_save_document_answer[file-question__configuration9-None-question__format_validators9-some-file.pdf-None-SaveDocumentFileAnswer-True-option-slug-True-False] +# name: test_save_document_answer[files-question__configuration9-None-question__format_validators9-answer__value9-None-SaveDocumentFilesAnswer-True-option-slug-True-False] dict({ - 'saveDocumentFileAnswer': dict({ + 'saveDocumentFilesAnswer': dict({ 'answer': dict({ - '__typename': 'FileAnswer', - 'fileValue': dict({ - 'name': 'some-file.pdf', - 'uploadUrl': 'http://minio/upload-url', - }), + '__typename': 'FilesAnswer', + 'fileValue': list([ + dict({ + 'name': 'some-file.pdf', + 'uploadUrl': 'http://minio/upload-url', + }), + ]), }), 'clientMutationId': 'testid', }), }) # --- -# name: test_save_document_answer[file-question__configuration9-None-question__format_validators9-some-file.pdf-None-SaveDocumentFileAnswer-True-option-slug-True-True] - Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=file), value=None) +# name: test_save_document_answer[files-question__configuration9-None-question__format_validators9-answer__value9-None-SaveDocumentFilesAnswer-True-option-slug-True-True] + Answer(document=Document(form=Form(slug=eight-traditional)), question=Question(slug=environmental-ten, type=files), value=None) # --- # name: test_save_document_answer[float-question__configuration2-None-question__format_validators2-2.1-None-SaveDocumentFloatAnswer-True-option-slug-False-False] dict({ diff --git a/caluma/caluma_form/tests/__snapshots__/test_history.ambr b/caluma/caluma_form/tests/__snapshots__/test_history.ambr index 88dc7720a..f4e563e57 100644 --- a/caluma/caluma_form/tests/__snapshots__/test_history.ambr +++ b/caluma/caluma_form/tests/__snapshots__/test_history.ambr @@ -72,7 +72,7 @@ 'edges': list([ dict({ 'node': dict({ - 'historyType': '+', + 'historyType': '~', 'value': 'first row value', }), }), @@ -84,7 +84,7 @@ 'edges': list([ dict({ 'node': dict({ - 'historyType': '+', + 'historyType': '~', 'value': 'second row value', }), }), @@ -109,7 +109,7 @@ 'edges': list([ dict({ 'node': dict({ - 'historyType': '+', + 'historyType': '~', 'value': 'first row value', }), }), diff --git a/caluma/caluma_form/tests/__snapshots__/test_question.ambr b/caluma/caluma_form/tests/__snapshots__/test_question.ambr index 43ab5cf5e..3ce9bd3f4 100644 --- a/caluma/caluma_form/tests/__snapshots__/test_question.ambr +++ b/caluma/caluma_form/tests/__snapshots__/test_question.ambr @@ -214,15 +214,15 @@ }), }) # --- -# name: test_query_all_questions[file-question__configuration9-None-question__format_validators9] +# name: test_query_all_questions[files-question__configuration9-None-question__format_validators9] dict({ 'allQuestions': dict({ 'edges': list([ dict({ 'node': dict({ - '__typename': 'FileQuestion', + '__typename': 'FilesQuestion', 'hintText': '', - 'id': 'RmlsZVF1ZXN0aW9uOmVudmlyb25tZW50YWwtdGVu', + 'id': 'RmlsZXNRdWVzdGlvbjplbnZpcm9ubWVudGFsLXRlbg==', 'infoText': '', 'label': 'Bonnie Moreno', 'meta': dict({ @@ -987,13 +987,13 @@ }), }) # --- -# name: test_save_question[true-True-SaveFileQuestion] +# name: test_save_question[true-True-SaveFilesQuestion] dict({ - 'saveFileQuestion': dict({ + 'saveFilesQuestion': dict({ 'clientMutationId': 'testid', 'question': dict({ - '__typename': 'FileQuestion', - 'id': 'RmlsZVF1ZXN0aW9uOmVudmlyb25tZW50YWwtdGVu', + '__typename': 'FilesQuestion', + 'id': 'RmlsZXNRdWVzdGlvbjplbnZpcm9ubWVudGFsLXRlbg==', 'label': 'Bonnie Moreno', 'meta': dict({ }), diff --git a/caluma/caluma_form/tests/test_answer.py b/caluma/caluma_form/tests/test_answer.py index bb2a59bf9..67494edca 100644 --- a/caluma/caluma_form/tests/test_answer.py +++ b/caluma/caluma_form/tests/test_answer.py @@ -93,7 +93,7 @@ def test_remove_default_answer(db, snapshot, question, answer, schema_executor): False, ), (Question.TYPE_DATE, None, "2019-02-22", "SaveDefaultDateAnswer", True), - (Question.TYPE_FILE, None, None, "SaveDefaultFileAnswer", False), + (Question.TYPE_FILES, None, None, "SaveDefaultFilesAnswer", False), (Question.TYPE_TABLE, None, None, "SaveDefaultTableAnswer", True), ], ) @@ -200,7 +200,7 @@ def test_save_default_answer_graphql( (Question.TYPE_DYNAMIC_CHOICE, "5.5", None, False), (Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, [], None, False), (Question.TYPE_DATE, None, "2019-02-22", True), - (Question.TYPE_FILE, None, None, False), + (Question.TYPE_FILES, None, None, False), (Question.TYPE_TABLE, None, None, True), ], ) @@ -286,7 +286,7 @@ def test_delete_question_with_default(db, question, answer): with pytest.raises(models.Answer.DoesNotExist): answer.refresh_from_db() - assert models.Answer.history.count() == 2 + assert models.Answer.history.count() == 3 assert all( h.history_question_type == question.type for h in models.Answer.history.all() ) @@ -334,12 +334,12 @@ def validate(self, mutation, data, info): ) -@pytest.mark.parametrize("question__type", ["file"]) +@pytest.mark.parametrize("question__type", ["files"]) def test_file_answer_metadata(db, answer, schema_executor, minio_mock): query = """ query ans($id: ID!) { node(id:$id) { - ... on FileAnswer { + ... on FilesAnswer { fileValue: value { name downloadUrl @@ -349,7 +349,7 @@ def test_file_answer_metadata(db, answer, schema_executor, minio_mock): } } """ - vars = {"id": to_global_id("FileAnswer", str(answer.pk))} + vars = {"id": to_global_id("FilesAnswer", str(answer.pk))} # before "upload" old_stat = minio_mock.stat_object.return_value @@ -365,7 +365,7 @@ def test_file_answer_metadata(db, answer, schema_executor, minio_mock): # Before upload, no metadata is available result = schema_executor(query, variable_values=vars) assert not result.errors - assert result.data["node"]["fileValue"]["metadata"] is None + assert result.data["node"]["fileValue"][0]["metadata"] is None # After "upload", metadata should contain some useful data minio_mock.stat_object.return_value = old_stat @@ -376,7 +376,7 @@ def test_file_answer_metadata(db, answer, schema_executor, minio_mock): # Make sure all the values (especially in metadata) are JSON serializable. # This is something the schema_executor doesn't test, but may bite us in prod - metadata = result.data["node"]["fileValue"]["metadata"] + metadata = result.data["node"]["fileValue"][0]["metadata"] assert json.dumps(metadata) # Ensure some of the important properties having the right types, diff --git a/caluma/caluma_form/tests/test_document.py b/caluma/caluma_form/tests/test_document.py index 5fe422924..28d8494e6 100644 --- a/caluma/caluma_form/tests/test_document.py +++ b/caluma/caluma_form/tests/test_document.py @@ -19,8 +19,8 @@ (Question.TYPE_MULTIPLE_CHOICE, None, ["somevalue", "anothervalue"], None), (Question.TYPE_TABLE, None, None, None), (Question.TYPE_DATE, None, None, "2019-02-22"), - (Question.TYPE_FILE, None, "some-file.pdf", None), - (Question.TYPE_FILE, None, "some-other-file.pdf", None), + (Question.TYPE_FILES, None, [{"name": "some-file.pdf"}], None), + (Question.TYPE_FILES, None, [{"name": "some-other-file.pdf"}], None), (Question.TYPE_DYNAMIC_CHOICE, "MyDataSource", "5.5", None), (Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, "MyDataSource", ["5.5"], None), ], @@ -82,7 +82,7 @@ def test_query_all_documents( } } } - ... on FileAnswer { + ... on FilesAnswer { fileValue: value { name downloadUrl @@ -98,19 +98,33 @@ def test_query_all_documents( } """ - search = isinstance(answer.value, list) and " ".join(answer.value) or answer.value + def _value(val): + # Extract searchable value from given input if it exists + ret = { + list: lambda: _value(val[0]), + dict: lambda: _value(list(val.keys())[0]), + } + return ret.get(type(val), lambda: val)() + + search = _value(answer.value) - if question.type == Question.TYPE_FILE: + if question.type == Question.TYPE_FILES: if answer.value == "some-other-file.pdf": settings.MINIO_STORAGE_AUTO_CREATE_MEDIA_BUCKET = False minio_mock.bucket_exists.return_value = False # we need to set the pk here in order to match the snapshots - answer.file = file_factory( - name=answer.value, pk="09c697fb-fd0a-4345-bb9c-99df350b0cdb" + answer.files.set( + [ + file_factory( + name=answer.value, + pk="09c697fb-fd0a-4345-bb9c-99df350b0cdb", + answer=answer, + ) + ] ) answer.value = None answer.save() - search = answer.file.name + search = answer.files.get().name result = schema_executor(query, variable_values={"search": str(search)}) assert not result.errors @@ -138,10 +152,10 @@ def test_complex_document_query_performance( form_question_factory(question=multiple_choice_question, form=form) question_option_factory.create_batch(10, question=multiple_choice_question) answer_factory(question=multiple_choice_question) - file_question = question_factory(type=Question.TYPE_FILE) + file_question = question_factory(type=Question.TYPE_FILES) form_question_factory(question=file_question, form=form) answer_factory( - question=file_question, value=None, document=document, file=file_factory() + question=file_question, value=None, document=document, files=[file_factory()] ) form_question = question_factory(type=Question.TYPE_FORM) @@ -203,7 +217,7 @@ def test_complex_document_query_performance( ... on ListAnswer { listValue: value } - ... on FileAnswer { + ... on FilesAnswer { fileValue: value { name downloadUrl @@ -533,25 +547,34 @@ def test_save_document( "SaveDocumentDateAnswer", True, ), - (Question.TYPE_FILE, {}, None, [], None, None, "SaveDocumentFileAnswer", False), ( - Question.TYPE_FILE, + Question.TYPE_FILES, + {}, + None, + [], + None, + None, + "SaveDocumentFilesAnswer", + False, + ), + ( + Question.TYPE_FILES, {}, None, [], - "some-file.pdf", + [{"name": "some-file.pdf"}], None, - "SaveDocumentFileAnswer", + "SaveDocumentFilesAnswer", True, ), ( - Question.TYPE_FILE, + Question.TYPE_FILES, {}, None, [], - "not-exist.pdf", + [{"name": "not-exist.pdf"}], None, - "SaveDocumentFileAnswer", + "SaveDocumentFilesAnswer", True, ), ( @@ -767,7 +790,7 @@ def test_save_document_answer( # noqa:C901 }} }} }} - ... on FileAnswer {{ + ... on FilesAnswer {{ fileValue: value {{ name uploadUrl @@ -795,11 +818,16 @@ def test_save_document_answer( # noqa:C901 inp["input"]["value"] = [str(document.pk) for document in documents] - if question.type == Question.TYPE_FILE: - if answer.value == "some-file.pdf": - minio_mock.bucket_exists.return_value = False - answer.value = None - answer.save() + if question.type == Question.TYPE_FILES: + if answer.value: + file_name = answer.value[0]["name"] + if file_name == "some-file.pdf": + minio_mock.bucket_exists.return_value = False + answer.value = None + answer.files.all().delete() + inp["input"]["value"] = [{"name": file_name}] + + answer.save() if question.type == Question.TYPE_DATE: inp["input"]["value"] = answer.date @@ -1266,7 +1294,7 @@ def test_copy_document( question__slug="other_question", ) file_question = form_question_factory( - form=main_form, question__type=Question.TYPE_FILE + form=main_form, question__type=Question.TYPE_FILES ) # main_document @@ -1335,8 +1363,11 @@ def test_copy_document( assert new_file_answer.value == file_answer.value # file is copied minio_mock.copy_object.assert_called() - assert new_file_answer.file.name == file_answer.file.name - assert new_file_answer.file.object_name != file_answer.file.object_name + + old_file = file_answer.files.first() + new_file = new_file_answer.files.first() + assert new_file.name == old_file.name + assert new_file.object_name != old_file.object_name # table docs and answers are moved new_table_answer = new_document.answers.get(question=table_question) diff --git a/caluma/caluma_form/tests/test_filter_by_answer.py b/caluma/caluma_form/tests/test_filter_by_answer.py index 87b8cfdbd..f734ea339 100644 --- a/caluma/caluma_form/tests/test_filter_by_answer.py +++ b/caluma/caluma_form/tests/test_filter_by_answer.py @@ -76,7 +76,7 @@ def test_query_all_questions( if qtype not in ( # some question types are not searchable - models.Question.TYPE_FILE, + models.Question.TYPE_FILES, models.Question.TYPE_TABLE, models.Question.TYPE_FORM, models.Question.TYPE_DYNAMIC_CHOICE, diff --git a/caluma/caluma_form/tests/test_history.py b/caluma/caluma_form/tests/test_history.py index d18a74eae..37a890ed8 100644 --- a/caluma/caluma_form/tests/test_history.py +++ b/caluma/caluma_form/tests/test_history.py @@ -172,13 +172,13 @@ def test_historical_file_answer( document = document_factory(form=f) q1 = form_question_factory( - question__type=models.Question.TYPE_FILE, + question__type=models.Question.TYPE_FILES, question__slug="test_question1", form=f, ) save_answer_query = """ - mutation saveAnswer($input: SaveDocumentFileAnswerInput!) { - saveDocumentFileAnswer(input: $input) { + mutation saveAnswer($input: SaveDocumentFilesAnswerInput!) { + saveDocumentFilesAnswer(input: $input) { clientMutationId } } @@ -186,7 +186,7 @@ def test_historical_file_answer( input = { "document": str(document.pk), - "value": "my_file - rev 1", + "value": [{"name": "my_file - rev 1"}], "question": q1.question.slug, } @@ -194,17 +194,19 @@ def test_historical_file_answer( assert not result.errors hist_file_1 = models.File.history.first() timestamp1 = timezone.now() + file1 = document.answers.get(question=q1.question).files.get() + file_id = str(file1.pk) - input["value"] = "my_file - rev 2" + input["value"] = [{"name": "my_file - rev 2", "id": file_id}] result = schema_executor(save_answer_query, variable_values={"input": input}) assert not result.errors hist_file_2 = models.File.history.first() timestamp2 = timezone.now() - input["value"] = "my_file - rev 3" + input["value"] = [{"name": "my_file - rev 3", "id": file_id}] result = schema_executor(save_answer_query, variable_values={"input": input}) assert not result.errors - file3 = document.answers.get(question=q1.question).file + file3 = document.answers.get(question=q1.question).files.first() minio_mock.copy_object.assert_called() minio_mock.remove_object.assert_called() @@ -215,7 +217,7 @@ def test_historical_file_answer( historicalAnswers (asOf: $asOf) { edges { node { - ...on HistoricalFileAnswer { + ...on HistoricalFilesAnswer { __typename historyUserId value (asOf: $asOf) { @@ -240,8 +242,8 @@ def test_historical_file_answer( assert not result.errors assert ( result.data["documentAsOf"]["historicalAnswers"]["edges"][0]["node"]["value"][ - "downloadUrl" - ] + 0 + ]["downloadUrl"] == f"http://minio/download-url/{hist_file_1.pk}_{hist_file_1.name}" ) @@ -250,8 +252,8 @@ def test_historical_file_answer( assert not result.errors assert ( result.data["documentAsOf"]["historicalAnswers"]["edges"][0]["node"]["value"][ - "downloadUrl" - ] + 0 + ]["downloadUrl"] == f"http://minio/download-url/{hist_file_2.pk}_{hist_file_2.name}" ) @@ -262,8 +264,8 @@ def test_historical_file_answer( assert not result.errors assert ( result.data["documentAsOf"]["historicalAnswers"]["edges"][0]["node"]["value"][ - "downloadUrl" - ] + 0 + ]["downloadUrl"] == f"http://minio/download-url/{file3.pk}_{file3.name}" ) diff --git a/caluma/caluma_form/tests/test_jexl.py b/caluma/caluma_form/tests/test_jexl.py index 7178693cf..cb0529076 100644 --- a/caluma/caluma_form/tests/test_jexl.py +++ b/caluma/caluma_form/tests/test_jexl.py @@ -308,7 +308,7 @@ def test_answer_transform_on_hidden_question(info, form_and_document): (Question.TYPE_TEXTAREA, None), (Question.TYPE_TEXT, None), (Question.TYPE_TABLE, []), - (Question.TYPE_FILE, None), + (Question.TYPE_FILES, None), (Question.TYPE_DYNAMIC_CHOICE, None), (Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, []), # Those should not appear in a JEXL answer transform diff --git a/caluma/caluma_form/tests/test_model.py b/caluma/caluma_form/tests/test_model.py index 56dadc974..70e5c68e4 100644 --- a/caluma/caluma_form/tests/test_model.py +++ b/caluma/caluma_form/tests/test_model.py @@ -4,6 +4,7 @@ from ...caluma_form.models import File, Question +@pytest.mark.parametrize("have_history", [True, False]) def test_delete_file_answer( db, question_factory, @@ -13,17 +14,28 @@ def test_delete_file_answer( form, document, mocker, + have_history, ): - file_question = question_factory(type=Question.TYPE_FILE) + copy = mocker.patch.object(Minio, "copy_object") + remove = mocker.patch.object(Minio, "remove_object") + + file_question = question_factory(type=Question.TYPE_FILES) form_question_factory(question=file_question, form=form) - answer = answer_factory( - question=file_question, value=None, document=document, file=file_factory() - ) - mocker.patch.object(Minio, "copy_object") - mocker.patch.object(Minio, "remove_object") + answer = answer_factory(question=file_question, value=None, document=document) + + if have_history: + file_ = answer.files.get() + file_.rename("new name") + file_.save() + assert answer.files.count() # just checking preconditions answer.delete() + + if have_history: + remove.assert_called() + copy.assert_called() + with pytest.raises(File.DoesNotExist): - answer.file.refresh_from_db() + answer.files.get() def test_update_file(db, file_factory, mocker): diff --git a/caluma/caluma_form/tests/test_question.py b/caluma/caluma_form/tests/test_question.py index 8b534e68a..3d1037a66 100644 --- a/caluma/caluma_form/tests/test_question.py +++ b/caluma/caluma_form/tests/test_question.py @@ -21,7 +21,7 @@ (models.Question.TYPE_CHOICE, {}, None, []), (models.Question.TYPE_MULTIPLE_CHOICE, {}, None, []), (models.Question.TYPE_FORM, {}, None, []), - (models.Question.TYPE_FILE, {}, None, []), + (models.Question.TYPE_FILES, {}, None, []), (models.Question.TYPE_DYNAMIC_CHOICE, {}, "MyDataSource", []), (models.Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, {}, "MyDataSource", []), (models.Question.TYPE_STATIC, {}, None, []), @@ -156,7 +156,7 @@ def test_query_all_questions( hintText calcExpression } - ... on FileQuestion { + ... on FilesQuestion { hintText } } @@ -233,7 +233,7 @@ def sorted_option_list(q): "SaveIntegerQuestion", "SaveFloatQuestion", "SaveDateQuestion", - "SaveFileQuestion", + "SaveFilesQuestion", "SaveCalculatedFloatQuestion", ], ) diff --git a/caluma/caluma_form/tests/test_type.py b/caluma/caluma_form/tests/test_type.py index ceea57556..3387a2da6 100644 --- a/caluma/caluma_form/tests/test_type.py +++ b/caluma/caluma_form/tests/test_type.py @@ -7,7 +7,7 @@ @pytest.mark.parametrize( "question__type,expected_typename,success", [ - (models.Question.TYPE_FILE, "FileAnswer", True), + (models.Question.TYPE_FILES, "FilesAnswer", True), (models.Question.TYPE_TEXT, "StringAnswer", True), (models.Question.TYPE_TEXTAREA, "StringAnswer", True), (models.Question.TYPE_FLOAT, "FloatAnswer", True), @@ -56,7 +56,7 @@ def test_answer_types( (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"), ], ) diff --git a/caluma/caluma_form/tests/test_validators.py b/caluma/caluma_form/tests/test_validators.py index aaacd3a33..34929a867 100644 --- a/caluma/caluma_form/tests/test_validators.py +++ b/caluma/caluma_form/tests/test_validators.py @@ -42,7 +42,7 @@ def test_validate_hidden_required_field( @pytest.mark.parametrize( "question__type,question__is_required", - [(Question.TYPE_FILE, "false"), (Question.TYPE_DATE, "false")], + [(Question.TYPE_FILES, "false"), (Question.TYPE_DATE, "false")], ) def test_validate_special_fields( db, form_question, question, document_factory, answer_factory, admin_user @@ -282,6 +282,7 @@ def test_validate_data_source( QuestionValidator().validate(data) +@pytest.mark.parametrize("answer__files", []) @pytest.mark.parametrize( "question__type,answer__value,expected_value", [ @@ -292,11 +293,11 @@ def test_validate_data_source( (Question.TYPE_INTEGER, None, None), (Question.TYPE_FLOAT, None, None), (Question.TYPE_DATE, None, None), - (Question.TYPE_FILE, None, None), + (Question.TYPE_FILES, None, None), (Question.TYPE_TEXTAREA, None, None), ], ) -@pytest.mark.parametrize("answer__date,answer__file", [(None, None)]) +@pytest.mark.parametrize("answer__date", [(None)]) def test_validate_empty_answers( db, form_question, @@ -368,6 +369,7 @@ def test_validate_required_integer_0( DocumentValidator().validate(document, admin_user) +@pytest.mark.parametrize("answer__files", []) @pytest.mark.parametrize( "question__type,answer__value", [ @@ -380,7 +382,7 @@ def test_validate_required_integer_0( (Question.TYPE_INTEGER, None), (Question.TYPE_FLOAT, None), (Question.TYPE_DATE, None), - (Question.TYPE_FILE, None), + (Question.TYPE_FILES, None), (Question.TYPE_TEXTAREA, None), (Question.TYPE_TABLE, None), (Question.TYPE_TABLE, []), @@ -388,9 +390,7 @@ def test_validate_required_integer_0( (Question.TYPE_DYNAMIC_CHOICE, None), ], ) -@pytest.mark.parametrize( - "answer__date,answer__file,question__is_required", [(None, None, "true")] -) +@pytest.mark.parametrize("answer__date,question__is_required", [(None, "true")]) def test_validate_required_empty_answers( db, admin_user, form_question, document, answer, question ): @@ -535,7 +535,7 @@ def test_dependent_question_is_hidden( DocumentValidator().validate(document, admin_user) -@pytest.mark.parametrize("question__type", ["file"]) +@pytest.mark.parametrize("question__type", ["files"]) @pytest.mark.parametrize("question__is_required", ["true"]) @pytest.mark.parametrize("question__is_hidden", ["false"]) def test_required_file(db, question, form, document, answer, admin_user, form_question): @@ -543,20 +543,19 @@ def test_required_file(db, question, form, document, answer, admin_user, form_qu assert document.form == form assert answer.document == document assert form in question.forms.all() - assert answer.file + assert answer.files.count() # remove the file - the_file = answer.file - answer.file = None - answer.save() + the_file = answer.files.first() + answer.files.all().delete() # ensure validation fails with no `file` value with pytest.raises(ValidationError): DocumentValidator().validate(document, admin_user) # put the file back - answer.file = the_file - answer.save() + the_file.save() + answer.files.set([the_file]) # then, validation must pass assert DocumentValidator().validate(document, admin_user) is None diff --git a/caluma/caluma_form/validators.py b/caluma/caluma_form/validators.py index 22fc7f520..2d6263e35 100644 --- a/caluma/caluma_form/validators.py +++ b/caluma/caluma_form/validators.py @@ -173,8 +173,9 @@ def _validate_question_table( f"Document {row_doc.pk} is not of form type {question.row_form.pk}." ) - def _validate_question_file(self, question, value, **kwargs): - pass + def _validate_question_files(self, question, value, **kwargs): + if not isinstance(value, list) and value is not None: + raise exceptions.ValidationError("Input files must be a list") def _validate_question_calculated_float(self, question, value, **kwargs): pass @@ -192,7 +193,7 @@ def validate( ): # Check all possible fields for value value = None - for i in ["documents", "file", "date", "value"]: + for i in ["documents", "files", "date", "value"]: value = kwargs.get(i, value) if value: break diff --git a/caluma/caluma_workflow/tests/test_case.py b/caluma/caluma_workflow/tests/test_case.py index 577a08760..8ac0918c3 100644 --- a/caluma/caluma_workflow/tests/test_case.py +++ b/caluma/caluma_workflow/tests/test_case.py @@ -330,7 +330,7 @@ def test_family_workitems(schema_executor, db, case_factory, work_item_factory): (Question.TYPE_TEXT, True), (Question.TYPE_FORM, True), (Question.TYPE_DATE, True), - (Question.TYPE_FILE, True), + (Question.TYPE_FILES, True), (Question.TYPE_TABLE, False), ], ) @@ -422,17 +422,17 @@ def test_order_by_question_answer_value( case_factory(document=d2) case_factory(document=d3) - elif type == Question.TYPE_FILE: + elif type == Question.TYPE_FILES: d1 = document_factory() d2 = document_factory() d3 = document_factory() - q1 = question_factory(type=Question.TYPE_FILE, slug="test_question1") + q1 = question_factory(type=Question.TYPE_FILES, slug="test_question1") answer_factory(question=q1, file__name="d", document=d1) answer_factory(question=q1, file__name="b", document=d2) answer_factory(question=q1, file__name="f", document=d3) - q2 = question_factory(type=Question.TYPE_FILE, slug="test_question2") + q2 = question_factory(type=Question.TYPE_FILES, slug="test_question2") answer_factory(question=q2, file__name="c", document=d1) answer_factory(question=q2, file__name="e", document=d2) answer_factory(question=q2, file__name="a", document=d3) @@ -465,7 +465,7 @@ def test_order_by_question_answer_value( ... on DateAnswer { dateValue: value } - ... on FileAnswer { + ... on FilesAnswer { fileValue: value { name }