From cb2739ed40a7afabf95f2ce0386b38dc07d919bb Mon Sep 17 00:00:00 2001 From: Jonas Metzener Date: Fri, 11 Nov 2022 10:42:10 +0100 Subject: [PATCH] feat(data_source): add question and context arguments for data sources BREAKING CHANGE: The method `get_data` on data sources receives two new arguments `question` (caluma question model) and `context` (dict). The method `validate_answer_value` on data sources receives a new argument `context` (dict). --- .../data_source_handlers.py | 4 +- caluma/caluma_data_source/data_sources.py | 19 +- caluma/caluma_data_source/schema.py | 21 ++- .../tests/__snapshots__/test_data_source.ambr | 44 +++++ .../caluma_data_source/tests/data_sources.py | 29 ++- .../tests/test_data_source.py | 41 +++++ caluma/caluma_data_source/utils.py | 4 +- caluma/caluma_form/domain_logic.py | 12 +- caluma/caluma_form/schema.py | 11 +- caluma/caluma_form/serializers.py | 24 ++- .../tests/__snapshots__/test_dynamic.ambr | 172 ++++++++++++++++++ caluma/caluma_form/tests/test_dynamic.py | 142 +++++++++++++++ caluma/caluma_form/validators.py | 30 ++- caluma/tests/__snapshots__/test_schema.ambr | 62 ++++++- 14 files changed, 574 insertions(+), 41 deletions(-) create mode 100644 caluma/caluma_form/tests/__snapshots__/test_dynamic.ambr create mode 100644 caluma/caluma_form/tests/test_dynamic.py diff --git a/caluma/caluma_data_source/data_source_handlers.py b/caluma/caluma_data_source/data_source_handlers.py index 3539457e6..7e233c3b2 100644 --- a/caluma/caluma_data_source/data_source_handlers.py +++ b/caluma/caluma_data_source/data_source_handlers.py @@ -53,12 +53,12 @@ def get_data_sources(dic=False): ] -def get_data_source_data(user, name): +def get_data_source_data(user, name, question, context): data_sources = get_data_sources(dic=True) if name not in data_sources: raise DataSourceException(f"No data_source found for name: {name}") - raw_data = data_sources[name]().try_get_data_with_fallback(user) + raw_data = data_sources[name]().try_get_data_with_fallback(user, question, context) if not is_iterable_and_no_string(raw_data): raise DataSourceException(f"Failed to parse data from source: {name}") diff --git a/caluma/caluma_data_source/data_sources.py b/caluma/caluma_data_source/data_sources.py index 2bb8df363..6b55d28b0 100644 --- a/caluma/caluma_data_source/data_sources.py +++ b/caluma/caluma_data_source/data_sources.py @@ -14,9 +14,10 @@ class BaseDataSource: two items. The first will be used for the option name, the second one for it's value. If only one value is provided, this value will also be used as choice name. - The `validate_answer_value`-method checks if each value in `self.get_data(user)` equals the value - of the parameter `value`. If this is correct the method returns the label as a String - and otherwise the method returns `False`. + The `validate_answer_value`-method checks if each value in + `self.get_data(user, question, context)` equals the value of the parameter + `value`. If this is correct the method returns the label as a String and + otherwise the method returns `False`. Examples: [['my-option', {"en": "english description", "de": "deutsche Beschreibung"}, ...] @@ -40,7 +41,7 @@ class BaseDataSource: ... info = 'User choices from "someapi"' ... ... @data_source_cache(timeout=3600) - ... def get_data(self, user): + ... def get_data(self, user, question, context): ... response = requests.get(f"https://someapi/?user={user.username}") ... return [result["value"] for result in response.json()["results"]] ``` @@ -53,11 +54,11 @@ class BaseDataSource: def __init__(self): pass - def get_data(self, user): # pragma: no cover + def get_data(self, user, question, context): # pragma: no cover raise NotImplementedError() - def validate_answer_value(self, value, document, question, user): - for data in self.get_data(user): + def validate_answer_value(self, value, document, question, user, context): + for data in self.get_data(user, question, context): label = data if is_iterable_and_no_string(data): label = data[-1] @@ -73,9 +74,9 @@ def validate_answer_value(self, value, document, question, user): return dynamic_option.label return False - def try_get_data_with_fallback(self, user): + def try_get_data_with_fallback(self, user, question, context): try: - new_data = self.get_data(user) + new_data = self.get_data(user, question, context) except Exception as e: logger.exception( f"Executing {type(self).__name__}.get_data() failed:" diff --git a/caluma/caluma_data_source/schema.py b/caluma/caluma_data_source/schema.py index 30c27dd83..4ce2968da 100644 --- a/caluma/caluma_data_source/schema.py +++ b/caluma/caluma_data_source/schema.py @@ -1,7 +1,8 @@ -from graphene import ConnectionField, String +from graphene import ConnectionField, JSONString, String from graphene.types import ObjectType from ..caluma_core.types import CountableConnectionBase +from ..caluma_form.models import Question from .data_source_handlers import get_data_source_data, get_data_sources @@ -27,10 +28,22 @@ class Meta: class Query(ObjectType): all_data_sources = ConnectionField(DataSourceConnection) - data_source = ConnectionField(DataSourceDataConnection, name=String(required=True)) + data_source = ConnectionField( + DataSourceDataConnection, + name=String(required=True), + question=String( + description="Slug of the question passed as context to the data source" + ), + context=JSONString( + description="JSON object passed as context to the data source" + ), + ) def resolve_all_data_sources(self, info, **kwargs): return get_data_sources() - def resolve_data_source(self, info, name, **kwargs): - return get_data_source_data(info.context.user, name) + def resolve_data_source(self, info, name, question=None, context=None, **kwargs): + if question: + question = Question.objects.get(pk=question) + + return get_data_source_data(info.context.user, name, question, context) diff --git a/caluma/caluma_data_source/tests/__snapshots__/test_data_source.ambr b/caluma/caluma_data_source/tests/__snapshots__/test_data_source.ambr index 1afb66aa0..cdb1a8f4f 100644 --- a/caluma/caluma_data_source/tests/__snapshots__/test_data_source.ambr +++ b/caluma/caluma_data_source/tests/__snapshots__/test_data_source.ambr @@ -1,3 +1,47 @@ +# name: test_data_source_context[False-False] + dict({ + 'dataSource': dict({ + 'edges': list([ + ]), + }), + }) +# --- +# name: test_data_source_context[False-True] + dict({ + 'dataSource': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'label': 'q: environmental-ten', + 'slug': 'option-without-context', + }), + }), + ]), + }), + }) +# --- +# name: test_data_source_context[True-False] + dict({ + 'dataSource': dict({ + 'edges': list([ + ]), + }), + }) +# --- +# name: test_data_source_context[True-True] + dict({ + 'dataSource': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'label': 'q: environmental-ten foo: bar', + 'slug': 'option-with-context', + }), + }), + ]), + }), + }) +# --- # name: test_data_source_defaults dict({ 'dataSource': dict({ diff --git a/caluma/caluma_data_source/tests/data_sources.py b/caluma/caluma_data_source/tests/data_sources.py index 8f633b68b..03947d4ad 100644 --- a/caluma/caluma_data_source/tests/data_sources.py +++ b/caluma/caluma_data_source/tests/data_sources.py @@ -9,7 +9,7 @@ class MyDataSource(BaseDataSource): default = [1, 2, 3] @data_source_cache(timeout=3600) - def get_data(self, user): + def get_data(self, user, question, context): return [ 1, (5.5,), @@ -36,7 +36,7 @@ def get_data_expire(self, info): class MyOtherDataSource(MyDataSource): - def validate_answer_value(self, value, document, question, info): + def validate_answer_value(self, value, document, question, info, context): return "Test 123" @@ -45,7 +45,7 @@ class MyFaultyDataSource(BaseDataSource): default = None @data_source_cache(timeout=3600) - def get_data(self, user): + def get_data(self, user, question, context): return "just a string" @@ -54,7 +54,7 @@ class MyOtherFaultyDataSource(BaseDataSource): default = None @data_source_cache(timeout=3600) - def get_data(self, user): + def get_data(self, user, question, context): return [["just", "some", "strings"]] @@ -63,7 +63,7 @@ class MyBrokenDataSource(BaseDataSource): default = [1, 2, 3] @data_source_cache(timeout=3600) - def get_data(self, user): + def get_data(self, user, question, context): raise Exception() @@ -72,5 +72,22 @@ class MyOtherBrokenDataSource(BaseDataSource): default = None @data_source_cache(timeout=3600) - def get_data(self, user): + def get_data(self, user, question, context): raise Exception() + + +class MyDataSourceWithContext(BaseDataSource): + info = "Data source using context for testing" + + def get_data(self, user, question, context): + if not question: + return [] + + slug = "option-without-context" + label = f"q: {question.slug}" + + if context: + slug = "option-with-context" + label += f" foo: {context['foo']}" + + return [[slug, label]] diff --git a/caluma/caluma_data_source/tests/test_data_source.py b/caluma/caluma_data_source/tests/test_data_source.py index 86710b593..8516373ef 100644 --- a/caluma/caluma_data_source/tests/test_data_source.py +++ b/caluma/caluma_data_source/tests/test_data_source.py @@ -1,3 +1,5 @@ +import json + import pytest from django.core.cache import cache from django.utils import translation @@ -254,3 +256,42 @@ def __str__(self): created_by_user="asdf", created_by_group="foobar", ).exists() + + +@pytest.mark.parametrize("has_question", [True, False]) +@pytest.mark.parametrize("has_context", [True, False]) +def test_data_source_context( + db, snapshot, schema_executor, settings, question, has_question, has_context +): + settings.DATA_SOURCE_CLASSES = [ + "caluma.caluma_data_source.tests.data_sources.MyDataSourceWithContext" + ] + + query = """ + query($question: String, $context: JSONString) { + dataSource( + name: "MyDataSourceWithContext" + question: $question + context: $context + ) { + edges { + node { + label + slug + } + } + } + } + """ + + variables = {} + + if has_question: + variables["question"] = question.slug + + if has_context: + variables["context"] = json.dumps({"foo": "bar"}) + + result = schema_executor(query, variable_values=variables) + assert not result.errors + snapshot.assert_match(result.data) diff --git a/caluma/caluma_data_source/utils.py b/caluma/caluma_data_source/utils.py index c419b0e5f..8b2b95636 100644 --- a/caluma/caluma_data_source/utils.py +++ b/caluma/caluma_data_source/utils.py @@ -9,10 +9,10 @@ def data_source_cache(timeout=None): def decorator(method): @functools.wraps(method) - def handle_cache(self, info): + def handle_cache(self, *args, **kwargs): key = f"data_source_{type(self).__name__}" - get_data_method = functools.partial(method, self, info) + get_data_method = functools.partial(method, self, *args, **kwargs) return cache.get_or_set(key, get_data_method, timeout) return handle_cache diff --git a/caluma/caluma_form/domain_logic.py b/caluma/caluma_form/domain_logic.py index 485e1649f..1a34a7f61 100644 --- a/caluma/caluma_form/domain_logic.py +++ b/caluma/caluma_form/domain_logic.py @@ -102,7 +102,11 @@ def update_answer_files(cls, answer: models.Answer, files: list): @staticmethod def validate_for_save( - data: dict, user: BaseUser, answer: models.Answer = None, origin: bool = False + data: dict, + user: BaseUser, + answer: models.Answer = None, + origin: bool = False, + data_source_context: dict = None, ) -> dict: question = data["question"] @@ -127,7 +131,11 @@ def validate_for_save( del data["value"] validators.AnswerValidator().validate( - **data, user=user, instance=answer, origin=origin + **data, + user=user, + instance=answer, + origin=origin, + data_source_context=data_source_context, ) return data diff --git a/caluma/caluma_form/schema.py b/caluma/caluma_form/schema.py index 2d8800408..5ed4a4c79 100644 --- a/caluma/caluma_form/schema.py +++ b/caluma/caluma_form/schema.py @@ -328,12 +328,17 @@ class Meta: class DynamicQuestion(graphene.Interface): - options = ConnectionField(DataSourceDataConnection) + options = ConnectionField( + DataSourceDataConnection, + context=graphene.JSONString( + description="JSON object passed as context to the data source" + ), + ) data_source = graphene.String(required=True) hint_text = graphene.String() - def resolve_options(self, info, *args, **kwargs): - return get_data_source_data(info.context.user, self.data_source) + def resolve_options(self, info, context=None, *args, **kwargs): + return get_data_source_data(info.context.user, self.data_source, self, context) class DynamicChoiceQuestion(QuestionQuerysetMixin, FormDjangoObjectType): diff --git a/caluma/caluma_form/serializers.py b/caluma/caluma_form/serializers.py index b660d1061..34b33d929 100644 --- a/caluma/caluma_form/serializers.py +++ b/caluma/caluma_form/serializers.py @@ -6,6 +6,7 @@ DateField, FloatField, IntegerField, + JSONField, ListField, PrimaryKeyRelatedField, ) @@ -524,9 +525,21 @@ class Meta: class SaveAnswerSerializer(serializers.ModelSerializer): + data_source_context = JSONField( + encoder=None, + required=False, + allow_null=True, + write_only=True, + help_text="JSON object passed as context to the data source of dynamic questions", + ) + def validate(self, data): data = domain_logic.SaveAnswerLogic.validate_for_save( - data, self.context["request"].user, self.instance, True + data, + self.context["request"].user, + self.instance, + True, + data.pop("data_source_context", None), ) return super().validate(data) @@ -542,7 +555,14 @@ def update(self, instance, validated_data): class Meta: model = models.Answer - fields = ["question", "document", "meta", "value"] + fields = [ + "question", + "document", + "meta", + "value", + "question", + "data_source_context", + ] class SaveDocumentStringAnswerSerializer(SaveAnswerSerializer): diff --git a/caluma/caluma_form/tests/__snapshots__/test_dynamic.ambr b/caluma/caluma_form/tests/__snapshots__/test_dynamic.ambr new file mode 100644 index 000000000..2f5f248f5 --- /dev/null +++ b/caluma/caluma_form/tests/__snapshots__/test_dynamic.ambr @@ -0,0 +1,172 @@ +# name: test_dynamic_choice_options_context[None-dynamic_choice-MyDataSourceWithContext] + dict({ + 'allQuestions': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'id': 'RHluYW1pY0Nob2ljZVF1ZXN0aW9uOmVudmlyb25tZW50YWwtdGVu', + 'options': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'label': 'q: environmental-ten', + 'slug': 'option-without-context', + }), + }), + ]), + }), + }), + }), + ]), + }), + }) +# --- +# name: test_dynamic_choice_options_context[None-dynamic_multiple_choice-MyDataSourceWithContext] + dict({ + 'allQuestions': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'id': 'RHluYW1pY011bHRpcGxlQ2hvaWNlUXVlc3Rpb246ZW52aXJvbm1lbnRhbC10ZW4=', + 'options': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'label': 'q: environmental-ten', + 'slug': 'option-without-context', + }), + }), + ]), + }), + }), + }), + ]), + }), + }) +# --- +# name: test_dynamic_choice_options_context[context0-dynamic_choice-MyDataSourceWithContext] + dict({ + 'allQuestions': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'id': 'RHluYW1pY0Nob2ljZVF1ZXN0aW9uOmVudmlyb25tZW50YWwtdGVu', + 'options': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'label': 'q: environmental-ten foo: bar', + 'slug': 'option-with-context', + }), + }), + ]), + }), + }), + }), + ]), + }), + }) +# --- +# name: test_dynamic_choice_options_context[context0-dynamic_multiple_choice-MyDataSourceWithContext] + dict({ + 'allQuestions': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'id': 'RHluYW1pY011bHRpcGxlQ2hvaWNlUXVlc3Rpb246ZW52aXJvbm1lbnRhbC10ZW4=', + 'options': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'label': 'q: environmental-ten foo: bar', + 'slug': 'option-with-context', + }), + }), + ]), + }), + }), + }), + ]), + }), + }) +# --- +# name: test_save_dynamic_choice_options_context[dynamic_choice-option-with-context-None-False-MyDataSourceWithContext] + dict({ + 'saveDocumentStringAnswer': None, + }) +# --- +# name: test_save_dynamic_choice_options_context[dynamic_choice-option-with-context-context0-True-MyDataSourceWithContext] + dict({ + 'saveDocumentStringAnswer': dict({ + 'answer': dict({ + 'selectedOption': dict({ + 'label': 'q: environmental-ten foo: bar', + 'slug': 'option-with-context', + }), + }), + }), + }) +# --- +# name: test_save_dynamic_choice_options_context[dynamic_choice-option-without-context-None-True-MyDataSourceWithContext] + dict({ + 'saveDocumentStringAnswer': dict({ + 'answer': dict({ + 'selectedOption': dict({ + 'label': 'q: environmental-ten', + 'slug': 'option-without-context', + }), + }), + }), + }) +# --- +# name: test_save_dynamic_choice_options_context[dynamic_choice-option-without-context-context3-False-MyDataSourceWithContext] + dict({ + 'saveDocumentStringAnswer': None, + }) +# --- +# name: test_save_dynamic_choice_options_context[dynamic_multiple_choice-value4-context4-True-MyDataSourceWithContext] + dict({ + 'saveDocumentListAnswer': dict({ + 'answer': dict({ + 'selectedOptions': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'label': 'q: environmental-ten foo: bar', + 'slug': 'option-with-context', + }), + }), + ]), + }), + }), + }), + }) +# --- +# name: test_save_dynamic_choice_options_context[dynamic_multiple_choice-value5-None-False-MyDataSourceWithContext] + dict({ + 'saveDocumentListAnswer': None, + }) +# --- +# name: test_save_dynamic_choice_options_context[dynamic_multiple_choice-value6-None-True-MyDataSourceWithContext] + dict({ + 'saveDocumentListAnswer': dict({ + 'answer': dict({ + 'selectedOptions': dict({ + 'edges': list([ + dict({ + 'node': dict({ + 'label': 'q: environmental-ten', + 'slug': 'option-without-context', + }), + }), + ]), + }), + }), + }), + }) +# --- +# name: test_save_dynamic_choice_options_context[dynamic_multiple_choice-value7-context7-False-MyDataSourceWithContext] + dict({ + 'saveDocumentListAnswer': None, + }) +# --- diff --git a/caluma/caluma_form/tests/test_dynamic.py b/caluma/caluma_form/tests/test_dynamic.py new file mode 100644 index 000000000..703196811 --- /dev/null +++ b/caluma/caluma_form/tests/test_dynamic.py @@ -0,0 +1,142 @@ +import json + +import pytest + +from caluma.caluma_form.models import Question + + +@pytest.mark.parametrize("question__data_source", ["MyDataSourceWithContext"]) +@pytest.mark.parametrize( + "question__type", + [Question.TYPE_DYNAMIC_CHOICE, Question.TYPE_DYNAMIC_MULTIPLE_CHOICE], +) +@pytest.mark.parametrize("context", [{"foo": "bar"}, None]) +def test_dynamic_choice_options_context( + db, snapshot, question, schema_executor, settings, context +): + settings.DATA_SOURCE_CLASSES = [ + "caluma.caluma_data_source.tests.data_sources.MyDataSourceWithContext" + ] + + query = """ + query($question: String!, $context: JSONString) { + allQuestions(filter: { slugs: [$question] }) { + edges { + node { + id + ...on DynamicChoiceQuestion { + options(context: $context) { + edges { + node { + slug + label + } + } + } + } + ...on DynamicMultipleChoiceQuestion { + options(context: $context) { + edges { + node { + slug + label + } + } + } + } + } + } + } + } + """ + + variables = {"question": question.pk} + + if context: + variables["context"] = json.dumps(context) + + result = schema_executor(query, variable_values=variables) + + assert not result.errors + snapshot.assert_match(result.data) + + +@pytest.mark.parametrize("question__data_source", ["MyDataSourceWithContext"]) +@pytest.mark.parametrize( + "question__type,value,context,success", + [ + (Question.TYPE_DYNAMIC_CHOICE, "option-with-context", {"foo": "bar"}, True), + (Question.TYPE_DYNAMIC_CHOICE, "option-with-context", None, False), + (Question.TYPE_DYNAMIC_CHOICE, "option-without-context", None, True), + (Question.TYPE_DYNAMIC_CHOICE, "option-without-context", {"foo": "bar"}, False), + ( + Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, + ["option-with-context"], + {"foo": "bar"}, + True, + ), + (Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, ["option-with-context"], None, False), + (Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, ["option-without-context"], None, True), + ( + Question.TYPE_DYNAMIC_MULTIPLE_CHOICE, + ["option-without-context"], + {"foo": "bar"}, + False, + ), + ], +) +def test_save_dynamic_choice_options_context( + db, snapshot, question, document, schema_executor, settings, value, context, success +): + settings.DATA_SOURCE_CLASSES = [ + "caluma.caluma_data_source.tests.data_sources.MyDataSourceWithContext" + ] + + if question.type == Question.TYPE_DYNAMIC_CHOICE: + query = """ + mutation($input: SaveDocumentStringAnswerInput!) { + saveDocumentStringAnswer(input: $input) { + answer { + ...on StringAnswer { + selectedOption { + slug + label + } + } + } + } + } + """ + elif question.type == Question.TYPE_DYNAMIC_MULTIPLE_CHOICE: + query = """ + mutation($input: SaveDocumentListAnswerInput!) { + saveDocumentListAnswer(input: $input) { + answer { + ...on ListAnswer { + selectedOptions { + edges { + node { + slug + label + } + } + } + } + } + } + } + """ + + variables = { + "input": { + "question": question.pk, + "document": str(document.pk), + "value": value, + "dataSourceContext": json.dumps(context), + } + } + + result = schema_executor(query, variable_values=variables) + + assert bool(result.errors) != success + snapshot.assert_match(result.data) diff --git a/caluma/caluma_form/validators.py b/caluma/caluma_form/validators.py index ab50776c0..5930a232b 100644 --- a/caluma/caluma_form/validators.py +++ b/caluma/caluma_form/validators.py @@ -107,18 +107,20 @@ def _validate_question_multiple_choice(self, question, value, **kwargs): ) def _validate_question_dynamic_choice( - self, question, value, document, user, **kwargs + self, question, value, document, user, data_source_context, **kwargs ): if not isinstance(value, str): raise CustomValidationError( f'Invalid value "{value}". Must be of type str.', slugs=[question.slug] ) - self._validate_dynamic_option(question, document, value, user) + self._validate_dynamic_option( + question, document, value, user, data_source_context + ) self._remove_unused_dynamic_options(question, document, [value]) def _validate_question_dynamic_multiple_choice( - self, question, value, document, user, **kwargs + self, question, value, document, user, data_source_context, **kwargs ): if not isinstance(value, list): raise CustomValidationError( @@ -131,16 +133,20 @@ def _validate_question_dynamic_multiple_choice( f'Invalid value: "{v}". Must be of type string', slugs=[question.slug], ) - self._validate_dynamic_option(question, document, v, user) + self._validate_dynamic_option( + question, document, v, user, data_source_context + ) self._remove_unused_dynamic_options(question, document, value) - def _validate_dynamic_option(self, question, document, option, user): + def _validate_dynamic_option( + self, question, document, option, user, data_source_context + ): data_source = get_data_sources(dic=True)[question.data_source] data_source_object = data_source() valid_label = data_source_object.validate_answer_value( - option, document, question, user + option, document, question, user, data_source_context ) if valid_label is False: raise CustomValidationError( @@ -199,6 +205,7 @@ def validate( validation_context=None, instance=None, origin=False, + data_source_context=None, **kwargs, ): # Check all possible fields for value @@ -222,6 +229,7 @@ def validate( validation_context=validation_context, instance=instance, origin=origin, + data_source_context=data_source_context, ) format_validators = get_format_validators(dic=True) @@ -230,7 +238,14 @@ def validate( class DocumentValidator: - def validate(self, document, user, validation_context=None, **kwargs): + def validate( + self, + document, + user, + validation_context=None, + data_source_context=None, + **kwargs, + ): if not validation_context: validation_context = self._validation_context(document) @@ -247,6 +262,7 @@ def validate(self, document, user, validation_context=None, **kwargs): documents=answer.documents.all(), user=user, validation_context=validation_context, + data_source_context=data_source_context, ) def _validation_context(self, document): diff --git a/caluma/tests/__snapshots__/test_schema.ambr b/caluma/tests/__snapshots__/test_schema.ambr index 4961dac5a..2b7e91b49 100644 --- a/caluma/tests/__snapshots__/test_schema.ambr +++ b/caluma/tests/__snapshots__/test_schema.ambr @@ -975,7 +975,14 @@ meta: GenericScalar! source: Question forms(offset: Int, before: String, after: String, first: Int, last: Int, filter: [FormFilterSetType], order: [FormOrderSetType]): FormConnection - options(before: String, after: String, first: Int, last: Int): DataSourceDataConnection + options( + """JSON object passed as context to the data source""" + context: JSONString + before: String + after: String + first: Int + last: Int + ): DataSourceDataConnection dataSource: String! """The ID of the object""" @@ -1001,7 +1008,14 @@ meta: GenericScalar! source: Question forms(offset: Int, before: String, after: String, first: Int, last: Int, filter: [FormFilterSetType], order: [FormOrderSetType]): FormConnection - options(before: String, after: String, first: Int, last: Int): DataSourceDataConnection + options( + """JSON object passed as context to the data source""" + context: JSONString + before: String + after: String + first: Int + last: Int + ): DataSourceDataConnection dataSource: String! """The ID of the object""" @@ -1055,7 +1069,14 @@ } interface DynamicQuestion { - options(before: String, after: String, first: Int, last: Int): DataSourceDataConnection + options( + """JSON object passed as context to the data source""" + context: JSONString + before: String + after: String + first: Int + last: Int + ): DataSourceDataConnection dataSource: String! hintText: String } @@ -1856,7 +1877,19 @@ analyticsTable(slug: String!): AnalyticsTable allAnalyticsFields(offset: Int, before: String, after: String, first: Int, last: Int, filter: [AnalyticsFieldFilterSetType], order: [AnalyticsFieldOrderSetType]): AnalyticsFieldConnection allDataSources(before: String, after: String, first: Int, last: Int): DataSourceConnection - dataSource(name: String!, before: String, after: String, first: Int, last: Int): DataSourceDataConnection + dataSource( + name: String! + + """Slug of the question passed as context to the data source""" + question: String + + """JSON object passed as context to the data source""" + context: JSONString + before: String + after: String + first: Int + last: Int + ): DataSourceDataConnection allWorkflows(offset: Int, before: String, after: String, first: Int, last: Int, filter: [WorkflowFilterSetType], order: [WorkflowOrderSetType]): WorkflowConnection allTasks(offset: Int, before: String, after: String, first: Int, last: Int, filter: [TaskFilterSetType], order: [TaskOrderSetType]): TaskConnection allCases(offset: Int, before: String, after: String, first: Int, last: Int, filter: [CaseFilterSetType], order: [CaseOrderSetType]): CaseConnection @@ -2389,6 +2422,9 @@ document: ID! meta: JSONString value: Date + + """JSON object passed as context to the data source of dynamic questions""" + dataSourceContext: JSONString clientMutationId: String } @@ -2402,6 +2438,9 @@ question: ID! document: ID! meta: JSONString + + """JSON object passed as context to the data source of dynamic questions""" + dataSourceContext: JSONString clientMutationId: String } @@ -2415,6 +2454,9 @@ document: ID! meta: JSONString value: Float + + """JSON object passed as context to the data source of dynamic questions""" + dataSourceContext: JSONString clientMutationId: String } @@ -2435,6 +2477,9 @@ document: ID! meta: JSONString value: Int + + """JSON object passed as context to the data source of dynamic questions""" + dataSourceContext: JSONString clientMutationId: String } @@ -2448,6 +2493,9 @@ document: ID! meta: JSONString value: [String] + + """JSON object passed as context to the data source of dynamic questions""" + dataSourceContext: JSONString clientMutationId: String } @@ -2466,6 +2514,9 @@ document: ID! meta: JSONString value: String + + """JSON object passed as context to the data source of dynamic questions""" + dataSourceContext: JSONString clientMutationId: String } @@ -2481,6 +2532,9 @@ """List of document IDs representing the rows in the table.""" value: [ID] + + """JSON object passed as context to the data source of dynamic questions""" + dataSourceContext: JSONString clientMutationId: String }