import collections
import contextlib
import datetime

import floppyforms as forms
from django.apps import apps
from django.contrib.auth import get_user_model
from django.forms import BaseFormSet, formset_factory
from django.template.loader import render_to_string
from django.urls import reverse, NoReverseMatch
from django.utils.translation import gettext as _

from apps.configuration.models import Category, Configuration, Key, Questiongroup
from apps.configuration.utils import (
    get_choices_from_model,
    get_choices_from_questiongroups,
)
from apps.qcat.errors import (
    ConfigurationError,
    ConfigurationErrorInvalidCondition,
    ConfigurationErrorInvalidConfiguration,
    ConfigurationErrorInvalidOption,
    ConfigurationErrorInvalidQuestiongroupCondition,
    ConfigurationErrorNoConfigurationFound,
    ConfigurationErrorNotInDatabase,
)
from apps.qcat.utils import is_empty_list_of_dicts
from apps.questionnaire.models import File
from .fields import XMLCompatCharField

User = get_user_model()


class BaseConfigurationObject:
    """
    This is the base class for all Questionnaire Configuration objects.
    """

    def __init__(self, parent_object, configuration):
        """
        Sets the following attributes for each configuration object:

            ``self.configuration``: The configuration dictionary.

            ``self.keyword``: The keyword identifier of the object.

            ``self.configuration_object``: The database configuration
                object

            ``self.configuration_keyword``: The code of the current
                configuration

            ``self.parent_object``: The parent configuration object.

            ``self.label``: The (translated) label.

            ``self.children``: The child configuration objects if
                available.
        """
        validate_type(
            configuration, dict, self.name_current, "list of dicts", self.name_parent
        )
        self.configuration = configuration
        self.validate_options()

        keyword = self.configuration.get("keyword")
        if not isinstance(keyword, str):
            raise ConfigurationErrorInvalidConfiguration(
                "keyword", "str", self.name_current
            )
        self.keyword = keyword

        if isinstance(
            self,
            (QuestionnaireSection, QuestionnaireCategory, QuestionnaireSubcategory),
        ):
            try:
                self.configuration_object = Category.objects.get(keyword=self.keyword)
            except Category.DoesNotExist:
                raise ConfigurationErrorNotInDatabase(Category, self.keyword)
        elif isinstance(self, QuestionnaireQuestiongroup):
            try:
                self.configuration_object = Questiongroup.objects.get(
                    keyword=self.keyword
                )
            except Questiongroup.DoesNotExist:
                raise ConfigurationErrorNotInDatabase(Questiongroup, self.keyword)
        elif isinstance(self, QuestionnaireQuestion):
            try:
                self.configuration_object = Key.objects.get(keyword=self.keyword)
            except Key.DoesNotExist:
                raise ConfigurationErrorNotInDatabase(Key, self.keyword)
        else:
            raise Exception("Unknown instance")

        self.configuration_keyword = parent_object.configuration_keyword
        self.edition = parent_object.edition
        self.parent_object = parent_object

        self.helptext = ""
        self.label = ""
        self.label_view = ""
        translation = self.configuration_object.translation
        if translation:
            translation_kwargs = dict(
                configuration=self.configuration_keyword, edition=self.edition
            )
            self.helptext = translation.get_translation(
                keyword="helptext", **translation_kwargs
            )
            self.label = translation.get_translation(
                keyword="label", **translation_kwargs
            )
            self.label_view = translation.get_translation(
                keyword="label_view", **translation_kwargs
            )
            if self.label_view is None:
                self.label_view = self.label
            if isinstance(self, QuestionnaireQuestion):
                self.label_filter = translation.get_translation(
                    keyword="label_filter", **translation_kwargs
                )
                if self.label_filter is None:
                    self.label_filter = self.label_view

        # Should be at the bottom of the function
        children = []
        configuration_children = self.configuration.get(self.name_children)
        if configuration_children:
            if (
                not isinstance(configuration_children, list)
                or len(configuration_children) == 0
            ):
                raise ConfigurationErrorInvalidConfiguration(
                    self.name_children, "list of dicts", self.name_current
                )
            for configuration_child in configuration_children:
                children.append(self.Child(self, configuration_child))
            self.children = children

    def validate_options(self):
        """
        Validate a configuration dict to check if it contains invalid
        options as keys.

        Raises:
            :class:`qcat.errors.ConfigurationErrorInvalidOption`
        """
        invalid_options = list(set(self.configuration) - set(self.valid_options))
        if len(invalid_options) > 0:
            raise ConfigurationErrorInvalidOption(
                invalid_options[0], self.configuration, self
            )

    def get_translation_ids(self) -> list:
        # Recursively get the IDs of all translations objects of the
        # configuration object and its children.

        # Not all configuration objects have a translation
        # (e.g. QuestionnaireConfiguration).
        try:
            res = [self.configuration_object.translation.id]
        except AttributeError:
            res = []

        # Not all configuration objects have children (e.g.
        # QuestionnaireQuestion).
        try:
            for child in self.children:
                res += child.get_translation_ids()
        except AttributeError:
            pass

        # QuestionnaireQuestion objects have values.
        if isinstance(self, QuestionnaireQuestion):
            res += [v.translation.id for v in self.value_objects]

        return res


class QuestionnaireQuestion(BaseConfigurationObject):
    """
    A class representing the configuration of a Question of the
    Questionnaire. A Question basically consists of the Key and optional
    Values (for Questions with predefined Answers)
    """

    valid_options = [
        "keyword",
        "view_options",
        "form_options",
        "filter_options",
        "summary",
    ]
    valid_field_types = [
        "bool",
        "cb_bool",
        "char",
        "checkbox",
        "multi_select",
        "date",
        "file",
        "hidden",
        "image",
        "image_checkbox",
        "link_video",
        "measure",
        "radio",
        "select",
        "select_type",
        "text",
        "todo",
        "user_id",
        "link_id",
        "int",
        "float",
        "map",
        "select_model",
        "select_conditional_questiongroup",
        "select_conditional_custom",
        "display_only",
        "wms_layer",
    ]
    translation_original_prefix = "original_"
    translation_translation_prefix = "translation_"
    translation_old_prefix = "old_"
    value_image_path = "assets/img/"
    name_current = "questions"
    name_parent = "questiongroups"
    name_children = ""
    Child = None

    def __init__(self, parent_object, configuration):
        """
        Parameter ``configuration`` is a ``dict`` containing the
        configuration of the Question. It needs to have the following
        format::

          {
            # The keyword of the key of the question.
            "keyword": "KEY",

            # (optional)
            "view_options": {
              # Default: "default"
              "template": "TEMPLATE_NAME",

              # Default: ""
              "label_position": "",

              # Default: "h5"
              "label_tag": "h5",

              # Default: ""
              "layout": "stacked",

              # Default: false
              "with_raw_values": true,

              # Default: false
              "in_list": true,

              # Default: false
              "is_name": true,
            },

            # (optional)
            "form_options": {
              # Default: "default"
              "template": "TEMPLATE_NAME",

              # Default: None
              "max_length": 500,

              # Default: 3
              "num_rows": 5,

              # Default: ""
              "helptext_position": "tooltip",

              # Default: ""
              "label_position": "placeholder",

              # Default: []
              "questiongroup_conditions": [],
            },

            # (optional)
            "filter_options": {
                "order": 1
            }
          }

        .. seealso::
            For more information on the format and the configuration
            options, please refer to the documentation:
            :doc:`/configuration/questiongroup`

        Raises:
            :class:`qcat.errors.ConfigurationErrorInvalidConfiguration`,
            ``ConfigurationErrorNotInDatabase``.
        """
        super().__init__(parent_object, configuration)
        self.questiongroup = parent_object

        self.key_config = self.configuration_object.configuration

        self.field_type = self.key_config.get("type", "char")
        if self.field_type not in self.valid_field_types:
            raise ConfigurationErrorInvalidOption(self.field_type, "type", "Key")

        view_options = self.key_config.get("view_options", {})
        if configuration.get("view_options"):
            view_options.update(configuration.get("view_options"))
        self.view_options = view_options

        form_options = self.key_config.get("form_options", {})
        if configuration.get("form_options"):
            form_options.update(configuration.get("form_options"))
        self.form_options = form_options

        self.in_list = self.view_options.get("in_list", False) is True
        self.is_name = self.view_options.get("is_name", False) is True
        self.is_geometry = self.view_options.get("is_geometry", False) is True

        self.max_length = self.form_options.get("max_length", None)
        if self.max_length and not isinstance(self.max_length, int):
            self.max_length = None
        self.num_rows = self.form_options.get("num_rows", 3)

        filter_options = self.key_config.get("filter_options", {})
        if configuration.get("filter_options"):
            filter_options.update(configuration.get("filter_options", {}))
        self.filter_options = filter_options

        summary_config = self.key_config.get("summary", {})
        if configuration.get("summary"):
            summary_config.update(configuration.get("summary"))
        self.summary = summary_config

        self.images = []
        self.choices = ()
        self.choices_helptexts = []
        self.value_objects = []
        translation_kwargs = dict(
            configuration=self.configuration_keyword, edition=self.edition
        )

        if self.field_type in ["bool"]:
            self.choices = ((1, _("Yes")), (0, _("No")))
        elif self.field_type in ["cb_bool"]:
            self.choices = ((1, self.label),)
        elif self.field_type in [
            "measure",
            "checkbox",
            "image_checkbox",
            "select_type",
            "select",
            "radio",
            "select_conditional_custom",
            "multi_select",
        ]:
            self.value_objects = self.configuration_object.values.all()
            if len(self.value_objects) == 0:
                raise ConfigurationErrorNotInDatabase(
                    self, f"[values of key {self.keyword}]"
                )
            if self.field_type in [
                "select_type",
                "select",
                "select_conditional_custom",
            ]:
                choices = [("", "-", "")]
            else:
                choices = []
            ordered_values = False
            for i, v in enumerate(self.value_objects):
                if v.order_value:
                    ordered_values = True
                if self.field_type in ["measure"]:
                    choices.append(
                        (
                            i + 1,
                            v.get_translation(keyword="label", **translation_kwargs),
                            v.get_translation(keyword="helptext", **translation_kwargs),
                        )
                    )
                else:
                    choices.append(
                        (
                            v.keyword,
                            v.get_translation(keyword="label", **translation_kwargs),
                            v.get_translation(keyword="helptext", **translation_kwargs),
                        )
                    )
                if self.field_type in ["image_checkbox"]:
                    self.images.append(
                        "{}{}".format(
                            self.value_image_path, v.configuration.get("image_name")
                        )
                    )
            if ordered_values is False:
                try:
                    choices = sorted(choices, key=lambda tup: tup[1])
                except TypeError:
                    pass
            self.choices = tuple([c[:2] for c in choices])
            self.choices_helptexts = [c[2] for c in choices]

        self.additional_translations = {}
        if self.field_type in ["measure"]:
            translation = self.configuration_object.translation
            label_left = translation.get_translation(
                keyword="label_left", **translation_kwargs
            )
            label_right = translation.get_translation(
                keyword="label_right", **translation_kwargs
            )
            self.additional_translations.update(
                {"label_left": label_left, "label_right": label_right}
            )

        self.conditional = self.form_options.get("conditional", False)

        question_conditions = []
        for question_cond in self.form_options.get("question_conditions", []):
            try:
                cond_expression, cond_name = question_cond.split("|")
            except ValueError:
                raise ConfigurationErrorInvalidCondition(
                    question_cond, 'Needs to have form "expression|name"'
                )
            # Check the condition expression
            try:
                cond_expression = eval(f"{0}{cond_expression}")
            except SyntaxError:
                raise ConfigurationErrorInvalidQuestiongroupCondition(
                    question_cond,
                    'Expression "{}" is not a valid Python condition'.format(
                        cond_expression
                    ),
                )
            question_conditions.append(question_cond)

        self.question_conditions = question_conditions
        self.question_condition = self.form_options.get("question_condition")

        conditions = []
        for condition in self.form_options.get("conditions", []):
            try:
                cond_value, cond_expression, cond_key = condition.split("|")
            except ValueError:
                raise ConfigurationErrorInvalidCondition(
                    condition, 'Needs to have form "value|condition|key"'
                )
            # Check that value exists
            if cond_value not in [str(v[0]) for v in self.choices]:
                raise ConfigurationErrorInvalidCondition(
                    condition,
                    'Value "{}" of condition not found in the Key\''
                    "s choices".format(cond_value),
                )
            # Check the condition expression
            try:
                # todo: don't use eval (here and further down)
                cond_expression = eval(cond_expression)
            except SyntaxError:
                raise ConfigurationErrorInvalidCondition(
                    condition,
                    'Expression "{}" is not a valid Python '
                    "condition".format(cond_expression),
                )
            if not isinstance(cond_expression, bool):
                raise ConfigurationErrorInvalidCondition(
                    condition, "Only the following Python expressions are valid: bool"
                )
            # TODO
            # Check that the key exists in the same questiongroup.
            # cond_key_object = self.questiongroup.get_question_by_key_keyword(
            #     cond_key)
            # if cond_key_object is None:
            #     raise ConfigurationErrorInvalidCondition(
            #         condition,
            #         'Key "{}" is not in the same questiongroup'.format(
            #             cond_key))
            # if not (
            #         self.field_type == 'image_checkbox' and
            #         cond_key_object.field_type == 'image_checkbox'):
            #     raise ConfigurationErrorInvalidCondition(
            #         condition, 'Only valid for types "image_checkbox"')
            conditions.append((cond_value, cond_expression, cond_key))
        self.conditions = conditions

        questiongroup_conditions = []
        for questiongroup_condition in self.form_options.get(
            "questiongroup_conditions", []
        ):
            try:
                cond_expression, cond_name = questiongroup_condition.split("|")
            except ValueError:
                raise ConfigurationErrorInvalidQuestiongroupCondition(
                    questiongroup_condition, 'Needs to have form "expression|name"'
                )
            # Check the condition expression
            try:
                cond_expression = eval(f"{0}{cond_expression}")
            except SyntaxError:
                raise ConfigurationErrorInvalidQuestiongroupCondition(
                    questiongroup_condition,
                    'Expression "{}" is not a valid Python condition'.format(
                        cond_expression
                    ),
                )
            questiongroup_conditions.append(questiongroup_condition)

        self.questiongroup_conditions = questiongroup_conditions

        # TODO
        self.required = False

    def add_form(
        self,
        formfields,
        templates,
        options,
        show_translation=False,
        edit_mode="edit",
        questionnaire_data=None,
    ):
        """
        Adds one or more fields to a dictionary of formfields.

        Args:
            ``formfields`` (dict): A dictionary of formfields.

            ``templates`` (dict): A dictionary with templates to be used
            to render the questions.

            ``options`` (dict): A dictionary with configuration options
            to be used by the template when rendering the questions.

            ``show_translation`` (bool): A boolean indicating whether to
            add additional fields for translation (``True``) or not
            (``False``). Defaults to ``False``.

            ``edit_mode`` (string): A string indicating the current mode
            of the form (eg. if it is read-only). Defaults to ``edit``.
            Valid options are:

                * ``edit``: Default form rendering.

                * ``view``: Read-only mode, all form fields rendered as
                  disabled.

        Returns:
            ``dict``. The updated formfields dictionary.

            ``dict``. The updated templates dictionary.
        """
        form_template = "default"
        if self.field_type == "measure":
            form_template = "inline_3"
        elif self.field_type == "image_checkbox":
            form_template = "no_label"
        form_template = "form/question/{}.html".format(
            self.form_options.get("template", form_template)
        )
        field = None
        translation_field = None
        widget = None

        field_options = self.form_options
        field_options.update(
            {
                "helptext": self.helptext,
                "helptext_choices": self.choices_helptexts,
                "additional_translations": self.additional_translations,
            }
        )

        attrs = {}
        if edit_mode == "view":
            # Read-only mode, disable all input fields.
            attrs.update({"disabled": "disabled"})

        if field_options.get("label_position") == "placeholder":
            attrs.update({"placeholder": self.label})

        if self.question_conditions:
            field_options.update({"data-question-conditions": self.question_conditions})

        if self.question_condition:
            field_options.update({"data-question-condition": self.question_condition})

        attrs.update(self.form_options.get("field_options", {}))
        attrs.update(
            {
                "conditional": self.conditional,
                "questiongroup_conditions": ",".join(self.questiongroup_conditions),
            }
        )

        # Disable inherited questions.
        if self.parent_object.inherited_configuration:
            attrs.update({"disabled": "disabled"})

        if self.field_type in ["char", "wms_layer"]:
            max_length = self.max_length
            if max_length is None:
                max_length = 2000
            widget = TextInput(attrs)
            translation_widget = forms.HiddenInput(attrs)
            if show_translation is True:
                widget = forms.HiddenInput(attrs)
                translation_widget = TextInput(attrs)
            widget.options = field_options
            translation_widget.options = field_options
            field = XMLCompatCharField(
                label=self.label,
                widget=widget,
                required=self.required,
                max_length=max_length,
            )
            translation_field = XMLCompatCharField(
                label=self.label,
                widget=translation_widget,
                required=self.required,
                max_length=max_length,
            )
        elif self.field_type == "link_video":
            widget = TextInput(attrs)
            widget.options = field_options
            field = forms.CharField(
                label=self.label, widget=widget, required=self.required
            )
        elif self.field_type in ["date"]:
            widget = DateInput(attrs)
            widget.options = field_options
            field = forms.CharField(
                label=self.label, widget=widget, required=self.required
            )
        elif self.field_type in ["int", "float"]:
            field_options.update({"field_type": self.field_type})
            if self.field_type == "float":
                attrs.update({"step": "any"})
            now_year = datetime.datetime.now().year
            if attrs.get("max") == "now":
                attrs.update({"max": now_year})
            if attrs.get("min") == "now":
                attrs.update({"min": now_year})
            widget = NumberInput(attrs)
            widget.options = field_options
            field = forms.CharField(
                label=self.label, widget=widget, required=self.required
            )
        elif self.field_type in ["user_id", "link_id", "hidden", "display_only", "map"]:
            widget = HiddenInput()
            if self.field_type == "user_id":
                widget.css_class = "select-user-id"
            elif self.field_type == "link_id":
                widget.css_class = "select-link-id is-cleared"
            field = forms.CharField(label=None, widget=widget, required=self.required)
        elif self.field_type == "text":
            max_length = self.max_length
            if max_length is None:
                max_length = 5000
            attrs.update({"rows": self.num_rows})
            widget = forms.Textarea(attrs=attrs)
            translation_widget = forms.HiddenInput(attrs=attrs)
            if show_translation is True:
                widget = forms.HiddenInput(attrs=attrs)
                translation_widget = forms.Textarea(attrs=attrs)
            field = XMLCompatCharField(
                label=self.label,
                widget=widget,
                required=self.required,
                max_length=max_length,
            )
            translation_field = XMLCompatCharField(
                label=self.label, widget=translation_widget, required=self.required
            )
        elif self.field_type == "bool":
            widget = RadioSelect(choices=self.choices, attrs=attrs)
            widget.options = field_options
            if self.form_options.get("extra") == "inline":
                widget.template_name = "form/field/radio_inline.html"
            if self.keyword == "accept_conditions":
                widget.template_name = "form/field/accept_conditions.html"
            field = forms.IntegerField(
                label=self.label, widget=widget, required=self.required
            )
        elif self.field_type == "measure":
            widget = MeasureSelect(attrs=attrs)
            widget.options = field_options
            if self.form_options.get("layout", "") == "stacked":
                widget = MeasureSelectStacked()
            field = forms.ChoiceField(
                label=self.label,
                choices=self.choices,
                widget=widget,
                required=self.required,
            )
        elif self.field_type == "select":
            widget = Select(attrs=attrs)
            widget.options = field_options
            widget.searchable = False
            field = forms.ChoiceField(
                label=self.label,
                choices=self.choices,
                widget=widget,
                required=self.required,
            )
        elif self.field_type == "radio":
            widget = RadioSelect(choices=self.choices, attrs=attrs)
            widget.options = field_options
            field = forms.ChoiceField(
                label=self.label,
                choices=self.choices,
                widget=widget,
                required=self.required,
            )
        elif self.field_type in ["checkbox", "cb_bool"]:
            widget = Checkbox(attrs=attrs)
            if self.form_options.get("layout", "") == "measure":
                widget = MeasureCheckbox()
            widget.options = field_options
            field = forms.MultipleChoiceField(
                label=self.label,
                widget=widget,
                choices=self.choices,
                required=self.required,
            )
        elif self.field_type in ["multi_select"]:
            widget = MultiSelect(attrs=attrs)
            widget.options = field_options
            field = forms.MultipleChoiceField(
                label=self.label,
                widget=widget,
                choices=self.choices,
                required=self.required,
            )
        elif self.field_type == "image_checkbox":
            # Make the image paths available to the widget
            widget = ImageCheckbox(attrs=attrs)
            widget.images = self.images
            widget.options = field_options
            field = forms.MultipleChoiceField(
                label=self.label,
                widget=widget,
                choices=self.choices,
                required=self.required,
            )
        elif self.field_type in ["image", "file"]:
            if self.field_type == "file":
                attrs.update({"css_class": "upload-file"})
            widget = FileUpload(attrs=attrs)
            formfields[f"file_{self.keyword}"] = forms.FileField(
                widget=widget, required=self.required, label=self.label
            )
            field = forms.CharField(required=self.required, widget=forms.HiddenInput())
        elif self.field_type in ["select_type", "select_conditional_custom"]:
            if self.field_type == "select_conditional_custom":
                attrs.update({"disabled": "disabled"})
            widget = Select(attrs=attrs)
            widget.options = field_options
            widget.searchable = True
            field = forms.ChoiceField(
                label=self.label,
                widget=widget,
                choices=self.choices,
                required=self.required,
            )
        elif self.field_type == "todo":
            field = forms.CharField(
                label=self.label,
                widget=forms.TextInput(
                    attrs={"readonly": "readonly", "value": "[TODO]"}
                ),
                required=self.required,
            )
        elif self.field_type == "select_model":
            attrs.update(
                {
                    "data-key-keyword": self.keyword,
                }
            )
            widget = Select(attrs=attrs)
            widget.options = field_options
            widget.searchable = True
            choices = [("", "-")]
            choices.extend(get_choices_from_model(self.form_options.get("model")))
            field = forms.ChoiceField(
                label=self.label, widget=widget, choices=choices, required=self.required
            )
        elif self.field_type == "select_conditional_questiongroup":
            attrs.update(
                {
                    "data-key-keyword": self.keyword,
                }
            )
            widget = Select(attrs=attrs)
            widget.options = field_options
            widget.searchable = True
            choices = [("", "-")]
            questiongroups = self.form_options.get("options_by_questiongroups", [])
            widget.options_order = questiongroups
            choices.extend(
                get_choices_from_questiongroups(
                    questionnaire_data,
                    questiongroups,
                    self.configuration_keyword,
                    self.edition,
                )
            )
            field = ConditionalQuestiongroupChoiceField(
                label=self.label,
                widget=widget,
                choices=choices,
                required=self.required,
                question=self,
            )
        else:
            raise ConfigurationErrorInvalidOption(self.field_type, "type", self)

        if translation_field is None:
            # Values which are not translated
            formfields[self.keyword] = field
            templates[self.keyword] = form_template
            options[self.keyword] = field_options
        else:
            # Store the old values in a hidden field
            old = forms.CharField(
                label=self.label, widget=forms.HiddenInput(), required=self.required
            )
            if show_translation:
                formfields[
                    f"{self.translation_translation_prefix}{self.keyword}"
                ] = translation_field
            formfields[f"{self.translation_original_prefix}{self.keyword}"] = field
            formfields[f"{self.translation_old_prefix}{self.keyword}"] = old
            for f in [
                f"{self.translation_translation_prefix}{self.keyword}",
                f"{self.translation_original_prefix}{self.keyword}",
                f"{self.translation_old_prefix}{self.keyword}",
            ]:
                templates[f] = form_template
                options[f] = field_options

        if widget:
            widget.conditional = self.conditional
            widget.questiongroup_conditions = ",".join(self.questiongroup_conditions)

        return formfields, templates, options

    def get_details(self, data=None, measure_label=None, questionnaire_object=None):
        if data is None:
            data = {}
        MAX_MEASURE_LEVEL = 5
        template_values = self.view_options
        template_values.update(
            {
                "additional_translations": self.additional_translations,
            }
        )
        value = data.get(self.keyword)
        if self.field_type in ["select_conditional_questiongroup"]:
            # Determine the options currently valid based on availability of
            # certain questiongroups in the data JSON.
            questiongroups = self.form_options.get("options_by_questiongroups", [])
            questionnaire_data = {}
            if questionnaire_object:
                questionnaire_data = questionnaire_object.data
            self.choices = get_choices_from_questiongroups(
                questionnaire_data,
                questiongroups,
                self.configuration_keyword,
                self.edition,
            )
        if self.field_type in [
            "bool",
            "measure",
            "checkbox",
            "image_checkbox",
            "select_type",
            "select",
            "cb_bool",
            "radio",
            "select_conditional_questiongroup",
            "select_conditional_custom",
            "multi_select",
        ]:
            # Look up the labels for the predefined values
            if not isinstance(value, list):
                value = [value]
            values = self.lookup_choices_labels_by_keywords(value)
        if self.field_type in ["char", "text", "todo", "date", "int", "display_only"]:
            template_name = "textarea"
            template_values.update(
                {
                    "key": self.label_view,
                    "value": value,
                }
            )
        elif self.field_type in ["map"]:
            template_name = "map"
            try:
                map_url = reverse(
                    "{}:questionnaire_view_map".format(
                        self.view_options.get("configuration")
                    ),
                    kwargs={"identifier": questionnaire_object.code},
                )
            except NoReverseMatch:
                map_url = None
            template_values.update(
                {
                    "key": self.label_view,
                    "value": value,
                    "questionnaire_object": questionnaire_object,
                    "map_url": map_url,
                }
            )
        elif self.field_type in ["float"]:
            template_name = "float"
            template_values.update(
                {
                    "key": self.label_view,
                    "value": value,
                    "decimals": self.form_options.get("field_options", {}).get(
                        "decimals"
                    ),
                }
            )
        elif self.field_type in [
            "bool",
            "select_type",
            "select",
            "select_conditional_questiongroup",
            "select_conditional_custom",
        ]:
            template_name = "textinput"
            template_values.update(
                {
                    "key": self.label_view,
                    "value": values[0],
                }
            )
        elif self.field_type in ["measure"]:
            template_name = "measure_bar"
            level = None
            try:
                pos = [c[1] for c in self.choices].index(values[0])
                level = round(pos / len(self.choices) * MAX_MEASURE_LEVEL)
            except ValueError:
                pass
            if measure_label is None:
                key = self.label_view
            else:
                key = measure_label
            template_values.update(
                {
                    "key": key,
                    "value": values[0],
                    "level": level,
                }
            )
        elif self.field_type in ["checkbox", "cb_bool", "radio", "multi_select"]:
            # Keep only values which were selected.
            values = [v for v in values if v]

            template_name = "checkbox"
            if self.view_options.get("with_raw_values") is True:
                # Also add the raw keywords of the values.
                value_keywords = []
                for v in value:
                    if v is not None:
                        i = [y[0] for y in list(self.choices)].index(v)
                        value_keywords.append(self.choices[i][0])
                values = list(zip(values, value_keywords))
            template_values.update(
                {
                    "key": self.label_view,
                    "values": values,
                }
            )
        elif self.field_type in ["image_checkbox"]:
            conditional_outputs = []
            for v in value:
                conditional_rendered = None
                for cond in self.conditions:
                    if v != cond[0]:
                        continue
                    cond_key_object = self.questiongroup.get_question_by_key_keyword(
                        cond[2]
                    )
                    conditional_rendered = cond_key_object.get_details(data)
                conditional_outputs.append(conditional_rendered)

            # Look up the image paths for the values
            images = []
            value_keywords = []
            for v in value:
                if v is not None:
                    i = [y[0] for y in list(self.choices)].index(v)
                    value_keywords.append(self.choices[i][0])
                    images.append(self.images[i])
            template_name = "image_checkbox"
            if self.conditional:
                template_name = "image_checkbox_conditional"
            template_values.update(
                {
                    "key": self.label_view,
                    "values": list(
                        zip(values, images, conditional_outputs, value_keywords)
                    ),
                }
            )
        elif self.field_type in ["link_video"]:
            template_name = "video"
            template_values.update(
                {
                    "key": self.label_view,
                    "value": value,
                }
            )
        elif self.field_type in ["image", "file"]:
            file_data = File.get_data(uid=value)
            template_name = "file"
            preview_image = ""
            if file_data:
                preview_image = file_data.get("interchange_list")[1][0]
            template_values.update(
                {
                    "content_type": file_data.get("content_type"),
                    "preview_image": preview_image,
                    "key": self.label_view,
                    "value": file_data.get("url"),
                }
            )
        elif self.field_type in ["user_id"]:
            template_name = "user_display"
            if value is not None:
                unknown_user = False
                try:
                    user = User.objects.get(pk=value)
                    user_display = user.get_display_name()
                except User.DoesNotExist:
                    unknown_user = True
                    user_display = "Unknown User"
                template_values.update(
                    {
                        "value": user_display,
                        "user_id": value,
                        "unknown_user": unknown_user,
                    }
                )
            else:
                return "\n"

        elif self.field_type == "hidden":
            template_name = "hidden"
            template_values.update(
                {
                    "key": self.label_view,
                    "value": value,
                }
            )

        elif self.field_type == "select_model":
            template_name = "select_model"
            model = apps.get_model(
                app_label="configuration", model_name=self.form_options["model"]
            )
            template_values.update(
                {
                    "value": value,
                    "label": self.label_view,
                }
            )
            try:
                template_values["text"] = model.objects.get(id=value).__str__()
            except model.DoesNotExist:
                # Edge condition for old cases without ID but with display value
                template_values["text"] = data.get(f"{self.keyword}_display", "")
        elif self.field_type in ["link_id"]:
            return "\n"

        elif self.field_type in ["wms_layer"]:
            template_name = "wms_layer"
            template_values.update(
                {
                    "layer": value,
                    "wms_url": self.view_options.get("wms_url"),
                }
            )
        else:
            raise ConfigurationErrorInvalidOption(self.field_type, "type", self)

        if (
            self.form_options.get("layout") == "stacked"
            or self.view_options.get("layout") == "stacked"
        ):
            if self.view_options.get("label_position") == "none":
                key = None
            # Add all values and their measure value.
            all_values = []
            for choice in self.choices:
                current_level = 1
                for v in values:
                    if v == choice[1]:
                        current_level = MAX_MEASURE_LEVEL
                all_values.append((current_level, choice[1]))
            template_values.update(
                {
                    "all_values": all_values,
                    "label_text_direction": self.view_options.get(
                        "label_text_direction"
                    ),
                }
            )

        template_name = self.view_options.get("template", template_name)
        if template_name == "raw":
            return template_values
        template = f"details/field/{template_name}.html"
        return render_to_string(template, template_values)

    def lookup_choices_labels_by_keywords(self, keywords):
        """
        Small helper function to lookup the label of choices (values of
        the keys) based on their keyword. If a label is not found, an
        empty string is added as label.

        Args:
            ``keywords`` (list): A list with value keywords.

        Returns:
            ``list``. A list with labels of the values.
        """
        labels = []
        for keyword in keywords:
            if (
                not isinstance(keyword, str)
                and not isinstance(keyword, bool)
                and not isinstance(keyword, int)
            ):
                labels.append("")
            labels.append(dict(self.choices).get(keyword))
        return labels


class QuestionnaireQuestiongroup(BaseConfigurationObject):
    """
    A class representing the configuration of a Questiongroup of the
    Questionnaire.
    """

    valid_options = [
        "keyword",
        "questions",
        "view_options",
        "form_options",
    ]
    default_template = "default"
    default_min_num = 1
    name_current = "questiongroups"
    name_parent = "subcategories"
    name_children = "questions"
    Child = QuestionnaireQuestion
    inherited_configuration = None
    inherited_questiongroup = None

    def __init__(self, parent_object, configuration):
        """
        Parameter ``configuration`` is a ``dict`` containing the
        configuration of the Questiongroup. It needs to have the
        following format::

          {
            # The keyword of the questiongroup.
            "keyword": "QUESTIONGROUP_KEYWORD",

            # (optional)
            "view_options": {
              # Default: "default"
              "template": "TEMPLATE_NAME",

              # Default: ""
              "conditional_question": "KEY_KEYWORD",

              # Default: ""
              "layout": "before_table"
            },

            # (optional)
            "form_options": {
              # Default: "default"
              "template": "TEMPLATE_NAME",

              # Default: 1
              "min_num": 2,

              # Default: 1
              "max_num: 3,

              # Default: ""
              "numbered": "NUMBERED",

              # Default: ""
              "detail_level": "DETAIL_LEVEL",

              # Default: ""
              "questiongroup_condition": "CONDITION_NAME",

              # Default: "" - can also be a list!
              "layout": "before_table",

              # Default: ""
              "row_class": "no-top-margin".

              # Default: "h4"
              "label_tag": "h5",

              # Default: ""
              "label_class": "",

              # Default: ""
              "table_columns": 2
            },

            # A list of questions.
            "questions": [
              # ...
            ]
          }

        .. seealso::
            For more information on the format and the configuration
            options, please refer to the documentation:
            :doc:`/configuration/questiongroup`

        Raises:
            :class:`qcat.errors.ConfigurationErrorInvalidConfiguration`
        """
        super().__init__(parent_object, configuration)
        self.questions = self.children

        self.configuration = self.configuration_object.configuration

        view_options = self.configuration.get("view_options", {})
        if configuration.get("view_options"):
            view_options.update(configuration.get("view_options"))
        self.view_options = view_options

        form_options = self.configuration.get("form_options", {})
        if configuration.get("form_options"):
            form_options.update(configuration.get("form_options"))
        self.form_options = form_options

        self.min_num = self.form_options.get("min_num", self.default_min_num)
        if not isinstance(self.min_num, int) or self.min_num < 1:
            raise ConfigurationErrorInvalidConfiguration(
                "min_num", "integer >= 1", "questiongroup"
            )

        self.max_num = self.form_options.get("max_num", self.min_num)
        if not isinstance(self.max_num, int) or self.max_num < 1:
            raise ConfigurationErrorInvalidConfiguration(
                "max_num", "integer >= 1", "questiongroup"
            )

        self.questiongroup_condition = self.form_options.get("questiongroup_condition")

        self.numbered = self.form_options.get("numbered", "")
        if self.numbered not in ["inline", "prefix"]:
            self.numbered = ""

        self.detail_level = self.form_options.get("detail_level")

        self.inherited_configuration = self.configuration.get("inherited_configuration")
        self.inherited_questiongroup = self.configuration.get("inherited_questiongroup")

        # TODO
        self.required = False

    def get_form(
        self,
        post_data=None,
        initial_data=None,
        show_translation=False,
        edit_mode="edit",
        edited_questiongroups=None,
        initial_links=None,
        questionnaire_data=None,
    ):
        """
        Returns:
            ``forms.formset_factory``. A formset consisting of one or
            more form fields representing a set of questions belonging
            together and which can possibly be repeated multiple times.
        """
        if edited_questiongroups is None:
            edited_questiongroups = []
        form_template = "form/questiongroup/{}.html".format(
            self.form_options.get("template", "default")
        )
        # todo: this is a workaround.
        # inspect following problem: the form_template throws an error
        # when the config is loaded from the lru_cache.
        # this is might be caused by mro or mutable types as method
        # kwargs.
        if self.form_options.get("template", "").endswith(".html"):
            form_template = self.form_options.get("template")

        formfields = {}
        templates = {}
        options = {}
        for f in self.questions:
            formfields, templates, options = f.add_form(
                formfields,
                templates,
                options,
                show_translation,
                edit_mode=edit_mode,
                questionnaire_data=questionnaire_data,
            )

        if self.numbered != "":
            formfields["__order"] = forms.IntegerField(
                label="order", widget=forms.HiddenInput()
            )
            if isinstance(initial_data, list):
                initial_data = sorted(initial_data, key=lambda qg: qg.get("__order", 0))

        Form = type("Form", (forms.Form,), formfields)

        formset_options = {
            "max_num": self.max_num,
            "min_num": self.min_num,
            "extra": 0,
            "validate_max": True,
            "validate_min": False,
        }

        if self.required is True:
            FormSet = formset_factory(Form, formset=RequiredFormSet, **formset_options)
        else:
            FormSet = formset_factory(Form, **formset_options)

        if initial_data and len(initial_data) == 1 and initial_data[0] == {}:
            initial_data = None

        has_changes = False
        if self.keyword in edited_questiongroups:
            has_changes = True

        # TODO: Highlight changes disabled.
        # For the time being, the function to show changes has been
        # disabled. Delete the following line to reenable it.
        has_changes = False

        config = self.form_options
        config.update(
            {
                "keyword": self.keyword,
                "helptext": self.helptext,
                "label": self.label,
                "templates": templates,
                "options": options,
                "questiongroup_condition": self.questiongroup_condition,
                "numbered": self.numbered,
                "detail_level": self.detail_level,
                "template": form_template,
                "has_changes": has_changes,
            }
        )

        if self.form_options.get("link"):
            link_name = self.form_options.get("link")
            curr_initial_data = []
            curr_initial_links = initial_links.get(link_name, [])
            for link in curr_initial_links:
                curr_initial_data.append({"link_id": link.get("id")})
            initial_data = curr_initial_data

            try:
                link = reverse(f"{link_name}:questionnaire_link_search")
            except NoReverseMatch:
                link = None
            config.update(
                {
                    "search_url": link,
                    "initial_links": curr_initial_links,
                }
            )

        # This is a fix for editing UNCCD cases where boolean values were stored
        # incorrectly as "false" and "true" instead of 0 and 1.
        if initial_data is not None:
            for data_dict in initial_data:
                for key, value in data_dict.items():
                    if key not in [
                        "unccd_partnership",
                        "unccd_property_rights",
                        "unccd_local_stakeholders",
                        "unccd_population_involved",
                        "unccd_impact_biodiversity_conservation",
                        "unccd_impact_cc_mitigation",
                        "unccd_impact_cc_adaptation",
                        "unccd_cost_benefit_analysis",
                        "unccd_technology_disseminated",
                        "unccd_incentives",
                        "unccd_replicability",
                    ]:
                        continue
                    if value is False:
                        data_dict[key] = 0
                    else:
                        data_dict[key] = 1

        return config, FormSet(post_data, prefix=self.keyword, initial=initial_data)

    def get_rendered_questions(self, data, questionnaire_object=None):
        questiongroups = []
        for d in data:
            rendered_questions = []
            if self.view_options.get("extra") == "measure_other":
                measure_label = d.get(self.questions[0].keyword, "")
                rendered_questions.append(
                    self.questions[1].get_details(
                        d,
                        measure_label=measure_label,
                        questionnaire_object=questionnaire_object,
                    )
                )
                if len(self.questions) > 2:
                    for question in self.questions[2:]:
                        if question.conditional:
                            continue
                        rendered_questions.append(
                            question.get_details(
                                d, questionnaire_object=questionnaire_object
                            )
                        )
            else:
                for question in self.questions:
                    if question.conditional:
                        continue

                    question_details = question.get_details(
                        d, questionnaire_object=questionnaire_object
                    )
                    if question_details:
                        rendered_questions.append(question_details)
            questiongroups.append(rendered_questions)
        return questiongroups

    def get_details(self, data=None, links=None, questionnaire_object=None):
        if data is None:
            data = []
        view_template = "details/questiongroup/{}.html".format(
            self.view_options.get("template", "default")
        )
        questiongroups = self.get_rendered_questions(
            data, questionnaire_object=questionnaire_object
        )
        config = self.view_options
        config.update(
            {
                "numbered": self.numbered,
                "label": self.label,
                "label_class": self.view_options.get("label_class"),
            }
        )
        template_values = {
            "questiongroups": questiongroups,
            "config": config,
        }
        if links:
            template_values.update(
                {
                    "links": links,
                }
            )
        if self.view_options.get("raw_questions", False) is True:
            raw_questions = []
            for d in data:
                raw_questions.append(self.get_raw_data([d]))
            template_values.update({"raw_questions": raw_questions})
        if self.view_options.get("with_keys", False) is True:
            keys = []
            for q in self.questions:
                keys.append(q.label)
            template_values.update({"keys": keys})
        return render_to_string(view_template, template_values)

    def get_question_by_key_keyword(self, key_keyword):
        for question in self.questions:
            if question.keyword == key_keyword:
                return question
        return None

    def get_top_subcategory(self):
        """
        Helper function to get the top subcategory of a questiongroup. This is
        used to display a nicer error message in the form, also stating the
        subcategory (with numbering) in which the error occurred.
        """
        parent = self.parent_object
        next_parent = parent.parent_object
        while not isinstance(next_parent, QuestionnaireCategory):
            parent = next_parent
            next_parent = next_parent.parent_object
        return parent

    def get_raw_data(self, data):
        """
        Return only the raw data of a questiongroup. Data belonging to
        this questiongroup is returned as a flat dictionary. Predefined
        values are looked up to return their display value. The label of
        the key is also added to the dict.

        Args:
            ``questionnaire_data`` (dict): The questionnaire data
            dictionary.

        Returns:
            ``dict``. A flat dictionary with only the keys and values of
            the current questiongroup.
        """
        raw_data = {}
        for question in self.questions:
            for questiongroup_data in data:
                question_data = questiongroup_data.get(question.keyword)
                # Still look up the display values for fields
                # with predefined internal values.
                if question.field_type in [
                    "bool",
                    "measure",
                    "checkbox",
                    "image_checkbox",
                    "select_type",
                    "radio",
                ]:
                    if not isinstance(question_data, list):
                        question_data = [question_data]
                    question_data = question.lookup_choices_labels_by_keywords(
                        question_data
                    )
                raw_data[question.keyword] = question_data
                raw_data[f"label_{question.keyword}"] = question.label_view
        return raw_data


class QuestionnaireSubcategory(BaseConfigurationObject):
    """
    A class representing the configuration of a Subcategory of the
    Questionnaire.
    """

    valid_options = [
        "keyword",
        "questiongroups",
        "subcategories",
        "form_options",
        "view_options",
    ]
    name_current = "subcategories"
    name_parent = "categories"
    name_children = "questiongroups"
    Child = QuestionnaireQuestiongroup

    def __init__(self, parent_object, configuration):
        """
        Parameter ``configuration`` is a ``dict`` containing the
        configuration of the Subcategory. It needs to have the following
        format::

          {
            # The keyword of the subcategory.
            "keyword": "SUBCAT_KEYWORD",

            # (optional)
            "view_options": {
              # Default: "default"
              "template": "TEMPLATE_NAME",

              # Default: false
              "raw_questions": true,

              # Default: None
              "table_grouping": []
            },

            # (optional)
            "form_options": {
              # Default: "default"
              "template": "TEMPLATE_NAME",

              # Default: ""
              "label_tag": "h3",

              # Default: ""
              "label_class": "top-margin",

              # Default: []
              "questiongroup_conditions": [],

              # Default: ""
              "questiongroup_conditions_template": ""
            },

            # A list of questiongroups.
            "questiongroups": [
              # ...
            ],

            # A list of subcategories.
            "subcategories": [
              # ...
            ]
          }

        .. seealso::
            For more information on the format and the configuration
            options, please refer to the documentation:
            :doc:`/configuration/subcategory`

        Raises:
            :class:`qcat.errors.ConfigurationErrorInvalidConfiguration`,
            ``ConfigurationErrorNotInDatabase``.
        """
        super().__init__(parent_object, configuration)

        view_options = self.configuration.get("view_options", {})
        if configuration.get("view_options"):
            view_options.update(configuration.get("view_options"))
        self.view_options = view_options

        form_options = self.configuration.get("form_options", {})
        if configuration.get("form_options"):
            form_options.update(configuration.get("form_options"))
        self.form_options = form_options

        # A Subcategory can have further subcategories or questiongroups
        subcategories = []
        conf_subcategories = self.configuration.get("subcategories", [])
        for conf_subcategory in conf_subcategories:
            subcategories.append(QuestionnaireSubcategory(self, conf_subcategory))
        self.subcategories = subcategories

        questiongroups = []
        conf_questiongroups = self.configuration.get("questiongroups", [])
        for conf_questiongroup in conf_questiongroups:
            questiongroups.append(QuestionnaireQuestiongroup(self, conf_questiongroup))
        self.questiongroups = questiongroups

        if len(self.subcategories) > 0:
            self.children = self.subcategories
        else:
            self.children = self.questiongroups

        self.link_questiongroups = []
        if self.form_options.get("has_links", False) is True:
            for qg in self.questiongroups:
                if qg.form_options.get("link"):
                    self.link_questiongroups.append(qg.keyword)

        self.table_grouping = self.view_options.get("table_grouping", None)
        self.table_headers = []
        self.table_helptexts = []
        if self.table_grouping:
            for questiongroup in self.questiongroups:
                if questiongroup.keyword in [g[0] for g in self.table_grouping]:
                    for question in questiongroup.questions:
                        self.table_headers.append(question.label)
                        self.table_helptexts.append(question.helptext)

    def get_form(
        self,
        post_data=None,
        initial_data=None,
        show_translation=False,
        edit_mode="edit",
        edited_questiongroups=None,
        initial_links=None,
    ):
        """
        Returns:
            ``dict``. A dict with configuration elements, namely ``label``.
            ``list``. A list of formsets of question groups, together
            forming a subcategory.
        """
        if initial_data is None:
            initial_data = {}
        if edited_questiongroups is None:
            edited_questiongroups = []
        form_template = "form/subcategory/{}.html".format(
            self.form_options.get("template", "default")
        )
        formsets = []
        config = self.form_options

        if config.get("questiongroup_conditions_template"):
            config[
                "questiongroup_conditions_template_path"
            ] = "form/field/{}.html".format(
                config.get("questiongroup_conditions_template")
            )

        config.update(
            {
                "label": self.label,
                "keyword": self.keyword,
                "helptext": self.helptext,
                "form_template": form_template,
            }
        )
        has_changes = False
        for questiongroup in self.questiongroups:
            questionset_initial_data = initial_data.get(questiongroup.keyword)
            formsets.append(
                questiongroup.get_form(
                    post_data=post_data,
                    initial_data=questionset_initial_data,
                    show_translation=show_translation,
                    edit_mode=edit_mode,
                    edited_questiongroups=edited_questiongroups,
                    initial_links=initial_links,
                    questionnaire_data=initial_data,
                )
            )
            config["next_level"] = "questiongroups"
            if questiongroup.keyword in edited_questiongroups:
                has_changes = True
        for subcategory in self.subcategories:
            formsets.append(
                subcategory.get_form(
                    post_data=post_data,
                    initial_data=initial_data,
                    show_translation=show_translation,
                    edit_mode=edit_mode,
                    edited_questiongroups=edited_questiongroups,
                    initial_links=initial_links,
                )
            )
            config["next_level"] = "subcategories"

        # TODO: Highlight changes disabled.
        # For the time being, the function to show changes has been
        # disabled. Delete the following line to reenable it.
        has_changes = False

        config.update({"has_changes": has_changes})

        if self.table_grouping:
            config.update(
                {
                    "table_grouping": self.table_grouping,
                    "table_headers": self.table_headers,
                    "table_helptexts": self.table_helptexts,
                }
            )

        return config, formsets

    def has_content(self, data):
        """
        Whether this subcategory has content (data) or not.

        Args:
            data: The data dictionary.

        Returns:
            bool.
        """
        for questiongroup in self.questiongroups:
            questiongroup_data = data.get(questiongroup.keyword, [])
            if not is_empty_list_of_dicts(questiongroup_data):
                return True
        return False

    def get_details(self, data=None, links=None, questionnaire_object=None):
        """
        Returns:
            ``string``. A rendered representation of the subcategory
            with its questiongroups.

            ``bool``. A boolean indicating whether the subcategory and
            its questiongroups have some data in them or not.
        """
        if data is None:
            data = {}
        view_template = "details/subcategory/{}.html".format(
            self.view_options.get("template", "default")
        )
        rendered_questiongroups = []
        rendered_questions = []
        raw_questiongroups = []
        has_content = False
        for questiongroup in self.questiongroups:
            questiongroup_links = {}
            if questiongroup.keyword in self.link_questiongroups:
                try:
                    link_configuration_code = questiongroup.keyword.rsplit("__", 1)[1]
                except IndexError:
                    link_configuration_code = None

                if links and link_configuration_code is not None:
                    questiongroup_links[link_configuration_code] = links.get(
                        link_configuration_code, []
                    )

            questiongroup_data = data.get(questiongroup.keyword, [])
            if not is_empty_list_of_dicts(questiongroup_data) or questiongroup_links:
                has_content = True
                if self.table_grouping and questiongroup.keyword in [
                    item for sublist in self.table_grouping for item in sublist
                ]:
                    # Order the values of the questiongroups according
                    # to their questions
                    q_order = [q.keyword for q in questiongroup.questions]
                    # qg_data = []
                    # Add empty values for all keys not available to
                    # keep the order inside the table even with empty
                    # values.
                    for qg in questiongroup_data:
                        for q in q_order:
                            if q not in qg:
                                qg[q] = []

                        # Remove data entries not in q_order anymore (e.g.
                        # removed questions after edition update) to prevent
                        # error when sorting below.
                        for k in set(qg.keys()) - set(q_order):
                            del qg[k]

                    sorted_questiongroup_data = [
                        sorted(qg.items(), key=lambda i: q_order.index(i[0]))
                        for qg in questiongroup_data
                    ]

                    data_labelled = []
                    for qg in sorted_questiongroup_data:
                        qg_labelled = []
                        for q in qg:
                            q_value = q[1]
                            q_obj = questiongroup.get_question_by_key_keyword(q[0])
                            if not q_obj:
                                continue
                            if not isinstance(q_value, list):
                                q_value = [q_value]
                            values = []
                            for v in q_value:
                                q_choice = next(
                                    (item for item in q_obj.choices if item[0] == v),
                                    None,
                                )
                                if q_choice:
                                    values.append(q_choice[1])
                                else:
                                    values.append(v)
                            qg_labelled.append((q_obj.label, values))
                        data_labelled.append(qg_labelled)
                    config = questiongroup.view_options
                    config.update(
                        {
                            "qg_keyword": questiongroup.keyword,
                            "data": sorted_questiongroup_data,
                            "data_labelled": data_labelled,
                            "label": questiongroup.label,
                        }
                    )
                    raw_questiongroups.append(config)
                elif self.view_options.get("raw_questions", False) is True:
                    config = questiongroup.view_options
                    config.update(
                        {
                            "qg": questiongroup.keyword,
                            "questions": questiongroup.get_rendered_questions(
                                questiongroup_data
                            ),
                        }
                    )
                    rendered_questions.append(config)
                else:
                    questiongroup_config = questiongroup.view_options
                    questiongroup_config.update(
                        {
                            "keyword": questiongroup.keyword,
                            "label": questiongroup.label,
                        }
                    )
                    rendered_questiongroups.append(
                        (
                            questiongroup_config,
                            questiongroup.get_details(
                                questiongroup_data,
                                links=questiongroup_links,
                                questionnaire_object=questionnaire_object,
                            ),
                        )
                    )
        subcategories = []
        for subcategory in self.subcategories:
            sub_rendered, sub_has_content = subcategory.get_details(
                data=data, links=links, questionnaire_object=questionnaire_object
            )
            if sub_has_content:
                subcategories.append(sub_rendered)
                has_content = True

        template_values = self.view_options
        template_values.update(
            {
                "questiongroups": rendered_questiongroups,
                "questions": rendered_questions,
                "subcategories": subcategories,
                "label": self.label_view,
                "label_position": self.view_options.get("label_position"),
                "label_tag": self.view_options.get("label_tag"),
                "label_class": self.view_options.get("label_class"),
                "numbering": self.form_options.get("numbering"),
                "helptext": self.helptext,
            }
        )
        if self.table_grouping:
            template_values.update(
                {
                    "table_grouping": self.table_grouping,
                    "table_headers": self.table_headers,
                    "raw_questiongroups": raw_questiongroups,
                }
            )

        if self.view_options.get("media_gallery", False) is True:
            media_data = self.parent_object.parent_object.parent_object.get_image_data(
                data
            )
            media_content = media_data.get("content", [])
            media_additional = media_data.get("additional", {})
            if media_content:
                # If there is at least one image (even though it might be the
                # header image), that's enough to show the current subcategory.
                has_content = True
            template_values.update(
                {
                    "media_content": media_content,
                    "media_additional": media_additional,
                }
            )

        rendered = render_to_string(view_template, template_values)
        return rendered, has_content


