Skip to content

Commit eb85705

Browse files
fix: filter reset extensions from granted extensions table (#38394)
1 parent f174486 commit eb85705

2 files changed

Lines changed: 124 additions & 1 deletion

File tree

lms/djangoapps/instructor/tests/test_api_v2.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Unit tests for instructor API v2 endpoints.
33
"""
44
import json
5-
from datetime import datetime
5+
from datetime import datetime, timedelta
66
from unittest.mock import Mock, patch
77
from urllib.parse import urlencode
88
from uuid import uuid4
@@ -1848,6 +1848,107 @@ def test_extension_data_structure(self, mock_title_or_url, mock_get_units, mock_
18481848
self.assertIsInstance(extension['unit_title'], str) # noqa: PT009
18491849
self.assertIsInstance(extension['unit_location'], str) # noqa: PT009
18501850

1851+
def test_reset_extension_with_none_date_excluded(self):
1852+
"""
1853+
Test that extensions reset via set_date_for_block(None) are excluded from results.
1854+
When an extension is reset, edx-when creates a UserDate with abs_date=None and rel_date=None,
1855+
causing actual_date to fall back to the original block due date. These reverted overrides
1856+
should not appear as granted extensions.
1857+
"""
1858+
original_due = datetime.now(UTC).replace(microsecond=0)
1859+
extended = original_due + timedelta(days=60)
1860+
set_dates_for_course(self.course_key, [(self.subsection.location, {'due': original_due})])
1861+
1862+
# Grant extension to student1, then reset it by passing None
1863+
set_date_for_block(self.course_key, self.subsection.location, 'due', extended, user=self.student1)
1864+
set_date_for_block(self.course_key, self.subsection.location, 'due', None, user=self.student1)
1865+
1866+
# Grant a real extension to student2
1867+
set_date_for_block(self.course_key, self.subsection.location, 'due', extended, user=self.student2)
1868+
1869+
self.client.force_authenticate(user=self.instructor)
1870+
response = self.client.get(self._get_url())
1871+
1872+
assert response.status_code == 200
1873+
results = response.data['results']
1874+
assert len(results) == 1
1875+
assert results[0]['username'] == 'student2'
1876+
assert results[0]['extended_due_date'] == extended.strftime('%Y-%m-%dT%H:%M:%SZ')
1877+
1878+
def test_reset_extension_matching_original_date_excluded(self):
1879+
"""
1880+
Test that extensions whose override date matches the original due date are excluded.
1881+
When an extension is reset, the override reverts to the original subsection date,
1882+
making it appear as if there's an active extension when there isn't one.
1883+
"""
1884+
original_due = datetime.now(UTC).replace(microsecond=0)
1885+
extended = original_due + timedelta(days=60)
1886+
set_dates_for_course(self.course_key, [(self.subsection.location, {'due': original_due})])
1887+
1888+
# Grant extension to student1, then "reset" it by setting it back to the original date
1889+
set_date_for_block(self.course_key, self.subsection.location, 'due', extended, user=self.student1)
1890+
set_date_for_block(self.course_key, self.subsection.location, 'due', original_due, user=self.student1)
1891+
1892+
# Grant a real extension to student2
1893+
set_date_for_block(self.course_key, self.subsection.location, 'due', extended, user=self.student2)
1894+
1895+
self.client.force_authenticate(user=self.instructor)
1896+
response = self.client.get(self._get_url())
1897+
1898+
assert response.status_code == 200
1899+
results = response.data['results']
1900+
assert len(results) == 1
1901+
assert results[0]['username'] == 'student2'
1902+
assert results[0]['extended_due_date'] == extended.strftime('%Y-%m-%dT%H:%M:%SZ')
1903+
1904+
def test_reset_extension_excluded_with_block_id_filter(self):
1905+
"""
1906+
Test that reset extensions are also excluded when filtering by block_id.
1907+
"""
1908+
original_due = datetime.now(UTC).replace(microsecond=0)
1909+
extended = original_due + timedelta(days=60)
1910+
set_dates_for_course(self.course_key, [(self.subsection.location, {'due': original_due})])
1911+
1912+
# Grant extension to student1, then reset it
1913+
set_date_for_block(self.course_key, self.subsection.location, 'due', extended, user=self.student1)
1914+
set_date_for_block(self.course_key, self.subsection.location, 'due', None, user=self.student1)
1915+
1916+
# Grant a real extension to student2
1917+
set_date_for_block(self.course_key, self.subsection.location, 'due', extended, user=self.student2)
1918+
1919+
self.client.force_authenticate(user=self.instructor)
1920+
params = {'block_id': str(self.subsection.location)}
1921+
response = self.client.get(self._get_url(), params)
1922+
1923+
assert response.status_code == 200
1924+
results = response.data['results']
1925+
assert len(results) == 1
1926+
assert results[0]['username'] == 'student2'
1927+
assert results[0]['extended_due_date'] == extended.strftime('%Y-%m-%dT%H:%M:%SZ')
1928+
1929+
def test_active_extensions_still_returned(self):
1930+
"""
1931+
Test that legitimate extensions (date differs from original) are still returned.
1932+
"""
1933+
original_due = datetime.now(UTC).replace(microsecond=0)
1934+
extended1 = original_due + timedelta(days=30)
1935+
extended2 = original_due + timedelta(days=60)
1936+
set_dates_for_course(self.course_key, [(self.subsection.location, {'due': original_due})])
1937+
1938+
set_date_for_block(self.course_key, self.subsection.location, 'due', extended1, user=self.student1)
1939+
set_date_for_block(self.course_key, self.subsection.location, 'due', extended2, user=self.student2)
1940+
1941+
self.client.force_authenticate(user=self.instructor)
1942+
response = self.client.get(self._get_url())
1943+
1944+
assert response.status_code == 200
1945+
results = response.data['results']
1946+
assert len(results) == 2
1947+
results_by_username = {r['username']: r for r in results}
1948+
assert results_by_username['student1']['extended_due_date'] == extended1.strftime('%Y-%m-%dT%H:%M:%SZ')
1949+
assert results_by_username['student2']['extended_due_date'] == extended2.strftime('%Y-%m-%dT%H:%M:%SZ')
1950+
1951+
18511952
@ddt.ddt
18521953
class IssuedCertificatesViewTest(SharedModuleStoreTestCase):
18531954
"""

lms/djangoapps/instructor/views/api_v2.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
from lms.djangoapps.instructor_task.models import InstructorTask, ReportStore
9797
from lms.djangoapps.instructor_task.tasks_helper.utils import upload_csv_file_to_report_store
9898
from openedx.core.djangoapps.course_groups.cohorts import is_course_cohorted
99+
from openedx.core.djangoapps.schedules.models import Schedule
99100
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
100101
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
101102
from openedx.core.lib.courses import get_course_by_id
@@ -581,6 +582,27 @@ def get_queryset(self):
581582
if str(row[3]) in units_dict # Ensure unit has due date
582583
]
583584

585+
# TODO: This filtering should ideally live in edx-when get_overrides_for_block/get_overrides_for_course.
586+
# See https://github.com/openedx/edx-when/issues/353
587+
# Filter out reset extensions (None dates or dates matching the original due date)
588+
if unit_due_date_extensions:
589+
version = getattr(course, 'course_version', None)
590+
schedule = Schedule(start_date=course.start)
591+
base_dates = edx_when_api.get_dates_for_course(
592+
course.id, schedule=schedule, published_version=version
593+
)
594+
relevant_locations = {str(ext.unit_location) for ext in unit_due_date_extensions}
595+
original_due_dates = {
596+
str(loc): date
597+
for (loc, field), date in base_dates.items()
598+
if field == 'due' and str(loc) in relevant_locations
599+
}
600+
unit_due_date_extensions = [
601+
ext for ext in unit_due_date_extensions
602+
if ext.extended_due_date is not None
603+
and ext.extended_due_date != original_due_dates.get(str(ext.unit_location))
604+
]
605+
584606
# Apply filters if any
585607
filter_value = email_or_username_filter.lower() if email_or_username_filter else None
586608

0 commit comments

Comments
 (0)