Skip to content

Commit

Permalink
feat(analytics): provide option labels
Browse files Browse the repository at this point in the history
Analytics now gives you the ability to not only extract slugs from choice
questions, but the corresponding labels as well.
  • Loading branch information
winged committed Sep 12, 2022
1 parent 94ae234 commit d1f7a38
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 3 deletions.
95 changes: 92 additions & 3 deletions caluma/caluma_analytics/simple_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from django.conf import settings
from django.db import connection
from django.utils import timezone
from django.utils import timezone, translation
from psycopg2.extras import DictCursor

from caluma.caluma_form import models as form_models
Expand Down Expand Up @@ -531,9 +531,9 @@ def __init__(self, parent, identifier, attr_name, subform_level, **kwargs):
super().__init__(parent=parent, identifier=identifier, **kwargs)
self.subform_level = subform_level
self.attr_name = attr_name
self._question = form_models.Question.objects.get(pk=self.identifier)

def supported_functions(self):
question = form_models.Question.objects.get(pk=self.identifier)
base_functions = [
models.AnalyticsField.FUNCTION_VALUE.upper(),
models.AnalyticsField.FUNCTION_COUNT.upper(),
Expand All @@ -557,7 +557,7 @@ def supported_functions(self):
form_models.Question.TYPE_FLOAT: numeric_functions,
}

return base_functions + function_support[question.type]
return base_functions + function_support[self._question.type]

def query_field(self):
# Only for top-level form do we need a Join field.
Expand All @@ -581,6 +581,95 @@ def query_field(self):
)
return value_field

def is_leaf(self):
if self._question.type == form_models.Question.TYPE_CHOICE:
return False
return super().is_leaf()

def is_value(self):
return (
self._question.type == form_models.Question.TYPE_CHOICE
or super().is_value()
)

@cached_property
def available_children(self):
if self._question.type == form_models.Question.TYPE_CHOICE:
return {
"label": ChoiceLabelField(
parent=self,
identifier="label",
visibility_source=self.visibility_source,
)
}
return super().available_children


class ChoiceLabelField(AttributeField):
def __init__(
self, parent, identifier, *, visibility_source, language=None, main_field=None
):
super().__init__(
parent=parent, identifier=identifier, visibility_source=visibility_source
)
self.language = language
self._main_field = main_field

def is_leaf(self):
return bool(self.language)

def is_value(self): # pragma: no cover
# without language, we return the default language as
# per request, so we can still "be" a value
return True

def source_path(self) -> List[str]:
"""Return the full source path of this field as a list."""
fragment = (
[self._main_field.identifier, self.identifier]
if self._main_field
else [self.identifier]
)
return self.parent.source_path() + fragment if self.parent else fragment

def query_field(self):
# The label is in the choices table, so we need to join
# that one.
label_join_field = sql.JoinField(
identifier=self.identifier,
extract=self.identifier,
table=self.visibility_source.options(),
filters=[],
outer_ref=("value #>>'{}'", "slug"), # noqa:P103
parent=self.parent.query_field() if self.parent else None,
)

language = self.language or translation.get_language()

value_field = sql.HStoreExtractorField(
"label",
"label",
parent=label_join_field,
hstore_key=language,
)

return value_field

@cached_property
def available_children(self):
if self.language:
return {}
return {
lang: ChoiceLabelField(
self.parent,
lang,
main_field=self,
visibility_source=self.visibility_source,
language=lang,
)
for lang, _ in settings.LANGUAGES
}


class DateExtractorField(AttributeField):
"""Specialisation of attribute field: Extract a date part."""
Expand Down
13 changes: 13 additions & 0 deletions caluma/caluma_analytics/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,19 @@ def expr(self, query):
return f"""(({self_alias}.{q_id} -> {key_param}) {extractor_op})"""


@dataclass
class HStoreExtractorField(AttrField):
hstore_key: Optional[str] = field(default=None)

def expr(self, query):
key_param = query.makeparam(self.hstore_key)
q_id = connection.ops.quote_name(self.extract)
self_alias = query.self_alias()
# Extract text from HStore field, so that it comes from the DB
# as actual text
return f"""({self_alias}.{q_id} -> {key_param})"""