class QuestionnaireCategory(BaseConfigurationObject):
    """
    A class representing the configuration of a Category of the
    Questionnaire.
    """

    valid_options = [
        "keyword",
        "subcategories",
        "view_options",
        "form_options",
    ]
    name_current = "categories"
    name_parent = "sections"
    name_children = "subcategories"
    Child = QuestionnaireSubcategory

    def __init__(self, parent_object, configuration):
        """
        Parameter ``configuration`` is a ``dict`` containing the
        configuration of the Category. It needs to have the following
        format::

          {
            # The keyword of the category.
            "keyword": "CAT_KEYWORD",

            # (optional)
            "view_options": {
              # Default: "default"
              "template": "TEMPLATE_NAME",

              # Default: false
              "use_raw_data": true,

              # Default: false
              "with_metadata": true,

              # Default: {}
              "additional_data": {
                "QUESTIONGROUP": ["KEY"]
              }
            },

            # A list of subcategories.
            "subcategories": [
              {
                # ...
              }
            ]
          }

        .. seealso::
            For more information on the format and the configuration
            options, please refer to the documentation:
            :doc:`/configuration/category`
        """
        super().__init__(parent_object, configuration)
        self.subcategories = self.children

        view_options = self.configuration.get("view_options", {})
        if configuration.get("view_options"):
            view_options.update(configuration.get("view_options"))
        self.view_options = view_options

        form_options = self.configuration.get("form_options", {})
        if configuration.get("form_options"):
            form_options.update(configuration.get("form_options"))
        self.form_options = form_options

    def get_link_questiongroups(self):
        qg = []
        for subcategory in self.subcategories:
            qg.extend(subcategory.link_questiongroups)
        return qg

    def get_form(
        self,
        post_data=None,
        initial_data=None,
        show_translation=False,
        edit_mode="edit",
        edited_questiongroups=None,
        initial_links=None,
    ):
        """
        Returns:
            ``dict``. A dict with configuration elements, namely ``label``.
            ``list``. A list of a list of subcategory formsets.
        """
        if edited_questiongroups is None:
            edited_questiongroups = []
        subcategory_formsets = []
        for subcategory in self.subcategories:
            subcategory_formsets.append(
                subcategory.get_form(
                    post_data=post_data,
                    initial_data=initial_data,
                    show_translation=show_translation,
                    edit_mode=edit_mode,
                    edited_questiongroups=edited_questiongroups,
                    initial_links=initial_links,
                )
            )
        has_changes = False
        for qg in self.get_questiongroups():
            if qg.keyword in edited_questiongroups:
                has_changes = True
                break

        # TODO: Highlight changes disabled.
        # For the time being, the function to show changes has been
        # disabled. Delete the following line to reenable it.
        has_changes = False

        config = {
            "label": self.label,
            "numbering": self.form_options.get("numbering"),
            "helptext": self.helptext,
            "has_changes": has_changes,
        }
        configuration = self.view_options.get("configuration")
        if configuration:
            config.update({"configuration": configuration})
        return config, subcategory_formsets

    def get_completeness(self, data):
        """
        Return the number of subcategories with content and the total number of
        subcategories.

        Args:
            data: The data dictionary.

        Returns:
            int, int.
        """
        complete = 0
        total = len(self.get_subcategories())
        for subcategory in self.subcategories:
            if subcategory.has_content(data) is True:
                complete += 1
        return complete, total

    def get_subcategories(self):
        """
        Return a list of all subcategories.

        Returns:
            list.
        """
        return [c for c in self.subcategories if c.questiongroups or c.subcategories]

    def get_details(
        self,
        data=None,
        permissions=None,
        edit_step_route="",
        questionnaire_object=None,
        csrf_token=None,
        edited_questiongroups=None,
        view_mode="view",
        links=None,
        review_config=None,
        user=None,
        completeness_percentage=0,
    ):
        if data is None:
            data = {}
        if permissions is None:
            permissions = []
        if edited_questiongroups is None:
            edited_questiongroups = []
        view_template = "details/category/{}.html".format(
            self.view_options.get("template", "default")
        )
        rendered_subcategories = []
        with_content = 0
        raw_data = {}
        metadata = {}
        for subcategory in self.subcategories:
            rendered_subcategory, has_content = subcategory.get_details(
                data, links=links, questionnaire_object=questionnaire_object
            )
            if has_content:
                category_config = {"keyword": subcategory.keyword}
                rendered_subcategories.append((rendered_subcategory, category_config))
                with_content += 1

        if (
            self.view_options.get("with_metadata", False) is True
            and questionnaire_object is not None
        ):
            metadata = questionnaire_object.get_metadata()

        if self.view_options.get("use_raw_data", False) is True:
            raw_data = self.get_raw_category_data(data)

        additional_data = {}
        additional_keys = self.view_options.get("additional_data", {})
        if additional_keys != {}:
            for qg in self.parent_object.parent_object.get_questiongroups():
                if qg.keyword not in [a[0] for a in additional_keys.items()]:
                    continue

                qg_data = data.get(qg.keyword, [])
                for key in additional_keys[qg.keyword]:
                    question = qg.get_question_by_key_keyword(key)
                    additional_entry = []
                    for d in qg_data:
                        k = d.get(key)
                        if k is None:
                            continue
                        if question.field_type in [
                            "bool",
                            "measure",
                            "checkbox",
                            "image_checkbox",
                            "select_type",
                            "radio",
                        ]:
                            if not isinstance(k, list):
                                k = [k]
                            k = question.lookup_choices_labels_by_keywords(k)
                        additional_entry.append(k)
                    additional_data[key] = additional_entry
                    additional_data[f"label_{key}"] = question.label_view

        questionnaire_identifier = "new"
        if questionnaire_object is not None:
            questionnaire_identifier = questionnaire_object.code

        configuration = self.view_options.get(
            "configuration", self.configuration_keyword
        )

        has_changes = False
        for qg in self.get_questiongroups():
            if qg.keyword in edited_questiongroups:
                has_changes = True
                break

        # TODO: Highlight changes disabled.
        # For the time being, the function to show changes has been
        # disabled. Delete the following line to reenable it.
        has_changes = False

        categories_with_content = self.get_subcategories()

        history = []
        if questionnaire_object is not None:
            history = questionnaire_object.get_history_versions(user)

        return render_to_string(
            view_template,
            {
                "subcategories": rendered_subcategories,
                "raw_data": raw_data,
                "additional_data": additional_data,
                "metadata": metadata,
                "label": self.label,
                "numbering": self.form_options.get("numbering"),
                "keyword": self.keyword,
                "csrf_token": csrf_token,
                "permissions": permissions,
                "view_mode": view_mode,
                "complete": with_content,
                "total": len(categories_with_content),
                "progress": int(with_content / len(categories_with_content) * 100),
                "edit_step_route": edit_step_route,
                "configuration_name": configuration,
                "questionnaire_identifier": questionnaire_identifier,
                "questionnaire_object": questionnaire_object,
                "has_changes": has_changes,
                "review_config": review_config,
                "user": user,
                "completeness_percentage": completeness_percentage,
                "history": history,
            },
        )

    def get_raw_category_data(self, questionnaire_data):
        """
        Return only the raw data of a category. The entire questionnaire
        data is scanned for the questiongroups belonging to the current
        category. Only the data of these questiongroups is then
        returned as a flat dict.

        .. important::
            This function may return unexpected outputs when used on
            categories with repeating questiongroups or with keys having
            the same keyword.

        Args:
            ``questionnaire_data`` (dict): The questionnaire data
            dictionary.

        Returns:
            ``dict``. A flat dictionary with only the keys and values of
            the current category.
        """
        raw_category_data = {}
        for subcategory in self.subcategories:
            for questiongroup in subcategory.questiongroups:
                questiongroups_data = questionnaire_data.get(questiongroup.keyword, {})
                raw_category_data.update(
                    questiongroup.get_raw_data(questiongroups_data)
                )
        return raw_category_data

    def get_questiongroups(self):
        def unnest_questiongroups(nested):
            ret = []
            try:
                for child in nested.children:
                    if not isinstance(child, QuestionnaireQuestiongroup):
                        ret.extend(unnest_questiongroups(child))
                    else:
                        ret.append(child)
            except AttributeError:
                pass
            return ret

        return unnest_questiongroups(self)


