Repository: comic/grand-challenge.org

Issue:
Two different choices can be the default in a reader study question
Only 1 choice should be default.


Discussion:
![Screenshot_20200323_163329](https://user-images.githubusercontent.com/12661555/77333719-22e67400-6d24-11ea-9549-b3ccd9b53429.png)

The form needs cleaning

- The remove button does not work at all
- Possible to set a choice on a single line text
- add choices and default to __str__
- allow choices without a default
- Server error if you leave one of the choices blank

Resolving patch:
diff --git a/app/grandchallenge/reader_studies/forms.py b/app/grandchallenge/reader_studies/forms.py
--- a/app/grandchallenge/reader_studies/forms.py
+++ b/app/grandchallenge/reader_studies/forms.py
@@ -116,7 +116,11 @@ def __init__(self, *args, **kwargs):
                 Field("question_text"),
                 Field("help_text"),
                 Field("answer_type"),
-                Fieldset("Add options", Formset("options")),
+                Fieldset(
+                    "Add options",
+                    Formset("options"),
+                    css_class="options-formset",
+                ),
                 Field("required"),
                 Field("image_port"),
                 Field("direction"),
@@ -187,6 +191,10 @@ class Meta:
 
 
 class CategoricalOptionForm(ModelForm):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields["title"].label = False
+
     class Meta:
         model = CategoricalOption
         fields = ("title", "default")
diff --git a/app/grandchallenge/reader_studies/models.py b/app/grandchallenge/reader_studies/models.py
--- a/app/grandchallenge/reader_studies/models.py
+++ b/app/grandchallenge/reader_studies/models.py
@@ -1061,6 +1061,9 @@ class CategoricalOption(models.Model):
     title = models.CharField(max_length=1024)
     default = models.BooleanField(default=False)
 
+    def __str__(self):
+        return f"{self.title} ({'' if self.default else 'not '}default)"
+
 
 class Answer(UUIDModel):
     """
diff --git a/app/grandchallenge/reader_studies/views.py b/app/grandchallenge/reader_studies/views.py
--- a/app/grandchallenge/reader_studies/views.py
+++ b/app/grandchallenge/reader_studies/views.py
@@ -220,23 +220,29 @@ class QuestionOptionMixin(object):
     def validate_options(self, form, _super):
         context = self.get_context_data()
         options = context["options"]
-        if form.data["answer_type"] in [
+        if form.data["answer_type"] not in [
             Question.ANSWER_TYPE_CHOICE,
             Question.ANSWER_TYPE_MULTIPLE_CHOICE,
-        ] and not any(option.get("title") for option in options.cleaned_data):
+        ]:
+            if getattr(self, "object", None):
+                self.object.options.all().delete()
+            return _super.form_valid(form)
+        data = options.cleaned_data
+        if len(list(filter(lambda x: x.get("default"), data))) > 1:
+            error = ["Only one option can be the default option"]
+            form.errors["answer_type"] = error
+            return self.form_invalid(form)
+        if not any(option.get("title") for option in data):
             error = [
                 "At least one option should be supplied for (multiple) choice questions"
             ]
             form.errors["answer_type"] = error
             return self.form_invalid(form)
-        if form.data["answer_type"] == Question.ANSWER_TYPE_CHOICE and not any(
-            option.get("default") for option in options.cleaned_data
-        ):
-            error = ["A default option is required for choice questions"]
-            form.errors["answer_type"] = error
-            return self.form_invalid(form)
         with transaction.atomic():
-            self.object = form.save()
+            try:
+                self.object = form.save()
+            except Exception:
+                return self.form_invalid(form)
             if options.is_valid():
                 options.instance = self.object
                 options.save()


Test patch: