Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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 @@ -2091,6 +2091,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 @@ -2203,7 +2254,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