Repository: oppia/oppia

Issue:
[2.6.0] [Web] Finishing exploration in collection with no later explorations should not show unrelated explorations
Repro:

1) Be signed out
2) Open https://oppiatestserver.appspot.com/collection/TkMhoPmSUH50
3) Click on the exploration & play it
4) Wait for exploration to complete

Expected: There should be no follow-up lessons suggested and there should be a link returning to the collection

Actual: Unrelated explorations are suggested (e.g. Test 12, Test 2, and sassa).


Discussion:
This behavior works correctly when logged in (the summary card is shown for the collection since it's the end, whereas logged out users just see a link 'Return to Collection')
Note: this was supposed to have been fixed in #4130, might be worth taking a look at that to see why this behaviour results.
https://github.com/oppia/oppia/issues/4076#issuecomment-348365348 provides better context on this. It's not clear yet why we couldn't repro the issue back in November but could repro it this week. I'm going to see if it still repros.
Confirming this is still happening for me on oppiatestserver. Checking whether this repros directly on the release branch for a local build.
I'm able to repro this locally using the release branch. I think it may be related to having recommendations, and it must be reproed on a phone (cannot be reproed on web).
Without recommendations on develop, this results in nothing showing at the end of exploration.
Confirming on develop with recommendations, the bug repros.
So it seems as though the 'Play Exploration' card that shows up on mobile is redirecting the user to an exploration URL which does not contain the collection ID.
This currently seems to be reproing on web when simulating a mobile screen using Chrome's dev tools.
It seems this may have been introduced in #2780. The original code for showing the exploration summary tile in ``collection_player.html`` passes along the ``collectionId`` using ``getCollectionId`` on mobile, but ``getCollectionId`` doesn't exist and never seemed to have existed within the scope of ``CollectionPlayer.js`` and context of ``collection_player.html``. The fix should be a simple patch to the HTML file to ensure the collection ID is properly passed along.
Unfortunately, it's not clear how we might prevent regressions of this in the future. This is almost a unfortunate side effect of JavaScript treating undefined as a false value. We can't introduce any unit tests which include HTML files, and it's not clear whether we can create protractor tests for mobile views specifically.

Ideally, this would be resolved with #2991 or other e2e mobile testing work.
Ah oops, all of this context was actually meant for #4493.
#4130 did not actually fix this since it didn't cover the end collection case for logged out users (regardless of where their progress was). One way of addressing this would be to update that end flow to always show the return collection summary tile if you complete the final exploration of the collection, unless you have some progress as a guest.
So I'm confused... Is this issue fixed or is it not fixed?

Resolving patch:
diff --git a/core/controllers/reader.py b/core/controllers/reader.py
--- a/core/controllers/reader.py
+++ b/core/controllers/reader.py
@@ -701,6 +701,10 @@ class RecommendationsHandler(base.BaseHandler):
     if there are upcoming explorations for the learner to complete.
     """
 
+    # TODO(bhenning): Move the recommendation selection logic & related tests to
+    # the domain layer as service methods or to the frontend to reduce the
+    # amount of logic needed in this handler.
+
     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
 
     @acl_decorators.can_play_exploration
@@ -716,40 +720,34 @@ def get(self, exploration_id):
             raise self.PageNotFoundException
 
         auto_recommended_exp_ids = []
-        if self.user_id and collection_id:
-            next_exp_ids_in_collection = (
-                collection_services.get_next_exploration_ids_to_complete_by_user(  # pylint: disable=line-too-long
-                    self.user_id, collection_id))
-            auto_recommended_exp_ids = list(
-                set(next_exp_ids_in_collection) -
-                set(author_recommended_exp_ids))
-        else:
-            next_exp_ids_in_collection = []
-            if collection_id:
+
+        if collection_id:
+            if self.user_id:
+                auto_recommended_exp_ids = (
+                    collection_services.get_next_exploration_ids_to_complete_by_user(  # pylint: disable=line-too-long
+                        self.user_id, collection_id))
+            else:
                 collection = collection_services.get_collection_by_id(
                     collection_id)
-                next_exp_ids_in_collection = (
+                auto_recommended_exp_ids = (
                     collection.get_next_exploration_ids_in_sequence(
                         exploration_id))
-            if next_exp_ids_in_collection:
-                auto_recommended_exp_ids = list(
-                    set(next_exp_ids_in_collection) -
-                    set(author_recommended_exp_ids))
-            elif include_system_recommendations:
-                system_chosen_exp_ids = (
-                    recommendations_services.get_exploration_recommendations(
-                        exploration_id))
-                filtered_exp_ids = list(
-                    set(system_chosen_exp_ids) -
-                    set(author_recommended_exp_ids))
-                auto_recommended_exp_ids = random.sample(
-                    filtered_exp_ids,
-                    min(MAX_SYSTEM_RECOMMENDATIONS, len(filtered_exp_ids)))
-
+        elif include_system_recommendations:
+            system_chosen_exp_ids = (
+                recommendations_services.get_exploration_recommendations(
+                    exploration_id))
+            filtered_exp_ids = list(
+                set(system_chosen_exp_ids) - set(author_recommended_exp_ids))
+            auto_recommended_exp_ids = random.sample(
+                filtered_exp_ids,
+                min(MAX_SYSTEM_RECOMMENDATIONS, len(filtered_exp_ids)))
+
+        recommended_exp_ids = set(
+            author_recommended_exp_ids + auto_recommended_exp_ids)
         self.values.update({
             'summaries': (
                 summary_services.get_displayable_exp_summary_dicts_matching_ids(
-                    author_recommended_exp_ids + auto_recommended_exp_ids)),
+                    recommended_exp_ids)),
         })
         self.render_json(self.values)
 


Test patch:
diff --git a/core/controllers/reader_test.py b/core/controllers/reader_test.py
--- a/core/controllers/reader_test.py
+++ b/core/controllers/reader_test.py
@@ -24,6 +24,7 @@
 from core.domain import exp_services
 from core.domain import learner_progress_services
 from core.domain import param_domain
+from core.domain import recommendations_services
 from core.domain import rights_manager
 from core.domain import stats_domain
 from core.domain import stats_services
@@ -409,6 +410,444 @@ def test_ratings_by_different_users(self):
         self.logout()
 
 
+class RecommendationsHandlerTests(test_utils.GenericTestBase):
+    """Backend integration tests for recommended explorations for after an
+    exploration is completed.
+    """
+    # Demo explorations.
+    EXP_ID_0 = '0'
+    EXP_ID_1 = '1'
+    EXP_ID_7 = '7'
+    EXP_ID_8 = '8'
+
+    # Explorations contained within the demo collection.
+    EXP_ID_19 = '19'
+    EXP_ID_20 = '20'
+    EXP_ID_21 = '21'
+
+    # Demo collection.
+    COL_ID = '0'
+
+    def setUp(self):
+        super(RecommendationsHandlerTests, self).setUp()
+
+        # Register users.
+        self.signup(self.EDITOR_EMAIL, self.EDITOR_USERNAME)
+        self.signup(self.NEW_USER_EMAIL, self.NEW_USER_USERNAME)
+        self.new_user_id = self.get_user_id_from_email(self.NEW_USER_EMAIL)
+
+        # Login and create activities.
+        self.login(self.EDITOR_EMAIL)
+        exp_services.load_demo(self.EXP_ID_0)
+        exp_services.load_demo(self.EXP_ID_1)
+        exp_services.load_demo(self.EXP_ID_7)
+        exp_services.load_demo(self.EXP_ID_8)
+        collection_services.load_demo(self.COL_ID)
+        self.logout()
+
+    def _get_exploration_ids_from_summaries(self, summaries):
+        return sorted([summary['id'] for summary in summaries])
+
+    def _get_recommendation_ids(
+            self, exploration_id, collection_id=None,
+            include_system_recommendations=None,
+            author_recommended_ids_str='[]'):
+        collection_id_param = (
+            '&collection_id=%s' % collection_id
+            if collection_id is not None else '')
+        include_recommendations_param = (
+            '&include_system_recommendations=%s' % (
+                include_system_recommendations)
+            if include_system_recommendations is not None else '')
+        recommendations_url = (
+            '/explorehandler/recommendations/%s?'
+            'stringified_author_recommended_ids=%s%s%s' % (
+                exploration_id, author_recommended_ids_str, collection_id_param,
+                include_recommendations_param))
+
+        response = self.testapp.get('/explore/%s' % exploration_id)
+        csrf_token = self.get_csrf_token_from_response(response)
+        summaries = self.get_json(recommendations_url, csrf_token)['summaries']
+        return self._get_exploration_ids_from_summaries(summaries)
+
+    # TODO(bhenning): Add tests for ensuring system explorations are properly
+    # sampled when there are many matched for a given exploration ID.
+
+    # TODO(bhenning): Verify whether recommended author-specified explorations
+    # are also played within the context of collections, and whether that's
+    # desirable.
+
+    def _set_recommendations(self, exp_id, recommended_ids):
+        recommendations_services.set_recommendations(exp_id, recommended_ids)
+
+    def _complete_exploration_in_collection(self, exp_id):
+        collection_services.record_played_exploration_in_collection_context(
+            self.new_user_id, self.COL_ID, exp_id)
+
+    def _complete_entire_collection_in_order(self):
+        self._complete_exploration_in_collection(self.EXP_ID_19)
+        self._complete_exploration_in_collection(self.EXP_ID_20)
+        self._complete_exploration_in_collection(self.EXP_ID_21)
+        self._complete_exploration_in_collection(self.EXP_ID_0)
+
+    # Logged in standard viewer tests.
+    def test_logged_in_with_no_sysexps_no_authexps_no_col_has_no_exps(self):
+        """Check there are no recommended explorations when a user is logged in,
+        finishes an exploration in-viewer, but there are no recommended
+        explorations and no author exploration IDs.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, include_system_recommendations=True)
+        self.assertEqual(recommendation_ids, [])
+
+    def test_logged_in_with_some_sysexps_no_authexps_no_col_has_some_exps(self):
+        """Check there are recommended explorations when a user is logged in,
+        finishes an exploration in-viewer, and there are system recommendations.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._set_recommendations(self.EXP_ID_0, [self.EXP_ID_1, self.EXP_ID_8])
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, include_system_recommendations=True)
+        self.assertEqual(recommendation_ids, [self.EXP_ID_1, self.EXP_ID_8])
+
+    def test_logged_in_with_no_sysexps_some_authexps_no_col_has_some_exps(self):
+        """Check there are some recommended explorations when a user is logged
+        in, finishes an exploration in-viewer, and there are author-specified
+        exploration IDs.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, include_system_recommendations=True,
+            author_recommended_ids_str='["7","8"]')
+        self.assertEqual(recommendation_ids, [self.EXP_ID_7, self.EXP_ID_8])
+
+    def test_logged_in_with_sysexps_and_authexps_no_col_has_some_exps(self):
+        """Check there are recommended explorations when a user is logged in,
+        finishes an exploration in-viewer, and there are both author-specified
+        exploration IDs and recommendations from the system.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._set_recommendations(self.EXP_ID_0, [self.EXP_ID_1, self.EXP_ID_8])
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, include_system_recommendations=True,
+            author_recommended_ids_str='["7","8"]')
+        self.assertEqual(
+            recommendation_ids, [self.EXP_ID_1, self.EXP_ID_7, self.EXP_ID_8])
+
+    # Logged in in-editor tests.
+    def test_logged_in_preview_no_authexps_no_col_has_no_exps(self):
+        """Check there are no recommended explorations when a user is logged in,
+        finishes an exploration in-editor (no system recommendations since it's
+        a preview of the exploration), and there are no author exploration IDs.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        recommendation_ids = self._get_recommendation_ids(self.EXP_ID_0)
+        self.assertEqual(recommendation_ids, [])
+
+    def test_logged_in_preview_with_authexps_no_col_has_some_exps(self):
+        """Check there are some recommended explorations when a user is logged
+        in, finishes an exploration in-editor (no system recommendations), and
+        there are some author exploration IDs.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, author_recommended_ids_str='["7","8"]')
+        self.assertEqual(recommendation_ids, [self.EXP_ID_7, self.EXP_ID_8])
+
+    # Logged in collection tests.
+    def test_logged_in_no_sysexps_no_authexps_first_exp_in_col_has_exp(self):
+        """Check there is a recommended exploration when a user is logged in
+        and completes the first exploration of a collection.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._complete_exploration_in_collection(self.EXP_ID_19)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_19, collection_id=self.COL_ID)
+        # The next exploration in the collection should be recommended.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_20])
+
+    def test_logged_in_no_sysexps_no_authexps_mid_exp_in_col_has_exps(self):
+        """Check there are recommended explorations when a user is logged in
+        and completes a middle exploration of the collection (since more
+        explorations are needed to complete the collection).
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._complete_exploration_in_collection(self.EXP_ID_20)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_20, collection_id=self.COL_ID)
+        # The first & next explorations should be recommended, since the first
+        # is the next to complete with current progress, and the last
+        # exploration is unlocked by completing this exploration.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_19, self.EXP_ID_21])
+
+    def test_logged_in_no_sysexps_no_authexps_all_exps_in_col_has_no_exps(self):
+        """Check there are not recommended explorations when a user is logged in
+        and completes all explorations of the collection.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._complete_entire_collection_in_order()
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, collection_id=self.COL_ID)
+        # No explorations are recommended since the collection was completed.
+        self.assertEqual(recommendation_ids, [])
+
+    def test_logged_in_with_sysexps_no_authexps_first_exp_in_col_has_exp(self):
+        """Check there is a recommended exploration when a user is logged in
+        and completes the first exploration of a collection. Note that even
+        though the completed exploration has system recommendations, they are
+        ignored in favor of the collection's own recommendations.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._set_recommendations(
+            self.EXP_ID_19, [self.EXP_ID_1, self.EXP_ID_8])
+        self._complete_exploration_in_collection(self.EXP_ID_19)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_19, collection_id=self.COL_ID,
+            include_system_recommendations=True)
+        # The next exploration in the collection should be recommended.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_20])
+
+    def test_logged_in_with_sysexps_no_authexps_mid_exp_in_col_has_exps(self):
+        """Check there are recommended explorations when a user is logged in
+        and completes a middle exploration of the collection (since more
+        explorations are needed to complete the collection). Note that even
+        though the completed exploration has system recommendations, they are
+        ignored in favor of the collection's own recommendations.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._set_recommendations(
+            self.EXP_ID_20, [self.EXP_ID_1, self.EXP_ID_8])
+        self._complete_exploration_in_collection(self.EXP_ID_20)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_20, collection_id=self.COL_ID,
+            include_system_recommendations=True)
+        # The first & next explorations should be recommended, since the first
+        # is the next to complete with current progress, and the last
+        # exploration is unlocked by completing this exploration.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_19, self.EXP_ID_21])
+
+    def test_logged_in_sysexps_no_authexps_all_exps_in_col_has_no_exps(self):
+        """Check there are not recommended explorations when a user is logged in
+        and completes all explorations of the collection. This is true even if
+        there are system recommendations for the last exploration.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._complete_entire_collection_in_order()
+        self._set_recommendations(
+            self.EXP_ID_0, [self.EXP_ID_1, self.EXP_ID_8])
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, collection_id=self.COL_ID,
+            include_system_recommendations=True)
+        # No explorations are recommended since the collection was completed.
+        self.assertEqual(recommendation_ids, [])
+
+    def test_logged_in_no_sysexps_with_authexps_first_exp_in_col_has_exps(self):
+        """Check there is are recommended explorations when a user is logged in
+        and completes the first exploration of a collection where that
+        exploration also has author-specified explorations.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._complete_exploration_in_collection(self.EXP_ID_19)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_19, collection_id=self.COL_ID,
+            author_recommended_ids_str='["7","8"]')
+        # The next exploration in the collection should be recommended along
+        # with author specified explorations.
+        self.assertEqual(
+            recommendation_ids, [self.EXP_ID_20, self.EXP_ID_7, self.EXP_ID_8])
+
+    def test_logged_in_no_sysexps_with_authexps_mid_exp_in_col_has_exps(self):
+        """Check there are recommended explorations when a user is logged in
+        and completes a middle exploration of the collection, and that these
+        recommendations include author-specified explorations.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._complete_exploration_in_collection(self.EXP_ID_20)
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_20, collection_id=self.COL_ID,
+            author_recommended_ids_str='["7","21"]')
+        # The first & next explorations should be recommended, along with author
+        # specified explorations.
+        self.assertEqual(
+            recommendation_ids, [self.EXP_ID_19, self.EXP_ID_21, self.EXP_ID_7])
+
+    def test_logged_in_no_sysexps_authexps_all_exps_in_col_has_exps(self):
+        """Check there are still recommended explorations when a user is logged
+        in and completes all explorations of the collection if the last
+        exploration has author-specified explorations.
+        """
+        self.login(self.NEW_USER_EMAIL)
+        self._complete_entire_collection_in_order()
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, collection_id=self.COL_ID,
+            author_recommended_ids_str='["7","8"]')
+        # Only author specified explorations should be recommended since all
+        # others in the collection have been completed.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_7, self.EXP_ID_8])
+
+    # Logged out standard viewer tests.
+    def test_logged_out_with_no_sysexps_no_authexps_no_col_has_no_exps(self):
+        """Check there are no recommended explorations when a user is logged
+        out, finishes an exploration in-viewer, but there are no recommended
+        explorations and no author exploration IDs.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, include_system_recommendations=True)
+        self.assertEqual(recommendation_ids, [])
+
+    def test_logged_out_with_sysexps_no_authexps_no_col_has_some_exps(self):
+        """Check there are recommended explorations when a user is logged out,
+        finishes an exploration in-viewer, and there are system recommendations.
+        """
+        self._set_recommendations(self.EXP_ID_0, [self.EXP_ID_1, self.EXP_ID_8])
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, include_system_recommendations=True)
+        self.assertEqual(recommendation_ids, [self.EXP_ID_1, self.EXP_ID_8])
+
+    def test_logged_out_no_sysexps_some_authexps_no_col_has_some_exps(self):
+        """Check there are some recommended explorations when a user is logged
+        out, finishes an exploration in-viewer, and there are author-specified
+        exploration IDs.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, include_system_recommendations=True,
+            author_recommended_ids_str='["7","8"]')
+        self.assertEqual(recommendation_ids, [self.EXP_ID_7, self.EXP_ID_8])
+
+    def test_logged_out_with_sysexps_and_authexps_no_col_has_some_exps(self):
+        """Check there are recommended explorations when a user is logged in,
+        finishes an exploration in-viewer, and there are both author-specified
+        exploration IDs and recommendations from the system.
+        """
+        self._set_recommendations(self.EXP_ID_0, [self.EXP_ID_1, self.EXP_ID_8])
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, include_system_recommendations=True,
+            author_recommended_ids_str='["7","8"]')
+        self.assertEqual(
+            recommendation_ids, [self.EXP_ID_1, self.EXP_ID_7, self.EXP_ID_8])
+
+    # Logged out collection tests.
+    def test_logged_out_no_sysexps_no_authexps_first_exp_in_col_has_exp(self):
+        """Check there is a recommended exploration when a user is logged out
+        and completes the first exploration of a collection.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_19, collection_id=self.COL_ID)
+        # The next exploration in the collection should be recommended.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_20])
+
+    def test_logged_out_no_sysexps_no_authexps_mid_exp_in_col_has_exp(self):
+        """Check there is a recommended exploration when a user is logged out
+        and completes a middle exploration of the collection.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_20, collection_id=self.COL_ID)
+        # Only the last exploration should be recommended since logged out users
+        # follow a linear path through the collection.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_21])
+
+    def test_logged_out_no_sysexps_no_authexps_last_exp_col_has_no_exps(self):
+        """Check there are not recommended explorations when a user is logged
+        out and completes the last exploration in the collection.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, collection_id=self.COL_ID)
+        self.assertEqual(recommendation_ids, [])
+
+    def test_logged_out_with_sysexps_no_authexps_first_exp_in_col_has_exp(self):
+        """Check there is a recommended exploration when a user is logged out
+        and completes the first exploration of a collection. Note that even
+        though the completed exploration has system recommendations, they are
+        ignored in favor of the collection's own recommendations.
+        """
+        self._set_recommendations(
+            self.EXP_ID_19, [self.EXP_ID_1, self.EXP_ID_8])
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_19, collection_id=self.COL_ID,
+            include_system_recommendations=True)
+        # The next exploration in the collection should be recommended.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_20])
+
+    def test_logged_out_with_sysexps_no_authexps_mid_exp_in_col_has_exp(self):
+        """Check there is a recommended explorations when a user is logged out
+        and completes a middle exploration of the collection. Note that even
+        though the completed exploration has system recommendations, they are
+        ignored in favor of the collection's own recommendations.
+        """
+        self._set_recommendations(
+            self.EXP_ID_20, [self.EXP_ID_1, self.EXP_ID_8])
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_20, collection_id=self.COL_ID,
+            include_system_recommendations=True)
+        # Only the last exploration should be recommended since logged out users
+        # follow a linear path through the collection.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_21])
+
+    def test_logged_out_sysexps_no_authexps_last_exp_in_col_has_no_exps(self):
+        """Check there are not recommended explorations when a user is logged
+        out and completes the last exploration of the collection. This is true
+        even if there are system recommendations for the last exploration.
+        """
+        self._set_recommendations(
+            self.EXP_ID_0, [self.EXP_ID_1, self.EXP_ID_8])
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, collection_id=self.COL_ID,
+            include_system_recommendations=True)
+        # The collection is completed, so no other explorations should be
+        # recommended.
+        self.assertEqual(recommendation_ids, [])
+
+    def test_logged_out_no_sysexps_but_authexps_first_exp_in_col_has_exps(self):
+        """Check there is are recommended explorations when a user is logged out
+        and completes the first exploration of a collection where that
+        exploration also has author-specified explorations.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_19, collection_id=self.COL_ID,
+            author_recommended_ids_str='["7","8"]')
+        # The next exploration in the collection should be recommended along
+        # with author specified explorations.
+        self.assertEqual(
+            recommendation_ids, [self.EXP_ID_20, self.EXP_ID_7, self.EXP_ID_8])
+
+    def test_logged_out_no_sysexps_with_authexps_mid_exp_in_col_has_exps(self):
+        """Check there are recommended explorations when a user is logged out
+        and completes a middle exploration of the collection where that
+        exploration also has author-specified explorations.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_20, collection_id=self.COL_ID,
+            author_recommended_ids_str='["7"]')
+        # Both the next exploration & the author-specified explorations should
+        # be recommended.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_21, self.EXP_ID_7])
+
+    def test_logged_out_no_sysexps_with_dup_authexps_mid_col_exp_has_exps(self):
+        """test_logged_out_no_sysexps_with_authexps_mid_exp_in_col_has_exps but
+        also checks that exploration IDs are de-duped if the next exploration
+        overlaps with the author-specified explorations.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_20, collection_id=self.COL_ID,
+            author_recommended_ids_str='["7", "21"]')
+        # Both the next exploration & the author-specified explorations should
+        # be recommended.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_21, self.EXP_ID_7])
+
+    def test_logged_out_no_sysexps_authexps_last_exp_in_col_has_exps(self):
+        """Check there are still recommended explorations when a user is logged
+        out and completes all explorations of the collection if the last
+        exploration has author-specified explorations.
+        """
+        recommendation_ids = self._get_recommendation_ids(
+            self.EXP_ID_0, collection_id=self.COL_ID,
+            author_recommended_ids_str='["7","8"]')
+        # Only author specified explorations should be recommended since all
+        # others in the collection have been completed.
+        self.assertEqual(recommendation_ids, [self.EXP_ID_7, self.EXP_ID_8])
+
+
 class FlagExplorationHandlerTests(test_utils.GenericTestBase):
     """Backend integration tests for flagging an exploration."""