Repository: oppia/oppia

Issue:
Cannot save any questions in the question editor.
**Describe the bug**
I can't save any changes when editing questions in the question editor.

**To Reproduce**
Steps to reproduce the behavior:
 1. Go to the question editor page.
 2. Select an existing question.
 3. Edit it, and try to save.

**Observed behavior**
The question cannot be saved. The save button is disabled.

**Expected behavior**
The save button should be clickable.

**Video**

https://user-images.githubusercontent.com/10575562/111264429-bcb5c980-85e4-11eb-9e8b-65cc03f3b435.mp4




Discussion:
Hello @seanlip, I've opened one issue similar to this #12163, If you would like me to shut it down then please do let me know.
Thanks
@seanlip and I found that selecting a rubric in the editor seems to enable the save button.

Suggested fixes:
1. Show warnings if "save" is not possible.
2. User should not have to reselect the rubric to trigger the save button.
@Rijuta-s will you be able to look into this?
@kevintab95 @Rijuta-s Please since this is a blocking bug we need a quick fix, would you be able to look at this soon?
Hi I will take this up and can raise a PR by 22nd EOD.
Update: On investigation, I found that the edit flow for the skill difficulties does not exist i.e. changes to skill difficulties do not persist. I am working on a fix.
@kevintab95 Any estimated date for this fix?
Hi @vojtechjelinek I should be able to raise a PR by 24th EOD.

Resolving patch:
diff --git a/core/controllers/question_editor.py b/core/controllers/question_editor.py
--- a/core/controllers/question_editor.py
+++ b/core/controllers/question_editor.py
@@ -153,42 +153,28 @@ def put(self, question_id):
         """Updates the QuestionSkillLink models with respect to the given
         question.
         """
-        if self.payload.get('action') == 'update_difficulty':
-            new_difficulty = self.payload.get('new_difficulty')
-            skill_id = self.payload.get('skill_id')
-            if skill_id is None:
-                raise self.InvalidInputException(
-                    'The \'skill_id\' field is missing in payload')
-            if new_difficulty is None:
-                raise self.InvalidInputException(
-                    'The \'new_difficulty\' field is missing in payload')
-            question_services.update_question_skill_link_difficulty(
-                question_id, skill_id, float(new_difficulty))
-        elif self.payload.get('action') == 'edit_links':
-            difficulty = self.payload.get('difficulty')
-            skill_ids_task_list = self.payload.get('skill_ids_task_list')
-            if skill_ids_task_list is None:
+        skill_ids_task_list = self.payload.get('skill_ids_task_list')
+        if skill_ids_task_list is None:
+            raise self.InvalidInputException(
+                'Missing fields \'skill_ids_task_list\'in payload')
+
+        for task_dict in skill_ids_task_list:
+            if not 'id' in task_dict:
                 raise self.InvalidInputException(
-                    'Missing fields \'skill_ids_task_list\'in payload')
-
-            for task_dict in skill_ids_task_list:
-                if not 'id' in task_dict:
-                    raise self.InvalidInputException(
-                        'Missing skill ID for edit_links.')
-                if task_dict['task'] == 'remove':
-                    question_services.delete_question_skill_link(
-                        self.user_id, question_id, task_dict['id'])
-                elif task_dict['task'] == 'add':
-                    if difficulty is None:
-                        raise self.InvalidInputException(
-                            'Missing field \'difficulty\' in payload')
-                    question_services.create_new_question_skill_link(
-                        self.user_id, question_id, task_dict['id'], difficulty)
-                else:
-                    raise self.InvalidInputException(
-                        'Invalid task for edit_links.')
-        else:
-            raise self.InvalidInputException('Invalid action in payload.')
+                    'Missing skill ID.')
+            if task_dict['task'] == 'remove':
+                question_services.delete_question_skill_link(
+                    self.user_id, question_id, task_dict['id'])
+            elif task_dict['task'] == 'add':
+                question_services.create_new_question_skill_link(
+                    self.user_id, question_id, task_dict['id'],
+                    task_dict['difficulty'])
+            elif task_dict['task'] == 'update_difficulty':
+                question_services.update_question_skill_link_difficulty(
+                    question_id, task_dict['id'],
+                    float(task_dict['difficulty']))
+            else:
+                raise self.InvalidInputException('Invalid task.')
 
         self.render_json(self.values)
 