@dataclass
class JoinField(Field):
table: Optional[Union[str, Query]] = field(default=None)
Expand Down
52 changes: 52 additions & 0 deletions caluma/caluma_analytics/tests/test_form_field_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,58 @@ def test_get_fields(
)


def test_get_fields_for_choice(
db,
settings,
snapshot,
form_and_document,
info,
case,
form_question_factory,
question_option_factory,
):
form, document, *_ = form_and_document(True, True)
choice_q = form_question_factory(
form=form,
question__slug="some_choice",
question__type="choice",
).question
question_option_factory.create_batch(3, question=choice_q)

# we need a case doc for this to work
case.document = document
case.save()

start = CaseStartingObject(info, disable_visibilities=True)

# Choice field should be value and non-leaf, as it
# has the label as sub-field
fields = start.get_fields(depth=1, prefix="document[top_form]")
choice_field = fields["document[top_form].some_choice"]
assert not choice_field.is_leaf()
assert choice_field.is_value()

# Check if choice label fields give correct information
# about being values / leaves as well
fields = start.get_fields(depth=2, prefix="document[top_form].some_choice")
assert set(fields.keys()) == set(
[
"document[top_form].some_choice.label",
"document[top_form].some_choice.label.de",
"document[top_form].some_choice.label.en",
"document[top_form].some_choice.label.fr",
]
)

assert fields["document[top_form].some_choice.label"].is_leaf() is False
assert fields["document[top_form].some_choice.label"].is_value() is True
assert fields["document[top_form].some_choice.label.en"].is_leaf() is True
assert fields["document[top_form].some_choice.label.en"].is_value() is True

for path, field in fields.items():
assert ".".join(field.source_path()) == path


@pytest.mark.parametrize("analytics_table__starting_object", ["cases"])
def test_extract_form_field_values(
db,
Expand Down
61 changes: 61 additions & 0 deletions caluma/caluma_analytics/tests/test_simple_table.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import math
import random

import pytest

Expand Down Expand Up @@ -139,3 +140,63 @@ def test_unusual_aliases(db, table, analytics_cases, alias, request):
# and that the alias is represented in the columns
assert result
assert alias in result[0]


@pytest.mark.parametrize("use_lang", [None, "de", "en"])
def test_extract_choice_labels(
settings,
db,
form_question_factory,
question_option_factory,
answer_factory,
example_analytics,
analytics_cases,
use_lang,
form_and_document,
):
settings.LANGUAGES = [("de", "de"), ("en", "en")]

# We need a form with a choice question ...
form, *_ = form_and_document(False, False)
choice_q = form_question_factory(form=form, question__type="choice").question
options = question_option_factory.create_batch(4, question=choice_q)

# ... as well as some cases with corresponding answers in their docs
for case in analytics_cases:
answer_factory(
question=choice_q,
document=case.document,
value=random.choice(options).option.slug,
)

# just checking assumptions..
assert case.document.form_id == form.pk

# we're only interested in seeing the choice field work here
example_analytics.fields.all().delete()
lang_suffix = f".{use_lang}" if use_lang else ""
example_analytics.fields.create(
data_source=f"document[top_form].{choice_q.slug}",
alias="choice_value",
)
example_analytics.fields.create(
data_source=f"document[top_form].{choice_q.slug}.label{lang_suffix}",
alias="choice_label",
)

# Define valid outputs for label and value (slug)
lang = use_lang if use_lang else settings.LANGUAGE_CODE
valid_options = {opt.slug: opt.label[lang] for opt in choice_q.options.all()}

# Run the analysis
table = SimpleTable(example_analytics)
result = table.get_records()

choice_count = 0
assert len(result)
for row in result:
if row["choice_value"]:
choice_count += 1
assert row["choice_value"] in valid_options
assert row["choice_label"] == valid_options[row["choice_value"]]
assert choice_count >= len(analytics_cases)
1 change: 1 addition & 0 deletions caluma/caluma_analytics/visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ def __init__(self, info, is_disabled):
work_items = qs_method_factory(workflow_schema.WorkItem)
documents = qs_method_factory(form_schema.Document)
answers = qs_method_factory(form_schema.Answer, form_models.Answer)
options = qs_method_factory(form_schema.Option, form_models.Option)

0 comments on commit d1f7a38

Please sign in to comment.