Repository: oppia/oppia

Issue:
Update docstrings in the Python backend code to follow the style guide.
We've been a bit lax about adding docstrings to code in the backend. Instead, we should be following the style guide: https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments -- docstrings should define args, return values, exceptions, etc. The aim of this issue is to ensure that the backend code is properly documented; we will tackle this on a file-by-file basis.

Note: if you're a new contributor, this issue is a particularly good introductory one to tackle, since it will require reading, understanding and documenting the purpose of a file in the codebase, and help you get acquainted with how the Oppia backend is structured. It's important that you're able to communicate and write clearly, though!

Some useful tips:
-  Please follow our [style guide](https://github.com/oppia/oppia/wiki/Coding-style-guide#general) (and also the [Google Python style guide](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments)) when writing docstrings. Make sure to follow the patterns established there as closely as possible, including spacing, capitalization and punctuation. Also, it's a good idea to look at some of the existing files (see below) to get a sense of the correct format -- we recommend following this closely in order to maintain uniformity across the codebase.
- If applicable, make sure that the docstrings for all functions correctly specify the args, return values, etc. If a function has no args, you can omit the Args section; if it has no return value, you can omit the Returns section.
- If a docstring is not present in the code, or the existing one is too vague for you to understand what's going on just by looking at it, then it should be rewritten to be more clear. You'll often need to read the code as well, and do a grep on the callsites to see how it's being used, in order to get a clear understanding of what it's doing. This is good practice for learning how to navigate the codebase, and is an intended part of this project.
- If you feel that parts of the codebase are unclear or incorrectly factored, please feel free to bring this up! The whole point of docstrings is to improve code health and accessibility for new developers, so if you spot other things that can be done in service of that aim, we should also look into doing that.

**Completed:**
- [x] core.controllers.base @shaz13 
- [x] core.controllers.cron @saeedjassani
- [x] core.controllers.reader @96mohitm
- [x] core.domain.activity_domain @andromfins
- [x] core.domain.activity_services @souravsingh
- [x] core.domain.classifier_services @andromfins 
- [x] core.domain.collection_domain @domevit
- [x] core.domain.collection_services @kahkhang 
- [x] core.domain.config_services @vibhor98 
- [x] core.domain.email_manager @Oishikatta 
- [x] core.domain.exp_domain @domevit
- [x] core.domain.exp_services @DubeySandeep
- [x] core.domain.feedback_domain @terrameijar 
- [x] core.domain.feedback_jobs_continuous
- [x] core.domain.feedback_services @andimeo
- [x] core.domain.fs_domain @kartik-25 
- [x] core.domain.html_cleaner @seanlip
- [x] core.domain.param_domain @rohan-gulati
- [x] core.domain.rating_services @BojOnTheBeat 
- [x] core.domain.rights_manager @ayejay
- [x] core.domain.stats_domain @Arunabh98
- [x] core.domain.stats_jobs_continuous @Arunabh98 
- [x] core.domain.stats_jobs_one_off @zpuller
- [x] core.domain.stats_services @manoj-makkuboy 
- [x] core.domain.subscription_services @vibhor98 
- [x] core.domain.summary_services @domevit 
- [x] core.domain.user_jobs_continuous @shubha1593 
- [x] core.domain.user_services @Nalinc 
- [x] core.domain.value_generators_domain @meetsha 
- [x] core.jobs @brianrodri
- [x] core.platform.app_identity.gae_app_identity_services @ubhisat
- [x] core.platform.models @pranavsid98 
- [x] core.platform.transactions.gae_transaction_services @ubhisat
- [x] core.storage.base_model.gae_models @vojtechjelinek
- [x] core.storage.collection.gae_models @kaggis
- [x] core.storage.email.gae_models @rajan-garg
- [x] core.storage.exploration.gae_models @kirankonduru
- [x] core.storage.feedback.gae_models @naveenshukla
- [x] core.storage.file.gae_models @Ian91 
- [x] core.storage.job.gae_models @sasacocic
- [x] core.storage.statistics.gae_models @abhishekarya286
- [x] core.storage.user.gae_models @Anthony-Alridge 
- [x] core.tests.test_util_jobs @seanlip
- [x] core.tests.test_utils @vibhor98
- [x] utils @yashLadha 


Discussion:
@seanlip I would like to work in the issue.

Hi @souravsingh, do you think you could finish up #2051 first, please? Thanks! 

@seanlip I would like to work on this issue.

Hi @andromfins -- now that you've had a go at writing docstrings for a single class, would you mind picking one or two other files and doing the same thing for those? Let me know which ones you pick, so that I can mark you off against them.

Thanks!

Hi, i'm working on **core.domain.classifier_services**.

OK, thanks @andromfins! 

@seanlip Can I take a stab at other file to add docstrings?

Yep, sounds good. Could you take core.domain.html_cleaner? Thanks!

@andromfins -- are you still working on this?

  @seanlip Yes!

I would like to work on `core.domain.stats_domain`

@Arunabh98 assigned you on `core.domain.stats_domain`

Almost completed `core.domain.stats_domain`. Now working on `core.domain.stats_jobs_continuous`

hi @seanlip 
I am working on this issue.
I am writing the docstrings for /core/storage/collection/gae_models.py as i was assigned as a starting project.

@seanlip can I work on core.controllers.cron?
Yep, sounds good! I've updated the list.
@seanlip I'd like to work on core.storage.job.gae_models.
Sounds good, thanks @sasacocic! I've updated the list. 
@seanlip I'd like to work on core.platform.models. Thanks

@seanlip I'd like to work on core.domain.stats_services
Sounds good, thanks @arjunmahishi! I've assigned you. 
@seanlip I'd like to work on core.domain.summary_services
Sounds good, thanks! I've assigned you.
@seanlip Hello, i would work on core.domain.config_domain if possible.
@tvallois I have assigned you to that. Thanks
@seanlip I can do core.domain.rights_manager.py
Sounds good @fodra, I've assigned you. Thanks! 
@seanlip I would like to take _core.tests.test_util_jobs_
Sounds great, I've assigned you. Thanks @terrameijar! 
@seanlip could i work on  core.storage.statistics.gae_models?

I'd like to contribute to this, could you please assign me one of the sub tasks?
Hi @fsam95, welcome to Oppia! Could you please follow the instructions [here](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up) first?

@cosmicAsymmetry The list of tasks that are yet to be done is in the "Not started" section. Would you be up for taking a look at the corresponding files, and picking one that looks approachable? Note that test_utils has some prior work that you might be able to extend, though it's slightly unusual because the functions within it are meant mainly for supporting backend testing (and are therefore not directly user-facing). Thanks!
I've followed the instructions :). 
Thanks @fsam95! Assigned you. 
@seanlip I want to take one of the files but it looks like all the files in "Not started" section have been documented? Can you assign me to one which has not been taken? Thanks! :)
Hi @zhengxit -- actually, all those files still need work, I think, be it improving the existing docstrings, writing new docstrings for methods/functions which don't have them, or making sure that everything follows the correct style. So, please feel free to pick one of them!
(P.S. If you see a particular file in the "Not started" section that you're not sure what to do with, just ask.)
@seanlip I would like to take **core.controllers.base**

Assigned. Thanks @shaz13! 
I'm starting work on **core.jobs**.
@seanlip Please assign me an issue.I also want to contribute.
can I take #2394 Docstrings for controllers/base.py
@seanlip I am taking core.domain.stats_services
OK, thanks! Assigned you.
hey...I am working on core.domain.stats_services....I can see previous docstrings in it.So, what exactly is incomplete in this project.
Hello @arjunsuri, you can look at the previous PRs' ( alike #3448, #2459 ) to get an idea about expected format. I looked into the file [core.domain.stats_services. ](https://github.com/oppia/oppia/blob/develop/core/domain/stats_services.py) there are still few functions missing Args, Return information which you should document in your PR. If you still have any doubts feel free to ping us in the Gitter channel. 

Resolving patch:
diff --git a/core/domain/rights_manager.py b/core/domain/rights_manager.py
--- a/core/domain/rights_manager.py
+++ b/core/domain/rights_manager.py
@@ -113,7 +113,12 @@ def validate(self):
                 editor_viewer)
 
     def to_dict(self):
