We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Creation of answer tables for question items stopped working
### Subject of the issue

Answer categories can be grouped together as items.
They share the same answer categories and and an overarching question.
They should be displayed like so:


__Did you ever ...__

|      | yes | no | no answer |
|------|-----|----|-----------|
| do x |    |     |             |
| do y |    |     |             |

This tabular display is no longer being produced.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ddionrails/instruments/models/question.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 """ Model definitions for ddionrails.instruments app: Question """
4 
5 from __future__ import annotations
6 
7 import copy
8 import textwrap
9 import uuid
10 from collections import OrderedDict
11 from typing import Dict, List, Optional, Union
12 
13 from django.contrib.postgres.fields.jsonb import JSONField as JSONBField
14 from django.core.exceptions import ObjectDoesNotExist
15 from django.db import models
16 from django.db.models import QuerySet
17 from django.urls import reverse
18 
19 from ddionrails.base.mixins import ModelMixin
20 from ddionrails.concepts.models import Concept
21 from ddionrails.imports.helpers import hash_with_namespace_uuid
22 from ddionrails.studies.models import Study
23 
24 from .instrument import Instrument
25 
26 
27 class Question(ModelMixin, models.Model):
28     """
29     Stores a single question, related to :model:`instruments.Tnstrument`.
30     """
31 
32     ##############
33     # attributes #
34     ##############
35     id = models.UUIDField(  # pylint: disable=C0103
36         primary_key=True,
37         default=uuid.uuid4,
38         editable=True,
39         db_index=True,
40         help_text="UUID of the question. Dependent on the associated instrument.",
41     )
42     name = models.CharField(
43         max_length=255, db_index=True, help_text="Name of the question"
44     )
45     label = models.TextField(
46         blank=True,
47         verbose_name="Label (English)",
48         help_text="Label of the question (English)",
49     )
50     label_de = models.TextField(
51         blank=True,
52         verbose_name="Label (German)",
53         help_text="Label of the question (German)",
54     )
55     description = models.TextField(
56         blank=True,
57         verbose_name="Description (Markdown, English)",
58         help_text="Description of the question (Markdown, English)",
59     )
60     description_de = models.TextField(
61         blank=True,
62         verbose_name="Description (Markdown, German)",
63         help_text="Description of the question (Markdown, German)",
64     )
65     sort_id = models.IntegerField(
66         blank=True,
67         null=True,
68         verbose_name="Sort ID",
69         help_text="Sort order of questions within one instrument",
70     )
71     items = JSONBField(
72         default=list, null=True, blank=True, help_text="Items are elements in a question"
73     )
74 
75     #############
76     # relations #
77     #############
78     instrument = models.ForeignKey(
79         Instrument,
80         blank=True,
81         null=True,
82         related_name="questions",
83         on_delete=models.CASCADE,
84     )
85 
86     def save(
87         self, force_insert=False, force_update=False, using=None, update_fields=None
88     ):
89         """"Set id and call parents save(). """
90         self.id = hash_with_namespace_uuid(
91             self.instrument_id, self.name, cache=False
92         )  # pylint: disable=C0103
93         super().save(
94             force_insert=force_insert,
95             force_update=force_update,
96             using=using,
97             update_fields=update_fields,
98         )
99 
100     class Meta:  # pylint: disable=missing-docstring,too-few-public-methods
101         unique_together = ("instrument", "name")
102 
103     class DOR:  # pylint: disable=missing-docstring,too-few-public-methods
104         id_fields = ["instrument", "name"]
105         io_fields = ["name", "label", "description", "instrument"]
106 
107     def __str__(self) -> str:
108         """ Returns a string representation using the "instrument" and "name" fields """
109         return f"{self.instrument}/{self.name}"
110 
111     def get_absolute_url(self) -> str:
112         """ Returns a canonical URL for the model
113 
114         Uses the "study", "instrument" and "name" fields
115         """
116         return reverse(
117             "inst:question_detail",
118             kwargs={
119                 "study_name": self.instrument.study.name,
120                 "instrument_name": self.instrument.name,
121                 "question_name": self.name,
122             },
123         )
124 
125     @staticmethod
126     def layout_class() -> str:
127         """ Returns the layout class (used in templates) """
128         return "question"
129 
130     def previous_question(self) -> Optional[Question]:
131         """ Returns the previous question object or None
132             i.e. the question object with the preceding sort_id
133         """
134         try:
135             return self.instrument.questions.get(sort_id=self.sort_id - 1)
136         except ObjectDoesNotExist:
137             return None
138 
139     def next_question(self) -> Optional[Question]:
140         """ Returns the next question object or None
141             i.e. the question object with the following sort_id
142         """
143         try:
144             return self.instrument.questions.get(sort_id=self.sort_id + 1)
145         except ObjectDoesNotExist:
146             return None
147 
148     def get_period(self, period_id=False, default=None):
149         """ Returns the related period_id | period_name | Period instance | a default """
150         try:
151             period = self.instrument.period
152             if period_id is True:
153                 return period.id
154             if period_id == "name":
155                 return period.name
156             return period
157         except AttributeError:
158             return default
159 
160     def get_related_question_set(self, all_studies=False, by_study_and_period=False):
161         concept_list = self.get_concepts()
162         if all_studies:
163             study_list = Study.objects.all()
164         else:
165             study_list = [self.instrument.study]
166         direct_questions = Question.objects.filter(
167             concepts_questions__concept_id__in=concept_list,
168             instrument__study_id__in=study_list,
169         )
170         indirect_questions = Question.objects.filter(
171             questions_variables__variable__concept__in=concept_list,
172             instrument__study_id__in=study_list,
173         )
174         combined_set = direct_questions | indirect_questions
175         combined_set = combined_set.distinct()
176         if by_study_and_period:
177             result = OrderedDict()
178             for study in study_list:
179                 result[study.name] = OrderedDict()
180                 result[study.name]["no period"] = list()
181                 for period in study.periods.order_by("name"):
182                     result[study.name][period.name] = list()
183             for question in combined_set:
184                 result[question.instrument.study.name][
185                     question.get_period(period_id="name", default="no period")
186                 ].append(question)
187             return result
188         return combined_set
189 
190     def get_concepts(self) -> QuerySet:
191         """ Retrieve the related Concepts of this Question
192 
193             A question and concept are related if
194             their relation is defined in ConceptQuestion.
195         """
196         return Concept.objects.filter(concepts_questions__question_id=self.pk).distinct()
197 
198     def translation_languages(self) -> List[str]:
199         """ Returns a list of translation languages, e.g. ["de"] """
200         keys = list(self.items[0].keys())
201         keys_first_item = copy.deepcopy(keys)
202         return [key.replace("text_", "") for key in keys_first_item if "text_" in key]
203 
204     @staticmethod
205     def translate_item(item: Dict, language: str) -> None:
206         item["text"] = item.get("text_%s" % language, item.get("text", ""))
207         item["instruction"] = item.get(
208             "instruction_%s" % language, item.get("instruction", "")
209         )
210         for answer in item.get("answers", []):
211             answer["label"] = answer.get("label_%s" % language, answer.get("label", ""))
212 
213     def translations(self) -> Dict[str, List]:
214         """ Returns a dictionary containing translations
215             of the Question's items for each translation language
216         """
217         results = {}
218         try:
219             for language in self.translation_languages():
220                 results[language] = self.item_array(language=language)
221         except:
222             return {}
223         return results
224 
225     def item_array(self, language=None) -> List:
226         """ Returns a list containing the items of this Question object """
227         items = copy.deepcopy(self.items)
228         items = items.values() if items.__class__ == dict else items
229         for item in items:
230             if language:
231                 self.translate_item(item, language)
232             if "item" not in item:
233                 item["item"] = "root"
234             if "sn" not in item:
235                 item["sn"] = 0
236         items = sorted(items, key=lambda x: int(x["sn"]))
237         before = None
238         for index, item in enumerate(items):
239             try:
240                 current = item["answer_list"]
241             except:
242                 current = None
243             try:
244                 after = items[index + 1]["answer_list"]
245             except:
246                 after = None
247             if current and current == before:
248                 if current == after:
249                     item["layout"] = "matrix_element"
250                 else:
251                     item["layout"] = "matrix_footer"
252             elif current and current == after:
253                 item["layout"] = "matrix_header"
254             else:
255                 item["layout"] = "individual"
256             before = current
257         return items
258 
259     def to_topic_dict(self, language: str = "en") -> Dict:
260         """ Returns a topic dictionary representation of the Question object """
261         if language == "de":
262             title = self.label_de if self.label_de != "" else self.title()
263         else:
264             title = self.title()
265         try:
266             concept_name = self.questions_variables.first().variable.concept.name
267         # afuetterer: there might be no concept?
268         except AttributeError:
269             concept_name = ""
270         return dict(
271             title=title,
272             key="question_%s" % self.id,
273             name=self.name,
274             type="question",
275             concept_key="concept_%s" % concept_name,
276         )
277 
278     def comparison_string(
279         self, to_string: bool = False, wrap: int = 50
280     ) -> Union[str, List]:
281         comparison_string_array = ["Question: %s" % self.title()]
282         for item in self.items:
283             comparison_string_array += [
284                 "",
285                 "Item: %s (scale: %s)" % (item.get("item"), item.get("scale")),
286                 item.get("text", ""),
287             ]
288             comparison_string_array += [
289                 "%s: %s" % (a["value"], a["label"]) for a in item.get("answers", [])
290             ]
291         if wrap:
292             cs_temp = [textwrap.wrap(line, wrap) for line in comparison_string_array]
293             comparison_string_array = []
294             for line_list in cs_temp:
295                 if line_list == []:
296                     comparison_string_array.append("")
297                 else:
298                     comparison_string_array += line_list
299         if to_string:
300             return "\n".join(comparison_string_array)
301         return comparison_string_array
302
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ddionrails/instruments/models/question.py b/ddionrails/instruments/models/question.py
--- a/ddionrails/instruments/models/question.py
+++ b/ddionrails/instruments/models/question.py
@@ -32,7 +32,7 @@
     ##############
     # attributes #
     ##############
-    id = models.UUIDField(  # pylint: disable=C0103
+    id = models.UUIDField(  # pylint: disable=invalid-name
         primary_key=True,
         default=uuid.uuid4,
         editable=True,
@@ -87,9 +87,10 @@
         self, force_insert=False, force_update=False, using=None, update_fields=None
     ):
         """"Set id and call parents save(). """
-        self.id = hash_with_namespace_uuid(
+
+        self.id = hash_with_namespace_uuid(  # pylint: disable=invalid-name
             self.instrument_id, self.name, cache=False
-        )  # pylint: disable=C0103
+        )
         super().save(
             force_insert=force_insert,
             force_update=force_update,
@@ -158,6 +159,10 @@
             return default
 
     def get_related_question_set(self, all_studies=False, by_study_and_period=False):
+        """
+        pylint: disable=fixme
+        TODO: instruments.models.question.get_related_question_set needs docstring
+        """
         concept_list = self.get_concepts()
         if all_studies:
             study_list = Study.objects.all()
@@ -203,6 +208,10 @@
 
     @staticmethod
     def translate_item(item: Dict, language: str) -> None:
+        """
+        pylint: disable=fixme
+        TODO: instruments.models.question.translate_item needs documentation
+        """
         item["text"] = item.get("text_%s" % language, item.get("text", ""))
         item["instruction"] = item.get(
             "instruction_%s" % language, item.get("instruction", "")
@@ -215,6 +224,8 @@
             of the Question's items for each translation language
         """
         results = {}
+        # pylint: disable=fixme
+        # TODO: instruments.models.question.translations add missing exception Type
         try:
             for language in self.translation_languages():
                 results[language] = self.item_array(language=language)
@@ -222,6 +233,11 @@
             return {}
         return results
 
+    # pylint: disable=fixme
+    # TODO Refactor instruments.models.question.item array and associated
+    # BODY item_array method is used to create a data structure intended
+    # BODY for display purposes. This function is overly comlex and its the return
+    # BODY value still needs to be processed in the template.
     def item_array(self, language=None) -> List:
         """ Returns a list containing the items of this Question object """
         items = copy.deepcopy(self.items)
@@ -236,10 +252,8 @@
         items = sorted(items, key=lambda x: int(x["sn"]))
         before = None
         for index, item in enumerate(items):
-            try:
-                current = item["answer_list"]
-            except:
-                current = None
+
+            current = item.get("answer_list", None)
             try:
                 after = items[index + 1]["answer_list"]
             except:
@@ -278,6 +292,10 @@
     def comparison_string(
         self, to_string: bool = False, wrap: int = 50
     ) -> Union[str, List]:
+        """
+        pylint: disable=fixme
+        TODO: instruments.models.question.comparison_string needs docstring
+        """
         comparison_string_array = ["Question: %s" % self.title()]
         for item in self.items:
             comparison_string_array += [

{"golden_diff": "diff --git a/ddionrails/instruments/models/question.py b/ddionrails/instruments/models/question.py\n--- a/ddionrails/instruments/models/question.py\n+++ b/ddionrails/instruments/models/question.py\n@@ -32,7 +32,7 @@\n     ##############\n     # attributes #\n     ##############\n-    id = models.UUIDField(  # pylint: disable=C0103\n+    id = models.UUIDField(  # pylint: disable=invalid-name\n         primary_key=True,\n         default=uuid.uuid4,\n         editable=True,\n@@ -87,9 +87,10 @@\n         self, force_insert=False, force_update=False, using=None, update_fields=None\n     ):\n         \"\"\"\"Set id and call parents save(). \"\"\"\n-        self.id = hash_with_namespace_uuid(\n+\n+        self.id = hash_with_namespace_uuid(  # pylint: disable=invalid-name\n             self.instrument_id, self.name, cache=False\n-        )  # pylint: disable=C0103\n+        )\n         super().save(\n             force_insert=force_insert,\n             force_update=force_update,\n@@ -158,6 +159,10 @@\n             return default\n \n     def get_related_question_set(self, all_studies=False, by_study_and_period=False):\n+        \"\"\"\n+        pylint: disable=fixme\n+        TODO: instruments.models.question.get_related_question_set needs docstring\n+        \"\"\"\n         concept_list = self.get_concepts()\n         if all_studies:\n             study_list = Study.objects.all()\n@@ -203,6 +208,10 @@\n \n     @staticmethod\n     def translate_item(item: Dict, language: str) -> None:\n+        \"\"\"\n+        pylint: disable=fixme\n+        TODO: instruments.models.question.translate_item needs documentation\n+        \"\"\"\n         item[\"text\"] = item.get(\"text_%s\" % language, item.get(\"text\", \"\"))\n         item[\"instruction\"] = item.get(\n             \"instruction_%s\" % language, item.get(\"instruction\", \"\")\n@@ -215,6 +224,8 @@\n             of the Question's items for each translation language\n         \"\"\"\n         results = {}\n+        # pylint: disable=fixme\n+        # TODO: instruments.models.question.translations add missing exception Type\n         try:\n             for language in self.translation_languages():\n                 results[language] = self.item_array(language=language)\n@@ -222,6 +233,11 @@\n             return {}\n         return results\n \n+    # pylint: disable=fixme\n+    # TODO Refactor instruments.models.question.item array and associated\n+    # BODY item_array method is used to create a data structure intended\n+    # BODY for display purposes. This function is overly comlex and its the return\n+    # BODY value still needs to be processed in the template.\n     def item_array(self, language=None) -> List:\n         \"\"\" Returns a list containing the items of this Question object \"\"\"\n         items = copy.deepcopy(self.items)\n@@ -236,10 +252,8 @@\n         items = sorted(items, key=lambda x: int(x[\"sn\"]))\n         before = None\n         for index, item in enumerate(items):\n-            try:\n-                current = item[\"answer_list\"]\n-            except:\n-                current = None\n+\n+            current = item.get(\"answer_list\", None)\n             try:\n                 after = items[index + 1][\"answer_list\"]\n             except:\n@@ -278,6 +292,10 @@\n     def comparison_string(\n         self, to_string: bool = False, wrap: int = 50\n     ) -> Union[str, List]:\n+        \"\"\"\n+        pylint: disable=fixme\n+        TODO: instruments.models.question.comparison_string needs docstring\n+        \"\"\"\n         comparison_string_array = [\"Question: %s\" % self.title()]\n         for item in self.items:\n             comparison_string_array += [\n", "issue": "Creation of answer tables for question items stopped working\n### Subject of the issue\r\n\r\nAnswer categories can be grouped together as items.\r\nThey share the same answer categories and and an overarching question.\r\nThey should be displayed like so:\r\n\r\n\r\n__Did you ever ...__\r\n\r\n|      | yes | no | no answer |\r\n|------|-----|----|-----------|\r\n| do x |    |     |             |\r\n| do y |    |     |             |\r\n\r\nThis tabular display is no longer being produced.\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\n\"\"\" Model definitions for ddionrails.instruments app: Question \"\"\"\n\nfrom __future__ import annotations\n\nimport copy\nimport textwrap\nimport uuid\nfrom collections import OrderedDict\nfrom typing import Dict, List, Optional, Union\n\nfrom django.contrib.postgres.fields.jsonb import JSONField as JSONBField\nfrom django.core.exceptions import ObjectDoesNotExist\nfrom django.db import models\nfrom django.db.models import QuerySet\nfrom django.urls import reverse\n\nfrom ddionrails.base.mixins import ModelMixin\nfrom ddionrails.concepts.models import Concept\nfrom ddionrails.imports.helpers import hash_with_namespace_uuid\nfrom ddionrails.studies.models import Study\n\nfrom .instrument import Instrument\n\n\nclass Question(ModelMixin, models.Model):\n    \"\"\"\n    Stores a single question, related to :model:`instruments.Tnstrument`.\n    \"\"\"\n\n    ##############\n    # attributes #\n    ##############\n    id = models.UUIDField(  # pylint: disable=C0103\n        primary_key=True,\n        default=uuid.uuid4,\n        editable=True,\n        db_index=True,\n        help_text=\"UUID of the question. Dependent on the associated instrument.\",\n    )\n    name = models.CharField(\n        max_length=255, db_index=True, help_text=\"Name of the question\"\n    )\n    label = models.TextField(\n        blank=True,\n        verbose_name=\"Label (English)\",\n        help_text=\"Label of the question (English)\",\n    )\n    label_de = models.TextField(\n        blank=True,\n        verbose_name=\"Label (German)\",\n        help_text=\"Label of the question (German)\",\n    )\n    description = models.TextField(\n        blank=True,\n        verbose_name=\"Description (Markdown, English)\",\n        help_text=\"Description of the question (Markdown, English)\",\n    )\n    description_de = models.TextField(\n        blank=True,\n        verbose_name=\"Description (Markdown, German)\",\n        help_text=\"Description of the question (Markdown, German)\",\n    )\n    sort_id = models.IntegerField(\n        blank=True,\n        null=True,\n        verbose_name=\"Sort ID\",\n        help_text=\"Sort order of questions within one instrument\",\n    )\n    items = JSONBField(\n        default=list, null=True, blank=True, help_text=\"Items are elements in a question\"\n    )\n\n    #############\n    # relations #\n    #############\n    instrument = models.ForeignKey(\n        Instrument,\n        blank=True,\n        null=True,\n        related_name=\"questions\",\n        on_delete=models.CASCADE,\n    )\n\n    def save(\n        self, force_insert=False, force_update=False, using=None, update_fields=None\n    ):\n        \"\"\"\"Set id and call parents save(). \"\"\"\n        self.id = hash_with_namespace_uuid(\n            self.instrument_id, self.name, cache=False\n        )  # pylint: disable=C0103\n        super().save(\n            force_insert=force_insert,\n            force_update=force_update,\n            using=using,\n            update_fields=update_fields,\n        )\n\n    class Meta:  # pylint: disable=missing-docstring,too-few-public-methods\n        unique_together = (\"instrument\", \"name\")\n\n    class DOR:  # pylint: disable=missing-docstring,too-few-public-methods\n        id_fields = [\"instrument\", \"name\"]\n        io_fields = [\"name\", \"label\", \"description\", \"instrument\"]\n\n    def __str__(self) -> str:\n        \"\"\" Returns a string representation using the \"instrument\" and \"name\" fields \"\"\"\n        return f\"{self.instrument}/{self.name}\"\n\n    def get_absolute_url(self) -> str:\n        \"\"\" Returns a canonical URL for the model\n\n        Uses the \"study\", \"instrument\" and \"name\" fields\n        \"\"\"\n        return reverse(\n            \"inst:question_detail\",\n            kwargs={\n                \"study_name\": self.instrument.study.name,\n                \"instrument_name\": self.instrument.name,\n                \"question_name\": self.name,\n            },\n        )\n\n    @staticmethod\n    def layout_class() -> str:\n        \"\"\" Returns the layout class (used in templates) \"\"\"\n        return \"question\"\n\n    def previous_question(self) -> Optional[Question]:\n        \"\"\" Returns the previous question object or None\n            i.e. the question object with the preceding sort_id\n        \"\"\"\n        try:\n            return self.instrument.questions.get(sort_id=self.sort_id - 1)\n        except ObjectDoesNotExist:\n            return None\n\n    def next_question(self) -> Optional[Question]:\n        \"\"\" Returns the next question object or None\n            i.e. the question object with the following sort_id\n        \"\"\"\n        try:\n            return self.instrument.questions.get(sort_id=self.sort_id + 1)\n        except ObjectDoesNotExist:\n            return None\n\n    def get_period(self, period_id=False, default=None):\n        \"\"\" Returns the related period_id | period_name | Period instance | a default \"\"\"\n        try:\n            period = self.instrument.period\n            if period_id is True:\n                return period.id\n            if period_id == \"name\":\n                return period.name\n            return period\n        except AttributeError:\n            return default\n\n    def get_related_question_set(self, all_studies=False, by_study_and_period=False):\n        concept_list = self.get_concepts()\n        if all_studies:\n            study_list = Study.objects.all()\n        else:\n            study_list = [self.instrument.study]\n        direct_questions = Question.objects.filter(\n            concepts_questions__concept_id__in=concept_list,\n            instrument__study_id__in=study_list,\n        )\n        indirect_questions = Question.objects.filter(\n            questions_variables__variable__concept__in=concept_list,\n            instrument__study_id__in=study_list,\n        )\n        combined_set = direct_questions | indirect_questions\n        combined_set = combined_set.distinct()\n        if by_study_and_period:\n            result = OrderedDict()\n            for study in study_list:\n                result[study.name] = OrderedDict()\n                result[study.name][\"no period\"] = list()\n                for period in study.periods.order_by(\"name\"):\n                    result[study.name][period.name] = list()\n            for question in combined_set:\n                result[question.instrument.study.name][\n                    question.get_period(period_id=\"name\", default=\"no period\")\n                ].append(question)\n            return result\n        return combined_set\n\n    def get_concepts(self) -> QuerySet:\n        \"\"\" Retrieve the related Concepts of this Question\n\n            A question and concept are related if\n            their relation is defined in ConceptQuestion.\n        \"\"\"\n        return Concept.objects.filter(concepts_questions__question_id=self.pk).distinct()\n\n    def translation_languages(self) -> List[str]:\n        \"\"\" Returns a list of translation languages, e.g. [\"de\"] \"\"\"\n        keys = list(self.items[0].keys())\n        keys_first_item = copy.deepcopy(keys)\n        return [key.replace(\"text_\", \"\") for key in keys_first_item if \"text_\" in key]\n\n    @staticmethod\n    def translate_item(item: Dict, language: str) -> None:\n        item[\"text\"] = item.get(\"text_%s\" % language, item.get(\"text\", \"\"))\n        item[\"instruction\"] = item.get(\n            \"instruction_%s\" % language, item.get(\"instruction\", \"\")\n        )\n        for answer in item.get(\"answers\", []):\n            answer[\"label\"] = answer.get(\"label_%s\" % language, answer.get(\"label\", \"\"))\n\n    def translations(self) -> Dict[str, List]:\n        \"\"\" Returns a dictionary containing translations\n            of the Question's items for each translation language\n        \"\"\"\n        results = {}\n        try:\n            for language in self.translation_languages():\n                results[language] = self.item_array(language=language)\n        except:\n            return {}\n        return results\n\n    def item_array(self, language=None) -> List:\n        \"\"\" Returns a list containing the items of this Question object \"\"\"\n        items = copy.deepcopy(self.items)\n        items = items.values() if items.__class__ == dict else items\n        for item in items:\n            if language:\n                self.translate_item(item, language)\n            if \"item\" not in item:\n                item[\"item\"] = \"root\"\n            if \"sn\" not in item:\n                item[\"sn\"] = 0\n        items = sorted(items, key=lambda x: int(x[\"sn\"]))\n        before = None\n        for index, item in enumerate(items):\n            try:\n                current = item[\"answer_list\"]\n            except:\n                current = None\n            try:\n                after = items[index + 1][\"answer_list\"]\n            except:\n                after = None\n            if current and current == before:\n                if current == after:\n                    item[\"layout\"] = \"matrix_element\"\n                else:\n                    item[\"layout\"] = \"matrix_footer\"\n            elif current and current == after:\n                item[\"layout\"] = \"matrix_header\"\n            else:\n                item[\"layout\"] = \"individual\"\n            before = current\n        return items\n\n    def to_topic_dict(self, language: str = \"en\") -> Dict:\n        \"\"\" Returns a topic dictionary representation of the Question object \"\"\"\n        if language == \"de\":\n            title = self.label_de if self.label_de != \"\" else self.title()\n        else:\n            title = self.title()\n        try:\n            concept_name = self.questions_variables.first().variable.concept.name\n        # afuetterer: there might be no concept?\n        except AttributeError:\n            concept_name = \"\"\n        return dict(\n            title=title,\n            key=\"question_%s\" % self.id,\n            name=self.name,\n            type=\"question\",\n            concept_key=\"concept_%s\" % concept_name,\n        )\n\n    def comparison_string(\n        self, to_string: bool = False, wrap: int = 50\n    ) -> Union[str, List]:\n        comparison_string_array = [\"Question: %s\" % self.title()]\n        for item in self.items:\n            comparison_string_array += [\n                \"\",\n                \"Item: %s (scale: %s)\" % (item.get(\"item\"), item.get(\"scale\")),\n                item.get(\"text\", \"\"),\n            ]\n            comparison_string_array += [\n                \"%s: %s\" % (a[\"value\"], a[\"label\"]) for a in item.get(\"answers\", [])\n            ]\n        if wrap:\n            cs_temp = [textwrap.wrap(line, wrap) for line in comparison_string_array]\n            comparison_string_array = []\n            for line_list in cs_temp:\n                if line_list == []:\n                    comparison_string_array.append(\"\")\n                else:\n                    comparison_string_array += line_list\n        if to_string:\n            return \"\\n\".join(comparison_string_array)\n        return comparison_string_array\n", "path": "ddionrails/instruments/models/question.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\n\"\"\" Model definitions for ddionrails.instruments app: Question \"\"\"\n\nfrom __future__ import annotations\n\nimport copy\nimport textwrap\nimport uuid\nfrom collections import OrderedDict\nfrom typing import Dict, List, Optional, Union\n\nfrom django.contrib.postgres.fields.jsonb import JSONField as JSONBField\nfrom django.core.exceptions import ObjectDoesNotExist\nfrom django.db import models\nfrom django.db.models import QuerySet\nfrom django.urls import reverse\n\nfrom ddionrails.base.mixins import ModelMixin\nfrom ddionrails.concepts.models import Concept\nfrom ddionrails.imports.helpers import hash_with_namespace_uuid\nfrom ddionrails.studies.models import Study\n\nfrom .instrument import Instrument\n\n\nclass Question(ModelMixin, models.Model):\n    \"\"\"\n    Stores a single question, related to :model:`instruments.Tnstrument`.\n    \"\"\"\n\n    ##############\n    # attributes #\n    ##############\n    id = models.UUIDField(  # pylint: disable=invalid-name\n        primary_key=True,\n        default=uuid.uuid4,\n        editable=True,\n        db_index=True,\n        help_text=\"UUID of the question. Dependent on the associated instrument.\",\n    )\n    name = models.CharField(\n        max_length=255, db_index=True, help_text=\"Name of the question\"\n    )\n    label = models.TextField(\n        blank=True,\n        verbose_name=\"Label (English)\",\n        help_text=\"Label of the question (English)\",\n    )\n    label_de = models.TextField(\n        blank=True,\n        verbose_name=\"Label (German)\",\n        help_text=\"Label of the question (German)\",\n    )\n    description = models.TextField(\n        blank=True,\n        verbose_name=\"Description (Markdown, English)\",\n        help_text=\"Description of the question (Markdown, English)\",\n    )\n    description_de = models.TextField(\n        blank=True,\n        verbose_name=\"Description (Markdown, German)\",\n        help_text=\"Description of the question (Markdown, German)\",\n    )\n    sort_id = models.IntegerField(\n        blank=True,\n        null=True,\n        verbose_name=\"Sort ID\",\n        help_text=\"Sort order of questions within one instrument\",\n    )\n    items = JSONBField(\n        default=list, null=True, blank=True, help_text=\"Items are elements in a question\"\n    )\n\n    #############\n    # relations #\n    #############\n    instrument = models.ForeignKey(\n        Instrument,\n        blank=True,\n        null=True,\n        related_name=\"questions\",\n        on_delete=models.CASCADE,\n    )\n\n    def save(\n        self, force_insert=False, force_update=False, using=None, update_fields=None\n    ):\n        \"\"\"\"Set id and call parents save(). \"\"\"\n\n        self.id = hash_with_namespace_uuid(  # pylint: disable=invalid-name\n            self.instrument_id, self.name, cache=False\n        )\n        super().save(\n            force_insert=force_insert,\n            force_update=force_update,\n            using=using,\n            update_fields=update_fields,\n        )\n\n    class Meta:  # pylint: disable=missing-docstring,too-few-public-methods\n        unique_together = (\"instrument\", \"name\")\n\n    class DOR:  # pylint: disable=missing-docstring,too-few-public-methods\n        id_fields = [\"instrument\", \"name\"]\n        io_fields = [\"name\", \"label\", \"description\", \"instrument\"]\n\n    def __str__(self) -> str:\n        \"\"\" Returns a string representation using the \"instrument\" and \"name\" fields \"\"\"\n        return f\"{self.instrument}/{self.name}\"\n\n    def get_absolute_url(self) -> str:\n        \"\"\" Returns a canonical URL for the model\n\n        Uses the \"study\", \"instrument\" and \"name\" fields\n        \"\"\"\n        return reverse(\n            \"inst:question_detail\",\n            kwargs={\n                \"study_name\": self.instrument.study.name,\n                \"instrument_name\": self.instrument.name,\n                \"question_name\": self.name,\n            },\n        )\n\n    @staticmethod\n    def layout_class() -> str:\n        \"\"\" Returns the layout class (used in templates) \"\"\"\n        return \"question\"\n\n    def previous_question(self) -> Optional[Question]:\n        \"\"\" Returns the previous question object or None\n            i.e. the question object with the preceding sort_id\n        \"\"\"\n        try:\n            return self.instrument.questions.get(sort_id=self.sort_id - 1)\n        except ObjectDoesNotExist:\n            return None\n\n    def next_question(self) -> Optional[Question]:\n        \"\"\" Returns the next question object or None\n            i.e. the question object with the following sort_id\n        \"\"\"\n        try:\n            return self.instrument.questions.get(sort_id=self.sort_id + 1)\n        except ObjectDoesNotExist:\n            return None\n\n    def get_period(self, period_id=False, default=None):\n        \"\"\" Returns the related period_id | period_name | Period instance | a default \"\"\"\n        try:\n            period = self.instrument.period\n            if period_id is True:\n                return period.id\n            if period_id == \"name\":\n                return period.name\n            return period\n        except AttributeError:\n            return default\n\n    def get_related_question_set(self, all_studies=False, by_study_and_period=False):\n        \"\"\"\n        pylint: disable=fixme\n        TODO: instruments.models.question.get_related_question_set needs docstring\n        \"\"\"\n        concept_list = self.get_concepts()\n        if all_studies:\n            study_list = Study.objects.all()\n        else:\n            study_list = [self.instrument.study]\n        direct_questions = Question.objects.filter(\n            concepts_questions__concept_id__in=concept_list,\n            instrument__study_id__in=study_list,\n        )\n        indirect_questions = Question.objects.filter(\n            questions_variables__variable__concept__in=concept_list,\n            instrument__study_id__in=study_list,\n        )\n        combined_set = direct_questions | indirect_questions\n        combined_set = combined_set.distinct()\n        if by_study_and_period:\n            result = OrderedDict()\n            for study in study_list:\n                result[study.name] = OrderedDict()\n                result[study.name][\"no period\"] = list()\n                for period in study.periods.order_by(\"name\"):\n                    result[study.name][period.name] = list()\n            for question in combined_set:\n                result[question.instrument.study.name][\n                    question.get_period(period_id=\"name\", default=\"no period\")\n                ].append(question)\n            return result\n        return combined_set\n\n    def get_concepts(self) -> QuerySet:\n        \"\"\" Retrieve the related Concepts of this Question\n\n            A question and concept are related if\n            their relation is defined in ConceptQuestion.\n        \"\"\"\n        return Concept.objects.filter(concepts_questions__question_id=self.pk).distinct()\n\n    def translation_languages(self) -> List[str]:\n        \"\"\" Returns a list of translation languages, e.g. [\"de\"] \"\"\"\n        keys = list(self.items[0].keys())\n        keys_first_item = copy.deepcopy(keys)\n        return [key.replace(\"text_\", \"\") for key in keys_first_item if \"text_\" in key]\n\n    @staticmethod\n    def translate_item(item: Dict, language: str) -> None:\n        \"\"\"\n        pylint: disable=fixme\n        TODO: instruments.models.question.translate_item needs documentation\n        \"\"\"\n        item[\"text\"] = item.get(\"text_%s\" % language, item.get(\"text\", \"\"))\n        item[\"instruction\"] = item.get(\n            \"instruction_%s\" % language, item.get(\"instruction\", \"\")\n        )\n        for answer in item.get(\"answers\", []):\n            answer[\"label\"] = answer.get(\"label_%s\" % language, answer.get(\"label\", \"\"))\n\n    def translations(self) -> Dict[str, List]:\n        \"\"\" Returns a dictionary containing translations\n            of the Question's items for each translation language\n        \"\"\"\n        results = {}\n        # pylint: disable=fixme\n        # TODO: instruments.models.question.translations add missing exception Type\n        try:\n            for language in self.translation_languages():\n                results[language] = self.item_array(language=language)\n        except:\n            return {}\n        return results\n\n    # pylint: disable=fixme\n    # TODO Refactor instruments.models.question.item array and associated\n    # BODY item_array method is used to create a data structure intended\n    # BODY for display purposes. This function is overly comlex and its the return\n    # BODY value still needs to be processed in the template.\n    def item_array(self, language=None) -> List:\n        \"\"\" Returns a list containing the items of this Question object \"\"\"\n        items = copy.deepcopy(self.items)\n        items = items.values() if items.__class__ == dict else items\n        for item in items:\n            if language:\n                self.translate_item(item, language)\n            if \"item\" not in item:\n                item[\"item\"] = \"root\"\n            if \"sn\" not in item:\n                item[\"sn\"] = 0\n        items = sorted(items, key=lambda x: int(x[\"sn\"]))\n        before = None\n        for index, item in enumerate(items):\n\n            current = item.get(\"answer_list\", None)\n            try:\n                after = items[index + 1][\"answer_list\"]\n            except:\n                after = None\n            if current and current == before:\n                if current == after:\n                    item[\"layout\"] = \"matrix_element\"\n                else:\n                    item[\"layout\"] = \"matrix_footer\"\n            elif current and current == after:\n                item[\"layout\"] = \"matrix_header\"\n            else:\n                item[\"layout\"] = \"individual\"\n            before = current\n        return items\n\n    def to_topic_dict(self, language: str = \"en\") -> Dict:\n        \"\"\" Returns a topic dictionary representation of the Question object \"\"\"\n        if language == \"de\":\n            title = self.label_de if self.label_de != \"\" else self.title()\n        else:\n            title = self.title()\n        try:\n            concept_name = self.questions_variables.first().variable.concept.name\n        # afuetterer: there might be no concept?\n        except AttributeError:\n            concept_name = \"\"\n        return dict(\n            title=title,\n            key=\"question_%s\" % self.id,\n            name=self.name,\n            type=\"question\",\n            concept_key=\"concept_%s\" % concept_name,\n        )\n\n    def comparison_string(\n        self, to_string: bool = False, wrap: int = 50\n    ) -> Union[str, List]:\n        \"\"\"\n        pylint: disable=fixme\n        TODO: instruments.models.question.comparison_string needs docstring\n        \"\"\"\n        comparison_string_array = [\"Question: %s\" % self.title()]\n        for item in self.items:\n            comparison_string_array += [\n                \"\",\n                \"Item: %s (scale: %s)\" % (item.get(\"item\"), item.get(\"scale\")),\n                item.get(\"text\", \"\"),\n            ]\n            comparison_string_array += [\n                \"%s: %s\" % (a[\"value\"], a[\"label\"]) for a in item.get(\"answers\", [])\n            ]\n        if wrap:\n            cs_temp = [textwrap.wrap(line, wrap) for line in comparison_string_array]\n            comparison_string_array = []\n            for line_list in cs_temp:\n                if line_list == []:\n                    comparison_string_array.append(\"\")\n                else:\n                    comparison_string_array += line_list\n        if to_string:\n            return \"\\n\".join(comparison_string_array)\n        return comparison_string_array\n", "path": "ddionrails/instruments/models/question.py"}]}