Repository: oppia/oppia

Issue:
Ordering of explorations in a collection should not depend on ordering of nodes in the .yaml file
In a collection, the order of the explorations should depend only on the prerequisite skills (as defined, for example, in welcome_to_collections.yaml). In particular, the order should not depend on the ordering of nodes in the .yaml file.

For example, at this time, changing the ordering of the "Welcome to Oppia" and "Demonstrating soft rules" nodes in welcome_to_collections.yaml causes the collection player to display the corresponding explorations in a different order:

![screenshot from 2017-05-26 17 36 34](https://cloud.githubusercontent.com/assets/16965009/26516377/4261c1e4-423a-11e7-83ac-db387cf38b03.png)

Swapping nodes 4 and 5 in welcome_to_collections.yaml causes the (lower) smiley face and the lightbulb to be switched on this page, i.e. the "About Oppia" and "Demonstrating string classifier" explorations are switched (see "EDIT" at bottom about name discrepancy).

This might also affect [#2376](https://github.com/oppia/oppia/issues/2376). I will continue working on [#2569](https://github.com/oppia/oppia/issues/2569), but after the issue described above is fixed, relevant parts of #2569 may need to be updated.

EDIT: Also, in welcome_to_collections.yaml, the skill name "Demonstrating soft rules" does not match the corresponding exploration name ("Demonstrating string classifier"), while the other four explorations' names match the corresponding skill names.
Ordering of explorations in a collection should not depend on ordering of nodes in the .yaml file
In a collection, the order of the explorations should depend only on the prerequisite skills (as defined, for example, in welcome_to_collections.yaml). In particular, the order should not depend on the ordering of nodes in the .yaml file.

For example, at this time, changing the ordering of the "Welcome to Oppia" and "Demonstrating soft rules" nodes in welcome_to_collections.yaml causes the collection player to display the corresponding explorations in a different order:

![screenshot from 2017-05-26 17 36 34](https://cloud.githubusercontent.com/assets/16965009/26516377/4261c1e4-423a-11e7-83ac-db387cf38b03.png)

Swapping nodes 4 and 5 in welcome_to_collections.yaml causes the (lower) smiley face and the lightbulb to be switched on this page, i.e. the "About Oppia" and "Demonstrating string classifier" explorations are switched (see "EDIT" at bottom about name discrepancy).

This might also affect [#2376](https://github.com/oppia/oppia/issues/2376). I will continue working on [#2569](https://github.com/oppia/oppia/issues/2569), but after the issue described above is fixed, relevant parts of #2569 may need to be updated.

EDIT: Also, in welcome_to_collections.yaml, the skill name "Demonstrating soft rules" does not match the corresponding exploration name ("Demonstrating string classifier"), while the other four explorations' names match the corresponding skill names.


Discussion:
@DubeySandeep any updates on this?
Hi @DubeySandeep, still working on this?
Hi @shubha1593, I'm working on this (#4594). 
Thanks! :)
@DubeySandeep any updates on this?
Hi @DubeySandeep, still working on this?
Hi @shubha1593, I'm working on this (#4594). 
Thanks! :)

Resolving patch:
diff --git a/core/domain/collection_domain.py b/core/domain/collection_domain.py
--- a/core/domain/collection_domain.py
+++ b/core/domain/collection_domain.py
@@ -1019,6 +1019,26 @@ def _recursively_find_learned_skills(node):
         else:
             return []
 
+    def get_nodes_in_playable_order(self):
+        """Returns a list of collection nodes in linear, playable order and
+        assumes the nodes can fit a linear structure.
+
+        Returns:
+            list(CollectionNode). A sorted list of collection nodes.
+        """
+        sorted_exp_ids = self.init_exploration_ids
+        next_exp_ids = self.get_next_exploration_ids(sorted_exp_ids)
+        while next_exp_ids:
+            for next_exp_id in next_exp_ids:
+                if next_exp_id not in sorted_exp_ids:
+                    sorted_exp_ids.append(next_exp_id)
+            next_exp_ids = self.get_next_exploration_ids(sorted_exp_ids)
+
+        sorted_nodes_list = [
+            copy.deepcopy(self.get_node(exp_id)) for exp_id in sorted_exp_ids]
+
+        return sorted_nodes_list
+
     @classmethod
     def is_demo_collection_id(cls, collection_id):
         """Whether the collection id is that of a demo collection.
diff --git a/core/domain/summary_services.py b/core/domain/summary_services.py
--- a/core/domain/summary_services.py
+++ b/core/domain/summary_services.py
@@ -148,6 +148,8 @@ def get_learner_collection_dict_by_id(
         completed_exp_ids = []
 
     collection_dict = collection.to_dict()
+    collection_dict['nodes'] = [
+        node.to_dict() for node in collection.get_nodes_in_playable_order()]
     collection_dict['playthrough_dict'] = {
         'next_exploration_ids': next_exploration_ids,
         'completed_exploration_ids': completed_exp_ids


Test patch:
diff --git a/core/domain/collection_domain_test.py b/core/domain/collection_domain_test.py
--- a/core/domain/collection_domain_test.py
+++ b/core/domain/collection_domain_test.py
@@ -753,7 +753,6 @@ def test_next_explorations(self):
                 ['exp_id_0', 'exp_id_1', 'exp_id_2', 'exp_id_3']), [])
 
     def test_next_explorations_in_sequence(self):
-
         collection = collection_domain.Collection.create_default_collection(
             'collection_id')
         exploration_id = 'exp_id_0'
@@ -814,6 +813,85 @@ def test_next_explorations_in_sequence(self):
             collection.get_next_exploration_ids_in_sequence('exp_id_3'),
             [])
 
+    def test_nodes_are_in_playble_order(self):
+        # Create collection.
+        collection = collection_domain.Collection.create_default_collection(
+            'collection_id')
+
+        # There should be an empty node list in playable order for an empty
+        # collection.
+        self.assertEqual(collection.get_nodes_in_playable_order(), [])
+
+        # Add nodes to collection.
+        collection.add_node('exp_id_1')
+        collection.add_node('exp_id_2')
+        collection.add_node('exp_id_0')
+
+        # Without updating prerequisite and acquired skills the
+        # prerequisite and acquired skill_ids list is empty in each node, so
+        # the playable order will be equal to the order in which they are
+        # adeed.
+        sorted_nodes = collection.get_nodes_in_playable_order()
+        expected_explorations_list = ['exp_id_1', 'exp_id_2', 'exp_id_0']
+        observed_explration_list = [
+            node.exploration_id for node in sorted_nodes]
+
+        self.assertEqual(expected_explorations_list, observed_explration_list)
+
+        # Add skills to collection.
+        collection.add_skill('skill0')
+        collection.add_skill('skill1')
+        collection.add_skill('skill2')
+
+        # Updating 1st node's prerequisite and acquired skill_ids.
+        collection_node0 = collection.get_node('exp_id_1')
+        collection_node0.update_prerequisite_skill_ids(['skill0'])
+        collection_node0.update_acquired_skill_ids(['skill1'])
+
+        # The 1st node will be removed from the playable order as this node
+        # is not acheivable i.e, there is no way to fullfil the prerequisite
+        # of this node.
+        sorted_nodes = collection.get_nodes_in_playable_order()
+        expected_explorations_list = ['exp_id_2', 'exp_id_0']
+        observed_explration_list = [
+            node.exploration_id for node in sorted_nodes]
+
+        self.assertEqual(expected_explorations_list, observed_explration_list)
+
+        # Updating 3rd node's prerequisite and acquired skill_ids.
+        collection_node2 = collection.get_node('exp_id_0')
+        collection_node2.update_prerequisite_skill_ids([])
+        collection_node2.update_acquired_skill_ids(['skill0'])
+
+        # Now the 1st node's prerequisite skills can be acheived through the
+        # 3rd node (exp_id_0 ---> exp_id_1).
+        sorted_nodes = collection.get_nodes_in_playable_order()
+        expected_explorations_list = ['exp_id_2', 'exp_id_0', 'exp_id_1']
+        observed_explration_list = [
+            node.exploration_id for node in sorted_nodes]
+        self.assertEqual(observed_explration_list, expected_explorations_list)
+
+        # Updating 2nd node's prerequisite and acquired skill_ids.
+        collection_node1 = collection.get_node('exp_id_2')
+        collection_node1.update_prerequisite_skill_ids(['skill1'])
+        collection_node1.update_acquired_skill_ids(['skill2'])
+
+        # Sorting nodes in linear and playable order.
+        sorted_nodes = collection.get_nodes_in_playable_order()
+
+        # Expected order of explorations.
+        expected_explorations_list = ['exp_id_0', 'exp_id_1', 'exp_id_2']
+
+        # Observed order of exploration.
+        observed_explration_list = [
+            node.exploration_id for node in sorted_nodes]
+
+        # Validates the number of nodes present in collection after sorting.
+        self.assertEqual(len(sorted_nodes), 3)
+
+        # Checks the order of exploration.
+        self.assertEqual(observed_explration_list, expected_explorations_list)
+
     def test_next_explorations_with_invalid_exploration_ids(self):
         collection = collection_domain.Collection.create_default_collection(
             'collection_id')