-        """Returns a dict suitable for use by the frontend."""
+        """Returns a dict suitable for use by the frontend.
+
+        Returns:
+            dict. A dict version of ActivityRights suitable for use by the
+                frontend.
+        """
         if self.community_owned:
             return {
                 'cloned_from': self.cloned_from,
@@ -140,6 +145,18 @@ def to_dict(self):
 
 
 def _get_activity_rights_from_model(activity_rights_model, activity_type):
+    """Constructs an ActivityRights object from the given activity rights model.
+
+    Args:
+        activity_rights_model: ActivityRightsModel. Activity rights from the
+            datastore.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+
+    Returns:
+        ActivityRights. The rights object created from the model.
+    """
     return ActivityRights(
         activity_rights_model.id,
         activity_rights_model.owner_ids,
@@ -160,6 +177,17 @@ def _save_activity_rights(
         commit_cmds):
     """Saves an ExplorationRights or CollectionRights domain object to the
     datastore.
+
+    Args:
+        committer_id: str. ID of the committer.
+        activity_rights: ActivityRights. The rights object for the given
+            activity.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+        commit_message: str. Descriptive message for the commit.
+        commit_cmds: list(dict). A list of commands describing what kind of
+            commit was done.
     """
     activity_rights.validate()
 
@@ -180,22 +208,50 @@ def _save_activity_rights(
     model.commit(committer_id, commit_message, commit_cmds)
 
 
-# Update summary of changed activity (note that the activity rights id is the
-# same as the activity id).
-# TODO(msl): get rid of inline imports by refactoring code
 def _update_exploration_summary(activity_rights):
+    """Updates the exploration summary for the activity associated with the
+    given rights object.
+
+    The ID of rights object is the same as the ID of associated activity.
+
+    Args:
+        activity_rights: ActivityRights. The rights object for the given
+            activity.
+    """
+    # TODO(msl): get rid of inline imports by refactoring code.
     from core.domain import exp_services
     exp_services.update_exploration_summary(
         activity_rights.id, None)
 
 
 def _update_collection_summary(activity_rights):
+    """Updates the collection summary for the given activity associated with
+    the given rights object.
+
+    The ID of rights object is the same as the ID of associated activity.
+
+    Args:
+        activity_rights: ActivityRights. The rights object for the given
+            activity.
+    """
     from core.domain import collection_services
     collection_services.update_collection_summary(
         activity_rights.id, None)
 
 
 def _update_activity_summary(activity_type, activity_rights):
+    """Updates the activity summary for the given activity associated with
+    the given rights object.
+
+    The ID of rights object is the same as the ID of associated activity.
+
+    Args:
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+        activity_rights: ActivityRights. The rights object for the given
+            activity.
+    """
     if activity_type == feconf.ACTIVITY_TYPE_EXPLORATION:
         _update_exploration_summary(activity_rights)
     elif activity_type == feconf.ACTIVITY_TYPE_COLLECTION:
@@ -204,10 +260,18 @@ def _update_activity_summary(activity_type, activity_rights):
 
 def update_activity_first_published_msec(
         activity_type, activity_id, first_published_msec):
-    """Updates the first_published_msec field for an activity. This is only
-    called during the one-off job ExplorationFirstPublishedOneOffJob. Callers
-    are responsible for ensuring that this value is not already set before
-    updating it.
+    """Updates the first_published_msec field for the given activity.
+
+    The caller is responsible for ensuring that this value is not already
+    set before updating it.
+
+    Args:
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+        activity_id: str. ID of the activity.
+        first_published_msec: float. First publication time in milliseconds
+            since the Epoch.
     """
     activity_rights = _get_activity_rights(activity_type, activity_id)
     commit_cmds = [{
@@ -222,6 +286,13 @@ def update_activity_first_published_msec(
 
 
 def create_new_exploration_rights(exploration_id, committer_id):
+    """Creates a new exploration rights object and saves it to the datastore.
+    Subscribes the committer to the new exploration.
+
+    Args:
+        exploration_id: str. ID of the exploration.
+        committer_id: str. ID of the committer.
+    """
     exploration_rights = ActivityRights(
         exploration_id, [committer_id], [], [])
     commit_cmds = [{'cmd': CMD_CREATE_NEW}]
@@ -242,7 +313,20 @@ def create_new_exploration_rights(exploration_id, committer_id):
 
 
 def get_exploration_rights(exploration_id, strict=True):
-    """Retrieves the rights for this exploration from the datastore."""
+    """Retrieves the rights for this exploration from the datastore.
+
+    Args:
+        exploration_id: str. ID of the exploration.
+        strict: bool. Whether to raise an error if there is no exploration
+            matching the given ID.
+
+    Returns:
+        ActivityRights. The rights object for the given exploration.
+
+    Raises:
+        EntityNotFoundError. The exploration with ID exploration_id was not
+            found in the datastore.
+    """
     model = exp_models.ExplorationRightsModel.get(
         exploration_id, strict=strict)
     if model is None:
@@ -252,21 +336,52 @@ def get_exploration_rights(exploration_id, strict=True):
 
 
 def is_exploration_private(exploration_id):
+    """Returns whether exploration is private.
+
+    Args:
+        exploration_id: str. ID of the exploration.
+
+    Returns:
+        bool. Whether the exploration is private or not.
+    """
     exploration_rights = get_exploration_rights(exploration_id)
     return exploration_rights.status == ACTIVITY_STATUS_PRIVATE
 
 
 def is_exploration_public(exploration_id):
+    """Returns whether exploration is public.
+
+    Args:
+        exploration_id: str. ID of the exploration.
+
+    Returns:
+        bool. Whether the exploration is public.
+    """
     exploration_rights = get_exploration_rights(exploration_id)
     return exploration_rights.status == ACTIVITY_STATUS_PUBLIC
 
 
 def is_exploration_cloned(exploration_id):
+    """Returns whether the exploration is a clone of another exploration.
+
+    Args:
+        exploration_id: str. ID of the exploration.
+
+    Returns:
+        bool. Whether the exploration is a clone of another exploration.
+    """
     exploration_rights = get_exploration_rights(exploration_id)
     return bool(exploration_rights.cloned_from)
 
 
 def create_new_collection_rights(collection_id, committer_id):
+    """Creates a new collection rights object and saves it to the datastore.
+    Subscribes the committer to the new collection.
+
+    Args:
+        collection_id: str. ID of the collection.
+        committer_id: str. ID of the committer.
+    """
     collection_rights = ActivityRights(collection_id, [committer_id], [], [])
     commit_cmds = [{'cmd': CMD_CREATE_NEW}]
 
@@ -285,7 +400,19 @@ def create_new_collection_rights(collection_id, committer_id):
 
 
 def get_collection_rights(collection_id, strict=True):
-    """Retrieves the rights for this collection from the datastore."""
+    """Retrieves the rights for this collection from the datastore.
+
+    Args:
+        collection_id: str. ID of the collection.
+        strict: bool. Whether to raise an error if ID is not found.
+
+    Returns:
+        ActivityRights. The rights object for the collection.
+
+    Raises:
+        EntityNotFoundError. The collection with ID collection_id is not found
+            in the datastore.
+    """
     model = collection_models.CollectionRightsModel.get(
         collection_id, strict=strict)
     if model is None:
@@ -295,26 +422,61 @@ def get_collection_rights(collection_id, strict=True):
 
 
 def get_collection_owner_names(collection_id):
-    """Retrieves the owners for this collection from the datastore."""
+    """Retrieves the owners for this collection from the datastore.
+
+    Args:
+        collection_id: str. ID of the collection.
+
+    Returns:
+        list(str). Human-readable usernames (or truncated email addresses) of
+            owners for this collection.
+    """
     collection_rights = get_collection_rights(collection_id)
     return user_services.get_human_readable_user_ids(
         collection_rights.owner_ids)
 
 
 def is_collection_private(collection_id):
+    """Returns whether the collection is private.
+
+    Args:
+        collection_id: str. ID of the collection.
+
+    Returns:
+        bool. Whether the collection is private.
+    """
     collection_rights = get_collection_rights(collection_id)
     return collection_rights.status == ACTIVITY_STATUS_PRIVATE
 
 
 def is_collection_public(collection_id):
+    """Returns whether the collection is public.
+
+    Args:
+        collection_id: str. ID of the collection.
+
+    Returns:
+        bool. Whether the collection is public.
+    """
     collection_rights = get_collection_rights(collection_id)
     return collection_rights.status == ACTIVITY_STATUS_PUBLIC
 
 
 def _get_activity_rights(activity_type, activity_id):
-    """This function returns None if this function fails to retrieve the rights
-    object for a given activity based on its type. If the activity_type value
-    provided is unknown, an Exception is raised.
+    """Retrieves the rights object for the given activity
+    based on its type.
+
+    Args:
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+        activity_id: str. ID of the activity.
+
+    Returns:
+        ActivityRights. The rights object associated with the given activity.
+
+    Raises:
+        Exception. activity_type provided is unknown.
     """
     if activity_type == feconf.ACTIVITY_TYPE_EXPLORATION:
         return get_exploration_rights(activity_id, strict=False)
@@ -340,11 +502,21 @@ def __init__(self, user_id):
         self._is_moderator = None
 
     def is_admin(self):
+        """Returns whether this user is an administrator.
+
+        Returns:
+            bool. Whether this user is an administrator.
+        """
         if self._is_admin is None:
             self._is_admin = self.user_id in config_domain.ADMIN_IDS.value
         return self._is_admin
 
     def is_moderator(self):
+        """Returns whether this user is a moderator.
+
+        Returns:
+            bool. Whether this user is a moderator.
+        """
         if self._is_moderator is None:
             self._is_moderator = (
                 self.is_admin() or
@@ -396,16 +568,34 @@ def _can_delete(self, rights_object):
             is_deleting_own_private_object or is_mod_deleting_public_object)
 
     def is_owner(self, activity_type, activity_id):
+        """Returns whether this user is an owner of the given activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user is an owner of the given activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
         return self._is_owner(activity_rights)
 
     def has_editing_rights(self, activity_type, activity_id):
-        """Whether this user has editing rights for this activity.
+        """Returns whether this user has editing rights for the given activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
 
-        This is true if the activity is community-owned, or if the user is in
-        the owner/editor list for the activity.
+        Returns:
+            bool. Whether the user is in the owner/editor list for the given
+                activity.
         """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
@@ -413,31 +603,84 @@ def has_editing_rights(self, activity_type, activity_id):
         return self._has_editing_rights(activity_rights)
 
     def has_viewing_rights(self, activity_type, activity_id):
+        """Returns whether this user has viewing rights for the given activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user has the right to view the given activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
         return self._has_viewing_rights(activity_rights)
 
     def can_play(self, activity_type, activity_id):
-        """Whether the user can play the reader view of this activity."""
+        """Returns whether this user has rights to play the given activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user has the right to play the given activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
         return self._can_play(activity_rights)
 
     def can_view(self, activity_type, activity_id):
-        """Whether the user can view the editor page for this activity."""
+        """Returns whether this user has rights to view the given activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user has the right to view the given activity.
+        """
         return self.can_play(activity_type, activity_id)
 
+    # TODO(sll): Add a check here for whether a user is banned or not,
+    # rather than having this check in the controller.
     def can_edit(self, activity_type, activity_id):
-        # TODO(sll): Add a check here for whether a user is banned or not,
-        # rather than having this check in the controller.
+        """Returns whether this user has rights to edit the given activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user has the right to edit the given activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
         return self._can_edit(activity_rights)
 
     def can_delete(self, activity_type, activity_id):
+        """Returns whether this user has rights to delete the given activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user has the right to delete the given activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
@@ -445,11 +688,37 @@ def can_delete(self, activity_type, activity_id):
 
     def can_change_private_viewability(
             self, activity_type, activity_id):
-        """Note that this requires the activity in question to be private."""
+        """Returns whether this user is allowed to change the viewability of
+        the given private activity.
+
+        The caller is responsible for ensuring that the given activity is
+        private.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user is allowed to change the viewability of
+                the given private activity.
+        """
 
         return self.can_publish(activity_type, activity_id)
 
     def can_publish(self, activity_type, activity_id):
+        """Returns whether this user is allowed to publish the given activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user is allowed to publish the given activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
@@ -462,6 +731,19 @@ def can_publish(self, activity_type, activity_id):
         return self.is_owner(activity_type, activity_id) or self.is_admin()
 
     def can_unpublish(self, activity_type, activity_id):
+        """Returns whether this user is allowed to unpublish the given
+        activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user is allowed to unpublish the given
+                activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
@@ -474,6 +756,19 @@ def can_unpublish(self, activity_type, activity_id):
         return self.is_moderator()
 
     def can_modify_roles(self, activity_type, activity_id):
+        """Returns whether this user has the right to modify roles for the given
+        activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user has the right to modify roles for the given
+                activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
@@ -483,6 +778,19 @@ def can_modify_roles(self, activity_type, activity_id):
         return self.is_admin() or self.is_owner(activity_type, activity_id)
 
     def can_release_ownership(self, activity_type, activity_id):
+        """Returns whether this user is allowed to release ownership of the
+        given activity to the community.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user is allowed to release ownership of the
+                given activity to the community.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
@@ -492,6 +800,18 @@ def can_release_ownership(self, activity_type, activity_id):
         return self.can_modify_roles(activity_type, activity_id)
 
     def can_publicize(self, activity_type, activity_id):
+        """Returns whether this user is allowed to publicize the given
+        activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user has a right to publicize the given activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
@@ -501,6 +821,19 @@ def can_publicize(self, activity_type, activity_id):
         return self.is_moderator()
 
     def can_unpublicize(self, activity_type, activity_id):
+        """Returns whether this user is allowed to unpublicize the given
+        activity.
+
+        Args:
+            activity_type: str. The type of activity. Possible values:
+                feconf.ACTIVITY_TYPE_EXPLORATION
+                feconf.ACTIVITY_TYPE_COLLECTION
+            activity_id: str. ID of the activity.
+
+        Returns:
+            bool. Whether the user has a right to unpublicize the given
+                activity.
+        """
         activity_rights = _get_activity_rights(activity_type, activity_id)
         if activity_rights is None:
             return False
@@ -512,16 +845,30 @@ def can_unpublicize(self, activity_type, activity_id):
 
 def _assign_role(
         committer_id, assignee_id, new_role, activity_id, activity_type):
-    """Args:
-    - committer_id: str. The user_id of the user who is performing the action.
-    - activity_rights: The storage object for the rights of this activity (
-        one of: ExplorationRightsModel or CollectionRightsModel).
-    - assignee_id: str. The user_id of the user whose role is being changed.
-    - new_role: str. The name of the new role: either 'owner', 'editor' or
-        'viewer'.
-    - activity_id: str. The ID of the exploration or collection.
-    - activity_type: str. One of feconf.ACTIVITY_TYPE_EXPLORATION or
-        feconf.ACTIVITY_TYPE_COLLECTION.
+    """Assigns a new role to the user.
+
+    Args:
+        committer_id: str. ID of the user who is performing the action.
+        activity_rights: ExplorationRightsModel|CollectionRightsModel. The
+            storage object for the rights of the given activity.
+        assignee_id: str. ID of the user whose role is being changed.
+        new_role: str. The name of the new role: One of
+            ROLE_OWNER
+            ROLE_EDITOR
+            ROLE_VIEWER
+        activity_id: str. ID of the activity.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+
+    Raises:
+        Exception. The committer does not have rights to modify a role.
+        Exception. The user already owns the activity.
+        Exception. The user can already edit the activity.
+        Exception. The activity is already publicly editable.
+        Exception. The user can already view the activity.
+        Exception. The activity is already publicly viewable.
+        Exception. The role is invalid.
     """
     if not Actor(committer_id).can_modify_roles(activity_type, activity_id):
         logging.error(
@@ -593,6 +940,18 @@ def _assign_role(
 
 
 def _release_ownership_of_activity(committer_id, activity_id, activity_type):
+    """Releases ownership of the given activity to the community.
+
+    Args:
+        committer_id: str. ID of the user who is performing the action.
+        activity_id: str. ID of the activity.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+
+    Raise:
+        Exception. The committer does not have release rights.
+    """
     if not Actor(committer_id).can_release_ownership(
             activity_type, activity_id):
         logging.error(
@@ -618,16 +977,16 @@ def _release_ownership_of_activity(committer_id, activity_id, activity_type):
 
 def _change_activity_status(
         committer_id, activity_id, activity_type, new_status, commit_message):
-    """Change the status of an activity. Commits changes.
+    """Changes the status of the given activity.
 
     Args:
-    - committer_id: str. The id of the user who is performing the update
-        action.
-    - activity_id: str. The id of the collection or activity.
-    - activity_type: str. One of feconf.ACTIVITY_TYPE_EXPLORATION or
-        feconf.ACTIVITY_TYPE_COLLECTION.
-    - new_status: str. The new status of the exploration.
-    - commit_message: str. The human-written commit message for this change.
+        committer_id: str. ID of the user who is performing the update action.
+        activity_id: str. ID of the activity.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+        new_status: str. The new status of the activity.
+        commit_message: str. The human-written commit message for this change.
     """
     activity_rights = _get_activity_rights(activity_type, activity_id)
     old_status = activity_rights.status
@@ -655,6 +1014,19 @@ def _change_activity_status(
 
 
 def _publish_activity(committer_id, activity_id, activity_type):
+    """Publishes the given activity.
+
+    Args:
+        committer_id: str. ID of the committer.
+        activity_id: str. ID of the activity.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+
+    Raises:
+        Exception. The committer does not have rights to publish the
+            activity.
+    """
     if not Actor(committer_id).can_publish(activity_type, activity_id):
         logging.error(
             'User %s tried to publish %s %s but was refused '
@@ -667,6 +1039,19 @@ def _publish_activity(committer_id, activity_id, activity_type):
 
 
 def _unpublish_activity(committer_id, activity_id, activity_type):
+    """Unpublishes the given activity.
+
+    Args:
+        committer_id: str. ID of the committer.
+        activity_id: str. ID of the activity.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+
+    Raises:
+        Exception. The committer does not have rights to unpublish the
+            activity.
+    """
     if not Actor(committer_id).can_unpublish(activity_type, activity_id):
         logging.error(
             'User %s tried to unpublish %s %s but was refused '
@@ -681,6 +1066,18 @@ def _unpublish_activity(committer_id, activity_id, activity_type):
 
 
 def _publicize_activity(committer_id, activity_id, activity_type):
+    """Publicizes the given activity.
+
+    Args:
+        committer_id: str. ID of the committer.
+        activity_id: str. ID of the activity.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+
+    Raises:
+        Exception. The committer does not have rights to publicize the activity.
+    """
     if not Actor(committer_id).can_publicize(activity_type, activity_id):
         logging.error(
             'User %s tried to publicize %s %s but was refused '
@@ -694,7 +1091,19 @@ def _publicize_activity(committer_id, activity_id, activity_type):
 
 
 def _unpublicize_activity(committer_id, activity_id, activity_type):
-    """Unpublicizes an exploration. Commits changes."""
+    """Unpublicizes the given activity.
+
+    Args:
+        committer_id: str. ID of the committer.
+        activity_id: str. ID of the activity.
+        activity_type: str. The type of activity. Possible values:
+            feconf.ACTIVITY_TYPE_EXPLORATION
+            feconf.ACTIVITY_TYPE_COLLECTION
+
+    Raises:
+        Exception. The committer does not have rights to unpublicize the
+            activity.
+    """
     if not Actor(committer_id).can_unpublicize(activity_type, activity_id):
         logging.error(
             'User %s tried to unpublicize exploration %s but was refused '
@@ -709,18 +1118,23 @@ def _unpublicize_activity(committer_id, activity_id, activity_type):
 # Rights functions for activities.
 def assign_role_for_exploration(
         committer_id, exploration_id, assignee_id, new_role):
-    """Assign `assignee_id` to the given role and subscribes the assignee
-    to future exploration updates.
+    """Assigns a user to the given role and subscribes the assignee to future
+    exploration updates.
 
     The caller should ensure that assignee_id corresponds to a valid user in
     the system.
 
     Args:
-    - committer_id: str. The user_id of the user who is performing the action.
-    - exploration_id: str. The exploration id.
-    - assignee_id: str. The user_id of the user whose role is being changed.
-    - new_role: str. The name of the new role: either 'owner', 'editor' or
-        'viewer'.
+        committer_id: str. ID of the committer.
+        exploration_id: str. ID of the exploration.
+        assignee_id: str. ID of the user whose role is being changed.
+        new_role: str. The name of the new role: One of
+            ROLE_OWNER
+            ROLE_EDITOR
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _assign_role.
     """
     _assign_role(
         committer_id, assignee_id, new_role, exploration_id,
@@ -731,9 +1145,15 @@ def assign_role_for_exploration(
 
 
 def release_ownership_of_exploration(committer_id, exploration_id):
-    """Releases ownership of an exploration to the community.
+    """Releases ownership of the given exploration to the community.
+
+    Args:
+        committer_id: str. ID of the committer.
+        exploration_id: str. ID of the exploration.
 
-    Commits changes.
+    Raises:
+        Exception. This could potentially throw an exception from
+            _release_ownership_of_activity.
     """
     _release_ownership_of_activity(
         committer_id, exploration_id, feconf.ACTIVITY_TYPE_EXPLORATION)
@@ -741,9 +1161,22 @@ def release_ownership_of_exploration(committer_id, exploration_id):
 
 def set_private_viewability_of_exploration(
         committer_id, exploration_id, viewable_if_private):
-    """Sets the viewable_if_private attribute for an exploration's rights
-    object. If viewable_if_private is True, this allows an private exploration
+    """Sets the viewable_if_private attribute for the given exploration's rights
+    object.
+
+    If viewable_if_private is True, this allows a private exploration
     to be viewed by anyone with the link.
+
+    Args:
+        committer_id: str. ID of the committer.
+        exploration_id: str. ID of the exploration.
+        viewable_if_private: bool. Whether the exploration should be made
+            viewable (by anyone with the link).
+
+    Raises:
+        Exception. The committer does not have the permission to perform change
+            action.
+        Exception. If the viewable_if_private property is already as desired.
     """
     if not Actor(committer_id).can_change_private_viewability(
             feconf.ACTIVITY_TYPE_EXPLORATION, exploration_id):
@@ -778,35 +1211,67 @@ def set_private_viewability_of_exploration(
 
 
 def publish_exploration(committer_id, exploration_id):
-    """This is called by the publish_exploration_and_update_user_profiles
-    function in exp_services.py. It publishes an exploration and
-    commits changes.
+    """Publishes the given exploration.
 
     It is the responsibility of the caller to check that the exploration is
     valid prior to publication.
+
+    Args:
+        committer_id: str. ID of the committer.
+        exploration_id: str. ID of the exploration.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _publish_activity.
     """
     _publish_activity(
         committer_id, exploration_id, feconf.ACTIVITY_TYPE_EXPLORATION)
 
 
 def unpublish_exploration(committer_id, exploration_id):
-    """Unpublishes an exploration. Commits changes."""
+    """Unpublishes the given exploration.
+
+    Args:
+        committer_id: str. ID of the committer.
+        exploration_id: str. ID of the exploration.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _unpublish_activity.
+    """
     _unpublish_activity(
         committer_id, exploration_id, feconf.ACTIVITY_TYPE_EXPLORATION)
 
 
 def publicize_exploration(committer_id, exploration_id):
-    """Publicizes an exploration. Commits changes.
+    """Publicizes the given exploration.
 
     It is the responsibility of the caller to check that the exploration is
     valid prior to publicizing it.
+
+    Args:
+        committer_id: str. ID of the committer.
+        exploration_id: str. ID of the exploration.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _publicize_activity.
     """
     _publicize_activity(
         committer_id, exploration_id, feconf.ACTIVITY_TYPE_EXPLORATION)
 
 
 def unpublicize_exploration(committer_id, exploration_id):
-    """Unpublicizes an exploration. Commits changes."""
+    """Unpublicizes the given exploration.
+
+    Args:
+        committer_id: str. ID of the committer.
+        exploration_id: str. ID of the exploration.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _unpublicize_activity.
+    """
     _unpublicize_activity(
         committer_id, exploration_id, feconf.ACTIVITY_TYPE_EXPLORATION)
 
@@ -814,18 +1279,23 @@ def unpublicize_exploration(committer_id, exploration_id):
 # Rights functions for collections.
 def assign_role_for_collection(
         committer_id, collection_id, assignee_id, new_role):
-    """Assign `assignee_id` to the given role and subscribes the assignee
+    """Assign the given user to the given role and subscribes the assignee
     to future collection updates.
 
     The caller should ensure that assignee_id corresponds to a valid user in
     the system.
 
     Args:
-    - committer_id: str. The user_id of the user who is performing the action.
-    - collection_id: str. The collection id.
-    - assignee_id: str. The user_id of the user whose role is being changed.
-    - new_role: str. The name of the new role: either 'owner', 'editor' or
-        'viewer'.
+        committer_id: str. ID of the committer.
+        collection_id: str. ID of the collection.
+        assignee_id: str. ID of the user whose role is being changed.
+        new_role: str. The name of the new role: One of
+            ROLE_OWNER
+            ROLE_EDITOR
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _assign_role.
     """
     _assign_role(
         committer_id, assignee_id, new_role, collection_id,
@@ -836,43 +1306,82 @@ def assign_role_for_collection(
 
 
 def release_ownership_of_collection(committer_id, collection_id):
-    """Releases ownership of an collection to the community.
+    """Releases ownership of the given collection to the community.
 
-    Commits changes.
+    Args:
+        committer_id: str. ID of the committer.
+        collection_id: str. ID of the collection.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _release_ownership_of_activity.
     """
     _release_ownership_of_activity(
         committer_id, collection_id, feconf.ACTIVITY_TYPE_COLLECTION)
 
 
 def publish_collection(committer_id, collection_id):
-    """This is called by the publish_collection_and_update_user_profiles
-    function in collection_services.py. It publishes an collection and commits
-    changes.
+    """Publishes the given collection.
 
     It is the responsibility of the caller to check that the collection is
     valid prior to publication.
+
+    Args:
+        committer_id: str. ID of the committer.
+        collection_id: str. ID of the collection.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _publish_activity.
     """
     _publish_activity(
         committer_id, collection_id, feconf.ACTIVITY_TYPE_COLLECTION)
 
 
 def unpublish_collection(committer_id, collection_id):
-    """Unpublishes an collection. Commits changes."""
+    """Unpublishes the given collection.
+
+    Args:
+        committer_id: str. ID of the committer.
+        collection_id: str. ID of the collection.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _unpublish_activity.
+
+    """
     _unpublish_activity(
         committer_id, collection_id, feconf.ACTIVITY_TYPE_COLLECTION)
 
 
 def publicize_collection(committer_id, collection_id):
-    """Publicizes an collection. Commits changes.
+    """Publicizes the given collection.
 
     It is the responsibility of the caller to check that the collection is
     valid prior to publicizing it.
+
+    Args:
+        committer_id: str. ID of the committer.
+        collection_id: str. ID of the collection.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _publicize_activity.
     """
     _publicize_activity(
         committer_id, collection_id, feconf.ACTIVITY_TYPE_COLLECTION)
 
 
 def unpublicize_collection(committer_id, collection_id):
-    """Unpublicizes an collection. Commits changes."""
+    """Unpublicizes the given collection.
+
+    Args:
+        committer_id: str. ID of the committer.
+        collection_id: str. ID of the collection.
+
+    Raises:
+        Exception. This could potentially throw an exception from
+            _unpublicize_activity.
+    """
     _unpublicize_activity(
         committer_id, collection_id, feconf.ACTIVITY_TYPE_COLLECTION)


Test patch: