Commit 76057fc4 by Syed Hassan Raza Committed by Adam Palay

semantic task_input for certificate generation ECOM-3505

parent 03925474
......@@ -260,5 +260,10 @@ def generate_certificate_for_user(request):
return HttpResponseBadRequest(msg)
# Attempt to generate certificate
generate_certificates_for_students(request, params["course_key"], students=[params["user"]])
generate_certificates_for_students(
request,
params["course_key"],
student_set="specific_student",
specific_student_id=params["user"].id
)
return HttpResponse(200)
......@@ -720,7 +720,6 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase):
response = self.client.post(
url,
data=json.dumps([self.certificate_exception]),
content_type='application/json'
)
# Assert Success
......@@ -736,24 +735,49 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase):
u"Certificate generation started for white listed students."
)
def test_generate_certificate_exceptions_invalid_user_list_error(self):
def test_generate_certificate_exceptions_whitelist_not_generated(self):
"""
Test generate certificates exceptions api endpoint returns error
when called with certificate exceptions with empty 'user_id' field
Test generate certificates exceptions api endpoint returns success
when calling with new certificate exception.
"""
url = reverse(
'generate_certificate_exceptions',
kwargs={'course_id': unicode(self.course.id), 'generate_for': 'new'}
)
# assign empty user_id
self.certificate_exception.update({'user_id': ''})
response = self.client.post(
url,
content_type='application/json'
)
# Assert Success
self.assertEqual(response.status_code, 200)
res_json = json.loads(response.content)
# Assert Request is successful
self.assertTrue(res_json['success'])
# Assert Message
self.assertEqual(
res_json['message'],
u"Certificate generation started for white listed students."
)
def test_generate_certificate_exceptions_generate_for_incorrect_value(self):
"""
Test generate certificates exceptions api endpoint returns error
when calling with generate_for without 'new' or 'all' value.
"""
url = reverse(
'generate_certificate_exceptions',
kwargs={'course_id': unicode(self.course.id), 'generate_for': ''}
)
response = self.client.post(
url,
data=json.dumps([self.certificate_exception]),
content_type='application/json'
)
# Assert Failure
self.assertEqual(response.status_code, 400)
......@@ -764,7 +788,7 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase):
# Assert Message
self.assertEqual(
res_json['message'],
u"Invalid data, user_id must be present for all certificate exceptions."
u'Invalid data, generate_for must be "new" or "all".'
)
......
......@@ -3032,41 +3032,24 @@ def generate_certificate_exceptions(request, course_id, generate_for=None):
"""
course_key = CourseKey.from_string(course_id)
try:
certificate_white_list = json.loads(request.body)
except ValueError:
return JsonResponse({
'success': False,
'message': _('Invalid Json data, Please refresh the page and then try again.')
}, status=400)
users = [exception.get('user_id', False) for exception in certificate_white_list]
if generate_for == 'all':
# Generate Certificates for all white listed students
students = User.objects.filter(
certificatewhitelist__course_id=course_key,
certificatewhitelist__whitelist=True
)
elif not all(users):
# Invalid data, user_id must be present for all certificate exceptions
students = 'all_whitelisted'
elif generate_for == 'new':
students = 'whitelisted_not_generated'
else:
# Invalid data, generate_for must be present for all certificate exceptions
return JsonResponse(
{
'success': False,
'message': _('Invalid data, user_id must be present for all certificate exceptions.'),
'message': _('Invalid data, generate_for must be "new" or "all".'),
},
status=400
)
else:
students = User.objects.filter(
id__in=users,
certificatewhitelist__course_id=course_key,
certificatewhitelist__whitelist=True
)
if students:
# generate certificates for students if 'students' list is not empty
instructor_task.api.generate_certificates_for_students(request, course_key, students=students)
instructor_task.api.generate_certificates_for_students(request, course_key, student_set=students)
response_payload = {
'success': True,
......@@ -3275,8 +3258,10 @@ def re_validate_certificate(request, course_key, generated_certificate):
certificate_invalidation.deactivate()
# We need to generate certificate only for a single student here
students = [certificate_invalidation.generated_certificate.user]
instructor_task.api.generate_certificates_for_students(request, course_key, students=students)
student = certificate_invalidation.generated_certificate.user
instructor_task.api.generate_certificates_for_students(
request, course_key, student_set="specific_student", specific_student_id=student.id
)
def validate_request_data_and_get_certificate(certificate_invalidation, course_key):
......
......@@ -45,6 +45,13 @@ from bulk_email.models import CourseEmail
from util import milestones_helpers
class SpecificStudentIdMissingError(Exception):
"""
Exception indicating that a student id was not provided when generating a certificate for a specific student.
"""
pass
def get_running_instructor_tasks(course_id):
"""
Returns a query of InstructorTask objects of running tasks for a given course.
......@@ -437,17 +444,34 @@ def submit_export_ora2_data(request, course_key):
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
def generate_certificates_for_students(request, course_key, students=None): # pylint: disable=invalid-name
def generate_certificates_for_students(request, course_key, student_set=None, specific_student_id=None): # pylint: disable=invalid-name
"""
Submits a task to generate certificates for given students enrolled in the course or
all students if argument 'students' is None
Submits a task to generate certificates for given students enrolled in the course.
Arguments:
course_key : Course Key
student_set : Semantic for student collection for certificate generation.
Options are:
'all_whitelisted': All Whitelisted students.
'whitelisted_not_generated': Whitelisted students which does not got certificates yet.
'specific_student': Single student for certificate generation.
specific_student_id : Student ID when student_set is 'specific_student'
Raises AlreadyRunningError if certificates are currently being generated.
Raises SpecificStudentIdMissingError if student_set is 'specific_student' and specific_student_id is 'None'
"""
if students:
if student_set:
task_type = 'generate_certificates_student_set'
task_input = {'student_set': student_set}
if student_set == 'specific_student':
task_type = 'generate_certificates_certain_student'
students = [student.id for student in students]
task_input = {'students': students}
if specific_student_id is None:
raise SpecificStudentIdMissingError(
"Attempted to generate certificate for a single student, "
"but no specific student id provided"
)
task_input.update({'specific_student_id': specific_student_id})
else:
task_type = 'generate_certificates_all_student'
task_input = {}
......@@ -466,20 +490,14 @@ def generate_certificates_for_students(request, course_key, students=None): # p
return instructor_task
def regenerate_certificates(request, course_key, statuses_to_regenerate, students=None):
def regenerate_certificates(request, course_key, statuses_to_regenerate):
"""
Submits a task to regenerate certificates for given students enrolled in the course or
all students if argument 'students' is None.
Submits a task to regenerate certificates for given students enrolled in the course.
Regenerate Certificate only if the status of the existing generated certificate is in 'statuses_to_regenerate'
list passed in the arguments.
Raises AlreadyRunningError if certificates are currently being generated.
"""
if students:
task_type = 'regenerate_certificates_certain_student'
students = [student.id for student in students]
task_input = {'students': students}
else:
task_type = 'regenerate_certificates_all_student'
task_input = {}
......
......@@ -1409,30 +1409,56 @@ def generate_students_certificates(
json column, otherwise generate certificates for all enrolled students.
"""
start_time = time()
enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id)
students_to_generate_certs_for = CourseEnrollment.objects.users_enrolled_in(course_id)
student_set = task_input.get('student_set')
if student_set == 'all_whitelisted':
# Generate Certificates for all white listed students.
students_to_generate_certs_for = students_to_generate_certs_for.filter(
certificatewhitelist__course_id=course_id,
certificatewhitelist__whitelist=True
)
elif student_set == 'whitelisted_not_generated':
# All Whitelisted students
students_to_generate_certs_for = students_to_generate_certs_for.filter(
certificatewhitelist__course_id=course_id,
certificatewhitelist__whitelist=True
)
# Whitelisted students which got certificates already.
certificate_generated_students = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id=course_id,
)
certificate_generated_students_ids = set(certificate_generated_students.values_list('user_id', flat=True))
students = task_input.get('students', None)
students_to_generate_certs_for = students_to_generate_certs_for.exclude(
id__in=certificate_generated_students_ids
)
if students is not None:
enrolled_students = enrolled_students.filter(id__in=students)
elif student_set == "specific_student":
specific_student_id = task_input.get('specific_student_id')
students_to_generate_certs_for = students_to_generate_certs_for.filter(id=specific_student_id)
task_progress = TaskProgress(action_name, enrolled_students.count(), start_time)
task_progress = TaskProgress(action_name, students_to_generate_certs_for.count(), start_time)
current_step = {'step': 'Calculating students already have certificates'}
task_progress.update_task_state(extra_meta=current_step)
statuses_to_regenerate = task_input.get('statuses_to_regenerate', [])
if students is not None and not statuses_to_regenerate:
if student_set is not None and not statuses_to_regenerate:
# We want to skip 'filtering students' only when students are given and statuses to regenerate are not
students_require_certs = enrolled_students
students_require_certs = students_to_generate_certs_for
else:
students_require_certs = students_require_certificate(course_id, enrolled_students, statuses_to_regenerate)
students_require_certs = students_require_certificate(
course_id, students_to_generate_certs_for, statuses_to_regenerate
)
if statuses_to_regenerate:
# Mark existing generated certificates as 'unavailable' before regenerating
# We need to call this method after "students_require_certificate" otherwise "students_require_certificate"
# would return no results.
invalidate_generated_certificates(course_id, enrolled_students, statuses_to_regenerate)
invalidate_generated_certificates(course_id, students_to_generate_certs_for, statuses_to_regenerate)
task_progress.skipped = task_progress.total - len(students_require_certs)
......
......@@ -24,6 +24,7 @@ from instructor_task.api import (
generate_certificates_for_students,
regenerate_certificates,
submit_export_ora2_data,
SpecificStudentIdMissingError,
)
from instructor_task.api_helper import AlreadyRunningError
......@@ -295,6 +296,18 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
)
self._test_resubmission(api_call)
def test_certificate_generation_no_specific_student_id(self):
"""
Raises ValueError when student_set is 'specific_student' and 'specific_student_id' is None.
"""
with self.assertRaises(SpecificStudentIdMissingError):
generate_certificates_for_students(
self.create_task_request(self.instructor),
self.course.id,
student_set='specific_student',
specific_student_id=None
)
def test_certificate_generation_history(self):
"""
Tests that a new record is added whenever certificate generation/regeneration task is submitted.
......
......@@ -1628,8 +1628,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
Verify that certificates generated for all eligible students enrolled in a course.
"""
# create 10 students
students = [self.create_student(username='student_{}'.format(i), email='student_{}@example.com'.format(i))
for i in xrange(1, 11)]
students = self._create_students(10)
# mark 2 students to have certificates generated already
for student in students[:2]:
......@@ -1644,40 +1643,157 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
for student in students[2:7]:
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
current_task = Mock()
current_task.update_state = Mock()
instructor_task = Mock()
instructor_task.task_input = json.dumps({'students': None})
with self.assertNumQueries(214):
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = current_task
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
mock_queue.return_value = (0, "Successfully queued")
with patch('instructor_task.models.InstructorTask.objects.get') as instructor_task_object:
instructor_task_object.return_value = instructor_task
result = generate_students_certificates(
None, None, self.course.id, {}, 'certificates generated'
)
self.assertDictContainsSubset(
{
task_input = {'student_set': None}
expected_results = {
'action_name': 'certificates generated',
'total': 10,
'attempted': 8,
'succeeded': 5,
'failed': 3,
'skipped': 2
},
result
}
with self.assertNumQueries(214):
self.assertCertificatesGenerated(task_input, expected_results)
def test_certificate_generation_all_whitelisted(self):
"""
Verify that certificates generated for all white-listed students when using semantic task_input as
`all_whitelisted`.
"""
# create 5 students
students = self._create_students(5)
# white-list 5 students
for student in students:
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
task_input = {'student_set': 'all_whitelisted'}
expected_results = {
'action_name': 'certificates generated',
'total': 5,
'attempted': 5,
'succeeded': 5,
'failed': 0,
'skipped': 0
}
self.assertCertificatesGenerated(task_input, expected_results)
def test_certificate_generation_whitelist_already_generated(self):
"""
Verify that certificates generated for all white-listed students having certifcates already when using
semantic task_input as `all_whitelisted`.
"""
# create 5 students
students = self._create_students(5)
# white-list 5 students
for student in students:
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
# mark 5 students to have certificates generated already
for student in students:
GeneratedCertificateFactory.create(
user=student,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode='honor'
)
task_input = {'student_set': 'all_whitelisted'}
expected_results = {
'action_name': 'certificates generated',
'total': 5,
'attempted': 5,
'succeeded': 5,
'failed': 0,
'skipped': 0
}
self.assertCertificatesGenerated(task_input, expected_results)
def test_certificate_generation_whitelisted_not_generated(self):
"""
Verify that certificates only generated for those students which does not have certificates yet when
using semantic task_input as `whitelisted_not_generated`.
"""
# create 5 students
students = self._create_students(5)
# mark 2 students to have certificates generated already
for student in students[:2]:
GeneratedCertificateFactory.create(
user=student,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode='honor'
)
# white-list 5 students
for student in students:
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
task_input = {'student_set': 'whitelisted_not_generated'}
expected_results = {
'action_name': 'certificates generated',
'total': 3,
'attempted': 3,
'succeeded': 3,
'failed': 0,
'skipped': 0
}
self.assertCertificatesGenerated(
task_input,
expected_results
)
def test_certificate_generation_specific_student(self):
"""
Tests generating a certificate for a specific student.
"""
student = self.create_student(username="Hamnet", email="ham@ardenforest.co.uk")
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
task_input = {
'student_set': 'specific_student',
'specific_student_id': student.id
}
expected_results = {
'action_name': 'certificates generated',
'total': 1,
'attempted': 1,
'succeeded': 1,
'failed': 0,
'skipped': 0,
}
self.assertCertificatesGenerated(task_input, expected_results)
def test_specific_student_not_enrolled(self):
"""
Tests generating a certificate for a specific student if that student
is not enrolled in the course.
"""
student = self.create_student(username="jacques", email="antlers@ardenforest.co.uk")
task_input = {
'student_set': 'specific_student',
'specific_student_id': student.id
}
expected_results = {
'action_name': 'certificates generated',
'total': 1,
'attempted': 1,
'succeeded': 0,
'failed': 1,
'skipped': 0,
}
self.assertCertificatesGenerated(task_input, expected_results)
def test_certificate_regeneration_for_statuses_to_regenerate(self):
"""
Verify that certificates are regenerated for all eligible students enrolled in a course whose generated
certificate statuses lies in the list 'statuses_to_regenerate' given in task_input.
"""
# create 10 students
students = [self.create_student(username='student_{}'.format(i), email='student_{}@example.com'.format(i))
for i in xrange(1, 11)]
students = self._create_students(10)
# mark 2 students to have certificates generated already
for student in students[:2]:
......@@ -1710,31 +1826,22 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
for student in students[:7]:
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
current_task = Mock()
current_task.update_state = Mock()
# Certificates should be regenerated for students having generated certificates with status
# 'downloadable' or 'error' which are total of 5 students in this test case
task_input = {'statuses_to_regenerate': [CertificateStatuses.downloadable, CertificateStatuses.error]}
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = current_task
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
mock_queue.return_value = (0, "Successfully queued")
result = generate_students_certificates(
None, None, self.course.id, task_input, 'certificates generated'
)
self.assertDictContainsSubset(
{
expected_results = {
'action_name': 'certificates generated',
'total': 10,
'attempted': 5,
'succeeded': 5,
'failed': 0,
'skipped': 5
},
result
}
self.assertCertificatesGenerated(
task_input,
expected_results
)
def test_certificate_regeneration_with_expected_failures(self):
......@@ -1746,8 +1853,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
default_grade = '-1'
# create 10 students
students = [self.create_student(username='student_{}'.format(i), email='student_{}@example.com'.format(i))
for i in xrange(1, 11)]
students = self._create_students(10)
# mark 2 students to have certificates generated already
for student in students[:2]:
......@@ -1796,32 +1902,21 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
for student in students[:7]:
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
current_task = Mock()
current_task.update_state = Mock()
# Regenerated certificates for students having generated certificates with status
# 'deleted' or 'generating'
task_input = {'statuses_to_regenerate': [CertificateStatuses.deleted, CertificateStatuses.generating]}
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = current_task
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
mock_queue.return_value = (0, "Successfully queued")
result = generate_students_certificates(
None, None, self.course.id, task_input, 'certificates generated'
)
self.assertDictContainsSubset(
{
expected_results = {
'action_name': 'certificates generated',
'total': 10,
'attempted': 5,
'succeeded': 2,
'failed': 3,
'skipped': 5
},
result
)
}
self.assertCertificatesGenerated(task_input, expected_results)
generated_certificates = GeneratedCertificate.eligible_certificates.filter(
user__in=students,
course_id=self.course.id,
......@@ -1852,8 +1947,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
default_grade = '-1'
# create 10 students
students = [self.create_student(username='student_{}'.format(i), email='student_{}@example.com'.format(i))
for i in xrange(1, 11)]
students = self._create_students(10)
# mark 2 students to have certificates generated already
for student in students[:2]:
......@@ -1899,9 +1993,6 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
for student in students[:]:
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
current_task = Mock()
current_task.update_state = Mock()
# Regenerated certificates for students having generated certificates with status
# 'downloadable', 'error' or 'generating'
task_input = {
......@@ -1912,24 +2003,18 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
]
}
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = current_task
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
mock_queue.return_value = (0, "Successfully queued")
result = generate_students_certificates(
None, None, self.course.id, task_input, 'certificates generated'
)
self.assertDictContainsSubset(
{
expected_results = {
'action_name': 'certificates generated',
'total': 10,
'attempted': 8,
'succeeded': 8,
'failed': 0,
'skipped': 2
},
result
}
self.assertCertificatesGenerated(
task_input,
expected_results
)
generated_certificates = GeneratedCertificate.eligible_certificates.filter(
......@@ -1963,8 +2048,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
Verify that certificates are regenerated for all students passed in task_input.
"""
# create 10 students
students = [self.create_student(username='student_{}'.format(i), email='student_{}@example.com'.format(i))
for i in xrange(1, 11)]
students = self._create_students(10)
# mark 2 students to have certificates generated already
for student in students[:2]:
......@@ -2006,12 +2090,27 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
for student in students[:7]:
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
current_task = Mock()
current_task.update_state = Mock()
# Certificates should be regenerated for students having generated certificates with status
# 'downloadable' or 'error' which are total of 5 students in this test case
task_input = {'students': [student.id for student in students]}
task_input = {'student_set': "all_whitelisted"}
expected_results = {
'action_name': 'certificates generated',
'total': 7,
'attempted': 7,
'succeeded': 7,
'failed': 0,
'skipped': 0,
}
self.assertCertificatesGenerated(task_input, expected_results)
def assertCertificatesGenerated(self, task_input, expected_results):
"""
Generate certificates for the given task_input and compare with expected_results.
"""
current_task = Mock()
current_task.update_state = Mock()
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = current_task
......@@ -2022,17 +2121,22 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
)
self.assertDictContainsSubset(
{
'action_name': 'certificates generated',
'total': 10,
'attempted': 10,
'succeeded': 7,
'failed': 3,
'skipped': 0,
},
expected_results,
result
)
def _create_students(self, number_of_students):
"""
Create Students for course.
"""
return [
self.create_student(
username='student_{}'.format(index),
email='student_{}@example.com'.format(index)
)
for index in xrange(number_of_students)
]
class TestInstructorOra2Report(SharedModuleStoreTestCase):
"""
......
......@@ -2,7 +2,8 @@
<p class="under-heading">
<label>
<input type='radio' name='generate-exception-certificates-radio' checked="checked" value='new' aria-describedby='generate-exception-certificates-radio-new-tip'>
<span id='generate-exception-certificates-radio-new-tip'><%- gettext('Generate a Certificate for all ') %><strong><%- gettext('New') %></strong> <%- gettext('additions to the Exception list') %></span>
<span id='generate-exception-certificates-radio-new-tip'><%- gettext('Generate certificates for all users on the Exception list for whom certificates have not yet been run') %></span>
</label>
<br/>
<label>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment