Repository: fossasia/open-event-server

Issue:
Saving session and speaker fails if created before addition of custom form
If a speaker or session was created before addition of a required custom form, then it is impossible to send a PATCH request to edit them since new required field is not present in the request. So, custom form required checks must only be applied if either speaker or session is being created or custom form values are present in the PATCH request


Discussion:
In a PATCH request:

- If complex field values are present
  - Apply validation
- If complex field values are null or {}
  - Apply validation if previously there were complex field values assigned
- If complex field values are absent
  - Don't apply validation
It is becoming more and more complicated just for the sake of allowing organizer to edit status.

I think it's better if we allow organizers to edit session as they see fit and apply normal validations for speakers

Resolving patch:
diff --git a/app/api/schema/sessions.py b/app/api/schema/sessions.py
--- a/app/api/schema/sessions.py
+++ b/app/api/schema/sessions.py
@@ -99,7 +99,7 @@ def validate_fields(self, data, original_data):
                 "withdrawn",
             ]
         ),
-        allow_none=False,
+        allow_none=True,
         missing='draft',
     )
     created_at = fields.DateTime(dump_only=True)
diff --git a/app/api/sessions.py b/app/api/sessions.py
--- a/app/api/sessions.py
+++ b/app/api/sessions.py
@@ -284,9 +284,18 @@ def before_update_object(self, session, data, view_kwargs):
                 {'source': ''}, "Cannot edit session after the call for speaker is ended"
             )
 
-        data['complex_field_values'] = validate_custom_form_constraints_request(
-            'session', self.resource.schema, session, data
-        )
+        # We allow organizers and admins to edit session without validations
+        complex_field_values = data.get('complex_field_values', 'absent')
+        # Set default to 'absent' to differentiate between None and not sent
+        is_absent = complex_field_values == 'absent'
+        # True if values are not sent in data JSON
+        is_same = data.get('complex_field_values') == session.complex_field_values
+        # Using original value to ensure None instead of absent
+        # We stop checking validations for organizers only if they may result in data change or absent. See test_session_forms_api.py for more info
+        if not (is_organizer and (is_absent or is_same)):
+            data['complex_field_values'] = validate_custom_form_constraints_request(
+                'session', self.resource.schema, session, data
+            )
 
     def after_update_object(self, session, data, view_kwargs):
         """ Send email if session accepted or rejected """


Test patch:
diff --git a/tests/all/integration/api/session/test_session_forms_api.py b/tests/all/integration/api/session/test_session_forms_api.py
--- a/tests/all/integration/api/session/test_session_forms_api.py
+++ b/tests/all/integration/api/session/test_session_forms_api.py
@@ -494,3 +494,129 @@ def test_ignore_complex_custom_form_fields(db, client, user, jwt):
     assert session.complex_field_values['best_friend'] == 'Bester'
     assert session.complex_field_values['trans_fat_content'] == 20.08
     assert session.complex_field_values.get('shalimar') is None
+
+
+def test_edit_session_only_state(db, client, user, jwt):
+    # Should be allowed for organizers
+    user.is_admin = True
+    session = get_simple_custom_form_session(db, user)
+
+    data = json.dumps(
+        {
+            'data': {
+                'type': 'session',
+                'id': str(session.id),
+                "attributes": {"state": "withdrawn",},
+            }
+        }
+    )
+
+    response = client.patch(
+        f'/v1/sessions/{session.id}',
+        content_type='application/vnd.api+json',
+        headers=jwt,
+        data=data,
+    )
+
+    db.session.refresh(session)
+
+    assert response.status_code == 200
+    assert session.state == 'withdrawn'
+
+
+def test_edit_session_custom_allow(db, client, user, jwt):
+    """If there already is some complex field value of session,
+    and we don't send any new value, then it shouldn't be validated"""
+    user.is_admin = True
+    session = get_simple_custom_form_session(db, user)
+    session.complex_field_values = {'test': 'hello'}
+    db.session.commit()
+
+    data = json.dumps(
+        {
+            'data': {
+                'type': 'session',
+                'id': str(session.id),
+                "attributes": {"state": "withdrawn"},
+            }
+        }
+    )
+
+    response = client.patch(
+        f'/v1/sessions/{session.id}',
+        content_type='application/vnd.api+json',
+        headers=jwt,
+        data=data,
+    )
+
+    db.session.refresh(session)
+
+    assert response.status_code == 200
+    assert session.state == 'withdrawn'
+    assert session.complex_field_values == {'test': 'hello'}
+
+
+def test_edit_session_custom_same_allow(db, client, user, jwt):
+    """If there already is some complex field value of session,
+    and we don't send any new value, then it shouldn't be validated"""
+    user.is_admin = True
+    session = get_simple_custom_form_session(db, user)
+    session.complex_field_values = {'test': 'hello'}
+    db.session.commit()
+
+    data = json.dumps(
+        {
+            'data': {
+                'type': 'session',
+                'id': str(session.id),
+                "attributes": {
+                    "state": "withdrawn",
+                    "complex-field-values": {'test': 'hello'},
+                },
+            }
+        }
+    )
+
+    response = client.patch(
+        f'/v1/sessions/{session.id}',
+        content_type='application/vnd.api+json',
+        headers=jwt,
+        data=data,
+    )
+
+    db.session.refresh(session)
+
+    assert response.status_code == 200
+    assert session.state == 'withdrawn'
+    assert session.complex_field_values == {'test': 'hello'}
+
+
+def test_edit_session_custom_none_disallow(db, client, user, jwt):
+    """If there already is some complex field value of session,
+    and we send any null value, then it shouldn't be allowed"""
+    user.is_admin = True
+    session = get_simple_custom_form_session(db, user)
+    session.complex_field_values = {'test': 'hello'}
+    db.session.commit()
+
+    data = json.dumps(
+        {
+            'data': {
+                'type': 'session',
+                'id': str(session.id),
+                "attributes": {"state": "withdrawn", "complex_field_values": None},
+            }
+        }
+    )
+
+    response = client.patch(
+        f'/v1/sessions/{session.id}',
+        content_type='application/vnd.api+json',
+        headers=jwt,
+        data=data,
+    )
+
+    db.session.refresh(session)
+
+    assert response.status_code == 422
+    assert session.complex_field_values == {'test': 'hello'}