Repository: oppia/oppia

Issue:
Error on saving exploration -- language code mismatch
I just tried to update an existing public exploration on the test server by adding a new answer group: https://oppiatestserver.appspot.com/create/16#/gui/Number%20Input

However, trying to publish the change results in an error message that blocks me from publishing it:

![Screenshot from 2019-11-10 22-40-41](https://user-images.githubusercontent.com/10575562/68566678-e0269e00-040b-11ea-8e78-ec0aaaf51315.png)

This is a serious bug that needs to be fixed in advance of the upcoming release.

The error is a 400 error that is arising from here:

```
  File "/core/controllers/acl_decorators.py", line 1064, in test_can_save
    return handler(self, exploration_id, **kwargs)
  File "/core/controllers/editor.py", line 143, in put
    raise self.InvalidInputException(e)
InvalidInputException: Expected set of all languages available in incomplete_translation, needs_voiceover and assigned_voiceover to be the same as the supported audio languages, received [u'el', u'en', u'zh', u'vi', u'ca', u'it', u'ar', u'hi-en', u'cs', u'id', u'es', u'ru', u'nl', u'pt', u'no', u'tr', u'tl', u'lv', u'lt', u'th', u'ro', u'pl', u'kab', u'fr', u'bg', u'hr', u'bn', u'de', u'da', u'fa', u'hi', u'fi', u'hu', u'ja', u'he', u'sr', u'ko', u'sv', u'sk', u'uk', u'sl']
```


Discussion:
This is happening after adding new language in supported_audio_language list, though this was not expected, I'm looking into it.

Resolving patch:
diff --git a/core/domain/opportunity_services.py b/core/domain/opportunity_services.py
--- a/core/domain/opportunity_services.py
+++ b/core/domain/opportunity_services.py
@@ -18,6 +18,8 @@
 from __future__ import absolute_import  # pylint: disable=import-only-modules
 from __future__ import unicode_literals  # pylint: disable=import-only-modules
 
+import logging
+
 from constants import constants
 from core.domain import exp_fetchers
 from core.domain import opportunity_domain
@@ -58,11 +60,29 @@ def get_exploration_opportunity_summary_from_model(model):
         ExplorationOpportunitySummary. The corresponding
         ExplorationOpportunitySummary object.
     """
+    # We're making sure that the audio language codes in any exploration
+    # opportunity domain object match the ones in
+    # constants.SUPPORTED_AUDIO_LANGUAGES.
+    set_of_all_languages = set(
+        model.incomplete_translation_language_codes +
+        model.need_voice_artist_in_language_codes +
+        model.assigned_voice_artist_in_language_codes)
+    supported_language_codes = set([language['id'] for language in (
+        constants.SUPPORTED_AUDIO_LANGUAGES)])
+    missing_language_codes = list(
+        supported_language_codes - set_of_all_languages)
+    if missing_language_codes:
+        logging.info(
+            'Missing language codes %s in exploration opportunity model with '
+            'id %s' % (missing_language_codes, model.id))
+
+    new_incomplete_translation_language_codes = (
+        model.incomplete_translation_language_codes + missing_language_codes)
 
     return opportunity_domain.ExplorationOpportunitySummary(
         model.id, model.topic_id, model.topic_name, model.story_id,
         model.story_title, model.chapter_title, model.content_count,
-        model.incomplete_translation_language_codes, model.translation_counts,
+        new_incomplete_translation_language_codes, model.translation_counts,
         model.need_voice_artist_in_language_codes,
         model.assigned_voice_artist_in_language_codes)
 


Test patch:
diff --git a/core/domain/opportunity_services_test.py b/core/domain/opportunity_services_test.py
--- a/core/domain/opportunity_services_test.py
+++ b/core/domain/opportunity_services_test.py
@@ -18,6 +18,9 @@
 from __future__ import absolute_import  # pylint: disable=import-only-modules
 from __future__ import unicode_literals  # pylint: disable=import-only-modules
 
+import logging
+
+from constants import constants
 from core.domain import exp_domain
 from core.domain import exp_services
 from core.domain import opportunity_domain
@@ -598,3 +601,46 @@ def test_get_exploration_opportunity_summaries_by_ids_returns_list_of_objects(se
             opportunities['0'],
             opportunity_domain.ExplorationOpportunitySummary)
         self.assertEqual(opportunities['0'].id, '0')
+
+    def test_get_exploration_opportunity_summary_from_model_populates_new_lang(
+            self):
+        observed_log_messages = []
+
+        def _mock_logging_function(msg, *args):
+            """Mocks logging.info()."""
+            observed_log_messages.append(msg % args)
+
+        opportunities = (
+            opportunity_services.get_exploration_opportunity_summaries_by_ids(
+                ['0']))
+        self.assertEqual(len(opportunities), 1)
+
+        opportunity = opportunities['0']
+
+        self.assertFalse(
+            'new_lang' in opportunity.incomplete_translation_language_codes)
+
+        mock_supported_languages = constants.SUPPORTED_AUDIO_LANGUAGES + [{
+            'id': 'new_lang',
+            'description': 'New language',
+            'relatedLanguages': ['new_lang']
+        }]
+
+        self.assertEqual(len(observed_log_messages), 0)
+
+        with self.swap(logging, 'info', _mock_logging_function), self.swap(
+            constants, 'SUPPORTED_AUDIO_LANGUAGES', mock_supported_languages):
+            opportunities = (
+                opportunity_services
+                .get_exploration_opportunity_summaries_by_ids(['0']))
+            self.assertEqual(len(opportunities), 1)
+
+            opportunity = opportunities['0']
+
+            self.assertTrue(
+                'new_lang' in opportunity.incomplete_translation_language_codes)
+            self.assertEqual(len(observed_log_messages), 1)
+            self.assertEqual(
+                observed_log_messages[0],
+                'Missing language codes [u\'new_lang\'] in exploration '
+                'opportunity model with id 0')