Repository: oppia/oppia

Issue:
Implement collection skills update commands
This is milestone 1 (part b) of the Questions project.

This milestone will implement the ability to update skills in a collection by creating the appropriate update command, updating CollectionChange domain object, and updating the apply_change_list method to include skills. Refer to the design doc here for details: https://docs.google.com/document/d/1_EMD4JVn2H9aBX8OkkbieeXOcM6mx5EaB2tkHOiQ4tI/edit?ts=586f16a9#

Todo:
- [x] Add new update commands for skills to collection_domain
- [x] Update collection_domain.CollectionChange to accommodate skills
- [x] Update collection_services.apply_change_list to accommodate skills
- [x] Write a one-off mapreduce job to migrate existing explorations to use skill IDs with a skills list in the collection



Discussion:

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
@@ -1075,6 +1075,19 @@ def add_skill(self, skill_name):
         self.skills[skill_id] = CollectionSkill(skill_id, skill_name, [])
         self.next_skill_id += 1
 
+    def update_skill(self, skill_id, new_skill_name):
+        """Renames skill with specified id to the new skill name."""
+        if skill_id not in self.skills:
+            raise ValueError(
+                'Skill with ID "%s" does not exist.' % skill_id)
+
+        for skill in self.skills.values():
+            if skill.name == new_skill_name:
+                raise ValueError('Skill with name "%s" already exists.'
+                                 % new_skill_name)
+
+        self.skills[skill_id].name = new_skill_name
+
     def delete_skill(self, skill_id):
         """Deletes skill with specified id."""
         if skill_id not in self.skills:


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
@@ -495,6 +495,30 @@ def test_add_skill(self):
         collection.add_skill('skillname2')
         self.assertEqual(sorted(collection.skills.keys()), ['skill0', 'skill1'])
 
+    def test_update_skill(self):
+        """Test that update_skill correctly updates skills."""
+        collection = collection_domain.Collection.create_default_collection(
+            'exp_id')
+        self.assertEqual(collection.skills, {})
+
+        # Add a skill.
+        collection.add_skill('skillname1')
+        self.assertEqual(collection.skills.keys(), ['skill0'])
+        self.assertEqual(collection.skills['skill0'].name, 'skillname1')
+
+        # Update the skill name.
+        collection.update_skill('skill0', 'new skill name1')
+        self.assertEqual(collection.skills['skill0'].name, 'new skill name1')
+
+        # Update the skill name with new skill name which already exists.
+        collection.add_skill('skillname2')
+        self.assertEqual(collection.skills['skill0'].name, 'new skill name1')
+        self.assertEqual(collection.skills['skill1'].name, 'skillname2')
+        with self.assertRaisesRegexp(ValueError,
+                                     'Skill with name "%s" already exists.'
+                                     % 'new skill name1'):
+            collection.update_skill('skill1', 'new skill name1')
+
     def test_adding_duplicate_skill_raises_error(self):
         """Test that adding a duplicate skill name raises an error."""
         collection = collection_domain.Collection.create_default_collection(