44import datetime
55import ddt
66
7+ from itertools import product
78from pytz import utc
89from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import
910
1011from common .djangoapps .student .tests .factories import UserFactory
12+ from openedx .core .djangoapps .notifications .base_notification import (
13+ COURSE_NOTIFICATION_APPS ,
14+ COURSE_NOTIFICATION_TYPES ,
15+ )
1116from openedx .core .djangoapps .notifications .config .waffle import ENABLE_EMAIL_NOTIFICATIONS
12- from openedx .core .djangoapps .notifications .models import Notification
17+ from openedx .core .djangoapps .notifications .email_notifications import EmailCadence
18+ from openedx .core .djangoapps .notifications .models import CourseNotificationPreference , Notification
1319from openedx .core .djangoapps .notifications .email .utils import (
1420 add_additional_attributes_to_notifications ,
1521 create_app_notifications_dict ,
1622 create_datetime_string ,
1723 create_email_digest_context ,
1824 create_email_template_context ,
25+ decrypt_object ,
26+ decrypt_string ,
27+ encrypt_object ,
28+ encrypt_string ,
1929 get_course_info ,
2030 get_time_ago ,
2131 is_email_notification_flag_enabled ,
32+ update_user_preferences_from_patch ,
2233)
2334from xmodule .modulestore .tests .django_utils import ModuleStoreTestCase
2435from xmodule .modulestore .tests .factories import CourseFactory
@@ -102,8 +113,9 @@ def test_email_template_context(self):
102113 """
103114 Tests common header and footer context
104115 """
105- context = create_email_template_context ()
106- keys = ['platform_name' , 'mailing_address' , 'logo_url' , 'social_media' , 'notification_settings_url' ]
116+ context = create_email_template_context (self .user .username )
117+ keys = ['platform_name' , 'mailing_address' , 'logo_url' , 'social_media' ,
118+ 'notification_settings_url' , 'unsubscribe_url' ]
107119 for key in keys :
108120 assert key in context
109121
@@ -121,6 +133,7 @@ def test_email_digest_context(self, digest_frequency):
121133 end_date = datetime .datetime (2024 , 3 , 24 , 12 , 0 )
122134 params = {
123135 "app_notifications_dict" : app_dict ,
136+ "username" : self .user .username ,
124137 "start_date" : end_date - datetime .timedelta (days = 0 if digest_frequency == "Daily" else 6 ),
125138 "end_date" : end_date ,
126139 "digest_frequency" : digest_frequency ,
@@ -194,3 +207,227 @@ def test_waffle_flag_everyone_priority(self):
194207 assert is_email_notification_flag_enabled () is False
195208 assert is_email_notification_flag_enabled (self .user_1 ) is False
196209 assert is_email_notification_flag_enabled (self .user_2 ) is False
210+
211+
212+ class TestEncryption (ModuleStoreTestCase ):
213+ """
214+ Tests all encryption methods
215+ """
216+ def test_string_encryption (self ):
217+ """
218+ Tests if decrypted string is equal original string
219+ """
220+ string = "edx"
221+ encrypted = encrypt_string (string )
222+ decrypted = decrypt_string (encrypted )
223+ assert string == decrypted
224+
225+ def test_object_encryption (self ):
226+ """
227+ Tests if decrypted object is equal to original object
228+ """
229+ obj = {
230+ 'org' : 'edx'
231+ }
232+ encrypted = encrypt_object (obj )
233+ decrypted = decrypt_object (encrypted )
234+ assert obj == decrypted
235+
236+
237+ @ddt .ddt
238+ class TestUpdatePreferenceFromPatch (ModuleStoreTestCase ):
239+ """
240+ Tests if preferences are update according to patch data
241+ """
242+ def setUp (self ):
243+ """
244+ Setup test cases
245+ """
246+ super ().setUp ()
247+ self .user = UserFactory ()
248+ self .course_1 = CourseFactory .create (display_name = 'test course 1' , run = "Testing_course_1" )
249+ self .course_2 = CourseFactory .create (display_name = 'test course 2' , run = "Testing_course_2" )
250+ self .preference_1 = CourseNotificationPreference (course_id = self .course_1 .id , user = self .user )
251+ self .preference_2 = CourseNotificationPreference (course_id = self .course_2 .id , user = self .user )
252+ self .preference_1 .save ()
253+ self .preference_2 .save ()
254+ self .default_json = self .preference_1 .notification_preference_config
255+
256+ def is_channel_editable (self , app_name , notification_type , channel ):
257+ """
258+ Returns if channel is editable
259+ """
260+ if notification_type == 'core' :
261+ return channel not in COURSE_NOTIFICATION_APPS [app_name ]['non_editable' ]
262+ return channel not in COURSE_NOTIFICATION_TYPES [notification_type ]['non_editable' ]
263+
264+ def get_default_cadence_value (self , app_name , notification_type ):
265+ """
266+ Returns default email cadence value
267+ """
268+ if notification_type == 'core' :
269+ return COURSE_NOTIFICATION_APPS [app_name ]['core_email_cadence' ]
270+ return COURSE_NOTIFICATION_TYPES [notification_type ]['email_cadence' ]
271+
272+ @ddt .data (True , False )
273+ def test_value_param (self , new_value ):
274+ """
275+ Tests if value is updated for all notification types and for all channels
276+ """
277+ encrypted_username = encrypt_string (self .user .username )
278+ encrypted_patch = encrypt_object ({
279+ 'value' : new_value
280+ })
281+ update_user_preferences_from_patch (encrypted_username , encrypted_patch )
282+ preference_1 = CourseNotificationPreference .objects .get (course_id = self .course_1 .id , user = self .user )
283+ preference_2 = CourseNotificationPreference .objects .get (course_id = self .course_2 .id , user = self .user )
284+ for preference in [preference_1 , preference_2 ]:
285+ config = preference .notification_preference_config
286+ for app_name , app_prefs in config .items ():
287+ for noti_type , type_prefs in app_prefs ['notification_types' ].items ():
288+ for channel in ['web' , 'email' , 'push' ]:
289+ if self .is_channel_editable (app_name , noti_type , channel ):
290+ assert type_prefs [channel ] == new_value
291+ else :
292+ default_app_json = self .default_json [app_name ]
293+ default_notification_type_json = default_app_json ['notification_types' ][noti_type ]
294+ assert type_prefs [channel ] == default_notification_type_json [channel ]
295+
296+ @ddt .data (* product (['web' , 'email' , 'push' ], [True , False ]))
297+ @ddt .unpack
298+ def test_value_with_channel_param (self , param_channel , new_value ):
299+ """
300+ Tests if value is updated only for channel
301+ """
302+ encrypted_username = encrypt_string (self .user .username )
303+ encrypted_patch = encrypt_object ({
304+ 'channel' : param_channel ,
305+ 'value' : new_value
306+ })
307+ update_user_preferences_from_patch (encrypted_username , encrypted_patch )
308+ preference_1 = CourseNotificationPreference .objects .get (course_id = self .course_1 .id , user = self .user )
309+ preference_2 = CourseNotificationPreference .objects .get (course_id = self .course_2 .id , user = self .user )
310+ # pylint: disable=too-many-nested-blocks
311+ for preference in [preference_1 , preference_2 ]:
312+ config = preference .notification_preference_config
313+ for app_name , app_prefs in config .items ():
314+ for noti_type , type_prefs in app_prefs ['notification_types' ].items ():
315+ for channel in ['web' , 'email' , 'push' ]:
316+ if not self .is_channel_editable (app_name , noti_type , channel ):
317+ continue
318+ if channel == param_channel :
319+ assert type_prefs [channel ] == new_value
320+ if channel == 'email' :
321+ cadence_value = EmailCadence .NEVER
322+ if new_value :
323+ cadence_value = self .get_default_cadence_value (app_name , noti_type )
324+ assert type_prefs ['email_cadence' ] == cadence_value
325+ else :
326+ default_app_json = self .default_json [app_name ]
327+ default_notification_type_json = default_app_json ['notification_types' ][noti_type ]
328+ assert type_prefs [channel ] == default_notification_type_json [channel ]
329+
330+ @ddt .data (True , False )
331+ def test_value_with_course_id_param (self , new_value ):
332+ """
333+ Tests if value is updated for a single course only
334+ """
335+ encrypted_username = encrypt_string (self .user .username )
336+ encrypted_patch = encrypt_object ({
337+ 'value' : new_value ,
338+ 'course_id' : str (self .course_1 .id ),
339+ })
340+ update_user_preferences_from_patch (encrypted_username , encrypted_patch )
341+
342+ preference_2 = CourseNotificationPreference .objects .get (course_id = self .course_2 .id , user = self .user )
343+ self .assertDictEqual (preference_2 .notification_preference_config , self .default_json )
344+
345+ preference_1 = CourseNotificationPreference .objects .get (course_id = self .course_1 .id , user = self .user )
346+ config = preference_1 .notification_preference_config
347+ for app_name , app_prefs in config .items ():
348+ for noti_type , type_prefs in app_prefs ['notification_types' ].items ():
349+ for channel in ['web' , 'email' , 'push' ]:
350+ if self .is_channel_editable (app_name , noti_type , channel ):
351+ assert type_prefs [channel ] == new_value
352+ else :
353+ default_app_json = self .default_json [app_name ]
354+ default_notification_type_json = default_app_json ['notification_types' ][noti_type ]
355+ assert type_prefs [channel ] == default_notification_type_json [channel ]
356+
357+ @ddt .data (* product (['discussion' , 'updates' ], [True , False ]))
358+ @ddt .unpack
359+ def test_value_with_app_name_param (self , param_app_name , new_value ):
360+ """
361+ Tests if value is updated only for channel
362+ """
363+ encrypted_username = encrypt_string (self .user .username )
364+ encrypted_patch = encrypt_object ({
365+ 'app_name' : param_app_name ,
366+ 'value' : new_value
367+ })
368+ update_user_preferences_from_patch (encrypted_username , encrypted_patch )
369+ preference_1 = CourseNotificationPreference .objects .get (course_id = self .course_1 .id , user = self .user )
370+ preference_2 = CourseNotificationPreference .objects .get (course_id = self .course_2 .id , user = self .user )
371+ # pylint: disable=too-many-nested-blocks
372+ for preference in [preference_1 , preference_2 ]:
373+ config = preference .notification_preference_config
374+ for app_name , app_prefs in config .items ():
375+ for noti_type , type_prefs in app_prefs ['notification_types' ].items ():
376+ for channel in ['web' , 'email' , 'push' ]:
377+ if not self .is_channel_editable (app_name , noti_type , channel ):
378+ continue
379+ if app_name == param_app_name :
380+ assert type_prefs [channel ] == new_value
381+ if channel == 'email' :
382+ cadence_value = EmailCadence .NEVER
383+ if new_value :
384+ cadence_value = self .get_default_cadence_value (app_name , noti_type )
385+ assert type_prefs ['email_cadence' ] == cadence_value
386+ else :
387+ default_app_json = self .default_json [app_name ]
388+ default_notification_type_json = default_app_json ['notification_types' ][noti_type ]
389+ assert type_prefs [channel ] == default_notification_type_json [channel ]
390+
391+ @ddt .data (* product (['new_discussion_post' , 'content_reported' ], [True , False ]))
392+ @ddt .unpack
393+ def test_value_with_notification_type_param (self , param_notification_type , new_value ):
394+ """
395+ Tests if value is updated only for channel
396+ """
397+ encrypted_username = encrypt_string (self .user .username )
398+ encrypted_patch = encrypt_object ({
399+ 'notification_type' : param_notification_type ,
400+ 'value' : new_value
401+ })
402+ update_user_preferences_from_patch (encrypted_username , encrypted_patch )
403+ preference_1 = CourseNotificationPreference .objects .get (course_id = self .course_1 .id , user = self .user )
404+ preference_2 = CourseNotificationPreference .objects .get (course_id = self .course_2 .id , user = self .user )
405+ # pylint: disable=too-many-nested-blocks
406+ for preference in [preference_1 , preference_2 ]:
407+ config = preference .notification_preference_config
408+ for app_name , app_prefs in config .items ():
409+ for noti_type , type_prefs in app_prefs ['notification_types' ].items ():
410+ for channel in ['web' , 'email' , 'push' ]:
411+ if not self .is_channel_editable (app_name , noti_type , channel ):
412+ continue
413+ if noti_type == param_notification_type :
414+ assert type_prefs [channel ] == new_value
415+ if channel == 'email' :
416+ cadence_value = EmailCadence .NEVER
417+ if new_value :
418+ cadence_value = self .get_default_cadence_value (app_name , noti_type )
419+ assert type_prefs ['email_cadence' ] == cadence_value
420+ else :
421+ default_app_json = self .default_json [app_name ]
422+ default_notification_type_json = default_app_json ['notification_types' ][noti_type ]
423+ assert type_prefs [channel ] == default_notification_type_json [channel ]
424+
425+ def test_preference_not_updated_if_invalid_username (self ):
426+ """
427+ Tests if no preference is updated when username is not valid
428+ """
429+ username = f"{ self .user .username } -updated"
430+ enc_username = encrypt_string (username )
431+ enc_patch = encrypt_object ({"value" : True })
432+ with self .assertNumQueries (1 ):
433+ update_user_preferences_from_patch (enc_username , enc_patch )
0 commit comments