class QuestionnaireSection(BaseConfigurationObject):
    """
    A class representing the configuration of a Section of the
    Questionnaire.
    """

    valid_options = [
        "categories",
        "keyword",
        "view_options",
    ]
    name_current = "sections"
    name_parent = None
    name_children = "categories"
    Child = QuestionnaireCategory

    def __init__(self, parent_object, configuration):
        """
        Parameter ``configuration`` is a ``dict`` containing the
        configuration of the Section. It needs to have the following
        format::

          {
            # The keyword of the section.
            "keyword": "SECTION_KEYWORD",

            # (optional)
            "view_options": {
              # Default: "default"
              "template": "TEMPLATE_NAME",

              # Default: false
              "media_gallery": true
            },

            # A list of categories.
            "categories": [
              {
                # ...
              }
            ]
          }

        .. seealso::
            For more information on the format and the configuration
            options, please refer to the documentation:
            :doc:`/configuration/section`
        """
        super().__init__(parent_object, configuration)
        self.categories = self.children

        view_options = self.configuration.get("view_options", {})
        if configuration.get("view_options"):
            view_options.update(configuration.get("view_options"))
        self.view_options = view_options

        form_options = self.configuration.get("form_options", {})
        if configuration.get("form_options"):
            form_options.update(configuration.get("form_options"))
        self.form_options = form_options

    def get_completeness(self, data):
        """
        Return the number of subcategories with content and the total number of
        subcategories.

        Args:
            data: The data dictionary.

        Returns:
            int, int.
        """
        complete = 0
        total = 0
        for category in self.categories:
            complete_category, total_category = category.get_completeness(data)
            total += total_category
            complete += complete_category
        return complete, total

    def get_details(
        self,
        data=None,
        permissions=None,
        review_config=None,
        edit_step_route="",
        questionnaire_object=None,
        csrf_token=None,
        edited_questiongroups=None,
        view_mode="view",
        links=None,
        user=None,
        completeness_percentage=0,
    ):
        view_template = "details/section/{}.html".format(
            self.view_options.get("template", "default")
        )

        rendered_categories = []
        for category in self.categories:
            rendered_categories.append(
                category.get_details(
                    data,
                    permissions=permissions,
                    edit_step_route=edit_step_route,
                    questionnaire_object=questionnaire_object,
                    csrf_token=csrf_token,
                    edited_questiongroups=edited_questiongroups,
                    view_mode=view_mode,
                    links=links,
                    review_config=review_config,
                    user=user,
                    completeness_percentage=completeness_percentage,
                )
            )

        media_content = []
        media_additional = {}
        if self.view_options.get("media_gallery", False) is True:
            media_data = self.parent_object.get_image_data(data)
            media_content = media_data.get("content", [])
            media_additional = media_data.get("additional", {})

        return render_to_string(
            view_template,
            {
                "label": self.label,
                "keyword": self.keyword,
                "categories": rendered_categories,
                "media_content": media_content,
                "media_additional": media_additional,
            },
        )

    def get_questiongroups(self):
        def unnest_questiongroups(nested):
            ret = []
            try:
                for child in nested.children:
                    if not isinstance(child, QuestionnaireQuestiongroup):
                        ret.extend(unnest_questiongroups(child))
                    else:
                        ret.append(child)
            except AttributeError:
                pass
            return ret

        return unnest_questiongroups(self)


class QuestionnaireConfiguration(BaseConfigurationObject):
    """
    A class representing the configuration of a Questionnaire.

    .. seealso::
        For more information on the format and the configuration
        options, please refer to the documentation:
        :doc:`/configuration/questionnaire`
    """

    valid_options = [
        "sections",
        "modules",
    ]
    name_current = "-"
    name_parent = "-"
    name_children = "sections"
    Child = QuestionnaireSection

    def __init__(self, keyword, configuration_object=None):
        self.keyword = keyword
        self.configuration_keyword = keyword
        self.sections = []
        self.modules = []
        self.inherited_data = {}
        self.configuration_object = configuration_object
        if self.configuration_object is None:
            # read_configuration will handle errors if it does not exist
            with contextlib.suppress(Configuration.DoesNotExist):
                self.configuration_object = Configuration.latest_by_code(keyword)
        # Also store edition for easier access
        self.edition = None
        if self.configuration_object:
            self.edition = self.configuration_object.edition
        self.configuration_error = None
        # TODO The following code hits the database 25.000+ times 🤯
        try:
            self.read_configuration()
        except Exception as e:
            if isinstance(e, ConfigurationError):
                self.configuration_error = e
            else:
                raise e

    @property
    def has_new_edition(self):
        return self.configuration_object.has_new_edition

    def get_modules(self):
        return self.modules

    def get_inherited_data(self):
        return self.inherited_data

    def get_configuration_errors(self):
        return self.configuration_error

    @staticmethod
    def get_country_filter(country_keyword):
        """
        Return the query parameters representing a country filter.

        Args:
            country_keyword:

        Returns:

        """
        return f"filter__qg_location__country={country_keyword}"

    def add_category(self, category):
        self.categories.append(category)

    def get_category(self, keyword):
        for section in self.sections:
            for category in section.categories:
                if category.keyword == keyword:
                    return category
        return None

    def get_questiongroups(self):
        def unnest_questiongroups(nested):
            ret = []
            try:
                for child in nested.children:
                    if not isinstance(child, QuestionnaireQuestiongroup):
                        ret.extend(unnest_questiongroups(child))
                    else:
                        ret.append(child)
            except AttributeError:
                pass
            return ret

        return unnest_questiongroups(self)

    def get_questiongroup_by_keyword(self, keyword):
        for questiongroup in self.get_questiongroups():
            if questiongroup.keyword == keyword:
                return questiongroup
        return None

    def get_question_by_keyword(self, questiongroup_keyword, keyword):
        questiongroup = self.get_questiongroup_by_keyword(questiongroup_keyword)
        if questiongroup is not None:
            return questiongroup.get_question_by_key_keyword(keyword)
        return None

    def get_completeness(self, data):
        """
        Return the number of subcategories with content and the total number of
        subcategories.

        Args:
            data: The data dictionary.

        Returns:
            int, int.
        """
        complete = 0
        total = 0
        for section in self.sections:
            complete_section, total_section = section.get_completeness(data)
            complete += complete_section
            total += total_section
        return complete, total

    def get_details(
        self,
        data=None,
        permissions=None,
        review_config=None,
        edit_step_route="",
        questionnaire_object=None,
        csrf_token=None,
        edited_questiongroups=None,
        view_mode="view",
        links=None,
        user=None,
        completeness_percentage=0,
    ):
        rendered_sections = []
        for section in self.sections:
            rendered_sections.append(
                section.get_details(
                    data,
                    permissions=permissions,
                    review_config=review_config,
                    edit_step_route=edit_step_route,
                    questionnaire_object=questionnaire_object,
                    csrf_token=csrf_token,
                    edited_questiongroups=edited_questiongroups,
                    view_mode=view_mode,
                    links=links,
                    user=user,
                    completeness_percentage=completeness_percentage,
                )
            )
        return rendered_sections

    def get_toc_data(self):
        categories = []
        for section in self.sections:
            for category in section.categories:
                categories.append(
                    (
                        category.keyword,
                        category.label,
                        category.form_options.get("numbering"),
                    )
                )
        return categories

    def get_image_data(self, data):
        """
        Return image data from outside the category. Loops through all
        the fields to find the questiongroups containing images. For all
        these, basic information about the images are collected and
        returned as a list of dictionaries.

        Args:
            ``data`` (dict): A questionnaire data dictionary.

        Returns:
            ``list``. A list of dictionaries for each image. Each
            dictionary has the following entries:

            - ``image``: The URL of the original image.

            - ``interchange``: The data which can be used for the
              interchange of images.

            - ``caption``: The caption of the image. Corresponds to
              field ``image_caption``.

            - ``date``: The date of the image. Corresponds to field
              ``image_date``.

            - ``location``: The location of the image. Corresponds to field
              ``image_date``.

            - ``photographer``: The photographer of the image.
              Corresponds to field ``image_photographer``.
        """
        image_questiongroups = []
        additional_data = {}
        for questiongroup in self.get_questiongroups():
            if questiongroup.keyword == "qg_image_remarks":
                additional_data.update(
                    questiongroup.get_raw_data(data.get("qg_image_remarks", []))
                )
            for question in questiongroup.questions:
                if (
                    question.field_type == "image"
                    and data.get(questiongroup.keyword) is not None
                ):
                    image_questiongroups.extend(data.get(questiongroup.keyword))

        images = []
        for image in image_questiongroups:
            # Maybe it is not a real image (e.g. maps can also be uploaded as
            # images)
            if image.get("image") is None:
                continue
            image_data = File.get_data(uid=image.get("image"))
            images.append(
                {
                    "image": image_data.get("url"),
                    "interchange": image_data.get("interchange"),
                    "interchange_list": image_data.get("interchange_list"),
                    "caption": image.get("image_caption"),
                    "date": image.get("image_date"),
                    "location": image.get("image_location"),
                    "photographer": image.get("image_photographer"),
                    "absolute_path": image_data.get("absolute_path"),
                    "relative_path": image_data.get("relative_path"),
                    "target": image.get("image_target"),
                }
            )
        return {
            "content": images,
            "additional": additional_data,
        }

    def get_filter_keys(self):
        """
        Return a list of FilterKey named tuples containing information about the
        filterable keys.

        Returns:
            List of FilterKey named tuples.
        """
        # Note that path and label need to appear first so they can be used as
        # select options.
        FilterKey = collections.namedtuple(
            "FilterKey",
            [
                "path",
                "label",
                "order",
                "key",
                "questiongroup",
                "filter_type",
                "section_label",
            ],
        )
        filter_keys = []
        for questiongroup in self.get_questiongroups():
            for question in questiongroup.questions:
                if (
                    question.filter_options
                    and question.filter_options.get("order") is not None
                ):
                    section = questiongroup.get_top_subcategory().parent_object
                    filter_keys.append(
                        FilterKey(
                            path=f"{questiongroup.keyword}__{question.keyword}",
                            label=question.label_filter,
                            order=question.filter_options.get("order"),
                            key=question.keyword,
                            questiongroup=questiongroup.keyword,
                            filter_type=question.field_type,
                            section_label=section.label,
                        )
                    )
        return sorted(filter_keys, key=lambda k: k.order)

    def get_list_data(self, questionnaire_data_list):
        """
        Get the data for the list representation of questionnaires.
        Which questions are shown depends largely on the option
        ``in_list`` of the question configuration.

        Args:
            ``questionnaire_data_list`` (list): A list of Questionnaire
            data dicts.

        Returns:
            ``list``. A list of dicts. A dict containing the keys and
            values to be appearing in the list. The values are not
            translated.
        """
        # Collect which keys are to be shown in the list.
        list_configuration = []
        for questiongroup in self.get_questiongroups():
            for question in questiongroup.questions:
                if question.in_list is True:
                    list_configuration.append(
                        (questiongroup.keyword, question.keyword, question.field_type)
                    )

        questionnaire_value_list = []
        for questionnaire_data in questionnaire_data_list:
            questionnaire_value = {}
            for list_entry in list_configuration:
                for question_data in questionnaire_data.get(list_entry[0], []):
                    key = list_entry[1]
                    value = question_data.get(list_entry[1])
                    if list_entry[2] == "image":
                        key = "image"
                        if questionnaire_value.get(key):
                            # If there is already an image, do not add it again
                            continue
                        image_data = File.get_data(uid=value)
                        interchange_list = image_data.get("interchange_list")
                        if interchange_list:
                            value = interchange_list[0][0]
                    if list_entry[2] in [
                        "bool",
                        "measure",
                        "checkbox",
                        "image_checkbox",
                        "select_type",
                    ]:
                        # Look up the labels for the predefined values
                        if not isinstance(value, list):
                            value = [value]
                        qg = self.get_questiongroup_by_keyword(list_entry[0])
                        if qg is None:
                            break
                        k = qg.get_question_by_key_keyword(list_entry[1])
                        if k is None:
                            break
                        values = k.lookup_choices_labels_by_keywords(value)
                        if list_entry[2] in ["bool", "measure", "select_type"]:
                            value = values[0]
                    questionnaire_value[key] = value
            # 'remap' keys for description field, providing a consistent access key.
            mapping = {
                "approaches": "app_definition",
                "cca": "tech_definition",
                "cbp": "tech_definition",
                "sample": "key_5",
                "samplemodule": "modkey_01",
                "samplemulti": "mkey_01",
                "technologies": "tech_definition",
                "unccd": "unccd_description",
                "watershed": "app_definition",
            }

            # For testing configurations (e.g. 'sample', 'samplemulti'), there
            # is no qg_name/name questiongroup/key. Instead, it relies on the
            # (probably deprecated) "is_name" key in the configuration json. In
            # order to add their name correctly to the list data, they are added
            # manually here.
            if "name" not in questionnaire_value:
                name_key, name_qg = self.get_name_keywords()
                if name_key is not None:
                    questionnaire_value["name"] = questionnaire_data.get(name_qg, [{}])[
                        0
                    ].get(name_key, {})

            # If configuration mapping is not set up, a KeyError will be raised.
            questionnaire_value["definition"] = questionnaire_value.get(
                mapping[self.keyword], {"en": ""}
            )

            questionnaire_value_list.append(questionnaire_value)
        return questionnaire_value_list

    def get_name_keywords(self):
        """
        Return the keywords of the question and questiongroup which
        contain the name of the questionnaire as defined in the
        configuration by the ``is_name`` parameter.
        """
        question_keyword = None
        questiongroup_keyword = None
        for questiongroup in self.get_questiongroups():
            for question in questiongroup.questions:
                if question.is_name is True:
                    question_keyword = question.keyword
                    questiongroup_keyword = questiongroup.keyword
        return question_keyword, questiongroup_keyword

    def get_geometry_keywords(self):
        """
        Return the keywords of the question and questiongroup which
        contain the name of the questionnaire as defined in the
        configuration by the ``is_geometry`` parameter.
        """
        question_keyword = None
        questiongroup_keyword = None
        for questiongroup in self.get_questiongroups():
            for question in questiongroup.questions:
                if question.is_geometry is True:
                    question_keyword = question.keyword
                    questiongroup_keyword = questiongroup.keyword
        return question_keyword, questiongroup_keyword

    def get_description_keywords(self, keys):
        """
        Get a list of tuples in the form of 'questiongroup': 'keyword' for
        given keys.

        Args:
            keys: list
        Returns:
            list of namedtuples
        """
        question_keywords = []
        keyword = collections.namedtuple("Keyword", "questiongroup question")
        for questiongroup in self.get_questiongroups():
            for question in questiongroup.questions:
                if question.keyword in keys:
                    question_keywords.append(
                        keyword(questiongroup.keyword, question.keyword)
                    )
        return question_keywords

    def get_questionnaire_name(self, questionnaire_data):
        """
        Return the value of the key flagged with ``is_name`` of a
        Questionnaire.

        Args:
            ``questionnaire_data`` (dict): A translated questionnaire
            data dictionary.

        Returns:
            ``str``. Returns the value of the key or ``Unknown`` if the
            key was not found in the data dictionary.
        """
        question_keyword, questiongroup_keyword = self.get_name_keywords()
        if question_keyword:
            for x in questionnaire_data.get(questiongroup_keyword, []):
                return x.get(question_keyword)
        # fixme: what should happen in case no name is set? as of now, "{'en': 'Unknown name'}" is displayed, which is
        # fixme: ugly, but should not happen, as the name is validated by the publishers.
        return {"en": _("Unknown name")}

    def get_questionnaire_geometry(self, questionnaire_data):
        question_keyword, questiongroup_keyword = self.get_geometry_keywords()
        if question_keyword:
            for x in questionnaire_data.get(questiongroup_keyword, []):
                return x.get(question_keyword)
        return None

    def get_questionnaire_description(self, questionnaire_data, keys):
        """
        Get the contents of given strings

        Args:
            keys: list
            questionnaire_data: dict

        Returns:
            dict: language as key, concatenated content as value.
        """
        keywords = self.get_description_keywords(keys)
        excerpt_data = collections.defaultdict(str)

        for keyword in keywords:
            for x in questionnaire_data.get(keyword.questiongroup, []):
                if x.get(keyword.question):
                    for language, text in x[keyword.question].items():
                        excerpt_data[language] += f"{text} "
        return excerpt_data

    def get_user_fields(self):
        """
        [0]: questiongroup keyword
        [1]: key keyword (id)
        [2]: key keyword (displayname)
        [3]: user role
        """
        user_fields = []
        for questiongroup in self.get_questiongroups():
            user_role = questiongroup.form_options.get("user_role")
            if user_role is None:
                continue
            for question in questiongroup.questions:
                if question.field_type != "user_id":
                    continue
                user_fields.append(
                    (
                        questiongroup.keyword,
                        question.keyword,
                        question.form_options.get("display_field"),
                        user_role,
                    )
                )
        return user_fields

    def read_configuration(self):
        """
        This function reads an active configuration of a Questionnaire.
        If a configuration is found, it loads the configuration of all
        its sections.

        The configuration of the questionnaire needs to have the
        following format::

          {
            # See class QuestionnaireSection for the format of sections.
            "sections": [
              # ...
            ]
          }

        .. seealso::
            :class:`configuration.configuration.QuestionnaireSection`

        .. seealso::
            :doc:`/configuration/questionnaire`
        """
        if self.configuration_object is None:
            raise ConfigurationErrorNoConfigurationFound(self.keyword)

        self.configuration = self.configuration_object.data
        self.validate_options()

        conf_sections = self.configuration.get("sections")
        validate_type(conf_sections, list, "sections", "list of dicts", "-")

        for conf_section in conf_sections:
            self.sections.append(QuestionnaireSection(self, conf_section))
        self.children = self.sections

        self.modules = self.configuration.get("modules", [])

        inherited_data = {}
        for qg in self.get_questiongroups():
            if qg.inherited_configuration:
                inherited_by_configuration = inherited_data.get(
                    qg.inherited_configuration, {}
                )
                inherited_by_configuration.update(
                    {qg.inherited_questiongroup: qg.keyword}
                )
                inherited_data[qg.inherited_configuration] = inherited_by_configuration
        self.inherited_data = inherited_data


def validate_type(obj, type_, conf_name, type_name, parent_conf_name):
    """
    Validate a type of object.

    Args:
        ``obj`` (obj): The object to validate.

        ``type_`` (type): A Python type (e.g. ``list``, ``dict``).

        ``conf_name`` (str): The name of the configuration entry (used
        for the error message)

        ``type_name`` (str): The name of the expected type (used for the
        error message)

        ``parent_conf_name`` (str): The name of the parent configuration
        entry (used for the error message)

    Raises:
        :class:`qcat.errors.ConfigurationErrorInvalidConfiguration`
    """
    if not isinstance(obj, type_):
        raise ConfigurationErrorInvalidConfiguration(
            conf_name, type_name, parent_conf_name
        )


class DateInput(forms.DateInput):
    template_name = "form/field/dateinput.html"

    def get_context_data(self):
        ctx = super().get_context_data()
        ctx.update({"options": self.options, "date_format": "dd/mm/yy"})
        return ctx


class NumberInput(forms.NumberInput):
    template_name = "form/field/numberinput.html"

    def get_context_data(self):
        ctx = super().get_context_data()
        ctx.update({"options": self.options})
        return ctx


class TextInput(forms.TextInput):
    template_name = "form/field/textinput.html"

    def get_context_data(self):
        ctx = super().get_context_data()
        ctx.update(
            {
                "options": self.options,
            }
        )
        return ctx


class HiddenInput(forms.TextInput):
    template_name = "form/field/hidden.html"

    def get_context_data(self):
        ctx = super().get_context_data()
        if hasattr(self, "css_class"):
            ctx.update({"css_class": self.css_class})
        return ctx


class ConditionalMixin:
    def get_context_data(self):
        ctx = super().get_context_data()
        ctx.update(
            {
                "questiongroup_conditions": self.attrs["questiongroup_conditions"],
                "conditional": self.attrs["conditional"],
            }
        )
        return ctx


class RadioSelect(ConditionalMixin, forms.RadioSelect):
    """
    A custom form class for a Radio Select field. Allows to overwrite
    the template used.
    """

    template_name = "form/field/radio.html"

    def get_context_data(self):
        """
        Add the questiongroup conditions to the context data so they are
        available within the template of the widget.
        """
        ctx = super().get_context_data()
        ctx.update({"options": self.options})
        return ctx


class Select(ConditionalMixin, forms.Select):
    template_name = "form/field/select.html"

    def get_context_data(self):
        """
        Add a variable (searchable or not) to the context data so it is
        available within the template of the widget.
        """
        ctx = super().get_context_data()
        try:
            options_order = self.options_order
        except AttributeError:
            options_order = []
        ctx.update(
            {
                "searchable": self.searchable,
                "options_order": options_order,
                "options": self.options,
            }
        )
        return ctx


class MeasureSelect(ConditionalMixin, forms.RadioSelect):
    template_name = "form/field/measure.html"

    def get_context_data(self):
        """
        Add the questiongroup conditions to the context data so they are
        available within the template of the widget.
        """
        ctx = super().get_context_data()
        ctx.update({"options": self.options})
        return ctx


class MeasureSelectStacked(ConditionalMixin, forms.RadioSelect):
    template_name = "form/field/measure_stacked.html"

    def get_context_data(self):
        """
        Add the questiongroup conditions to the context data so they are
        available within the template of the widget.
        """
        ctx = super().get_context_data()
        ctx.update({"options": self.options})
        return ctx


class MultiSelect(ConditionalMixin, forms.SelectMultiple):
    template_name = "form/field/select.html"

    def get_context_data(self):
        ctx = super().get_context_data()
        ctx.update({"options": self.options, "searchable": True})
        return ctx


class Checkbox(ConditionalMixin, forms.CheckboxSelectMultiple):
    template_name = "form/field/checkbox.html"

    def get_context_data(self):
        """
        Add the questiongroup conditions to the context data so they are
        available within the template of the widget.
        """
        ctx = super().get_context_data()
        ctx.update({"options": self.options})
        return ctx


class ImageCheckbox(ConditionalMixin, forms.CheckboxSelectMultiple):
    template_name = "form/field/image_checkbox.html"

    def get_context_data(self):
        """
        Add the image paths to the context data so they are available
        within the template of the widget.
        """
        ctx = super().get_context_data()
        ctx.update({"images": self.images, "options": self.options})
        return ctx


class ConditionalQuestiongroupChoiceField(ConditionalMixin, forms.ChoiceField):
    """
    A Choice field whose choices are based on the presence of certain
    questiongroups in the data JSON.
    """

    def __init__(self, *args, **kwargs):
        self.question = kwargs.pop("question")
        super().__init__(*args, **kwargs)

    def validate(self, value):
        questiongroups = self.question.form_options.get("options_by_questiongroups", [])
        return value in questiongroups


class FileUpload(forms.FileInput):
    template_name = "form/field/file_upload.html"


class RequiredFormSet(BaseFormSet):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for form in self.forms:
            form.empty_permitted = True


class MeasureCheckbox(forms.CheckboxSelectMultiple):
    template_name = "form/field/checkbox_measure.html"