Test patch:
diff --git a/core/controllers/question_editor_test.py b/core/controllers/question_editor_test.py
--- a/core/controllers/question_editor_test.py
+++ b/core/controllers/question_editor_test.py
@@ -392,7 +392,13 @@ def test_put_with_non_admin_or_topic_manager_disallows_access(self):
         self.put_json(
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
-            ), {'new_difficulty': 0.6, 'action': 'update_difficulty'},
+            ), {
+                'skill_ids_task_list': [{
+                    'id': 'skill_2',
+                    'task': 'update_difficulty',
+                    'difficulty': 0.9
+                }]
+            },
             csrf_token=csrf_token,
             expected_status_int=401)
         self.logout()
@@ -414,20 +420,21 @@ def test_put_with_admin_email_allows_updation(self):
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
             ), {
-                'new_difficulty': 0.9,
-                'action': 'update_difficulty',
-                'skill_id': self.skill_id
+                'skill_ids_task_list': [{
+                    'id': self.skill_id,
+                    'task': 'update_difficulty',
+                    'difficulty': 0.9
+                }]
             }, csrf_token=csrf_token)
 
         self.put_json(
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
             ), {
-                'difficulty': 0.6,
-                'action': 'edit_links',
                 'skill_ids_task_list': [{
                     'id': 'skill_2',
-                    'task': 'add'
+                    'task': 'add',
+                    'difficulty': 0.6
                 }]
             }, csrf_token=csrf_token)
         (
@@ -443,8 +450,6 @@ def test_put_with_admin_email_allows_updation(self):
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
             ), {
-                'difficulty': 0.9,
-                'action': 'edit_links',
                 'skill_ids_task_list': [{
                     'id': 'skill_2',
                     'task': 'remove'
@@ -463,47 +468,27 @@ def test_put_with_invalid_input_throws_error(self):
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
             ), {
-                'new_difficulty': 0.9,
-                'action': 'update_difficulty'
-            }, csrf_token=csrf_token, expected_status_int=400)
-        self.put_json(
-            '%s/%s' % (
-                feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
-            ), {
-                'skill_id': 'skill_id',
-                'action': 'update_difficulty'
-            }, csrf_token=csrf_token, expected_status_int=400)
-        self.put_json(
-            '%s/%s' % (
-                feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
-            ), {
-                'new_difficulty': 0.9,
-                'action': 'invalid_action',
-                'skill_id': 'skill_id'
+                'skill_ids_task_list': [{
+                    'task': 'update_difficulty',
+                    'difficulty': 0.9
+                }]
             }, csrf_token=csrf_token, expected_status_int=400)
         self.put_json(
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
             ), {
-                'action': 'edit_links',
-                'skill_ids_task_list': [{
-                    'id': 'skill_2',
-                    'task': 'add'
-                }]
+                'skill_ids_task_list': {
+                    'task': 'invalid_task'
+                }
             }, csrf_token=csrf_token, expected_status_int=400)
         self.put_json(
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
-            ), {
-                'action': 'edit_links',
-                'difficulty': 0.5
-            }, csrf_token=csrf_token, expected_status_int=400)
+            ), {}, csrf_token=csrf_token, expected_status_int=400)
         self.put_json(
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
             ), {
-                'action': 'edit_links',
-                'difficulty': 0.5,
                 'skill_ids_task_list': [{
                     'id': 'skill_2',
                     'task': 'invalid'
@@ -513,8 +498,6 @@ def test_put_with_invalid_input_throws_error(self):
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
             ), {
-                'action': 'edit_links',
-                'difficulty': 0.5,
                 'skill_ids_task_list': [{
                     'task': 'add'
                 }]
@@ -531,9 +514,11 @@ def test_put_with_topic_manager_email_allows_updation(self):
             '%s/%s' % (
                 feconf.QUESTION_SKILL_LINK_URL_PREFIX, self.question_id
             ), {
-                'new_difficulty': 0.6,
-                'action': 'update_difficulty',
-                'skill_id': self.skill_id
+                'skill_ids_task_list': [{
+                    'id': self.skill_id,
+                    'task': 'update_difficulty',
+                    'difficulty': 0.6
+                }]
             }, csrf_token=csrf_token)
         (
             question_summaries, merged_question_skill_links, _) = (