|
7 | 7 | import logging |
8 | 8 |
|
9 | 9 | from casbin_adapter.models import CasbinRule |
10 | | -from django.db.models.signals import post_delete |
| 10 | +from django.conf import settings |
| 11 | +from django.db.models.signals import post_delete, pre_save |
11 | 12 | from django.dispatch import receiver |
12 | 13 |
|
13 | 14 | from openedx_authz.api.users import unassign_all_roles_from_user |
14 | 15 | from openedx_authz.models.core import ExtendedCasbinRule |
| 16 | +from openedx_authz.models.migrations import MigrationType, ScopeType |
| 17 | +from openedx_authz.tasks import migrate_course_authoring_async |
15 | 18 |
|
16 | 19 | try: |
17 | 20 | from openedx.core.djangoapps.user_api.accounts.signals import USER_RETIRE_LMS_CRITICAL |
18 | 21 | except ImportError: |
19 | 22 | USER_RETIRE_LMS_CRITICAL = None |
20 | 23 |
|
| 24 | +try: |
| 25 | + from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel, WaffleFlagOrgOverrideModel |
| 26 | +except ImportError: |
| 27 | + WaffleFlagCourseOverrideModel = None |
| 28 | + WaffleFlagOrgOverrideModel = None |
| 29 | + |
| 30 | + |
21 | 31 | logger = logging.getLogger(__name__) |
22 | 32 |
|
| 33 | +# Flag name to monitor for automatic migration |
| 34 | +AUTHZ_COURSE_AUTHORING_FLAG = "authz.enable_course_authoring" |
| 35 | + |
23 | 36 |
|
24 | 37 | @receiver(post_delete, sender=ExtendedCasbinRule) |
25 | 38 | def delete_casbin_rule_on_extended_rule_deletion(sender, instance, **kwargs): # pylint: disable=unused-argument |
@@ -82,3 +95,103 @@ def unassign_roles_on_user_retirement(sender, user, **kwargs): # pylint: disabl |
82 | 95 | # Only register the handler if the signal is available (i.e., running in Open edX) |
83 | 96 | if USER_RETIRE_LMS_CRITICAL is not None: |
84 | 97 | USER_RETIRE_LMS_CRITICAL.connect(unassign_roles_on_user_retirement) |
| 98 | + |
| 99 | + |
| 100 | +def trigger_course_authoring_migration( |
| 101 | + instance: WaffleFlagCourseOverrideModel | WaffleFlagOrgOverrideModel, |
| 102 | + scope_type: ScopeType, |
| 103 | + scope_key: str, |
| 104 | +) -> None: |
| 105 | + """Trigger an asynchronous migration run. |
| 106 | +
|
| 107 | + Args: |
| 108 | + instance: The waffle flag instance that triggered the migration |
| 109 | + scope_type (ScopeType): Type of scope being migrated: course or organization |
| 110 | + scope_key (str): Course ID or organization name |
| 111 | + """ |
| 112 | + if instance.waffle_flag != AUTHZ_COURSE_AUTHORING_FLAG: |
| 113 | + return |
| 114 | + |
| 115 | + last_flag_obj = None |
| 116 | + if isinstance(instance, WaffleFlagCourseOverrideModel): |
| 117 | + last_flag_obj = ( |
| 118 | + WaffleFlagCourseOverrideModel.objects.filter(course_id=instance.course_id).order_by("-id").first() |
| 119 | + ) |
| 120 | + elif isinstance(instance, WaffleFlagOrgOverrideModel): |
| 121 | + last_flag_obj = WaffleFlagOrgOverrideModel.objects.filter(org=instance.org).order_by("-id").first() |
| 122 | + |
| 123 | + if last_flag_obj and last_flag_obj.enabled == instance.enabled: |
| 124 | + logger.info("No change in waffle flag, skipping course migration") |
| 125 | + return |
| 126 | + |
| 127 | + if not instance.enabled: |
| 128 | + migration_type = MigrationType.ROLLBACK |
| 129 | + else: |
| 130 | + migration_type = MigrationType.FORWARD |
| 131 | + |
| 132 | + course_id_list = None |
| 133 | + org_id = None |
| 134 | + |
| 135 | + if scope_type == ScopeType.COURSE: |
| 136 | + course_id_list = [scope_key] |
| 137 | + elif scope_type == ScopeType.ORG: |
| 138 | + org_id = scope_key |
| 139 | + |
| 140 | + logger.info(f"Triggering {migration_type} migration for {scope_type}:{scope_key} due to waffle flag change") |
| 141 | + |
| 142 | + migrate_course_authoring_async( |
| 143 | + migration_type=migration_type, |
| 144 | + scope_type=scope_type, |
| 145 | + scope_key=scope_key, |
| 146 | + course_id_list=course_id_list, |
| 147 | + org_id=org_id, |
| 148 | + delete_after=True, |
| 149 | + ) |
| 150 | + |
| 151 | + |
| 152 | +@receiver(pre_save, sender=WaffleFlagCourseOverrideModel) |
| 153 | +def handle_course_waffle_flag_change(sender, instance, **kwargs) -> None: # pylint: disable=unused-argument |
| 154 | + """Handle changes to course-level waffle flags. |
| 155 | +
|
| 156 | + When the authz.enable_course_authoring flag is changed for a course, |
| 157 | + trigger the appropriate migration run. Only trigger if automatic migration |
| 158 | + is enabled in the settings. |
| 159 | +
|
| 160 | + Args: |
| 161 | + sender: The model class (WaffleFlagCourseOverrideModel) |
| 162 | + instance: The flag override instance being saved |
| 163 | + **kwargs: Additional keyword arguments from the signal |
| 164 | + """ |
| 165 | + if not settings.ENABLE_AUTOMATIC_AUTHZ_COURSE_AUTHORING_MIGRATION: |
| 166 | + logger.info("Automatic migration is disabled, skipping course migration") |
| 167 | + return |
| 168 | + |
| 169 | + trigger_course_authoring_migration( |
| 170 | + instance=instance, |
| 171 | + scope_type=ScopeType.COURSE, |
| 172 | + scope_key=str(instance.course_id), |
| 173 | + ) |
| 174 | + |
| 175 | + |
| 176 | +@receiver(pre_save, sender=WaffleFlagOrgOverrideModel) |
| 177 | +def handle_org_waffle_flag_change(sender, instance, **kwargs) -> None: # pylint: disable=unused-argument |
| 178 | + """Handle changes to organization-level waffle flags. |
| 179 | +
|
| 180 | + When the authz.enable_course_authoring flag is changed for an organization, |
| 181 | + trigger the appropriate migration run. Only trigger if automatic migration |
| 182 | + is enabled in the settings. |
| 183 | +
|
| 184 | + Args: |
| 185 | + sender: The model class (WaffleFlagOrgOverrideModel) |
| 186 | + instance: The flag override instance being saved |
| 187 | + **kwargs: Additional keyword arguments from the signal |
| 188 | + """ |
| 189 | + if not settings.ENABLE_AUTOMATIC_AUTHZ_COURSE_AUTHORING_MIGRATION: |
| 190 | + logger.info("Automatic migration is disabled, skipping organization migration") |
| 191 | + return |
| 192 | + |
| 193 | + trigger_course_authoring_migration( |
| 194 | + instance=instance, |
| 195 | + scope_type=ScopeType.ORG, |
| 196 | + scope_key=str(instance.org), |
| 197 | + ) |
0 commit comments