Skip to content

Commit

Permalink
feat(form): multiple files in file questions
Browse files Browse the repository at this point in the history
Change the model to allow multiple files per answer to be stored. Migrate
the existing questions to the new question type, and migrate the existing
answers to the new schema.

Updated the GraphQL schema to allow saving and updating multiple files
on an answer. 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.

BREAKING CHANGE: This renames the question type constant for file questions,
and changes the semantics of the answer value for file questions as well:
It is now a list of dicts instead of a single string. The response type
for querying file(s) answers now also is a list instead of a single dict.
  • Loading branch information
winged committed Jul 4, 2022
1 parent a394273 commit 88e51e8
Show file tree
Hide file tree
Showing 30 changed files with 831 additions and 368 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
FROM
"caluma_form_document" INNER JOIN "caluma_form_form" ON ("caluma_form_document"."form_id" = "caluma_form_form"."slug") -- qs ref
),
answer_b4215 AS (SELECT "caluma_form_answer"."created_at",
answer_af542 AS (SELECT "caluma_form_answer"."created_at",
"caluma_form_answer"."modified_at",
"caluma_form_answer"."created_by_user",
"caluma_form_answer"."created_by_group",
Expand All @@ -76,8 +76,7 @@
"caluma_form_answer"."value",
"caluma_form_answer"."meta",
"caluma_form_answer"."document_id",
"caluma_form_answer"."date",
"caluma_form_answer"."file_id"
"caluma_form_answer"."date"
FROM
"caluma_form_answer" INNER JOIN "caluma_form_question" ON ("caluma_form_answer"."question_id" = "caluma_form_question"."slug") -- qs ref
)
Expand All @@ -92,22 +91,22 @@
FROM document_2a07e AS "document_2a07e"
LEFT JOIN (
SELECT DISTINCT ON (document_id)
(answer_b4215.value #>>'{}') AS "analytics_result_blablub",
(answer_af542.value #>>'{}') AS "analytics_result_blablub",
"document_id"
FROM answer_b4215 AS "answer_b4215"
FROM answer_af542 AS "answer_af542"
WHERE "question_id" = 'top_question'
ORDER BY document_id


) AS "answer_b4215_caea5" ON (document_2a07e.id = "answer_b4215_caea5".document_id)
) AS "answer_af542_caea5" ON (document_2a07e.id = "answer_af542_caea5".document_id)
WHERE form_id = 'top_form'
ORDER BY id


) AS "document_2a07e_27f15" ON (case_ac50e.document_id = "document_2a07e_27f15".id)


) AS analytics_247d1
) AS analytics_2a8de
-- PARAMS:

''',
Expand Down Expand Up @@ -156,7 +155,7 @@
FROM
"caluma_form_document" INNER JOIN "caluma_form_form" ON ("caluma_form_document"."form_id" = "caluma_form_form"."slug") -- qs ref
),
answer_b4215 AS (SELECT "caluma_form_answer"."created_at",
answer_af542 AS (SELECT "caluma_form_answer"."created_at",
"caluma_form_answer"."modified_at",
"caluma_form_answer"."created_by_user",
"caluma_form_answer"."created_by_group",
Expand All @@ -167,8 +166,7 @@
"caluma_form_answer"."value",
"caluma_form_answer"."meta",
"caluma_form_answer"."document_id",
"caluma_form_answer"."date",
"caluma_form_answer"."file_id"
"caluma_form_answer"."date"
FROM
"caluma_form_answer" INNER JOIN "caluma_form_question" ON ("caluma_form_answer"."question_id" = "caluma_form_question"."slug") -- qs ref
)
Expand All @@ -183,22 +181,22 @@
FROM document_2a07e AS "document_2a07e"
LEFT JOIN (
SELECT DISTINCT ON (document_id)
(answer_b4215.value #>>'{}') AS "analytics_result_blablub",
(answer_af542.value #>>'{}') AS "analytics_result_blablub",
"document_id"
FROM answer_b4215 AS "answer_b4215"
FROM answer_af542 AS "answer_af542"
WHERE "question_id" = 'top_question'
ORDER BY document_id


) AS "answer_b4215_caea5" ON (document_2a07e.id = "answer_b4215_caea5".document_id)
) AS "answer_af542_caea5" ON (document_2a07e.id = "answer_af542_caea5".document_id)
WHERE form_id = 'top_form'
ORDER BY id


) AS "document_2a07e_27f15" ON (case_ac50e.document_id = "document_2a07e_27f15".id)


) AS analytics_247d1
) AS analytics_2a8de
WHERE analytics_result_blablub IN (%(flt_analytics_result_blablub_8201d)s, %(flt_analytics_result_blablub_8e5e6)s)
-- PARAMS:
-- flt_analytics_result_blablub_8201d: Shelly Watson
Expand Down Expand Up @@ -246,7 +244,7 @@
FROM
"caluma_form_document" INNER JOIN "caluma_form_form" ON ("caluma_form_document"."form_id" = "caluma_form_form"."slug") -- qs ref
),
answer_b4215 AS (SELECT "caluma_form_answer"."created_at",
answer_af542 AS (SELECT "caluma_form_answer"."created_at",
"caluma_form_answer"."modified_at",
"caluma_form_answer"."created_by_user",
"caluma_form_answer"."created_by_group",
Expand All @@ -257,8 +255,7 @@
"caluma_form_answer"."value",
"caluma_form_answer"."meta",
"caluma_form_answer"."document_id",
"caluma_form_answer"."date",
"caluma_form_answer"."file_id"
"caluma_form_answer"."date"
FROM
"caluma_form_answer" INNER JOIN "caluma_form_question" ON ("caluma_form_answer"."question_id" = "caluma_form_question"."slug") -- qs ref
)
Expand All @@ -273,22 +270,22 @@
FROM document_2a07e AS "document_2a07e"
LEFT JOIN (
SELECT DISTINCT ON (document_id)
(answer_b4215.value #>>'{}') AS "analytics_result_blablub",
(answer_af542.value #>>'{}') AS "analytics_result_blablub",
"document_id"
FROM answer_b4215 AS "answer_b4215"
FROM answer_af542 AS "answer_af542"
WHERE "question_id" = 'top_question'
ORDER BY document_id


) AS "answer_b4215_caea5" ON (document_2a07e.id = "answer_b4215_caea5".document_id)
) AS "answer_af542_caea5" ON (document_2a07e.id = "answer_af542_caea5".document_id)
WHERE form_id = 'top_form'
ORDER BY id


) AS "document_2a07e_27f15" ON (case_ac50e.document_id = "document_2a07e_27f15".id)


) AS analytics_247d1
) AS analytics_2a8de
-- PARAMS:

''',
Expand Down Expand Up @@ -333,7 +330,7 @@
FROM
"caluma_form_document" INNER JOIN "caluma_form_form" ON ("caluma_form_document"."form_id" = "caluma_form_form"."slug") -- qs ref
),
answer_b4215 AS (SELECT "caluma_form_answer"."created_at",
answer_af542 AS (SELECT "caluma_form_answer"."created_at",
"caluma_form_answer"."modified_at",
"caluma_form_answer"."created_by_user",
"caluma_form_answer"."created_by_group",
Expand All @@ -344,8 +341,7 @@
"caluma_form_answer"."value",
"caluma_form_answer"."meta",
"caluma_form_answer"."document_id",
"caluma_form_answer"."date",
"caluma_form_answer"."file_id"
"caluma_form_answer"."date"
FROM
"caluma_form_answer" INNER JOIN "caluma_form_question" ON ("caluma_form_answer"."question_id" = "caluma_form_question"."slug") -- qs ref
)
Expand All @@ -360,22 +356,22 @@
FROM document_2a07e AS "document_2a07e"
LEFT JOIN (
SELECT DISTINCT ON (document_id)
(answer_b4215.value #>>'{}') AS "analytics_result_blablub",
(answer_af542.value #>>'{}') AS "analytics_result_blablub",
"document_id"
FROM answer_b4215 AS "answer_b4215"
FROM answer_af542 AS "answer_af542"
WHERE "question_id" = 'top_question'
ORDER BY document_id


) AS "answer_b4215_caea5" ON (document_2a07e.id = "answer_b4215_caea5".document_id)
) AS "answer_af542_caea5" ON (document_2a07e.id = "answer_af542_caea5".document_id)
WHERE form_id = 'top_form'
ORDER BY id


) AS "document_2a07e_27f15" ON (case_ac50e.document_id = "document_2a07e_27f15".id)


) AS analytics_247d1
) AS analytics_2a8de
WHERE analytics_result_blablub IN (%(flt_analytics_result_blablub_8201d)s, %(flt_analytics_result_blablub_8e5e6)s)
-- PARAMS:
-- flt_analytics_result_blablub_8201d: Shelly Watson
Expand Down
2 changes: 1 addition & 1 deletion caluma/caluma_core/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class Meta:
permission_classes = None

@classmethod
def __init_subclass_with_meta__(
def __init_subclass_with_meta__( # noqa: C901
cls,
lookup_field=None,
lookup_input_kwarg=None,
Expand Down
86 changes: 64 additions & 22 deletions caluma/caluma_form/domain_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework.exceptions import ValidationError

from caluma.caluma_core.models import BaseModel
from caluma.caluma_core.relay import extract_global_id
from caluma.caluma_form import models, validators
from caluma.caluma_user.models import BaseUser
from caluma.utils import update_model
Expand Down Expand Up @@ -42,13 +43,61 @@ 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 reverse
# 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 = []

updated = []

for file_ in files:
file_id = extract_global_id(file_["id"]) if "id" in file_ else None
file_name = file_["name"]
file_model = (
answer.files.filter(pk=file_id).first()
if file_id
else models.File(answer=answer, name=file_name)
)
if not file_model:
# Client wants to update a file that doesn't exist anymore.
# Reject call - this is an inconsistency as either
# the file was never created, or was deleted in the mean
# time.
raise ValidationError("File for given answer not found")

if file_model.name != file_name and file_id:
# Renaming existing file
file_model.rename(file_name)
file_model.save()
elif not file_id:
# New file
file_model.save()
updated.append(file_model.pk)

for file in answer.files.exclude(pk__in=updated):
file.delete()

@staticmethod
def validate_for_save(
data: dict, user: BaseUser, answer: models.Answer = None, origin: bool = False
Expand All @@ -68,8 +117,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"]
Expand All @@ -91,35 +140,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)

Expand All @@ -129,21 +178,14 @@ 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
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,
Expand Down
Loading

0 comments on commit 88e51e8

Please sign in to comment.