def test_streak_resets_if_day_is_missed(self): """ Sample run for a 3 day streak and 1 day break with the learner coming back every other day. Therefore the streak keeps resetting. +---------+---------------------+--------------------+-------------------------+------------------+-----------------------------------------------+ | today | streak_length | last_day_of_streak | streak_length_to_celebrate | Note | +---------+---------------------+--------------------+-------------------------+------------------+-----------------------------------------------+ | 2/4/21 | 1 | 2/4/21 | None | Day 1 of Streak | No Accesses on 2/5/21 | 2/6/21 | 1 | 2/6/21 | None | Day 2 of streak was missed, so streak resets | No Accesses on 2/7/21 | 2/8/21 | 1 | 2/8/21 | None | Day 2 of streak was missed, so streak resets | No Accesses on 2/9/21 | 2/10/21 | 1 | 2/10/21 | None | Day 2 of streak was missed, so streak resets | No Accesses on 2/11/21 | 2/12/21 | 1 | 2/12/21 | None | Day 2 of streak was missed, so streak resets | +---------+---------------------+--------------------+-------------------------+------------------+-----------------------------------------------+ """ now = datetime.datetime.now(UTC) for i in range(1, self.STREAK_LENGTH_TO_CELEBRATE * 3 + 1, 2): with freeze_time(now + datetime.timedelta(days=i)): streak_length_to_celebrate = UserCelebration.perform_streak_updates( self.user, self.course_key) assert self.user.celebration.last_day_of_streak == ( now + datetime.timedelta(days=i)).date() assert streak_length_to_celebrate is None
def test_streak_does_not_reset_if_day_is_missed_with_longer_break(self): """ Sample run for a 3 day streak with the learner coming back every other day. See last column for explanation. +---------+---------------------+--------------------+-------------------------+------------------+ | today | streak_length | last_day_of_streak | streak_length_to_celebrate | Note | +---------+---------------------+--------------------+-------------------------+------------------+ | 2/4/21 | 1 | 2/4/21 | None | Day 1 of Streak | No Accesses on 2/5/21 | 2/6/21 | 2 | 2/6/21 | None | Day 2 of Streak | No Accesses on 2/7/21 | 2/8/21 | 3 | 2/8/21 | 3 | Day 3 of streak | No Accesses on 2/9/21 | 2/10/21 | 4 | 2/10/21 | None | Day 4 of streak | No Accesses on 2/11/21 | 2/12/21 | 5 | 2/12/21 | None | Day 5 of streak | +---------+---------------------+--------------------+-------------------------+------------------+ """ UserCelebration.STREAK_BREAK_LENGTH = 2 now = datetime.datetime.now(UTC) for i in range(1, self.STREAK_LENGTH_TO_CELEBRATE * 3 + 1, 2): with freeze_time(now + datetime.timedelta(days=i)): streak_length_to_celebrate = UserCelebration.perform_streak_updates( self.user, self.course_key) assert bool(streak_length_to_celebrate) == (i == 5)
def test_first_check_streak_celebration(self): STREAK_LENGTH_TO_CELEBRATE = UserCelebration.perform_streak_updates( self.user, self.course_key) today = datetime.datetime.now(UTC).date() assert self.user.celebration.streak_length == 1 assert self.user.celebration.last_day_of_streak == today assert STREAK_LENGTH_TO_CELEBRATE is None
def test_celebrate_twice_with_broken_streak_in_between(self): """ Sample run for a 3 day streak and 1 day break. See last column for explanation. +---------+---------------------+--------------------+-------------------------+------------------+-----------------------------------------------+ | today | streak_length | last_day_of_streak | streak_length_to_celebrate | Note | +---------+---------------------+--------------------+-------------------------+------------------+-----------------------------------------------+ | 2/4/21 | 1 | 2/4/21 | None | Day 1 of Streak | | 2/5/21 | 2 | 2/5/21 | None | Day 2 of Streak | | 2/6/21 | 3 | 2/6/21 | 3 | Completed 3 Day Streak so we should celebrate | No Accesses on 2/7/21 | 2/8/21 | 1 | 2/8/21 | None | Day 1 of Streak | | 2/9/21 | 2 | 2/9/21 | None | Day 2 of Streak | | 2/10/21 | 3 | 2/10/21 | 3 | Completed 3 Day Streak so we should celebrate | +---------+---------------------+--------------------+-------------------------+------------------+-----------------------------------------------+ """ now = datetime.datetime.now(UTC) for i in range( 1, self.STREAK_LENGTH_TO_CELEBRATE + self.STREAK_BREAK_LENGTH + self.STREAK_LENGTH_TO_CELEBRATE + 1): with freeze_time(now + datetime.timedelta(days=i)): if self.STREAK_LENGTH_TO_CELEBRATE < i <= self.STREAK_LENGTH_TO_CELEBRATE + self.STREAK_BREAK_LENGTH: # Don't make any checks during the break continue streak_length_to_celebrate = UserCelebration.perform_streak_updates( self.user, self.course_key) if i <= self.STREAK_LENGTH_TO_CELEBRATE: assert bool(streak_length_to_celebrate) == ( i == self.STREAK_LENGTH_TO_CELEBRATE) else: assert bool(streak_length_to_celebrate) == ( i == self.STREAK_LENGTH_TO_CELEBRATE + self.STREAK_BREAK_LENGTH + self.STREAK_LENGTH_TO_CELEBRATE)
def get_celebrations_dict(user, enrollment, course, browser_timezone): """ Returns a dict of celebrations that should be performed. """ if not enrollment: return { 'first_section': False, 'streak_length_to_celebrate': None, 'streak_discount_enabled': False, } streak_length_to_celebrate = UserCelebration.perform_streak_updates( user, course.id, browser_timezone ) celebrations = { 'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(enrollment), 'streak_length_to_celebrate': streak_length_to_celebrate, 'streak_discount_enabled': False, } if streak_length_to_celebrate: # We only want to offer the streak discount # if the course has not ended, is upgradeable and the user is not an enterprise learner if can_show_streak_discount_coupon(user, course): # Send course streak coupon event course_key = str(course.id) modes_dict = CourseMode.modes_for_course_dict(course_id=course_key, include_expired=False) verified_mode = modes_dict.get('verified', None) if verified_mode: celebrations['streak_discount_enabled'] = True return celebrations
def get_celebrations_dict(user, enrollment, course, browser_timezone): """ Returns a dict of celebrations that should be performed. """ if not enrollment: return { 'first_section': False, 'streak_length_to_celebrate': None, 'streak_discount_experiment_enabled': False, } streak_length_to_celebrate = UserCelebration.perform_streak_updates( user, course.id, browser_timezone) celebrations = { 'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(enrollment), 'streak_length_to_celebrate': streak_length_to_celebrate, 'streak_discount_experiment_enabled': False, } # We only want to bucket people into the AA-759 experiment if they are going to see the streak celebration if streak_length_to_celebrate: # We only want to bucket people into the AA-759 experiment # if the course has not ended, is upgradeable and the user is not an enterprise learner if can_show_streak_discount_experiment_coupon(user, course): celebrations[ 'streak_discount_experiment_enabled'] = STREAK_DISCOUNT_EXPERIMENT_FLAG.is_enabled( ) return celebrations
def test_celebration_with_user_configured_timezone(self): """ Check that the _get_now method uses the user's configured timezone over the browser timezone that is passed in as a parameter """ set_user_preference(self.user, 'time_zone', 'Asia/Tokyo') now = UserCelebration._get_now('America/New_York') # pylint: disable=protected-access assert str(now.tzinfo) == 'Asia/Tokyo'
def test_streak_masquerade(self): """ Don't update streak data when masquerading as a specific student """ # Update streak data when not masquerading with mock.patch.object(UserCelebration, '_update_streak') as update_streak_mock: for _ in range(1, self.STREAK_LENGTH_TO_CELEBRATE + 1): UserCelebration.perform_streak_updates(self.user, self.course_key) update_streak_mock.assert_called() # Don't update streak data when masquerading as a specific student with mock.patch( 'lms.djangoapps.courseware.masquerade.is_masquerading_as_specific_student', return_value=True): with mock.patch.object(UserCelebration, '_update_streak') as update_streak_mock: for _ in range(1, self.STREAK_LENGTH_TO_CELEBRATE + 1): UserCelebration.perform_streak_updates( self.user, self.course_key) update_streak_mock.assert_not_called()
def test_longest_streak_updates_correctly(self): """ Sample run for a 3 day streak and 1 day break. See last column for explanation. +---------+---------------------+--------------------+-------------------------+------------------+---------------------+ | today | streak_length | last_day_of_streak | streak_length_to_celebrate | Note | +---------+---------------------+--------------------+-------------------------+------------------+---------------------+ | 2/4/21 | 1 | 2/4/21 | None | longest_streak_ever is 1 | | 2/5/21 | 2 | 2/5/21 | None | longest_streak_ever is 2 | | 2/6/21 | 3 | 2/6/21 | 3 | longest_streak_ever is 3 | | 2/7/21 | 4 | 2/7/21 | None | longest_streak_ever is 4 | | 2/8/21 | 5 | 2/8/21 | None | longest_streak_ever is 5 | | 2/9/21 | 6 | 2/9/21 | None | longest_streak_ever is 6 | +---------+---------------------+--------------------+-------------------------+------------------+---------------------+ """ now = datetime.datetime.now(UTC) for i in range(1, (self.STREAK_LENGTH_TO_CELEBRATE * 2) + 1): with freeze_time(now + datetime.timedelta(days=i)): UserCelebration.perform_streak_updates(self.user, self.course_key) assert self.user.celebration.longest_ever_streak == i
def celebrations(self): """ Returns a list of celebrations that should be performed. """ browser_timezone = self.request.query_params.get('browser_timezone', None) return { 'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(self.enrollment_object), 'streak_length_to_celebrate': UserCelebration.perform_streak_updates( self.effective_user, self.course_key, browser_timezone ), }
def test_celebrate_only_once_with_multiple_calls_on_the_same_day(self): """ Sample run for a 3 day streak and 1 day break. See last column for explanation. +---------+---------------------+--------------------+-------------------------+------------------+----------------------------+ | today | streak_length | last_day_of_streak | streak_length_to_celebrate | Note | +---------+---------------------+--------------------+-------------------------+------------------+----------------------------+ | 2/4/21 | 1 | 2/4/21 | None | Day 1 of Streak | | 2/4/21 | 1 | 2/4/21 | None | Day 1 of Streak | | 2/5/21 | 2 | 2/5/21 | None | Day 2 of Streak | | 2/5/21 | 2 | 2/5/21 | None | Day 2 of Streak | | 2/6/21 | 3 | 2/6/21 | 3 | Completed 3 Day Streak so we should celebrate | | 2/6/21 | 3 | 2/6/21 | None | Already celebrated this streak. | +---------+---------------------+--------------------+-------------------------+------------------+----------------------------+ """ now = datetime.datetime.now(UTC) for i in range(1, self.STREAK_LENGTH_TO_CELEBRATE + 1): with freeze_time(now + datetime.timedelta(days=i)): streak_length_to_celebrate = UserCelebration.perform_streak_updates( self.user, self.course_key) assert bool(streak_length_to_celebrate) == ( i == self.STREAK_LENGTH_TO_CELEBRATE) streak_length_to_celebrate = UserCelebration.perform_streak_updates( self.user, self.course_key) assert streak_length_to_celebrate is None
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) original_user_is_staff = has_access(request.user, 'staff', course_key).has_access _, request.user = setup_masquerade( request, course_key, staff_access=has_access(request.user, 'staff', course_key), reset_masquerade_data=True, ) username = request.user.username if request.user.username else None course = course_detail(request, request.user.username, course_key) user_is_enrolled = CourseEnrollment.is_enrolled( request.user, course_key_string) browser_timezone = request.query_params.get('browser_timezone', None) celebrations = { 'streak_length_to_celebrate': UserCelebration.perform_streak_updates(request.user, course_key, browser_timezone) } courseware_meta = CoursewareMeta(course_key, request, request.user.username) can_load_courseware = courseware_meta.is_microfrontend_enabled_for_user( ) data = { 'course_id': course.id, 'username': username, 'is_staff': has_access(request.user, 'staff', course_key).has_access, 'original_user_is_staff': original_user_is_staff, 'number': course.display_number_with_default, 'org': course.display_org_with_default, 'tabs': get_course_tab_list(request.user, course), 'title': course.display_name_with_default, 'is_self_paced': getattr(course, 'self_paced', False), 'is_enrolled': user_is_enrolled, 'can_load_courseware': can_load_courseware, 'celebrations': celebrations, } context = self.get_serializer_context() context['course'] = course serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def test_celebrate_only_once_in_continuous_streak(self): """ Sample run for a 3 day streak and 1 day break. See last column for explanation. +---------+---------------------+--------------------+-------------------------+------------------+------------------+ | today | streak_length | last_day_of_streak | streak_length_to_celebrate | Note | +---------+---------------------+--------------------+-------------------------+------------------+------------------+ | 2/4/21 | 1 | 2/4/21 | None | Day 1 of Streak | | 2/5/21 | 2 | 2/5/21 | None | Day 2 of Streak | | 2/6/21 | 3 | 2/6/21 | 3 | Completed 3 Day Streak so we should celebrate | | 2/7/21 | 4 | 2/7/21 | None | Day 4 of Streak | | 2/8/21 | 5 | 2/8/21 | None | Day 5 of Streak | | 2/9/21 | 6 | 2/9/21 | None | Day 6 of Streak | +---------+---------------------+--------------------+-------------------------+------------------+------------------+ """ now = datetime.datetime.now(UTC) for i in range(1, (self.STREAK_LENGTH_TO_CELEBRATE * 2) + 1): with freeze_time(now + datetime.timedelta(days=i)): STREAK_LENGTH_TO_CELEBRATE = UserCelebration.perform_streak_updates( self.user, self.course_key) assert bool(STREAK_LENGTH_TO_CELEBRATE) == ( i == self.STREAK_LENGTH_TO_CELEBRATE)
def test_celebration_with_user_passed_in_timezone(self): """ Check that the _get_now method uses the user's timezone from the browser if none is configured """ now = UserCelebration._get_now('Asia/Tokyo') # pylint: disable=protected-access assert str(now.tzinfo) == 'Asia/Tokyo'