From 599d6637792b4313b05d07b7d94a607a1d8178de Mon Sep 17 00:00:00 2001
From: Kyle McCormick <kmccormick@edx.org>
Date: Mon, 4 Jan 2021 17:28:22 -0500
Subject: [PATCH] Add option to backfill org data as inactive

Add an `--inactive` option to the
`bulk_add_orgs_and_org_courses` management
command, which causes the backfill to set
`active=False` on all rows created in the
Organization and OrganizationCourse tables.

This will lower the potential data
integrity risk to production systems
such as courses.edx.org.

Upgrade edx-organizations to 6.6.0

TNL-7774
---
 .../commands/backfill_orgs_and_org_courses.py | 28 +++++++++---
 .../test_backfill_orgs_and_org_courses.py     | 45 ++++++++++++++-----
 requirements/edx/base.txt                     |  2 +-
 requirements/edx/development.txt              |  2 +-
 requirements/edx/testing.txt                  |  2 +-
 5 files changed, 61 insertions(+), 18 deletions(-)

diff --git a/cms/djangoapps/contentstore/management/commands/backfill_orgs_and_org_courses.py b/cms/djangoapps/contentstore/management/commands/backfill_orgs_and_org_courses.py
index 2bfd535279a..d0177f76969 100644
--- a/cms/djangoapps/contentstore/management/commands/backfill_orgs_and_org_courses.py
+++ b/cms/djangoapps/contentstore/management/commands/backfill_orgs_and_org_courses.py
@@ -107,6 +107,11 @@ class Command(BaseCommand):
             action='store_true',
             help="Show backfill, but do not apply changes to database."
         )
+        parser.add_argument(
+            '--inactive',
+            action='store_true',
+            help="Backfill data as inactive and do not re-activate any existing data."
+        )
 
     def handle(self, *args, **options):
         """
@@ -135,7 +140,12 @@ class Command(BaseCommand):
         if not confirm_changes(options, orgs, org_courseid_pairs):
             print("No changes applied.")
             return
-        bulk_add_data(orgs, org_courseid_pairs, dry_run=False)
+        bulk_add_data(
+            orgs,
+            org_courseid_pairs,
+            dry_run=False,
+            activate=(not options.get('inactive')),
+        )
 
 
 def confirm_changes(options, orgs, org_courseid_pairs):
@@ -158,7 +168,12 @@ def confirm_changes(options, orgs, org_courseid_pairs):
         raise CommandError("Only one of 'apply' and 'dry' may be specified")
     if options.get('apply'):
         return True
-    bulk_add_data(orgs, org_courseid_pairs, dry_run=True)
+    bulk_add_data(
+        orgs,
+        org_courseid_pairs,
+        dry_run=True,
+        activate=(not options.get('inactive')),
+    )
     if options.get('dry'):
         return False
     answer = ""
@@ -167,7 +182,7 @@ def confirm_changes(options, orgs, org_courseid_pairs):
     return answer.lower().startswith('y')
 
 
-def bulk_add_data(orgs, org_courseid_pairs, dry_run):
+def bulk_add_data(orgs, org_courseid_pairs, dry_run, activate):
     """
     Bulk-add the organizations and organization-course linkages.
 
@@ -182,6 +197,9 @@ def bulk_add_data(orgs, org_courseid_pairs, dry_run):
         org_courseid_pairs (list[tuple[dict, str]]):
             list of (org data dictionary, course key string) links to bulk-add.
         dry_run: Whether or not this run should be "dry" (ie, don't apply changes).
+        activate: Whether newly-added organizations and organization-course linkages
+            should be activated, and whether existing-but-inactive
+            organizations/linkages should be reactivated.
     """
     adding_phrase = "Dry-run of bulk-adding" if dry_run else "Bulk-adding"
     created_phrase = "Will create" if dry_run else "Created"
@@ -190,7 +208,7 @@ def bulk_add_data(orgs, org_courseid_pairs, dry_run):
     print("------------------------------------------------------")
     print(f"{adding_phrase} organizations...")
     orgs_created, orgs_reactivated = organizations_api.bulk_add_organizations(
-        orgs, dry_run=dry_run
+        orgs, dry_run=dry_run, activate=activate
     )
     print(f"{created_phrase} {len(orgs_created)} organizations:")
     for org_short_name in sorted(orgs_created):
@@ -202,7 +220,7 @@ def bulk_add_data(orgs, org_courseid_pairs, dry_run):
     print("------------------------------------------------------")
     print(f"{adding_phrase} organization-course linkages...")
     linkages_created, linkages_reactivated = organizations_api.bulk_add_organization_courses(
-        org_courseid_pairs, dry_run=dry_run
+        org_courseid_pairs, dry_run=dry_run, activate=activate
     )
     print(f"{created_phrase} {len(linkages_created)} organization-course linkages:")
     for org_short_name, course_id in sorted(linkages_created):
diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_backfill_orgs_and_org_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_backfill_orgs_and_org_courses.py
index 976e0dbbfea..56bc5fbe57b 100644
--- a/cms/djangoapps/contentstore/management/commands/tests/test_backfill_orgs_and_org_courses.py
+++ b/cms/djangoapps/contentstore/management/commands/tests/test_backfill_orgs_and_org_courses.py
@@ -117,26 +117,43 @@ class BackfillOrgsAndOrgCoursesTest(SharedModuleStoreTestCase):
             "command_line_args": [],
             "user_inputs": ["n"],
             "should_apply_changes": False,
+            "should_data_be_activated": True,
         },
         {
             "command_line_args": [],
             "user_inputs": ["x", "N"],
             "should_apply_changes": False,
+            "should_data_be_activated": True,
         },
         {
             "command_line_args": [],
             "user_inputs": ["", "", "YeS"],
             "should_apply_changes": True,
+            "should_data_be_activated": True,
+        },
+        {
+            "command_line_args": ["--inactive"],
+            "user_inputs": ["y"],
+            "should_apply_changes": True,
+            "should_data_be_activated": False,
         },
         {
             "command_line_args": ["--dry"],
             "user_inputs": [],
             "should_apply_changes": False,
+            "should_data_be_activated": True,
+        },
+        {
+            "command_line_args": ["--dry", "--inactive"],
+            "user_inputs": [],
+            "should_apply_changes": False,
+            "should_data_be_activated": False,
         },
         {
             "command_line_args": ["--apply"],
             "user_inputs": [],
             "should_apply_changes": True,
+            "should_data_be_activated": True,
         },
     )
     @ddt.unpack
@@ -161,6 +178,7 @@ class BackfillOrgsAndOrgCoursesTest(SharedModuleStoreTestCase):
             command_line_args,
             user_inputs,
             should_apply_changes,
+            should_data_be_activated,
     ):
         """
         Test that the command-line arguments and user input processing works as
@@ -184,31 +202,38 @@ class BackfillOrgsAndOrgCoursesTest(SharedModuleStoreTestCase):
             # then we expect one DRY bulk-add run *and* one REAL bulk-add run.
             assert mock_add_orgs.call_count == 2
             assert mock_add_org_courses.call_count == 2
-            assert mock_add_orgs.call_args_list[0].kwargs == {"dry_run": True}
-            assert mock_add_org_courses.call_args_list[0].kwargs == {"dry_run": True}
-            assert mock_add_orgs.call_args_list[1].kwargs == {"dry_run": False}
-            assert mock_add_org_courses.call_args_list[1].kwargs == {"dry_run": False}
+            assert mock_add_orgs.call_args_list[0].kwargs["dry_run"] is True
+            assert mock_add_org_courses.call_args_list[0].kwargs["dry_run"] is True
+            assert mock_add_orgs.call_args_list[1].kwargs["dry_run"] is False
+            assert mock_add_org_courses.call_args_list[1].kwargs["dry_run"] is False
         elif should_apply_changes:
             # If DID apply changes but the user WASN'T prompted,
             # then we expect just one REAL bulk-add run.
             assert mock_add_orgs.call_count == 1
             assert mock_add_org_courses.call_count == 1
-            assert mock_add_orgs.call_args.kwargs == {"dry_run": False}
-            assert mock_add_org_courses.call_args.kwargs == {"dry_run": False}
+            assert mock_add_orgs.call_args.kwargs["dry_run"] is False
+            assert mock_add_org_courses.call_args.kwargs["dry_run"] is False
         elif user_inputs:
             # If we DIDN'T apply changes but the user WAS prompted
             # then we expect just one DRY bulk-add run.
             assert mock_add_orgs.call_count == 1
             assert mock_add_org_courses.call_count == 1
-            assert mock_add_orgs.call_args.kwargs == {"dry_run": True}
-            assert mock_add_org_courses.call_args.kwargs == {"dry_run": True}
+            assert mock_add_orgs.call_args.kwargs["dry_run"] is True
+            assert mock_add_org_courses.call_args.kwargs["dry_run"] is True
         else:
             # Similarly, if we DIDN'T apply changes and the user WASN'T prompted
             # then we expect just one DRY bulk-add run.
             assert mock_add_orgs.call_count == 1
             assert mock_add_org_courses.call_count == 1
-            assert mock_add_orgs.call_args.kwargs == {"dry_run": True}
-            assert mock_add_org_courses.call_args.kwargs == {"dry_run": True}
+            assert mock_add_orgs.call_args.kwargs["dry_run"] is True
+            assert mock_add_org_courses.call_args.kwargs["dry_run"] is True
+
+        # Assert that the value of of the "active" kwarg is correct for all
+        # calls both bulk-add functions, whether or not they were dry runs.
+        for call in mock_add_orgs:
+            assert call.kwargs["activate"] == should_data_be_activated
+        for call in mock_add_org_courses:
+            assert call.kwargs["activate"] == should_data_be_activated
 
     def test_conflicting_arguments(self):
         """
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 37816091cbf..7d7760a5932 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -103,7 +103,7 @@ edx-event-routing-backends==2.0.0  # via -r requirements/edx/base.in
 edx-i18n-tools==0.5.3     # via ora2
 edx-milestones==0.3.0     # via -r requirements/edx/base.in
 edx-opaque-keys[django]==2.1.1  # via -r requirements/edx/paver.txt, edx-bulk-grades, edx-ccx-keys, edx-completion, edx-drf-extensions, edx-enterprise, edx-milestones, edx-organizations, edx-proctoring, edx-user-state-client, edx-when, lti-consumer-xblock, xmodule
-edx-organizations==6.5.0  # via -r requirements/edx/base.in
+edx-organizations==6.6.0  # via -r requirements/edx/base.in
 edx-proctoring-proctortrack==1.0.5  # via -r requirements/edx/base.in
 edx-proctoring==2.5.5     # via -r requirements/edx/base.in, edx-proctoring-proctortrack
 edx-rbac==1.3.3           # via edx-enterprise
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index abe391ca0d0..9915a1ca396 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -115,7 +115,7 @@ edx-i18n-tools==0.5.3     # via -r requirements/edx/testing.txt, ora2
 edx-lint==1.6             # via -r requirements/edx/testing.txt
 edx-milestones==0.3.0     # via -r requirements/edx/testing.txt
 edx-opaque-keys[django]==2.1.1  # via -r requirements/edx/testing.txt, edx-bulk-grades, edx-ccx-keys, edx-completion, edx-drf-extensions, edx-enterprise, edx-milestones, edx-organizations, edx-proctoring, edx-user-state-client, edx-when, lti-consumer-xblock, xmodule
-edx-organizations==6.5.0  # via -r requirements/edx/testing.txt
+edx-organizations==6.6.0  # via -r requirements/edx/testing.txt
 edx-proctoring-proctortrack==1.0.5  # via -r requirements/edx/testing.txt
 edx-proctoring==2.5.5     # via -r requirements/edx/testing.txt, edx-proctoring-proctortrack
 edx-rbac==1.3.3           # via -r requirements/edx/testing.txt, edx-enterprise
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 1668de1dc24..0c107092be4 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -112,7 +112,7 @@ edx-i18n-tools==0.5.3     # via -r requirements/edx/base.txt, -r requirements/ed
 edx-lint==1.6             # via -r requirements/edx/testing.in
 edx-milestones==0.3.0     # via -r requirements/edx/base.txt
 edx-opaque-keys[django]==2.1.1  # via -r requirements/edx/base.txt, edx-bulk-grades, edx-ccx-keys, edx-completion, edx-drf-extensions, edx-enterprise, edx-milestones, edx-organizations, edx-proctoring, edx-user-state-client, edx-when, lti-consumer-xblock, xmodule
-edx-organizations==6.5.0  # via -r requirements/edx/base.txt
+edx-organizations==6.6.0  # via -r requirements/edx/base.txt
 edx-proctoring-proctortrack==1.0.5  # via -r requirements/edx/base.txt
 edx-proctoring==2.5.5     # via -r requirements/edx/base.txt, edx-proctoring-proctortrack
 edx-rbac==1.3.3           # via -r requirements/edx/base.txt, edx-enterprise
-- 
GitLab