Repository: oppia/oppia

Issue:
InteractionAnswerSummariesAggregator breaks for deleted explorations
It appears that if InteractionAnswerSummariesAggregator runs across answers that correspond to deleted explorations, the job fails in the reduce step:

```
Traceback (most recent call last): (third_party/gae-mapreduce-1.9.17.0/mapreduce/handlers.py:508)
  File "third_party/gae-mapreduce-1.9.17.0/mapreduce/handlers.py", line 494, in handle
    tstate.input_reader, shard_state, tstate, ctx)
  File "third_party/gae-mapreduce-1.9.17.0/mapreduce/handlers.py", line 583, in _process_inputs
    entity, input_reader, ctx, tstate):
  File "third_party/gae-mapreduce-1.9.17.0/mapreduce/handlers.py", line 625, in _process_datum
    for output in result:
  File "core/domain/stats_jobs_continuous.py", line 614, in reduce
    exp = exp_services.get_exploration_by_id(exploration_id)
  File "core/domain/exp_services.py", line 227, in get_exploration_by_id
    exploration_id, strict=strict, version=version)
  File "core/storage/base_model/gae_models.py", line 598, in get
    return super(VersionedModel, cls).get(entity_id, strict=strict)
  File "core/storage/base_model/gae_models.py", line 88, in get
    (cls.__name__, entity_id))
EntityNotFoundError: Entity for class ExplorationModel with id xxxxxxxxxxx not found
```


Discussion:

Resolving patch:
diff --git a/core/domain/stats_jobs_continuous.py b/core/domain/stats_jobs_continuous.py
--- a/core/domain/stats_jobs_continuous.py
+++ b/core/domain/stats_jobs_continuous.py
@@ -606,12 +606,17 @@ def reduce(key, stringified_values):
         latest_version = versions[0]
         latest_interaction_id = versioned_interaction_ids[latest_version][0]
 
+        # Ensure the exploration corresponding to these answers exists.
+        exp = exp_services.get_exploration_by_id(
+            exploration_id, strict=False)
+        if exp is None:
+            return
+
         if exploration_version == VERSION_ALL:
             # If aggregating across all versions, verify that the latest answer
             # version is equal to the latest version of the exploration,
             # otherwise ignore all answers since none of them can be applied to
             # the latest version.
-            exp = exp_services.get_exploration_by_id(exploration_id)
             if state_name in exp.states:
                 loaded_interaction_id = exp.states[state_name].interaction.id
                 # Only check if the version mismatches if the new version has a


Test patch:
diff --git a/core/domain/stats_jobs_continuous_test.py b/core/domain/stats_jobs_continuous_test.py
--- a/core/domain/stats_jobs_continuous_test.py
+++ b/core/domain/stats_jobs_continuous_test.py
@@ -515,6 +515,58 @@ def test_one_answer(self):
 
             self.assertEqual(calculation_output, expected_calculation_output)
 
+    def test_one_answer_ignored_for_deleted_exploration(self):
+        with self.swap(
+            jobs_registry, 'ALL_CONTINUOUS_COMPUTATION_MANAGERS',
+            self.ALL_CC_MANAGERS_FOR_TESTS):
+
+            # setup example exploration
+            exp_id = 'eid'
+            exp = self.save_new_valid_exploration(exp_id, 'fake@user.com')
+            first_state_name = exp.init_state_name
+            exp_services.update_exploration('fake@user.com', exp_id, [{
+                'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
+                'state_name': first_state_name,
+                'property_name': exp_domain.STATE_PROPERTY_INTERACTION_ID,
+                'new_value': 'MultipleChoiceInput',
+            }], 'Update interaction type')
+            exp = exp_services.get_exploration_by_id(exp_id)
+            exp_version = exp.version
+
+            time_spent = 5.0
+            params = {}
+
+            self._record_start(
+                exp_id, exp_version, first_state_name, 'session1')
+            self.process_and_flush_pending_tasks()
+
+            # Add an answer.
+            event_services.AnswerSubmissionEventHandler.record(
+                exp_id, exp_version, first_state_name, 'MultipleChoiceInput', 0,
+                0, exp_domain.EXPLICIT_CLASSIFICATION, 'session1', time_spent,
+                params, 'answer1')
+
+            # Delete the exploration.
+            exp_services.delete_exploration('fake@user.com', exp_id)
+
+            # Now run the job.
+            ModifiedInteractionAnswerSummariesAggregator.start_computation()
+            self.assertEqual(
+                self.count_jobs_in_taskqueue(
+                    taskqueue_services.QUEUE_NAME_CONTINUOUS_JOBS), 1)
+            self.process_and_flush_pending_tasks()
+            self.assertEqual(
+                self.count_jobs_in_taskqueue(
+                    taskqueue_services.QUEUE_NAME_CONTINUOUS_JOBS), 0)
+
+            # There should be no job output corresponding to all versions since
+            # the exploration was deleted before the job could run. Note that
+            # this applies regardless of whether the job runs before or after
+            # deletion of the exploration.
+            calc_output_model = self._get_calc_output_model(
+                exp_id, first_state_name, 'AnswerFrequencies')
+            self.assertIsNone(calc_output_model)
+
     def test_answers_across_multiple_exploration_versions(self):
         with self.swap(
             jobs_registry, 'ALL_CONTINUOUS_COMPUTATION_MANAGERS',