Repository: oppia/oppia

Issue:
Add lint checks that ensure all `jobs.batch_jobs.*_jobs` modules are imported in `jobs.registry` 
Add lint checks that ensure all `jobs.batch_jobs.*_jobs` modules are imported in `jobs.registry`, this is needed because otherwise the jobs placed in the file won't be available in the release coordinator page. 
Add lint checks that ensure all `jobs.batch_jobs.*_jobs` modules are imported in `jobs.registry` 
Add lint checks that ensure all `jobs.batch_jobs.*_jobs` modules are imported in `jobs.registry`, this is needed because otherwise the jobs placed in the file won't be available in the release coordinator page. 


Discussion:
@SD-13 thanks for taking this up! When do you plan to fix it?
@U8NWXD probably will raise a PR next week
Great! I've updated the target completion date
@vojtechjelinek @U8NWXD , hey I got a question. We should add this lint check in `scripts/linters/python_linter.py` right? just want to confirm. And what do you think about creating a new function in `python_linter.py` to check those imports? Another option could be of using a custom plugin in pylint but honestly, that doesn't look efficient to me for at least this task since we will only check a single file.
I think @U8NWXD might be able to answer this better, I think we switched to pylint extension for almost all checks but you're right that it seems a bit weird to do this just for one file.
Unless there's a nice way to use a pylint extension for just one file, I agree with not using pylint
Okay, but I am still not in conclusion so I will try to implement both to get a better idea to choose. And Sorry @U8NWXD I missed the deadline but I have updated it with the new ones. 
Seems like `blog_post_search_indexing_jobs` is not imported into the [registry](https://github.com/oppia/oppia/blob/develop/core/jobs/registry.py). I will add it to my PR.
@SD-13 thanks for taking this up! When do you plan to fix it?
@U8NWXD probably will raise a PR next week
Great! I've updated the target completion date
@vojtechjelinek @U8NWXD , hey I got a question. We should add this lint check in `scripts/linters/python_linter.py` right? just want to confirm. And what do you think about creating a new function in `python_linter.py` to check those imports? Another option could be of using a custom plugin in pylint but honestly, that doesn't look efficient to me for at least this task since we will only check a single file.
I think @U8NWXD might be able to answer this better, I think we switched to pylint extension for almost all checks but you're right that it seems a bit weird to do this just for one file.
Unless there's a nice way to use a pylint extension for just one file, I agree with not using pylint
Okay, but I am still not in conclusion so I will try to implement both to get a better idea to choose. And Sorry @U8NWXD I missed the deadline but I have updated it with the new ones. 
Seems like `blog_post_search_indexing_jobs` is not imported into the [registry](https://github.com/oppia/oppia/blob/develop/core/jobs/registry.py). I will add it to my PR.

Resolving patch:
diff --git a/core/jobs/registry.py b/core/jobs/registry.py
--- a/core/jobs/registry.py
+++ b/core/jobs/registry.py
@@ -43,6 +43,7 @@
 # We need this to happen for every job in this registry file, because the
 # registry depends on JobMetaclass to handle the responsibility of keeping track
 # of every job.
+from core.jobs.batch_jobs import blog_post_search_indexing_jobs      # pylint: disable=unused-import  # isort: skip
 from core.jobs.batch_jobs import blog_validation_jobs                # pylint: disable=unused-import  # isort: skip
 from core.jobs.batch_jobs import collection_info_jobs                # pylint: disable=unused-import  # isort: skip
 from core.jobs.batch_jobs import email_deletion_jobs                 # pylint: disable=unused-import  # isort: skip
diff --git a/scripts/linters/pre_commit_linter.py b/scripts/linters/pre_commit_linter.py
--- a/scripts/linters/pre_commit_linter.py
+++ b/scripts/linters/pre_commit_linter.py
@@ -86,7 +86,7 @@
 OTHER_SHARD_NAME = 'other'
 
 # Shards are specified by a mapping from shard name to a list of the
-# paths in that shard. For exaple, `'1': ['core/domain/']` will create a
+# paths in that shard. For example, `'1': ['core/domain/']` will create a
 # shard named `'1'` that contains all the files under core/domain/. A
 # shard name matching OTHER_SHARD_NAME includes all files not under
 # another shard.  Currently we are not sharding the lint checks, so the
diff --git a/scripts/linters/python_linter.py b/scripts/linters/python_linter.py
--- a/scripts/linters/python_linter.py
+++ b/scripts/linters/python_linter.py
@@ -18,6 +18,7 @@
 
 from __future__ import annotations
 
+import ast
 import io
 import os
 import re
@@ -182,12 +183,73 @@ def perform_all_lint_checks(
                     'Python lint', False, [],
                     ['There are no Python files to lint.'])]
 
+        batch_jobs_dir: str = (
+            os.path.join(os.getcwd(), 'core', 'jobs', 'batch_jobs')
+        )
+        jobs_registry: str = (
+            os.path.join(os.getcwd(), 'core', 'jobs', 'registry.py')
+        )
+
         linter_stdout.append(self.lint_py_files())
         linter_stdout.append(self.check_import_order())
+        linter_stdout.append(check_jobs_imports(
+            batch_jobs_dir, jobs_registry)
+        )
 
         return linter_stdout
 
 
+def check_jobs_imports(
+    batch_jobs_dir: str, jobs_registry: str
+) -> concurrent_task_utils.TaskResult:
+    """This function is used to check that all `jobs.batch_jobs.*_jobs` are
+    imported in `jobs.registry`.
+
+    Args:
+        batch_jobs_dir: str. The path to the batch_jobs directory.
+        jobs_registry: str. The path to the jobs registry file.
+
+    Returns:
+        TaskResult. A TaskResult object representing the result of the lint
+        check.
+    """
+    jobs_files: List[str] = [
+        filename.split('.')[0] for filename in os.listdir(batch_jobs_dir)
+        if filename.endswith('_jobs.py')
+    ]
+
+    with open(jobs_registry, 'r', encoding='utf-8') as file:
+        contents = file.read()
+    tree = ast.parse(contents)
+
+    imports: List[str] = []
+    for node in ast.walk(tree):
+        if isinstance(node, ast.Import):
+            for n in node.names:
+                imports.append(n.name.split('.')[-1])
+        elif isinstance(node, ast.ImportFrom):
+            for alias in node.names:
+                imports.append(alias.name)
+
+    missing_imports: List[str] = []
+    for job_file in jobs_files:
+        if job_file not in imports:
+            missing_imports.append(job_file)
+
+    error_messages: List[str] = []
+    if missing_imports:
+        error_message = 'Following jobs should be imported in %s:\n%s' % (
+            os.path.relpath(jobs_registry), (', '.join(missing_imports))
+        )
+        error_messages.append(error_message)
+    return concurrent_task_utils.TaskResult(
+        'Check jobs imports in jobs registry',
+        bool(missing_imports),
+        error_messages,
+        error_messages
+    )
+
+
 def get_linters(
     files_to_lint: List[str]
 ) -> Tuple[None, ThirdPartyPythonLintChecksManager]:


Test patch:
diff --git a/scripts/linters/python_linter_test.py b/scripts/linters/python_linter_test.py
--- a/scripts/linters/python_linter_test.py
+++ b/scripts/linters/python_linter_test.py
@@ -64,6 +64,35 @@ def test_sorted_import_order(self) -> None:
         self.assertEqual('Import order', lint_task_report.name)
         self.assertFalse(lint_task_report.failed)
 
+    def test_valid_job_imports(self) -> None:
+        batch_jobs_dir: str = (
+            os.path.join(os.getcwd(), 'core', 'jobs', 'batch_jobs')
+        )
+        lint_task_report = (
+            python_linter.check_jobs_imports(batch_jobs_dir, VALID_PY_FILEPATH)
+        )
+        self.assertEqual(
+            'SUCCESS  Check jobs imports in jobs registry check passed',
+            lint_task_report.get_report()[-1])
+        self.assertEqual(
+            'Check jobs imports in jobs registry', lint_task_report.name)
+        self.assertFalse(lint_task_report.failed)
+
+    def test_invalid_job_imports(self) -> None:
+        batch_jobs_dir: str = (
+            os.path.join(os.getcwd(), 'core', 'jobs', 'batch_jobs')
+        )
+        lint_task_report = (
+            python_linter.check_jobs_imports(
+                batch_jobs_dir, INVALID_IMPORT_FILEPATH)
+        )
+        self.assertEqual(
+            'FAILED  Check jobs imports in jobs registry check failed',
+            lint_task_report.get_report()[-1])
+        self.assertEqual(
+            'Check jobs imports in jobs registry', lint_task_report.name)
+        self.assertTrue(lint_task_report.failed)
+
     def test_valid_file_with_pylint(self) -> None:
         lint_task_report = python_linter.ThirdPartyPythonLintChecksManager(
             [VALID_PY_FILEPATH]).lint_py_files()
diff --git a/scripts/linters/test_files/valid.py b/scripts/linters/test_files/valid.py
--- a/scripts/linters/test_files/valid.py
+++ b/scripts/linters/test_files/valid.py
@@ -19,6 +19,31 @@
 """
 
 from __future__ import annotations
+from core.jobs.batch_jobs import blog_post_search_indexing_jobs      # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import blog_validation_jobs                # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import collection_info_jobs                # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import email_deletion_jobs                 # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import exp_migration_jobs                  # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import (                                   # pylint: disable=unused-import  # isort: skip
+    exp_recommendation_computation_jobs)
+from core.jobs.batch_jobs import exp_search_indexing_jobs            # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import model_validation_jobs               # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import opportunity_management_jobs         # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import skill_migration_jobs                # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import story_migration_jobs                # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import topic_migration_jobs                # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import suggestion_stats_computation_jobs   # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import suggestion_migration_jobs   # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import translation_migration_jobs         # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import user_stats_computation_jobs         # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import math_interactions_audit_jobs        # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import mailchimp_population_jobs           # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import (                                   # pylint: disable=unused-import  # isort: skip
+      exp_version_history_computation_job)
+from core.jobs.batch_jobs import (                                   # pylint: disable=unused-import  # isort: skip
+    rejecting_suggestion_for_invalid_content_ids_jobs)
+from core.jobs.batch_jobs import user_settings_profile_picture_jobs  # pylint: disable=unused-import  # isort: skip
+from core.jobs.batch_jobs import store_profile_images_to_gcs_jobs    # pylint: disable=unused-import  # isort: skip
 
 
 class FakeClass: