Skip to content

Commit

Permalink
feat: [AXM-1568] api for shifting past due dates for multiple courses
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrylo-kh committed Feb 27, 2025
1 parent 1b74237 commit 988f8b1
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 78 deletions.
84 changes: 84 additions & 0 deletions openedx/features/course_experience/api/v1/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Tests utils of course expirience feature.
"""
import datetime

from django.urls import reverse
from django.utils import timezone
from rest_framework.test import APIRequestFactory

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.util.testing import EventTestMixin
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
from openedx.core.djangoapps.schedules.models import Schedule
from openedx.features.course_experience.api.v1.utils import reset_deadlines_for_course
from xmodule.modulestore.tests.factories import CourseFactory


class TestResetDeadlinesForCourse(EventTestMixin, BaseCourseHomeTests, MasqueradeMixin):
"""
Tests for reset deadlines endpoint.
"""
def setUp(self): # pylint: disable=arguments-differ
super().setUp("openedx.features.course_experience.api.v1.utils.tracker")
self.course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1000))

def test_reset_deadlines_for_course(self):
enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
enrollment.schedule.save()

request = APIRequestFactory().post(
reverse("course-experience-reset-course-deadlines"), {"course_key": self.course.id}
)
request.user = self.user

reset_deadlines_for_course(request, str(self.course.id), {})

assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date
self.assert_event_emitted(
"edx.ui.lms.reset_deadlines.clicked",
courserun_key=str(self.course.id),
is_masquerading=False,
is_staff=False,
org_key=self.course.org,
user_id=self.user.id,
)

def test_reset_deadlines_with_masquerade(self):
"""Staff users should be able to masquerade as a learner and reset the learner's schedule"""
student_username = self.user.username
student_user_id = self.user.id
student_enrollment = CourseEnrollment.enroll(self.user, self.course.id)
student_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
student_enrollment.schedule.save()

staff_enrollment = CourseEnrollment.enroll(self.staff_user, self.course.id)
staff_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
staff_enrollment.schedule.save()

self.switch_to_staff()
self.update_masquerade(course=self.course, username=student_username)

request = APIRequestFactory().post(
reverse("course-experience-reset-course-deadlines"), {"course_key": self.course.id}
)
request.user = self.staff_user
request.session = self.client.session

reset_deadlines_for_course(request, str(self.course.id), {})

updated_schedule = Schedule.objects.get(id=student_enrollment.schedule.id)
assert updated_schedule.start_date.date() == datetime.datetime.today().date()
updated_staff_schedule = Schedule.objects.get(id=staff_enrollment.schedule.id)
assert updated_staff_schedule.start_date == staff_enrollment.schedule.start_date
self.assert_event_emitted(
"edx.ui.lms.reset_deadlines.clicked",
courserun_key=str(self.course.id),
is_masquerading=True,
is_staff=False,
org_key=self.course.org,
user_id=student_user_id,
)
124 changes: 80 additions & 44 deletions openedx/features/course_experience/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""
Tests for reset deadlines endpoint.
"""

import datetime
from unittest import mock

import ddt
from django.urls import reverse
Expand All @@ -10,7 +12,6 @@

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.util.testing import EventTestMixin
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
from openedx.core.djangoapps.schedules.models import Schedule
Expand All @@ -19,14 +20,12 @@


@ddt.ddt
class ResetCourseDeadlinesViewTests(EventTestMixin, BaseCourseHomeTests, MasqueradeMixin):
class ResetCourseDeadlinesViewTests(BaseCourseHomeTests, MasqueradeMixin):
"""
Tests for reset deadlines endpoint.
"""
def setUp(self): # pylint: disable=arguments-differ
# Need to supply tracker name for the EventTestMixin. Also, EventTestMixin needs to come
# first in class inheritance so the setUp call here appropriately works
super().setUp('openedx.features.course_experience.api.v1.views.tracker')
super().setUp()
self.course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1000))

def test_reset_deadlines(self):
Expand All @@ -37,20 +36,11 @@ def test_reset_deadlines(self):
response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course': self.course.id})
assert response.status_code == 400
assert enrollment.schedule == Schedule.objects.get(id=enrollment.schedule.id)
self.assert_no_events_were_emitted()

# Test correct post body
response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id})
assert response.status_code == 200
assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date
self.assert_event_emitted(
'edx.ui.lms.reset_deadlines.clicked',
courserun_key=str(self.course.id),
is_masquerading=False,
is_staff=False,
org_key=self.course.org,
user_id=self.user.id,
)

@override_waffle_flag(RELATIVE_DATES_FLAG, active=True)
@override_waffle_flag(RELATIVE_DATES_DISABLE_RESET_FLAG, active=True)
Expand All @@ -62,36 +52,6 @@ def test_reset_deadlines_disabled(self):
response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id})
assert response.status_code == 200
assert enrollment.schedule == Schedule.objects.get(id=enrollment.schedule.id)
self.assert_no_events_were_emitted()

def test_reset_deadlines_with_masquerade(self):
""" Staff users should be able to masquerade as a learner and reset the learner's schedule """
student_username = self.user.username
student_user_id = self.user.id
student_enrollment = CourseEnrollment.enroll(self.user, self.course.id)
student_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
student_enrollment.schedule.save()

staff_enrollment = CourseEnrollment.enroll(self.staff_user, self.course.id)
staff_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
staff_enrollment.schedule.save()

self.switch_to_staff()
self.update_masquerade(course=self.course, username=student_username)

self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id})
updated_schedule = Schedule.objects.get(id=student_enrollment.schedule.id)
assert updated_schedule.start_date.date() == datetime.datetime.today().date()
updated_staff_schedule = Schedule.objects.get(id=staff_enrollment.schedule.id)
assert updated_staff_schedule.start_date == staff_enrollment.schedule.start_date
self.assert_event_emitted(
'edx.ui.lms.reset_deadlines.clicked',
courserun_key=str(self.course.id),
is_masquerading=True,
is_staff=False,
org_key=self.course.org,
user_id=student_user_id,
)

def test_post_unauthenticated_user(self):
self.client.logout()
Expand All @@ -115,3 +75,79 @@ def test_mobile_get_unauthenticated_user(self):
self.client.logout()
response = self.client.get(reverse('course-experience-course-deadlines-mobile', args=[self.course.id]))
assert response.status_code == 401


class ResetMultipleCourseDeadlines(BaseCourseHomeTests, MasqueradeMixin):
"""
Tests for reset deadlines endpoint.
"""

def setUp(self): # pylint: disable=arguments-differ
super().setUp()
self.course_1 = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1000))
self.course_2 = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1000))

def test_reset_multiple_course_deadlines(self):
enrollment = CourseEnrollment.enroll(self.user, self.course_1.id, CourseMode.VERIFIED)
enrollment_2 = CourseEnrollment.enroll(self.user, self.course_2.id, CourseMode.VERIFIED)

enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
enrollment.schedule.save()
enrollment_2.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
enrollment_2.schedule.save()
# Test body with incorrect body param (course_key is required)
response = self.client.post(
reverse("course-experience-reset-multiple-course-deadlines"), {"course": [self.course_1.id]}
)
assert response.status_code == 400
assert enrollment.schedule == Schedule.objects.get(id=enrollment.schedule.id)

# Test correct post body
response = self.client.post(
reverse("course-experience-reset-multiple-course-deadlines"),
{"course_keys": [str(self.course_1.id), str(self.course_2.id)]},
content_type="application/json",
)
assert response.status_code == 200
assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date
assert enrollment_2.schedule.start_date < Schedule.objects.get(id=enrollment_2.schedule.id).start_date
assert str(self.course_1.id) in response.data.get("success_course_keys")
assert str(self.course_2.id) in response.data.get("success_course_keys")

def test_reset_multiple_course_deadlines_failure(self):
"""
Raise exception on reset_deadlines_for_course and assert if failure course id is returned
"""
with mock.patch(
"openedx.features.course_experience.api.v1.views.reset_deadlines_for_course",
side_effect=Exception("Test Exception"),
):
response = self.client.post(
reverse("course-experience-reset-multiple-course-deadlines"),
{"course_keys": [str(self.course_1.id), str(self.course_2.id)]},
content_type="application/json",
)

assert response.status_code == 200
assert str(self.course_1.id) in response.data.get("failed_course_keys")
assert str(self.course_2.id) in response.data.get("failed_course_keys")

@override_waffle_flag(RELATIVE_DATES_FLAG, active=True)
@override_waffle_flag(RELATIVE_DATES_DISABLE_RESET_FLAG, active=True)
def test_reset_multiple_course_deadlines_disabled(self):
enrollment = CourseEnrollment.enroll(self.user, self.course_1.id, CourseMode.VERIFIED)
enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
enrollment.schedule.save()

response = self.client.post(
reverse("course-experience-reset-multiple-course-deadlines"), {"course_keys": [self.course_1.id]}
)
assert response.status_code == 200
assert enrollment.schedule == Schedule.objects.get(id=enrollment.schedule.id)

def test_post_unauthenticated_user(self):
self.client.logout()
response = self.client.post(
reverse("course-experience-reset-multiple-course-deadlines"), {"course_keys": [self.course_1.id]}
)
assert response.status_code == 401
11 changes: 10 additions & 1 deletion openedx/features/course_experience/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
from django.conf import settings
from django.urls import re_path

from openedx.features.course_experience.api.v1.views import reset_course_deadlines, CourseDeadlinesMobileView
from openedx.features.course_experience.api.v1.views import (
reset_course_deadlines,
reset_multiple_course_deadlines,
CourseDeadlinesMobileView,
)

urlpatterns = []

Expand All @@ -17,6 +21,11 @@
reset_course_deadlines,
name='course-experience-reset-course-deadlines'
),
re_path(
r'v1/reset_multiple_course_deadlines/',
reset_multiple_course_deadlines,
name='course-experience-reset-multiple-course-deadlines',
)
]

# URL for retrieving course deadlines info
Expand Down
53 changes: 53 additions & 0 deletions openedx/features/course_experience/api/v1/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

"""
Course Experience API utilities.
"""
from eventtracking import tracker
from opaque_keys.edx.keys import CourseKey

from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.masquerade import is_masquerading, setup_masquerade
from lms.djangoapps.course_api.api import course_detail
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
from openedx.features.course_experience.utils import dates_banner_should_display


def reset_deadlines_for_course(request, course_key, research_event_data={}): # lint-amnesty, pylint: disable=dangerous-default-value
"""
Set the start_date of a schedule to today, which in turn will adjust due dates for
sequentials belonging to a self paced course
Args:
request (Request): The request object
course_key (str): The course key
research_event_data (dict): Any data that should be included in the research tracking event
Example: sending the location of where the reset deadlines banner (i.e. outline-tab)
"""

course_key = CourseKey.from_string(course_key)
course_masquerade, user = setup_masquerade(
request,
course_key,
has_access(request.user, 'staff', course_key)
)

# We ignore the missed_deadlines because this util is used in endpoint from the Learning MFE for
# learners who have remaining attempts on a problem and reset their due dates in order to
# submit additional attempts. This can apply for 'completed' (submitted) content that would
# not be marked as past_due
_missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, user)
if not missed_gated_content:
reset_self_paced_schedule(user, course_key)

course_overview = course_detail(request, user.username, course_key)
# For context here, research_event_data should already contain `location` indicating
# the page/location dates were reset from and could also contain `block_id` if reset
# within courseware.
research_event_data.update({
'courserun_key': str(course_key),
'is_masquerading': is_masquerading(user, course_key, course_masquerade),
'is_staff': has_access(user, 'staff', course_key).has_access,
'org_key': course_overview.display_org_with_default,
'user_id': user.id,
})
tracker.emit('edx.ui.lms.reset_deadlines.clicked', research_event_data)
Loading

0 comments on commit 988f8b1

Please sign in to comment.