Merge pull request #26652 from edx/jhynes/microba-908_cleanup_allowlist
MB-908 | Prevent ability for learner to appear on both certificate invalidation and certificate exceptions lists
This commit is contained in:
@@ -56,7 +56,11 @@ from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, U
|
|||||||
from lms.djangoapps.bulk_email.models import BulkEmailFlag, CourseEmail, CourseEmailTemplate
|
from lms.djangoapps.bulk_email.models import BulkEmailFlag, CourseEmail, CourseEmailTemplate
|
||||||
from lms.djangoapps.certificates.api import generate_user_certificates
|
from lms.djangoapps.certificates.api import generate_user_certificates
|
||||||
from lms.djangoapps.certificates.models import CertificateStatuses
|
from lms.djangoapps.certificates.models import CertificateStatuses
|
||||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
from lms.djangoapps.certificates.tests.factories import (
|
||||||
|
CertificateInvalidationFactory,
|
||||||
|
CertificateWhitelistFactory,
|
||||||
|
GeneratedCertificateFactory
|
||||||
|
)
|
||||||
from lms.djangoapps.courseware.models import StudentModule
|
from lms.djangoapps.courseware.models import StudentModule
|
||||||
from lms.djangoapps.courseware.tests.factories import (
|
from lms.djangoapps.courseware.tests.factories import (
|
||||||
BetaTesterFactory,
|
BetaTesterFactory,
|
||||||
@@ -68,6 +72,10 @@ from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
|||||||
from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag
|
from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag
|
||||||
from lms.djangoapps.instructor.tests.utils import FakeContentTask, FakeEmail, FakeEmailInfo
|
from lms.djangoapps.instructor.tests.utils import FakeContentTask, FakeEmail, FakeEmailInfo
|
||||||
from lms.djangoapps.instructor.views.api import (
|
from lms.djangoapps.instructor.views.api import (
|
||||||
|
_check_if_learner_on_allowlist,
|
||||||
|
_check_if_learner_on_blocklist,
|
||||||
|
_get_certificate_for_user,
|
||||||
|
_get_student_from_request_data,
|
||||||
_split_input_list,
|
_split_input_list,
|
||||||
common_exceptions_400,
|
common_exceptions_400,
|
||||||
generate_unique_password,
|
generate_unique_password,
|
||||||
@@ -4362,3 +4370,176 @@ class TestBulkCohorting(SharedModuleStoreTestCase):
|
|||||||
self.verify_success_on_file_content(
|
self.verify_success_on_file_content(
|
||||||
'username,email,cohort\r\nfoo_username,bar_email,baz_cohort', mock_store_upload, mock_cohort_task
|
'username,email,cohort\r\nfoo_username,bar_email,baz_cohort', mock_store_upload, mock_cohort_task
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstructorCertificateExceptions(SharedModuleStoreTestCase):
|
||||||
|
"""
|
||||||
|
Tests for utility functions utilized in the Instructor Dashboard Certificates app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.global_staff = GlobalStaffFactory()
|
||||||
|
self.course = CourseFactory.create()
|
||||||
|
self.user = UserFactory()
|
||||||
|
CourseEnrollment.enroll(self.user, self.course.id)
|
||||||
|
|
||||||
|
def test_get_student_from_request_data(self):
|
||||||
|
"""
|
||||||
|
Test ability to retrieve a learner record using their username and course id
|
||||||
|
"""
|
||||||
|
student = _get_student_from_request_data({"user": self.user.username}, self.course.id)
|
||||||
|
|
||||||
|
assert student.username == self.user.username
|
||||||
|
|
||||||
|
def test_get_student_from_request_data_empty_username(self):
|
||||||
|
"""
|
||||||
|
Test that we receive an expected error when no learner's username or email is entered
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError) as error:
|
||||||
|
_get_student_from_request_data({"user": ""}, self.course.id)
|
||||||
|
|
||||||
|
assert str(error.value) == (
|
||||||
|
'Student username/email field is required and can not be empty. Kindly fill in username/email and then '
|
||||||
|
'press "Invalidate Certificate" button.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_student_from_request_data_user_dne(self):
|
||||||
|
"""
|
||||||
|
Test to verify an expected error message is returned when attempting to retrieve a learner that does not exist
|
||||||
|
in the LMS.
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError) as error:
|
||||||
|
_get_student_from_request_data({"user": "Neo"}, self.course.id)
|
||||||
|
|
||||||
|
assert str(error.value) == "Neo does not exist in the LMS. Please check your spelling and retry."
|
||||||
|
|
||||||
|
def test_get_student_from_request_data_user_not_enrolled(self):
|
||||||
|
"""
|
||||||
|
Test to verify an expected error message is returned when attempting to retrieve a learner that is not enrolled
|
||||||
|
in a course-run.
|
||||||
|
"""
|
||||||
|
new_course = CourseFactory.create()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as error:
|
||||||
|
_get_student_from_request_data({"user": self.user.username}, new_course.id)
|
||||||
|
|
||||||
|
assert str(error.value) == (
|
||||||
|
f"{self.user.username} is not enrolled in this course. Please check your spelling and retry."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_certificate_for_user(self):
|
||||||
|
"""
|
||||||
|
Test that attempts to retrieve a Certificate for a learner in a course-run.
|
||||||
|
"""
|
||||||
|
generated_certificate = GeneratedCertificateFactory.create(
|
||||||
|
user=self.user,
|
||||||
|
course_id=self.course.id,
|
||||||
|
mode='verified',
|
||||||
|
status=CertificateStatuses.downloadable,
|
||||||
|
)
|
||||||
|
|
||||||
|
retrieved_certificate = _get_certificate_for_user(self.course.id, self.user)
|
||||||
|
|
||||||
|
assert retrieved_certificate.id == generated_certificate.id
|
||||||
|
assert retrieved_certificate.user == self.user
|
||||||
|
assert retrieved_certificate.course_id == self.course.id
|
||||||
|
|
||||||
|
def test_get_certificate_for_user_no_certificate(self):
|
||||||
|
"""
|
||||||
|
Test to verify an expected error message is returned when attempting to retrieve a certificate for a learner
|
||||||
|
that does not exist yet.
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError) as error:
|
||||||
|
_get_certificate_for_user(self.course.id, self.user)
|
||||||
|
|
||||||
|
assert str(error.value) == (
|
||||||
|
f"The student {self.user} does not have certificate for the course {self.course.id.course}. Kindly "
|
||||||
|
"verify student username/email and the selected course are correct and try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_if_learner_on_allowlist(self):
|
||||||
|
"""
|
||||||
|
Test to verify that no learner is returned if the learner does not have an active entry on the allowlist.
|
||||||
|
"""
|
||||||
|
result = _check_if_learner_on_allowlist(self.course.id, self.user)
|
||||||
|
|
||||||
|
assert not result
|
||||||
|
|
||||||
|
def test_check_if_learner_on_allowlist_allowlist_entry_exists(self):
|
||||||
|
"""
|
||||||
|
Test that verifies the correct result is returned if a learner has an active entry on the allowlist.
|
||||||
|
"""
|
||||||
|
CertificateWhitelistFactory.create(
|
||||||
|
course_id=self.course.id,
|
||||||
|
user=self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _check_if_learner_on_allowlist(self.course.id, self.user)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
|
||||||
|
def test_check_if_learner_on_blocklist_no_cert(self):
|
||||||
|
"""
|
||||||
|
Test to verify that the correct result is returned if a learner does not currently have a certificate in a
|
||||||
|
course-run.
|
||||||
|
"""
|
||||||
|
result = _check_if_learner_on_blocklist(self.course.id, self.user)
|
||||||
|
|
||||||
|
assert not result
|
||||||
|
|
||||||
|
def test_check_if_learner_on_blocklist_with_cert(self):
|
||||||
|
"""
|
||||||
|
Test to verify that the correct result is returned if a learner has a certificate but does not have an active
|
||||||
|
entry on the blocklist.
|
||||||
|
"""
|
||||||
|
GeneratedCertificateFactory.create(
|
||||||
|
user=self.user,
|
||||||
|
course_id=self.course.id,
|
||||||
|
mode='verified',
|
||||||
|
status=CertificateStatuses.downloadable,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _check_if_learner_on_blocklist(self.course.id, self.user)
|
||||||
|
assert not result
|
||||||
|
|
||||||
|
def test_check_if_learner_on_blocklist_blocklist_entry_exists(self):
|
||||||
|
"""
|
||||||
|
Test to verify that the correct result is returned if a learner has a Certificate and an active entry on
|
||||||
|
the blocklist.
|
||||||
|
"""
|
||||||
|
generated_certificate = GeneratedCertificateFactory.create(
|
||||||
|
user=self.user,
|
||||||
|
course_id=self.course.id,
|
||||||
|
mode='verified',
|
||||||
|
status=CertificateStatuses.downloadable,
|
||||||
|
)
|
||||||
|
|
||||||
|
CertificateInvalidationFactory.create(
|
||||||
|
generated_certificate=generated_certificate,
|
||||||
|
invalidated_by=self.global_staff,
|
||||||
|
active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _check_if_learner_on_blocklist(self.course.id, self.user)
|
||||||
|
assert result
|
||||||
|
|
||||||
|
def test_check_if_learner_on_blocklist_inactive_blocklist_entry_exists(self):
|
||||||
|
"""
|
||||||
|
Test to verify that the correct result is returned if a learner has an inactive entry on the blocklist.
|
||||||
|
"""
|
||||||
|
generated_certificate = GeneratedCertificateFactory.create(
|
||||||
|
user=self.user,
|
||||||
|
course_id=self.course.id,
|
||||||
|
mode='verified',
|
||||||
|
status=CertificateStatuses.downloadable,
|
||||||
|
)
|
||||||
|
|
||||||
|
CertificateInvalidationFactory.create(
|
||||||
|
generated_certificate=generated_certificate,
|
||||||
|
invalidated_by=self.global_staff,
|
||||||
|
active=False
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _check_if_learner_on_blocklist(self.course.id, self.user)
|
||||||
|
assert not result
|
||||||
|
|||||||
@@ -743,6 +743,40 @@ class CertificateExceptionViewInstructorApiTest(SharedModuleStoreTestCase):
|
|||||||
'Certificate exception (user={user}) does not exist in certificate white list.' \
|
'Certificate exception (user={user}) does not exist in certificate white list.' \
|
||||||
' Please refresh the page and try again.'.format(user=self.certificate_exception['user_name'])
|
' Please refresh the page and try again.'.format(user=self.certificate_exception['user_name'])
|
||||||
|
|
||||||
|
def test_certificate_invalidation_already_exists(self):
|
||||||
|
"""
|
||||||
|
Test to confirm an error message is raised when generating a certificate exception for a learner that already
|
||||||
|
has an active certificate invalidation.
|
||||||
|
"""
|
||||||
|
# generate a certificate for the test learner in our course
|
||||||
|
generated_certificate = GeneratedCertificateFactory.create(
|
||||||
|
user=self.user,
|
||||||
|
course_id=self.course.id,
|
||||||
|
status=CertificateStatuses.downloadable,
|
||||||
|
mode='honor',
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a certificate invalidation tied to the generated certificate
|
||||||
|
CertificateInvalidationFactory.create(
|
||||||
|
generated_certificate=generated_certificate,
|
||||||
|
invalidated_by=self.global_staff,
|
||||||
|
)
|
||||||
|
|
||||||
|
# attempt to add learner to the allowlist, expect an error
|
||||||
|
response = self.client.post(
|
||||||
|
self.url,
|
||||||
|
data=json.dumps(self.certificate_exception),
|
||||||
|
content_type='application/json',
|
||||||
|
REQUEST_METHOD='POST'
|
||||||
|
)
|
||||||
|
|
||||||
|
res_json = json.loads(response.content.decode('utf-8'))
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert res_json['message'] == (
|
||||||
|
f"Student {self.user.username} is already on the certificate invalidation list and cannot be added to "
|
||||||
|
"the certificate exception list."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(CERT_QUEUE='certificates')
|
@override_settings(CERT_QUEUE='certificates')
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
@@ -997,6 +1031,30 @@ class TestCertificatesInstructorApiBulkWhiteListExceptions(SharedModuleStoreTest
|
|||||||
assert len(data['general_errors']) == 1
|
assert len(data['general_errors']) == 1
|
||||||
assert len(data['success']) == 0
|
assert len(data['success']) == 0
|
||||||
|
|
||||||
|
def test_certificate_invalidation_already_exists(self):
|
||||||
|
"""
|
||||||
|
Test to confirm an error message is raised when generating a certificate exception for a learner appears in the
|
||||||
|
CSV file who has an active certificate invalidation.
|
||||||
|
"""
|
||||||
|
# generate a certificate for the test learner in our course
|
||||||
|
generated_certificate = GeneratedCertificateFactory.create(
|
||||||
|
user=self.enrolled_user_1,
|
||||||
|
course_id=self.course.id,
|
||||||
|
status=CertificateStatuses.downloadable,
|
||||||
|
mode='honor',
|
||||||
|
)
|
||||||
|
|
||||||
|
CertificateInvalidationFactory.create(
|
||||||
|
generated_certificate=generated_certificate,
|
||||||
|
invalidated_by=self.global_staff,
|
||||||
|
)
|
||||||
|
|
||||||
|
# attempt to add learner to the allowlist, expect an error
|
||||||
|
csv_content = b"test_student1@example.com,notes"
|
||||||
|
data = self.upload_file(csv_content=csv_content)
|
||||||
|
assert len(data['row_errors']['user_on_certificate_invalidation_list']) == 1
|
||||||
|
assert data['row_errors']['user_on_certificate_invalidation_list'][0] == 'user "TestStudent1" in row# 1'
|
||||||
|
|
||||||
def upload_file(self, csv_content):
|
def upload_file(self, csv_content):
|
||||||
"""
|
"""
|
||||||
Upload a csv file.
|
Upload a csv file.
|
||||||
@@ -1272,3 +1330,26 @@ class CertificateInvalidationViewTests(SharedModuleStoreTestCase):
|
|||||||
|
|
||||||
# Assert Error Message
|
# Assert Error Message
|
||||||
assert res_json['message'] == 'Certificate Invalidation does not exist, Please refresh the page and try again.'
|
assert res_json['message'] == 'Certificate Invalidation does not exist, Please refresh the page and try again.'
|
||||||
|
|
||||||
|
def test_learner_already_on_certificate_exception_list(self):
|
||||||
|
"""
|
||||||
|
Test to make sure we don't allow a single to learner to appear on both the certificate exception and
|
||||||
|
invalidation lists.
|
||||||
|
"""
|
||||||
|
# add test learner to the allowlist
|
||||||
|
CertificateWhitelistFactory.create(user=self.enrolled_user_1, course_id=self.course.id)
|
||||||
|
|
||||||
|
# now try and add them to the invalidation list, expect an error
|
||||||
|
response = self.client.post(
|
||||||
|
self.url,
|
||||||
|
data=json.dumps(self.certificate_invalidation_data),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
res_json = json.loads(response.content.decode('utf-8'))
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert res_json['message'] == (
|
||||||
|
f"The student {self.enrolled_user_1.username} appears on the Certificate Exception list in course "
|
||||||
|
f"{self.course.id}. Please remove them from the Certificate Exception list before attempting to "
|
||||||
|
"invalidate their certificate."
|
||||||
|
)
|
||||||
|
|||||||
@@ -2647,13 +2647,20 @@ def certificate_exception_view(request, course_id):
|
|||||||
def add_certificate_exception(course_key, student, certificate_exception):
|
def add_certificate_exception(course_key, student, certificate_exception):
|
||||||
"""
|
"""
|
||||||
Add a certificate exception to CertificateWhitelist table.
|
Add a certificate exception to CertificateWhitelist table.
|
||||||
Raises ValueError in case Student is already white listed.
|
|
||||||
|
Raises ValueError in case Student is already white listed or if they appear on the block list.
|
||||||
|
|
||||||
:param course_key: identifier of the course whose certificate exception will be added.
|
:param course_key: identifier of the course whose certificate exception will be added.
|
||||||
:param student: User object whose certificate exception will be added.
|
:param student: User object whose certificate exception will be added.
|
||||||
:param certificate_exception: A dict object containing certificate exception info.
|
:param certificate_exception: A dict object containing certificate exception info.
|
||||||
:return: CertificateWhitelist item in dict format containing certificate exception info.
|
:return: CertificateWhitelist item in dict format containing certificate exception info.
|
||||||
"""
|
"""
|
||||||
|
if _check_if_learner_on_blocklist(course_key, student):
|
||||||
|
raise ValueError(
|
||||||
|
_("Student {user} is already on the certificate invalidation list and cannot be added to the certificate "
|
||||||
|
"exception list.").format(user=student.username)
|
||||||
|
)
|
||||||
|
|
||||||
if CertificateWhitelist.get_certificate_white_list(course_key, student):
|
if CertificateWhitelist.get_certificate_white_list(course_key, student):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
_("Student (username/email={user}) already in certificate exception list.").format(user=student.username)
|
_("Student (username/email={user}) already in certificate exception list.").format(user=student.username)
|
||||||
@@ -2667,7 +2674,8 @@ def add_certificate_exception(course_key, student, certificate_exception):
|
|||||||
'notes': certificate_exception.get('notes', '')
|
'notes': certificate_exception.get('notes', '')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
log.info('%s has been added to the whitelist in course %s', student.username, course_key)
|
|
||||||
|
log.info(f"Student {student.id} has been added to the whitelist in course {course_key}")
|
||||||
|
|
||||||
generated_certificate = GeneratedCertificate.eligible_certificates.filter(
|
generated_certificate = GeneratedCertificate.eligible_certificates.filter(
|
||||||
user=student,
|
user=student,
|
||||||
@@ -2835,17 +2843,24 @@ def generate_bulk_certificate_exceptions(request, course_id):
|
|||||||
{
|
{
|
||||||
general_errors: [errors related to csv file e.g. csv uploading, csv attachment, content reading etc. ],
|
general_errors: [errors related to csv file e.g. csv uploading, csv attachment, content reading etc. ],
|
||||||
row_errors: {
|
row_errors: {
|
||||||
data_format_error: [users/data in csv file that are not well formatted],
|
data_format_error: [users/data in csv file that are not well formatted],
|
||||||
user_not_exist: [csv with none exiting users in LMS system],
|
user_not_exist: [csv with none exiting users in LMS system],
|
||||||
user_already_white_listed: [users that are already white listed],
|
user_already_white_listed: [users that are already white listed],
|
||||||
user_not_enrolled: [rows with not enrolled users in the given course]
|
user_not_enrolled: [rows with not enrolled users in the given course],
|
||||||
|
user_on_certificate_invalidation_list: [users that are currently have an active blocklist entry]
|
||||||
},
|
},
|
||||||
success: [list of successfully added users to the certificate white list model]
|
success: [list of successfully added users to the certificate white list model]
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
user_index = 0
|
user_index = 0
|
||||||
notes_index = 1
|
notes_index = 1
|
||||||
row_errors_key = ['data_format_error', 'user_not_exist', 'user_already_white_listed', 'user_not_enrolled']
|
row_errors_key = [
|
||||||
|
'data_format_error',
|
||||||
|
'user_not_exist',
|
||||||
|
'user_already_white_listed',
|
||||||
|
'user_not_enrolled',
|
||||||
|
'user_on_certificate_invalidation_list'
|
||||||
|
]
|
||||||
course_key = CourseKey.from_string(course_id)
|
course_key = CourseKey.from_string(course_id)
|
||||||
students, general_errors, success = [], [], []
|
students, general_errors, success = [], [], []
|
||||||
row_errors = {key: [] for key in row_errors_key}
|
row_errors = {key: [] for key in row_errors_key}
|
||||||
@@ -2864,7 +2879,6 @@ def generate_bulk_certificate_exceptions(request, course_id):
|
|||||||
else:
|
else:
|
||||||
general_errors.append(_('Make sure that the file you upload is in CSV format with no '
|
general_errors.append(_('Make sure that the file you upload is in CSV format with no '
|
||||||
'extraneous characters or rows.'))
|
'extraneous characters or rows.'))
|
||||||
|
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
general_errors.append(_('Could not read uploaded file.'))
|
general_errors.append(_('Could not read uploaded file.'))
|
||||||
finally:
|
finally:
|
||||||
@@ -2878,7 +2892,7 @@ def generate_bulk_certificate_exceptions(request, course_id):
|
|||||||
if len(student) != 2:
|
if len(student) != 2:
|
||||||
if student:
|
if student:
|
||||||
build_row_errors('data_format_error', student[user_index], row_num)
|
build_row_errors('data_format_error', student[user_index], row_num)
|
||||||
log.info('invalid data/format in csv row# %s', row_num)
|
log.info(f'Invalid data/format in csv row# {row_num}')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
user = student[user_index]
|
user = student[user_index]
|
||||||
@@ -2886,17 +2900,21 @@ def generate_bulk_certificate_exceptions(request, course_id):
|
|||||||
user = get_user_by_username_or_email(user)
|
user = get_user_by_username_or_email(user)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
build_row_errors('user_not_exist', user, row_num)
|
build_row_errors('user_not_exist', user, row_num)
|
||||||
log.info('student %s does not exist', user)
|
log.info(f'Student {user} does not exist')
|
||||||
else:
|
else:
|
||||||
if CertificateWhitelist.get_certificate_white_list(course_key, user):
|
# make sure learner isn't on the blocklist
|
||||||
|
if _check_if_learner_on_blocklist(course_key, user):
|
||||||
|
build_row_errors('user_on_certificate_invalidation_list', user, row_num)
|
||||||
|
log.warning(f'Student {user.id} is blocked from receiving a Certificate in Course '
|
||||||
|
f'{course_key}')
|
||||||
|
# make sure user isn't already on the exception list
|
||||||
|
elif CertificateWhitelist.get_certificate_white_list(course_key, user):
|
||||||
build_row_errors('user_already_white_listed', user, row_num)
|
build_row_errors('user_already_white_listed', user, row_num)
|
||||||
log.warning('student %s already exist.', user.username)
|
log.warning(f'Student {user.id} already on exception list in Course {course_key}.')
|
||||||
|
|
||||||
# make sure user is enrolled in course
|
# make sure user is enrolled in course
|
||||||
elif not CourseEnrollment.is_enrolled(user, course_key):
|
elif not CourseEnrollment.is_enrolled(user, course_key):
|
||||||
build_row_errors('user_not_enrolled', user, row_num)
|
build_row_errors('user_not_enrolled', user, row_num)
|
||||||
log.warning('student %s is not enrolled in course.', user.username)
|
log.warning(f'Student {user.id} is not enrolled in Course {course_key}')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
CertificateWhitelist.objects.create(
|
CertificateWhitelist.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
@@ -2905,7 +2923,6 @@ def generate_bulk_certificate_exceptions(request, course_id):
|
|||||||
notes=student[notes_index]
|
notes=student[notes_index]
|
||||||
)
|
)
|
||||||
success.append(_('user "{username}" in row# {row}').format(username=user.username, row=row_num))
|
success.append(_('user "{username}" in row# {row}').format(username=user.username, row=row_num))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
general_errors.append(_('File is not attached.'))
|
general_errors.append(_('File is not attached.'))
|
||||||
|
|
||||||
@@ -2914,7 +2931,6 @@ def generate_bulk_certificate_exceptions(request, course_id):
|
|||||||
'row_errors': row_errors,
|
'row_errors': row_errors,
|
||||||
'success': success
|
'success': success
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(results)
|
return JsonResponse(results)
|
||||||
|
|
||||||
|
|
||||||
@@ -2935,13 +2951,23 @@ def certificate_invalidation_view(request, course_id):
|
|||||||
# Validate request data and return error response in case of invalid data
|
# Validate request data and return error response in case of invalid data
|
||||||
try:
|
try:
|
||||||
certificate_invalidation_data = parse_request_data(request)
|
certificate_invalidation_data = parse_request_data(request)
|
||||||
certificate = validate_request_data_and_get_certificate(certificate_invalidation_data, course_key)
|
student = _get_student_from_request_data(certificate_invalidation_data, course_key)
|
||||||
|
certificate = _get_certificate_for_user(course_key, student)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
return JsonResponse({'message': str(error)}, status=400)
|
return JsonResponse({'message': str(error)}, status=400)
|
||||||
|
|
||||||
# Invalidate certificate of the given student for the course course
|
# Invalidate certificate of the given student for the course course
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
|
if _check_if_learner_on_allowlist(course_key, student):
|
||||||
|
log.warning(f"Invalidating certificate for student {student.id} in course {course_key} failed. "
|
||||||
|
"Student is currently on the allow list.")
|
||||||
|
raise ValueError(
|
||||||
|
_("The student {student} appears on the Certificate Exception list in course {course}. Please "
|
||||||
|
"remove them from the Certificate Exception list before attempting to invalidate their "
|
||||||
|
"certificate.").format(student=student, course=course_key)
|
||||||
|
)
|
||||||
|
|
||||||
certificate_invalidation = invalidate_certificate(request, certificate, certificate_invalidation_data)
|
certificate_invalidation = invalidate_certificate(request, certificate, certificate_invalidation_data)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
return JsonResponse({'message': str(error)}, status=400)
|
return JsonResponse({'message': str(error)}, status=400)
|
||||||
@@ -3031,36 +3057,6 @@ def re_validate_certificate(request, course_key, generated_certificate):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_request_data_and_get_certificate(certificate_invalidation, course_key):
|
|
||||||
"""
|
|
||||||
Fetch and return GeneratedCertificate of the student passed in request data for the given course.
|
|
||||||
|
|
||||||
Raises ValueError in case of missing student username/email or
|
|
||||||
if student does not have certificate for the given course.
|
|
||||||
|
|
||||||
:param certificate_invalidation: dict containing certificate invalidation data
|
|
||||||
:param course_key: CourseKey object identifying the current course.
|
|
||||||
:return: GeneratedCertificate object of the student for the given course
|
|
||||||
"""
|
|
||||||
user = certificate_invalidation.get("user")
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
raise ValueError(
|
|
||||||
_('Student username/email field is required and can not be empty. '
|
|
||||||
'Kindly fill in username/email and then press "Invalidate Certificate" button.')
|
|
||||||
)
|
|
||||||
|
|
||||||
student = get_student(user, course_key)
|
|
||||||
|
|
||||||
certificate = GeneratedCertificate.certificate_for_student(student, course_key)
|
|
||||||
if not certificate:
|
|
||||||
raise ValueError(_(
|
|
||||||
"The student {student} does not have certificate for the course {course}. Kindly verify student "
|
|
||||||
"username/email and the selected course are correct and try again."
|
|
||||||
).format(student=student.username, course=course_key.course))
|
|
||||||
return certificate
|
|
||||||
|
|
||||||
|
|
||||||
def _get_boolean_param(request, param_name):
|
def _get_boolean_param(request, param_name):
|
||||||
"""
|
"""
|
||||||
Returns the value of the boolean parameter with the given
|
Returns the value of the boolean parameter with the given
|
||||||
@@ -3076,3 +3072,62 @@ def _create_error_response(request, msg):
|
|||||||
in JSON form.
|
in JSON form.
|
||||||
"""
|
"""
|
||||||
return JsonResponse({"error": msg}, 400)
|
return JsonResponse({"error": msg}, 400)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_student_from_request_data(request_data, course_key):
|
||||||
|
"""
|
||||||
|
Attempts to retrieve the student information from the incoming request data.
|
||||||
|
|
||||||
|
:param request: HttpRequest object
|
||||||
|
:param course_key: CourseKey object identifying the current course.
|
||||||
|
"""
|
||||||
|
user = request_data.get("user")
|
||||||
|
if not user:
|
||||||
|
raise ValueError(
|
||||||
|
_('Student username/email field is required and can not be empty. '
|
||||||
|
'Kindly fill in username/email and then press "Invalidate Certificate" button.')
|
||||||
|
)
|
||||||
|
|
||||||
|
return get_student(user, course_key)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_certificate_for_user(course_key, student):
|
||||||
|
"""
|
||||||
|
Attempts to retrieve a Certificate for a learner in a given course run key.
|
||||||
|
"""
|
||||||
|
log.info(f"Retrieving certificate for student {student.id} in course {course_key}")
|
||||||
|
certificate = GeneratedCertificate.certificate_for_student(student, course_key)
|
||||||
|
if not certificate:
|
||||||
|
raise ValueError(_(
|
||||||
|
"The student {student} does not have certificate for the course {course}. Kindly verify student "
|
||||||
|
"username/email and the selected course are correct and try again.").format(
|
||||||
|
student=student.username, course=course_key.course)
|
||||||
|
)
|
||||||
|
|
||||||
|
return certificate
|
||||||
|
|
||||||
|
|
||||||
|
def _check_if_learner_on_allowlist(course_key, student):
|
||||||
|
"""
|
||||||
|
Utility method that will try to determine if the learner is currently on the allowlist. This is a check that
|
||||||
|
occurs as part of adding a learner to the CertificateInvalidation list.
|
||||||
|
"""
|
||||||
|
log.info(f"Checking if student {student.id} is currently on the allowlist of course {course_key}")
|
||||||
|
return CertificateWhitelist.objects.filter(user=student, course_id=course_key, whitelist=True).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def _check_if_learner_on_blocklist(course_key, student):
|
||||||
|
"""
|
||||||
|
Utility method that will try to determine if the learner is currently on the block list. This is a check that
|
||||||
|
occurs as part of adding a learner to the Allow list.
|
||||||
|
|
||||||
|
The CertificateInvalidation model does not store a username or user id, just a reference to the id of the
|
||||||
|
invalidated Certificate. We check if the learner has a Certificate in the Course-Run and then use that to check if
|
||||||
|
the learner has an active entry on the block list.
|
||||||
|
"""
|
||||||
|
log.info(f"Checking if student {student.id} is currently on the blocklist of course {course_key}")
|
||||||
|
cert = GeneratedCertificate.certificate_for_student(student, course_key)
|
||||||
|
if cert:
|
||||||
|
return CertificateInvalidation.objects.filter(generated_certificate_id=cert.id, active=True).exists()
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
data_format_error: 'data-format-error',
|
data_format_error: 'data-format-error',
|
||||||
user_not_exist: 'user-not-exist',
|
user_not_exist: 'user-not-exist',
|
||||||
user_already_white_listed: 'user-already-white-listed',
|
user_already_white_listed: 'user-already-white-listed',
|
||||||
user_not_enrolled: 'user-not-enrolled'
|
user_not_enrolled: 'user-not-enrolled',
|
||||||
|
user_on_certificate_invalidation_list: 'user-on-certificate-invalidation-list'
|
||||||
};
|
};
|
||||||
|
|
||||||
return Backbone.View.extend({
|
return Backbone.View.extend({
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var template = this.loadTemplate('certificate-bulk-white-list');
|
var template = this.loadTemplate('certificate-bulk-white-list');
|
||||||
this.$el.html(template());
|
this.$el.html(template()); // xss-lint: disable=javascript-jquery-html
|
||||||
},
|
},
|
||||||
|
|
||||||
loadTemplate: function(name) {
|
loadTemplate: function(name) {
|
||||||
@@ -73,12 +74,91 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
display_response: function(data_from_server) {
|
display_response: function(data_from_server) {
|
||||||
|
var UserOnCertificateInvalidationList;
|
||||||
|
|
||||||
$('.bulk-exception-results').removeClass('hidden').empty();
|
$('.bulk-exception-results').removeClass('hidden').empty();
|
||||||
|
|
||||||
|
function generateDiv(group, heading, displayData) {
|
||||||
|
// inner function generate div and display response messages.
|
||||||
|
$('<div/>', {
|
||||||
|
class: 'message ' + group
|
||||||
|
}).appendTo('.bulk-exception-results').prepend( // eslint-disable-line max-len, xss-lint: disable=javascript-jquery-insert-into-target,javascript-jquery-prepend
|
||||||
|
"<button type='button' id= '" + group + "' class='arrow'> + </button>" + heading) // eslint-disable-line max-len, xss-lint: disable=javascript-concat-html
|
||||||
|
.append($('<ul/>', {
|
||||||
|
class: group
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (var i = 0; i < displayData.length; i++) { // eslint-disable-line vars-on-top
|
||||||
|
$('<li/>', {
|
||||||
|
text: displayData[i]
|
||||||
|
}).appendTo('div.message > .' + group); // eslint-disable-line max-len, xss-lint: disable=javascript-jquery-insert-into-target
|
||||||
|
}
|
||||||
|
$('div.message > .' + group).hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayText(qty, group) {
|
||||||
|
// inner function to display appropriate heading text
|
||||||
|
var text;
|
||||||
|
|
||||||
|
switch (group) {
|
||||||
|
case MESSAGE_GROUP.successfully_added:
|
||||||
|
text = qty > 1 ?
|
||||||
|
gettext(qty + ' learners are successfully added to exception list') :
|
||||||
|
gettext(qty + ' learner is successfully added to the exception list');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MESSAGE_GROUP.data_format_error:
|
||||||
|
text = qty > 1 ?
|
||||||
|
gettext(qty + ' records are not in correct format and not added to' +
|
||||||
|
' the exception list') :
|
||||||
|
gettext(qty + ' record is not in correct format and not added to the exception' +
|
||||||
|
' list');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MESSAGE_GROUP.user_not_exist:
|
||||||
|
text = qty > 1 ?
|
||||||
|
gettext(qty + ' learners do not exist in LMS and not added to the' +
|
||||||
|
' exception list') :
|
||||||
|
gettext(qty + ' learner does not exist in LMS and not added to the exception' +
|
||||||
|
' list');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MESSAGE_GROUP.user_already_white_listed:
|
||||||
|
text = qty > 1 ?
|
||||||
|
gettext(qty + ' learners are already white listed and not added to' +
|
||||||
|
' the exception list') :
|
||||||
|
gettext(qty + ' learner is already white listed and not added to the exception' +
|
||||||
|
' list');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MESSAGE_GROUP.user_not_enrolled:
|
||||||
|
text = qty > 1 ?
|
||||||
|
gettext(qty + ' learners are not enrolled in course and not added to' +
|
||||||
|
' the exception list') :
|
||||||
|
gettext(qty + ' learner is not enrolled in course and not added to the exception' +
|
||||||
|
' list');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MESSAGE_GROUP.user_on_certificate_invalidation_list:
|
||||||
|
text = qty > 1 ?
|
||||||
|
gettext(qty + ' learners already appear on the certificate invalidation list') :
|
||||||
|
gettext(qty + ' learner already appears on the certificate invalidation list');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
text = qty > 1 ?
|
||||||
|
gettext(qty + ' learners encountered unknown errors') :
|
||||||
|
gettext(qty + ' learner encountered an unknown error');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
// Display general error messages
|
// Display general error messages
|
||||||
if (data_from_server.general_errors.length) {
|
if (data_from_server.general_errors.length) {
|
||||||
var errors = data_from_server.general_errors;
|
var errors = data_from_server.general_errors;
|
||||||
generate_div(
|
generateDiv(
|
||||||
MESSAGE_GROUP.general_errors,
|
MESSAGE_GROUP.general_errors,
|
||||||
gettext('Uploaded file issues. Click on "+" to view.'),
|
gettext('Uploaded file issues. Click on "+" to view.'),
|
||||||
errors
|
errors
|
||||||
@@ -88,9 +168,9 @@
|
|||||||
// Display success message
|
// Display success message
|
||||||
if (data_from_server.success.length) {
|
if (data_from_server.success.length) {
|
||||||
var success_data = data_from_server.success;
|
var success_data = data_from_server.success;
|
||||||
generate_div(
|
generateDiv(
|
||||||
MESSAGE_GROUP.successfully_added,
|
MESSAGE_GROUP.successfully_added,
|
||||||
get_text(success_data.length, MESSAGE_GROUP.successfully_added),
|
getDisplayText(success_data.length, MESSAGE_GROUP.successfully_added),
|
||||||
success_data
|
success_data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,93 +181,51 @@
|
|||||||
|
|
||||||
if (row_errors.data_format_error.length) {
|
if (row_errors.data_format_error.length) {
|
||||||
var format_errors = row_errors.data_format_error;
|
var format_errors = row_errors.data_format_error;
|
||||||
generate_div(
|
generateDiv(
|
||||||
MESSAGE_GROUP.data_format_error,
|
MESSAGE_GROUP.data_format_error,
|
||||||
get_text(format_errors.length, MESSAGE_GROUP.data_format_error),
|
getDisplayText(format_errors.length, MESSAGE_GROUP.data_format_error),
|
||||||
format_errors
|
format_errors
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (row_errors.user_not_exist.length) {
|
if (row_errors.user_not_exist.length) {
|
||||||
var user_not_exist = row_errors.user_not_exist;
|
var user_not_exist = row_errors.user_not_exist;
|
||||||
generate_div(
|
generateDiv(
|
||||||
MESSAGE_GROUP.user_not_exist,
|
MESSAGE_GROUP.user_not_exist,
|
||||||
get_text(user_not_exist.length, MESSAGE_GROUP.user_not_exist),
|
getDisplayText(user_not_exist.length, MESSAGE_GROUP.user_not_exist),
|
||||||
user_not_exist
|
user_not_exist
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (row_errors.user_already_white_listed.length) {
|
if (row_errors.user_already_white_listed.length) {
|
||||||
var user_already_white_listed = row_errors.user_already_white_listed;
|
var user_already_white_listed = row_errors.user_already_white_listed;
|
||||||
generate_div(
|
generateDiv(
|
||||||
MESSAGE_GROUP.user_already_white_listed,
|
MESSAGE_GROUP.user_already_white_listed,
|
||||||
get_text(user_already_white_listed.length, MESSAGE_GROUP.user_already_white_listed),
|
getDisplayText(
|
||||||
|
user_already_white_listed.length,
|
||||||
|
MESSAGE_GROUP.user_already_white_listed
|
||||||
|
),
|
||||||
user_already_white_listed
|
user_already_white_listed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (row_errors.user_not_enrolled.length) {
|
if (row_errors.user_not_enrolled.length) {
|
||||||
var user_not_enrolled = row_errors.user_not_enrolled;
|
var user_not_enrolled = row_errors.user_not_enrolled;
|
||||||
generate_div(
|
generateDiv(
|
||||||
MESSAGE_GROUP.user_not_enrolled,
|
MESSAGE_GROUP.user_not_enrolled,
|
||||||
get_text(user_not_enrolled.length, MESSAGE_GROUP.user_not_enrolled),
|
getDisplayText(user_not_enrolled.length, MESSAGE_GROUP.user_not_enrolled),
|
||||||
user_not_enrolled
|
user_not_enrolled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
if (row_errors.user_on_certificate_invalidation_list.length) {
|
||||||
|
UserOnCertificateInvalidationList =
|
||||||
function generate_div(group, heading, display_data) {
|
row_errors.user_on_certificate_invalidation_list;
|
||||||
// inner function generate div and display response messages.
|
generateDiv(
|
||||||
$('<div/>', {
|
MESSAGE_GROUP.user_on_certificate_invalidation_list,
|
||||||
class: 'message ' + group
|
getDisplayText(
|
||||||
}).appendTo('.bulk-exception-results').prepend(
|
UserOnCertificateInvalidationList.length,
|
||||||
"<button type='button' id= '" + group + "' class='arrow'> + </button>" + heading
|
MESSAGE_GROUP.user_on_certificate_invalidation_list
|
||||||
).append($('<ul/>', {
|
),
|
||||||
class: group
|
UserOnCertificateInvalidationList
|
||||||
}));
|
);
|
||||||
|
|
||||||
for (var i = 0; i < display_data.length; i++) {
|
|
||||||
$('<li/>', {
|
|
||||||
text: display_data[i]
|
|
||||||
}).appendTo('div.message > .' + group);
|
|
||||||
}
|
}
|
||||||
$('div.message > .' + group).hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_text(qty, group) {
|
|
||||||
// inner function to display appropriate heading text
|
|
||||||
var text;
|
|
||||||
switch (group) {
|
|
||||||
case MESSAGE_GROUP.successfully_added:
|
|
||||||
text = qty > 1 ? gettext(qty + ' learners are successfully added to exception list') :
|
|
||||||
gettext(qty + ' learner is successfully added to the exception list');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MESSAGE_GROUP.data_format_error:
|
|
||||||
text = qty > 1 ? gettext(qty + ' records are not in correct format and not added to' +
|
|
||||||
' the exception list') :
|
|
||||||
gettext(qty + ' record is not in correct format and not added to the exception' +
|
|
||||||
' list');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MESSAGE_GROUP.user_not_exist:
|
|
||||||
text = qty > 1 ? gettext(qty + ' learners do not exist in LMS and not added to the' +
|
|
||||||
' exception list') :
|
|
||||||
gettext(qty + ' learner does not exist in LMS and not added to the exception list');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MESSAGE_GROUP.user_already_white_listed:
|
|
||||||
text = qty > 1 ? gettext(qty + ' learners are already white listed and not added to' +
|
|
||||||
' the exception list') :
|
|
||||||
gettext(qty + ' learner is already white listed and not added to the exception ' +
|
|
||||||
'list');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MESSAGE_GROUP.user_not_enrolled:
|
|
||||||
text = qty > 1 ? gettext(qty + ' learners are not enrolled in course and not added to' +
|
|
||||||
' the exception list') :
|
|
||||||
gettext(qty + ' learner is not enrolled in course and not added to the exception' +
|
|
||||||
' list');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ define([
|
|||||||
data_format_error: ['user 1 in row# 1'],
|
data_format_error: ['user 1 in row# 1'],
|
||||||
user_not_exist: ['user 2 in row# 2'],
|
user_not_exist: ['user 2 in row# 2'],
|
||||||
user_already_white_listed: ['user 3 in row# 3'],
|
user_already_white_listed: ['user 3 in row# 3'],
|
||||||
user_not_enrolled: ['user 4 in row# 4']
|
user_not_enrolled: ['user 4 in row# 4'],
|
||||||
|
user_on_certificate_invalidation_list: ['user 5 in row# 5']
|
||||||
},
|
},
|
||||||
success: []
|
success: []
|
||||||
});
|
});
|
||||||
@@ -89,6 +90,9 @@ define([
|
|||||||
expect($(SELECTORS.bulk_exception_results).text()).toContain('1 learner does not exist in LMS');
|
expect($(SELECTORS.bulk_exception_results).text()).toContain('1 learner does not exist in LMS');
|
||||||
expect($(SELECTORS.bulk_exception_results).text()).toContain('1 learner is already white listed');
|
expect($(SELECTORS.bulk_exception_results).text()).toContain('1 learner is already white listed');
|
||||||
expect($(SELECTORS.bulk_exception_results).text()).toContain('1 learner is not enrolled in course');
|
expect($(SELECTORS.bulk_exception_results).text()).toContain('1 learner is not enrolled in course');
|
||||||
|
expect($(SELECTORS.bulk_exception_results).text()).toContain(
|
||||||
|
'1 learner already appears on the certificate invalidation list'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('bind the ajax call and the result will be plural form of row errors', function() {
|
it('bind the ajax call and the result will be plural form of row errors', function() {
|
||||||
@@ -100,7 +104,8 @@ define([
|
|||||||
data_format_error: ['user 1 in row# 1', 'user 1 in row# 1'],
|
data_format_error: ['user 1 in row# 1', 'user 1 in row# 1'],
|
||||||
user_not_exist: ['user 2 in row# 2', 'user 2 in row# 2'],
|
user_not_exist: ['user 2 in row# 2', 'user 2 in row# 2'],
|
||||||
user_already_white_listed: ['user 3 in row# 3', 'user 3 in row# 3'],
|
user_already_white_listed: ['user 3 in row# 3', 'user 3 in row# 3'],
|
||||||
user_not_enrolled: ['user 4 in row# 4', 'user 4 in row# 4']
|
user_not_enrolled: ['user 4 in row# 4', 'user 4 in row# 4'],
|
||||||
|
user_on_certificate_invalidation_list: ['user 5 in row# 5', 'user 5 in row# 5']
|
||||||
},
|
},
|
||||||
success: []
|
success: []
|
||||||
});
|
});
|
||||||
@@ -115,6 +120,9 @@ define([
|
|||||||
expect($(SELECTORS.bulk_exception_results).text()).toContain('2 learners do not exist in LMS');
|
expect($(SELECTORS.bulk_exception_results).text()).toContain('2 learners do not exist in LMS');
|
||||||
expect($(SELECTORS.bulk_exception_results).text()).toContain('2 learners are already white listed');
|
expect($(SELECTORS.bulk_exception_results).text()).toContain('2 learners are already white listed');
|
||||||
expect($(SELECTORS.bulk_exception_results).text()).toContain('2 learners are not enrolled in course');
|
expect($(SELECTORS.bulk_exception_results).text()).toContain('2 learners are not enrolled in course');
|
||||||
|
expect($(SELECTORS.bulk_exception_results).text()).toContain(
|
||||||
|
'2 learners already appear on the certificate invalidation list'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('toggle message details', function() {
|
it('toggle message details', function() {
|
||||||
|
|||||||
Reference in New Issue
Block a user