Repository: carpentries/amy

Issue:
Server Error when editing community roles
Reported by @ErinBecker at `/workshops/person/<id>/edit/#communityroles` and `/communityroles/role/<another id>/edit/` in production.

Reproduced on test AMY:
```
TypeError at /communityroles/role/25/edit/
the JSON object must be str, bytes or bytearray, not list
Error during template rendering
In template /webapps/test-amy.carpentries.org/repo/amy/templates/bootstrap4/field.html, error at line 39

{% crispy_field field %}
```

Also encountered locally when creating a community role and immediately editing it:
```
Exception Value: | 'NoneType' object is not iterable
/home/eli/amy/amy/communityroles/fields.py, line 20, in get_context

```


Discussion:

Resolving patch:
diff --git a/amy/communityroles/fields.py b/amy/communityroles/fields.py
--- a/amy/communityroles/fields.py
+++ b/amy/communityroles/fields.py
@@ -1,9 +1,12 @@
 import json
+import logging
 
 from django import forms
 from django.http import QueryDict
 from django.utils.datastructures import MultiValueDict
 
+logger = logging.getLogger("amy")
+
 
 class CustomKeysWidget(forms.TextInput):
     template_name = "widgets/custom_keys_widget.html"
@@ -17,7 +20,14 @@ def apply_labels(self, labels: list[str]) -> None:
 
     def get_context(self, name: str, value: str, attrs: dict):
         value_deserialized = json.loads(value)
-        value_deserialized_dict = dict(value_deserialized)
+        try:
+            value_deserialized_dict = dict(value_deserialized)
+        except (ValueError, TypeError) as e:
+            logger.debug(
+                f"Failed to load custom key values {value_deserialized} to dict: {e}."
+            )
+            logger.debug("Proceeding without custom key values...")
+            value_deserialized_dict = {}
         default_values = dict([(label, "") for label in self.labels])
         context_value = default_values | value_deserialized_dict
 


Test patch:
diff --git a/amy/communityroles/tests/test_fields.py b/amy/communityroles/tests/test_fields.py
new file mode 100644
--- /dev/null
+++ b/amy/communityroles/tests/test_fields.py
@@ -0,0 +1,68 @@
+import json
+
+from communityroles.fields import CustomKeysWidget
+from workshops.tests.base import TestBase
+
+
+class TestCustomKeysField(TestBase):
+    def setUp(self):
+        self.widget = CustomKeysWidget()
+
+    def test_custom_keys_apply_labels(self):
+        # Arrange
+        custom_keys = ["Test", "Second test"]
+
+        # Act
+        self.widget.apply_labels(custom_keys)
+
+        # Assert
+        self.assertEqual(self.widget.labels, custom_keys)
+
+    def test_custom_keys_create_subwidgets(self):
+        # Arrange
+        custom_keys = ["Test", "Second test"]
+        values = {"Test": "test value", "Second test": "second test value"}
+        self.widget.apply_labels(custom_keys)
+
+        # Act
+        context = self.widget.get_context(
+            name="communityroles-custom_keys", value=json.dumps(values), attrs={}
+        )
+        subwidgets = context["widget"]["subwidgets"]
+        subwidget_labels = [w["label"] for w in subwidgets]
+        subwidget_values = [w["value"] for w in subwidgets]
+
+        # Assert
+        self.assertListEqual(subwidget_labels, custom_keys)
+        self.assertListEqual(subwidget_values, list(values.values()))
+
+    def test_custom_keys_value_null(self):
+        # Arrange
+        custom_keys = ["Test"]
+        self.widget.apply_labels(custom_keys)
+
+        # Act
+        context = self.widget.get_context(
+            name="communityroles-custom_keys", value="null", attrs={}
+        )
+        subwidget = context["widget"]["subwidgets"][0]
+
+        # Assert
+        self.assertEqual(subwidget["value"], None)
+
+    def test_custom_keys_value_invalid(self):
+        # Arrange
+        custom_keys = ["Test"]
+        # set a value which cannot be parsed as dict - should be ignored
+        values = ["Test value"]
+        self.widget.apply_labels(custom_keys)
+
+        # Act
+        context = self.widget.get_context(
+            name="communityroles-custom_keys", value=json.dumps(values), attrs={}
+        )
+        subwidget = context["widget"]["subwidgets"][0]
+
+        # Assert
+        # subwidget should use the default label as a fallback
+        self.assertEqual(subwidget["value"], None)