Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1f6fb47
feat: add certificate management v2 API endpoints
wgu-jesse-stewart Apr 21, 2026
d2de81c
fix: linting
wgu-jesse-stewart Apr 21, 2026
4a355f5
fix: linting
wgu-jesse-stewart Apr 21, 2026
ea687fe
fix: linting
wgu-jesse-stewart Apr 21, 2026
5f7d3df
fix: linting
wgu-jesse-stewart Apr 21, 2026
5d08560
feat: PR feedback
wgu-jesse-stewart Apr 22, 2026
a394008
fix: Removed the unused invalidated_user_ids
wgu-jesse-stewart Apr 22, 2026
0d8809e
feat: update tests
wgu-jesse-stewart Apr 22, 2026
33364c0
feat: update tests
wgu-jesse-stewart Apr 22, 2026
2d2c62a
feat: update tests
wgu-jesse-stewart Apr 22, 2026
6b9a2e9
feat: update tests
wgu-jesse-stewart Apr 22, 2026
66e1243
feat: PR feedback
wgu-jesse-stewart Apr 22, 2026
1b43fc5
fix: tests
wgu-jesse-stewart Apr 22, 2026
d8ff883
feat: PR feedback
wgu-jesse-stewart Apr 23, 2026
684a245
fix: build
wgu-jesse-stewart Apr 23, 2026
2902be9
fix: tests
wgu-jesse-stewart Apr 23, 2026
daf8ce0
feat: wrap create_certificate_invalidation_entry in atomic
wgu-jesse-stewart Apr 23, 2026
397b8f8
Merge branch 'master' into wgu-jesse-stewart/instructor_dashboard_cer…
wgu-jesse-stewart Apr 23, 2026
9e74963
feat: add logging and max_length
wgu-jesse-stewart Apr 24, 2026
0592918
Merge branch 'wgu-jesse-stewart/instructor_dashboard_certificates_v2'…
wgu-jesse-stewart Apr 24, 2026
a317b19
feat: show all exceptions granted records
wgu-jesse-stewart Apr 24, 2026
5736077
fix: tests
wgu-jesse-stewart Apr 24, 2026
a56b2c3
feat: PR feedback
wgu-jesse-stewart Apr 24, 2026
5d1ad42
Merge branch 'master' into wgu-jesse-stewart/instructor_dashboard_cer…
wgu-jesse-stewart Apr 24, 2026
120b662
style: sort imports
wgu-taylor-payne Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions lms/djangoapps/certificates/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ class CertificateStatuses:
requesting = 'requesting'

readable_statuses = {
downloadable: "already received",
notpassing: "didn't receive",
error: "error states",
audit_passing: "audit passing states",
audit_notpassing: "audit not passing states",
downloadable: "Received",
notpassing: "Not Received",
unavailable: "Invalidated",
error: "Error State",
audit_passing: "Audit - Passing",
audit_notpassing: "Audit - Not Passing",
}

PASSED_STATUSES = (downloadable, generating)
Expand Down
6 changes: 3 additions & 3 deletions lms/djangoapps/certificates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ def get_certificate_generation_candidates(self):
if not task_input.strip():
# if task input is empty, it means certificates were generated for all learners
# Translators: This string represents task was executed for all learners.
return _("All learners")
return _("All Learners")

task_input_json = json.loads(task_input)

Expand All @@ -607,9 +607,9 @@ def get_certificate_generation_candidates(self):
# for backwards compatibility.
if 'student_set' in task_input_json or 'students' in task_input_json:
# Translators: This string represents task was executed for students having exceptions.
return _("For exceptions")
return _("Granted Exceptions")
else:
return _("All learners")
return _("All Learners")

class Meta:
app_label = "certificates"
Expand Down
24 changes: 12 additions & 12 deletions lms/djangoapps/certificates/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,22 +267,22 @@ class TestCertificateGenerationHistory(OpenEdxEventsTestMixin, TestCase):
ENABLED_OPENEDX_EVENTS = []

@ddt.data(
({"student_set": "allowlisted_not_generated"}, "For exceptions", True),
({"student_set": "allowlisted_not_generated"}, "For exceptions", False),
({"student_set": "allowlisted_not_generated"}, "Granted Exceptions", True),
({"student_set": "allowlisted_not_generated"}, "Granted Exceptions", False),
# check "students" key for backwards compatibility
({"students": [1, 2, 3]}, "For exceptions", True),
({"students": [1, 2, 3]}, "For exceptions", False),
({}, "All learners", True),
({}, "All learners", False),
({"students": [1, 2, 3]}, "Granted Exceptions", True),
({"students": [1, 2, 3]}, "Granted Exceptions", False),
({}, "All Learners", True),
({}, "All Learners", False),
# test single status to regenerate returns correctly
({"statuses_to_regenerate": ['downloadable']}, 'already received', True),
({"statuses_to_regenerate": ['downloadable']}, 'already received', False),
({"statuses_to_regenerate": ['downloadable']}, 'Received', True),
({"statuses_to_regenerate": ['downloadable']}, 'Received', False),
# test that list of > 1 statuses render correctly
({"statuses_to_regenerate": ['downloadable', 'error']}, 'already received, error states', True),
({"statuses_to_regenerate": ['downloadable', 'error']}, 'already received, error states', False),
({"statuses_to_regenerate": ['downloadable', 'error']}, 'Received, Error State', True),
({"statuses_to_regenerate": ['downloadable', 'error']}, 'Received, Error State', False),
# test that only "readable" statuses are returned
({"statuses_to_regenerate": ['downloadable', 'not_readable']}, 'already received', True),
({"statuses_to_regenerate": ['downloadable', 'not_readable']}, 'already received', False),
({"statuses_to_regenerate": ['downloadable', 'not_readable']}, 'Received', True),
({"statuses_to_regenerate": ['downloadable', 'not_readable']}, 'Received', False),
)
@ddt.unpack
def test_get_certificate_generation_candidates(self, task_input, expected, is_regeneration):
Expand Down
55 changes: 53 additions & 2 deletions lms/djangoapps/instructor/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
UserFactory,
)
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import CertificateGenerationHistory
from lms.djangoapps.certificates.models import CertificateAllowlist, CertificateGenerationHistory
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
from lms.djangoapps.courseware.models import StudentModule
from lms.djangoapps.instructor.access import ROLE_DISPLAY_NAMES
Expand Down Expand Up @@ -2094,6 +2094,57 @@ def test_pagination(self):
assert 'previous' in response.data
assert 'results' in response.data

def test_granted_exceptions_without_certificates(self):
"""
Test that granted_exceptions filter shows allowlisted users
even if they don't have GeneratedCertificate records yet.
"""
# Add student1 to allowlist (has verified enrollment)
CertificateAllowlist.objects.create(
user=self.student1,
course_id=self.course_key,
allowlist=True,
notes='Medical emergency'
)

# Add student2 to allowlist (has audit enrollment, no certificate)
CertificateAllowlist.objects.create(
user=self.student2,
course_id=self.course_key,
allowlist=True,
notes='Special case'
)

# Create certificate only for student1
GeneratedCertificateFactory.create(
user=self.student1,
course_id=self.course_key,
status=CertificateStatuses.downloadable
)

self.client.force_authenticate(user=self.instructor)
params = {'filter': 'granted_exceptions'}
response = self.client.get(self._get_url(), params)

assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 2 # Both students should appear

results = {r['username']: r for r in response.data['results']}

# Verify student1 (has certificate)
assert 'student1' in results
assert results['student1']['enrollment_track'] == 'verified'
assert results['student1']['certificate_status'] == 'downloadable'
assert results['student1']['special_case'] == 'Exception'
assert results['student1']['exception_notes'] == 'Medical emergency'

# Verify student2 (no certificate, but should appear with enrollment data)
assert 'student2' in results
assert results['student2']['enrollment_track'] == 'audit'
assert results['student2']['certificate_status'] == 'audit_notpassing'
assert results['student2']['special_case'] == 'Exception'
assert results['student2']['exception_notes'] == 'Special case'


@ddt.ddt
class CertificateGenerationHistoryViewTest(SharedModuleStoreTestCase):
Expand Down Expand Up @@ -2206,7 +2257,7 @@ def test_history_entry_structure(self):
# Verify all required fields are present (snake_case from serializer)
assert entry['task_name'] == 'Regenerated'
assert 'date' in entry
assert entry['details'] == 'All learners'
assert entry['details'] == 'All Learners'

# Verify data types
assert isinstance(entry['task_name'], str)
Expand Down
Loading
Loading