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.


Resolving patch:
diff --git a/core/domain/email_manager.py b/core/domain/email_manager.py
--- a/core/domain/email_manager.py
+++ b/core/domain/email_manager.py
@@ -163,6 +163,22 @@ def log_new_error(*args, **kwargs):
 
 
 def _require_sender_id_is_valid(intent, sender_id):
+    """Ensure that the sender ID is valid, based on the email's intent.
+
+    Many emails are only allowed to be sent by a certain user or type of user,
+    e.g. 'admin' or an admin/moderator. This function will raise an exception
+    if the given sender is not allowed to send this type of email.
+
+    Args:
+        intent: str. The intent string, i.e. the purpose of the email.
+            Valid intent strings are defined in feconf.py.
+        sender_id: str. The ID of the user sending the email.
+
+    Raises:
+        Exception: The email intent is invalid.
+        Exception: The sender_id is not appropriate for the given intent.
+    """
+
     if intent not in SENDER_VALIDATORS:
         raise Exception('Invalid email intent string: %s' % intent)
     else:
@@ -184,7 +200,20 @@ def _send_email(
     Raises an Exception if the sender_id is not appropriate for the given
     intent. Currently we support only system-generated emails and emails
     initiated by moderator actions.
+
+    Args:
+        recipient_id: str. The user ID of the recipient.
+        sender_id: str. The user ID of the sender.
+        intent: str. The intent string for the email, i.e. the purpose/type.
+        email_subject: str. The subject of the email.
+        email_html_body: str. The body (message) of the email.
+        sender_email: str. The sender's email address.
+        bcc_admin: bool. Whether to send a copy of the email to the admin's
+            email address.
+        sender_name: str or None. The name to be shown in the "sender" field of
+            the email.
     """
+
     if sender_name is None:
         sender_name = EMAIL_SENDER_NAME.value
 
@@ -227,7 +256,19 @@ def _send_email_in_transaction():
 def _send_bulk_mail(
         recipient_ids, sender_id, intent, email_subject, email_html_body,
         sender_email, sender_name, instance_id=None):
-    """Sends an email to all given recipients."""
+    """Sends an email to all given recipients.
+
+    Args:
+        recipient_ids: list(str). The user IDs of the email recipients.
+        sender_id: str. The ID of the user sending the email.
+        intent: str. The intent string, i.e. the purpose of the email.
+        email_subject: str. The subject of the email.
+        email_html_body: str. The body (message) of the email.
+        sender_email: str. The sender's email address.
+        sender_name: str. The name to be shown in the "sender" field of the
+            email.
+        instance_id: str or None. The ID of the BulkEmailModel entity instance.
+    """
     _require_sender_id_is_valid(intent, sender_id)
 
     recipients_settings = user_services.get_users_settings(recipient_ids)
@@ -263,7 +304,15 @@ def _send_bulk_mail_in_transaction(instance_id=None):
 
 
 def send_mail_to_admin(email_subject, email_body):
-    """Sends email to admin."""
+    """Send an email to the admin email address.
+
+    The email is sent to the ADMIN_EMAIL_ADDRESS set in feconf.py.
+
+    Args:
+        email_subject: str. Subject of the email.
+        email_body: str. Body (message) of the email.
+    """
+
     app_id = app_identity_services.get_application_id()
     body = '(Sent from %s)\n\n%s' % (app_id, email_body)
 
@@ -275,9 +324,13 @@ def send_mail_to_admin(email_subject, email_body):
 def send_post_signup_email(user_id):
     """Sends a post-signup email to the given user.
 
-    The caller is responsible for ensuring that emails are allowed to be sent
-    to users (i.e. feconf.CAN_SEND_EMAILS is True).
+    Raises an exception if emails are not allowed to be sent to users (i.e.
+    feconf.CAN_SEND_EMAILS is False).
+
+    Args:
+        user_id: str. User ID of the user that signed up.
     """
+
     for key, content in SIGNUP_EMAIL_CONTENT.value.iteritems():
         if content == SIGNUP_EMAIL_CONTENT.default_value[key]:
             log_new_error(
@@ -299,11 +352,30 @@ def send_post_signup_email(user_id):
 
 
 def require_valid_intent(intent):
+    """Checks if the given intent is valid, and raises an exception if it is
+    not.
+
+    Raises:
+        Exception: The given intent did not match an entry in
+            feconf.VALID_MODERATOR_ACTIONS.
+    """
+
     if intent not in feconf.VALID_MODERATOR_ACTIONS:
         raise Exception('Unrecognized email intent: %s' % intent)
 
 
 def _get_email_config(intent):
+    """Return the default body for the email type matching the given moderator
+    action intent.
+
+    Args:
+        intent: str. The intent string (cause/purpose) of the email.
+
+    Returns:
+        str. The default body for the email type matching the given moderator
+            action intent.
+    """
+
     require_valid_intent(intent)
     return config_domain.Registry.get_config_property(
         feconf.VALID_MODERATOR_ACTIONS[intent]['email_config'])
@@ -313,7 +385,15 @@ def get_draft_moderator_action_email(intent):
     """Returns a draft of the text of the body for an email sent immediately
     following a moderator action. An empty body is a signal to the frontend
     that no email will be sent.
+
+    Args:
+        intent: str. The intent string (cause/purpose) of the email.
+
+    Returns:
+        str. Draft of the email body for an email sent after a moderator action,
+            or an empty string if no email should be sent.
     """
+
     try:
         require_moderator_email_prereqs_are_satisfied()
         return _get_email_config(intent).value
@@ -323,7 +403,12 @@ def get_draft_moderator_action_email(intent):
 
 def require_moderator_email_prereqs_are_satisfied():
     """Raises an exception if, for any reason, moderator emails cannot be sent.
+
+    Raises:
+        Exception: feconf.REQUIRE_EMAIL_ON_MODERATOR_ACTION is False.
+        Exception: feconf.CAN_SEND_EMAILS is False.
     """
+
     if not feconf.REQUIRE_EMAIL_ON_MODERATOR_ACTION:
         raise Exception(
             'For moderator emails to be sent, please ensure that '
@@ -339,9 +424,18 @@ def send_moderator_action_email(
     """Sends a email immediately following a moderator action (publicize,
     unpublish, delete) to the given user.
 
-    The caller is responsible for ensuring that emails are allowed to be sent
-    to users (i.e. feconf.CAN_SEND_EMAILS is True).
+    Raises an exception if emails are not allowed to be sent to users (i.e.
+    feconf.CAN_SEND_EMAILS is False).
+
+    Args:
+        sender_id: str. User ID of the sender.
+        recipient_id: str. User ID of the recipient.
+        intent: str. The intent string (cause/purpose) of the email.
+        exploration_title: str. The title of the exploration on which the
+            moderator action was taken.
+        email_body: str. The email content/message.
     """
+
     require_moderator_email_prereqs_are_satisfied()
     email_config = feconf.VALID_MODERATOR_ACTIONS[intent]
 
@@ -371,7 +465,23 @@ def send_role_notification_email(
 
     Email will only be sent if recipient wants to receive these emails (i.e.
     'can_receive_editor_role_email' is set True in recipent's preferences).
+
+    Args:
+        inviter_id: str. ID of the user who invited the recipient to the new
+            role.
+        recipient_id: str. User ID of the recipient.
+        recipient_role: str. Role given to the recipient. Must be defined in
+            EDITOR_ROLE_EMAIL_HTML_ROLES.
+        exploration_id: str. ID of the exploration for which the recipient has
+            been given the new role.
+        exploration_title: str. Title of the exploration for which the recipient
+            has been given the new role.
+
+    Raises:
+        Exception: The role is invalid (i.e. not defined in
+            EDITOR_ROLE_EMAIL_HTML_ROLES).
     """
+
     # Editor role email body and email subject templates.
     email_subject_template = (
         '%s - invitation to collaborate')
@@ -435,8 +545,15 @@ def send_feedback_message_email(recipient_id, feedback_messages):
     """Sends an email when creator receives feedback message to an exploration.
 
     Args:
-    - recipient_id: id of recipient user.
-    - feedback_messages: dictionary containing feedback messages.
+        recipient_id: str. User ID of recipient.
+        feedback_messages: dict. Contains feedback messages. Example:
+
+            {
+                'exploration_id': {
+                    'title': 'Exploration 1234',
+                    'messages': ['Feedback message 1', 'Feedback message 2']
+                }
+            }
     """
 
     email_subject = (
@@ -489,6 +606,19 @@ def send_feedback_message_email(recipient_id, feedback_messages):
 
 def send_suggestion_email(
         exploration_title, exploration_id, author_id, recipient_list):
+    """Send emails to notify the given recipients about new suggestion.
+
+    Each recipient will only be emailed if their email preferences allow for
+    incoming feedback message emails.
+
+    Args:
+        exploration_title: str. Title of the exploration with the new
+            suggestion.
+        exploration_id: str. The ID of the exploration with the new suggestion.
+        author_id: str. The user ID of the author of the suggestion.
+        recipient_list: list(str). The user IDs of the email recipients.
+    """
+
     email_subject = 'New suggestion for "%s"' % exploration_title
 
     email_body_template = (
@@ -532,6 +662,18 @@ def send_suggestion_email(
 def send_instant_feedback_message_email(
         recipient_id, sender_id, message, email_subject, exploration_title,
         exploration_id, thread_title):
+    """Send an email when a new message is posted to a feedback thread, or when
+    the thread's status is changed.
+
+    Args:
+        recipient_id: str. The user ID of the recipient.
+        sender_id: str. The user ID of the sender.
+        message: str. The message text or status change text from the sender.
+        email_subject: str. The subject line to be sent in the email.
+        exploration_title: str. The title of the exploration.
+        exploration_id: str. ID of the exploration the feedback thread is about.
+        thread_title: str. The title of the feedback thread.
+    """
 
     email_body_template = (
         'Hi %s,<br><br>'
@@ -569,6 +711,14 @@ def send_instant_feedback_message_email(
 
 def send_flag_exploration_email(
         exploration_title, exploration_id, reporter_id, report_text):
+    """Send an email to all moderators when an exploration is flagged.
+
+    Args:
+        exploration_title: str. The title of the flagged exporation.
+        exploration_id: str. The ID of the flagged exploration.
+        reporter_id: str. The user ID of the reporter.
+        report_text: str. The message entered by the reporter.
+    """
     email_subject = 'Exploration flagged by user: "%s"' % exploration_title
 
     email_body_template = (
@@ -601,6 +751,13 @@ def send_flag_exploration_email(
 
 
 def send_query_completion_email(recipient_id, query_id):
+    """Send an email to the initiator of a bulk email query with a link to view
+    the query results.
+
+    Args:
+        recipient_id: str. The recipient ID.
+        query_id: str. The query ID.
+    """
     email_subject = 'Query %s has successfully completed' % query_id
 
     email_body_template = (
@@ -626,6 +783,13 @@ def send_query_completion_email(recipient_id, query_id):
 
 
 def send_query_failure_email(recipient_id, query_id, query_params):
+    """Send an email to the initiator of a failed bulk email query.
+
+    Args:
+        recipient_id: str. The recipient ID.
+        query_id: str. The query ID.
+        query_params: dict. The parameters of the query, as key:value.
+    """
     email_subject = 'Query %s has failed' % query_id
 
     email_body_template = (


Test patch: