feat: updated management command to use default preferences (#36997)

This commit is contained in:
Ahtisham Shahid
2025-07-09 12:33:02 +05:00
committed by GitHub
parent 7899ba9074
commit 652fbfcb88
4 changed files with 121 additions and 14 deletions

View File

@@ -130,7 +130,7 @@ COURSE_NOTIFICATION_TYPES = {
'web': True,
'email': True,
'email_cadence': EmailCadence.DAILY,
'push': True,
'push': False,
'non_editable': [],
'content_template': _('<p><strong>{username}s </strong> {content_type} has been reported <strong> {'
'content}</strong></p>'),
@@ -179,7 +179,7 @@ COURSE_NOTIFICATION_TYPES = {
'info': '',
'web': True,
'email': False,
'push': True,
'push': False,
'email_cadence': EmailCadence.DAILY,
'non_editable': [],
'content_template': _('<{p}><{strong}>{course_update_content}</{strong}></{p}>'),

View File

@@ -11,6 +11,7 @@ from django.db import transaction
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, NotificationPreference
from openedx.core.djangoapps.notifications.utils import aggregate_notification_configs
from openedx.core.djangoapps.notifications.base_notification import NotificationTypeManager, COURSE_NOTIFICATION_APPS
logger = logging.getLogger(__name__)
@@ -40,6 +41,13 @@ class Command(BaseCommand):
action='store_true',
help="Simulate the migration without making any database changes."
)
parser.add_argument(
'--use-default',
nargs='+',
choices=['web', 'push', 'email', 'email_cadence'],
help="Specify which notification channels should use default values. Can accept multiple values"
" (e.g., --use-default web push email)."
)
@staticmethod
def _get_user_ids_to_process() -> Iterator[int]:
@@ -59,11 +67,28 @@ class Command(BaseCommand):
user_id: int,
app_name: str,
notification_type: str,
values: Dict[str, Any]
values: Dict[str, Any],
use_default: List[str] = None
) -> NotificationPreference:
"""
Helper function to create a NotificationPreference instance.
Args:
user_id: The user ID for whom the preference is being created
app_name: The name of the notification app
notification_type: The type of notification (e.g., 'assignment', 'discussion')
values: A dictionary containing the preference values for web, email, push, etc.
use_default: List of channels that should use default values
"""
if use_default:
non_core_defaults, core_defaults = NotificationTypeManager().get_notification_app_preference(app_name)
if non_core_defaults and notification_type in non_core_defaults:
for default in use_default:
values[default] = non_core_defaults[notification_type][default]
elif core_defaults and notification_type in core_defaults:
for default in use_default:
values[default] = COURSE_NOTIFICATION_APPS[app_name][f'core_{default}']
return NotificationPreference(
user_id=user_id,
app=app_name,
@@ -77,13 +102,20 @@ class Command(BaseCommand):
def _create_preferences_from_configs(
self,
user_id: int,
course_preferences_configs: List[Dict]
course_preferences_configs: List[Dict],
use_default: List[str] = None
) -> List[NotificationPreference]:
"""
Processes a list of preference configs for a single user.
Returns a list of NotificationPreference objects to be created.
Args:
user_id: The user ID to process preferences for
course_preferences_configs: List of preference configuration dictionaries
use_default: List of channels ('web', 'push', 'email') that should use default values
"""
new_account_preferences: List[NotificationPreference] = []
use_default = use_default or []
if not course_preferences_configs:
logger.debug(f"No course preferences found for user {user_id}. Skipping.")
@@ -118,7 +150,7 @@ class Command(BaseCommand):
)
continue
new_account_preferences.append(
self._create_preference_object(user_id, app_name, notification_type, values)
self._create_preference_object(user_id, app_name, notification_type, values, use_default)
)
# Handle core notification types
@@ -145,13 +177,17 @@ class Command(BaseCommand):
)
continue
new_account_preferences.append(
self._create_preference_object(user_id, app_name, core_type_name, core_values)
self._create_preference_object(user_id, app_name, core_type_name, core_values, use_default)
)
return new_account_preferences
def _process_batch(self, user_ids: List[int]) -> List[NotificationPreference]:
def _process_batch(self, user_ids: List[int], use_default: List[str] = None) -> List[NotificationPreference]:
"""
Fetches all preferences for a batch of users and processes them.
Args:
user_ids: List of user IDs to process
use_default: List of channels that should use default values
"""
all_new_preferences: List[NotificationPreference] = []
@@ -167,7 +203,7 @@ class Command(BaseCommand):
# 3. Process each user's grouped data.
for user_id, configs in prefs_by_user.items():
user_new_preferences = self._create_preferences_from_configs(user_id, configs)
user_new_preferences = self._create_preferences_from_configs(user_id, configs, use_default)
if user_new_preferences:
all_new_preferences.extend(user_new_preferences)
logger.debug(f"User {user_id}: Aggregated {len(configs)} course preferences "
@@ -181,6 +217,7 @@ class Command(BaseCommand):
def handle(self, *args: Any, **options: Any):
dry_run = options['dry_run']
batch_size = options['batch_size']
use_default = options.get('use_default', [])
if dry_run:
logger.info(self.style.WARNING("Performing a DRY RUN. No changes will be made to the database."))
@@ -190,6 +227,9 @@ class Command(BaseCommand):
NotificationPreference.objects.all().delete()
logger.info('Cleared all existing account-level notification preferences.')
if use_default:
logger.info(f"Using default values for channels: {', '.join(use_default)}")
user_id_iterator = self._get_user_ids_to_process()
user_id_batch: List[int] = []
@@ -203,7 +243,7 @@ class Command(BaseCommand):
try:
with transaction.atomic():
# Process the entire batch of users
preferences_to_create = self._process_batch(user_id_batch)
preferences_to_create = self._process_batch(user_id_batch, use_default)
if preferences_to_create:
if not dry_run:
@@ -237,7 +277,7 @@ class Command(BaseCommand):
if user_id_batch:
try:
with transaction.atomic():
preferences_to_create = self._process_batch(user_id_batch)
preferences_to_create = self._process_batch(user_id_batch, use_default)
if preferences_to_create:
if not dry_run:
NotificationPreference.objects.bulk_create(preferences_to_create)

View File

@@ -250,6 +250,73 @@ class MigrateNotificationPreferencesTestCase(TestCase):
'Performing a DRY RUN. No changes will be made to the database.'
)
@patch(f'{COMMAND_MODULE}.logger')
def test_handle_use_default_mode(self, mock_logger):
"""Test command execution while using default mode."""
sample_config = {
"grading": {
"enabled": True,
"notification_types": {
"core": {
"web": True,
"push": True,
"email": True,
"email_cadence": "Daily"
},
"ora_grade_assigned": {
"web": True,
"push": True,
"email": True,
"email_cadence": "Daily"
}
},
"core_notification_types": []
},
"discussion": {
"enabled": True,
"notification_types": {
"core": {
"web": False,
"push": False,
"email": False,
"email_cadence": "Weekly"
},
"new_discussion_post": {
"web": True,
"push": True,
"email": True,
"email_cadence": "Immediately"
}
},
"core_notification_types": ["response_on_followed_post"]
}
}
CourseNotificationPreference.objects.create(
user=self.user1,
course_id='course-v1:Test+Course+1',
notification_preference_config=sample_config
)
call_command(
'migrate_preferences_to_account_level_model',
'--use-default',
'push'
)
# Check that no actual database changes were made
self.assertEqual(NotificationPreference.objects.count(), 3)
self.assertEqual(
NotificationPreference.objects.get(type='ora_grade_assigned').push,
False
)
self.assertEqual(
NotificationPreference.objects.get(type='new_discussion_post').push,
False
)
self.assertEqual(
NotificationPreference.objects.get(type='response_on_followed_post').push,
True
)
def test_handle_normal_execution(self):
"""Test normal command execution without dry-run."""
CourseNotificationPreference.objects.create(

View File

@@ -338,7 +338,7 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
'content_reported': {
'web': True,
'email': True,
'push': True,
'push': False,
'info': '',
'email_cadence': 'Daily',
},
@@ -352,7 +352,7 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
'course_updates': {
'web': True,
'email': False,
'push': True,
'push': False,
'email_cadence': 'Daily',
'info': ''
},
@@ -1455,7 +1455,7 @@ class TestNotificationPreferencesView(APITestCase):
"content_reported": {
"web": True,
"email": True,
"push": True,
"push": False,
"email_cadence": "Daily"
},
"new_instructor_all_learners_post": {
@@ -1480,7 +1480,7 @@ class TestNotificationPreferencesView(APITestCase):
"course_updates": {
"web": True,
"email": False,
"push": True,
"push": False,
"email_cadence": "Daily"
},
"core": {