Repository: oppia/oppia

Issue:
[Contributor Dashboard] Fix null pointer with 'incomplete_translation_language_codes' field when rendering exploration opportunities 
Stack trace:

```
File "/core/controllers/acl_decorators.py", line 1183, in test_can_suggest
    return handler(self, **kwargs)
  File "/core/controllers/suggestion.py", line 262, in get
    self._render_suggestions(target_type, suggestions)
  File "/core/controllers/suggestion.py", line 217, in _render_suggestions
    _get_target_id_to_exploration_opportunity_dict(suggestions))
  File "/core/controllers/suggestion.py", line 68, in _get_target_id_to_exploration_opportunity_dict
    list(target_ids)))
  File "/core/domain/opportunity_services.py", line 451, in get_exploration_opportunity_summaries_by_ids
    exp_opportunity_summary_model))
  File "/core/domain/opportunity_services.py", line 69, in get_exploration_opportunity_summary_from_model
    model.need_voice_artist_in_language_codes +
AttributeError: 'NoneType' object has no attribute 'incomplete_translation_language_codes'
```


Discussion:
Found by @vojtechjelinek. Can you provide steps on how to reproduce? Thanks.
I think I just accessed the contributor dashboard and the translations there.
@vojtechjelinek can you be a little more specific? I'm trying to reproduce the bug :)
@kaylahardie It was quite a few weeks ago and I didn't really focus on that. I don't know what specific things you want. I had some contributions created previously and when I went to the contributor dashboard and clicked the translations category the error had shown. I think that the stack trace should contain a lot of info already, for example, that the problem is with `getsubmittedsuggestions/<target_type>/<suggestion_type>` handler.

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
@@ -446,10 +446,15 @@ def get_exploration_opportunity_summaries_by_ids(ids):
         opportunity_models.ExplorationOpportunitySummaryModel.get_multi(ids))
     opportunities = []
     for exp_opportunity_summary_model in exp_opportunity_summary_models:
-        exp_opportunity_summary = (
-            get_exploration_opportunity_summary_from_model(
-                exp_opportunity_summary_model))
-        opportunities.append(exp_opportunity_summary)
+        if exp_opportunity_summary_model is not None:
+            exp_opportunity_summary = (
+                get_exploration_opportunity_summary_from_model(
+                    exp_opportunity_summary_model))
+            opportunities.append(exp_opportunity_summary)
+        else:
+            logging.warning(
+                'When getting the exploration opportunity summary models for '
+                'ids: %s, one of the models was None.' % ids)
     return opportunities
 
 


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
@@ -615,6 +615,35 @@ def test_get_exploration_opportunity_summaries_by_ids_returns_list_of_objects(se
             opportunity_domain.ExplorationOpportunitySummary)
         self.assertEqual(opportunities[0].id, '0')
 
+    def test_get_exploration_opportunity_summaries_by_ids_warns_if_invalid_id(
+            self):
+        """Tests that a warning is logged if there is no
+        ExplorationOpportunitySummaryModel associated with one of the ids.
+        """
+        observed_log_messages = []
+
+        def _mock_logging_function(msg, *args):
+            """Mocks logging.warning()."""
+            observed_log_messages.append(msg % args)
+
+        logging_swap = self.swap(logging, 'warning', _mock_logging_function)
+
+        with logging_swap:
+            opportunities = (
+                opportunity_services.
+                get_exploration_opportunity_summaries_by_ids(
+                    ['badID'])
+            )
+
+        self.assertEqual(len(opportunities), 0)
+        self.assertEqual(
+            observed_log_messages,
+            [
+                'When getting the exploration opportunity summary models for '
+                'ids: [u\'badID\'], one of the models was None.'
+            ]
+        )
+
     def test_get_exploration_opportunity_summary_from_model_populates_new_lang(
             self):
         observed_log_messages = []