Commit 76f0fb0f by Adam Palay

Merge remote-tracking branch 'origin/release' into merge-release-into-master

parents c7552db4 3853538c
...@@ -398,7 +398,7 @@ class CourseMode(models.Model): ...@@ -398,7 +398,7 @@ class CourseMode(models.Model):
@classmethod @classmethod
def has_verified_mode(cls, course_mode_dict): def has_verified_mode(cls, course_mode_dict):
"""Check whether the modes for a course allow a student to pursue a verfied certificate. """Check whether the modes for a course allow a student to pursue a verified certificate.
Args: Args:
course_mode_dict (dictionary mapping course mode slugs to Modes) course_mode_dict (dictionary mapping course mode slugs to Modes)
......
...@@ -260,5 +260,10 @@ def generate_certificate_for_user(request): ...@@ -260,5 +260,10 @@ def generate_certificate_for_user(request):
return HttpResponseBadRequest(msg) return HttpResponseBadRequest(msg)
# Attempt to generate certificate # 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) return HttpResponse(200)
...@@ -217,6 +217,27 @@ class VerifiedUpgradeDeadlineDate(DateSummary): ...@@ -217,6 +217,27 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
return ecommerce_service.checkout_page_url(course_mode.sku) return ecommerce_service.checkout_page_url(course_mode.sku)
return reverse('verify_student_upgrade_and_verify', args=(self.course.id,)) return reverse('verify_student_upgrade_and_verify', args=(self.course.id,))
@property
def is_enabled(self):
"""
Whether or not this summary block should be shown.
By default, the summary is only shown if it has date and the date is in the
future and the user's enrollment is in upsell modes
"""
is_enabled = super(VerifiedUpgradeDeadlineDate, self).is_enabled
if not is_enabled:
return False
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
# Return `true` if user is not enrolled in course
if enrollment_mode is None and is_active is None:
return True
# Show the summary if user enrollment is in which allow user to upsell
return is_active and enrollment_mode in CourseMode.UPSELL_TO_VERIFIED_MODES
@lazy @lazy
def date(self): def date(self):
try: try:
......
...@@ -42,7 +42,9 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -42,7 +42,9 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
days_till_start=1, days_till_start=1,
days_till_end=14, days_till_end=14,
days_till_upgrade_deadline=4, days_till_upgrade_deadline=4,
enroll_user=True,
enrollment_mode=CourseMode.VERIFIED, enrollment_mode=CourseMode.VERIFIED,
course_min_price=100,
days_till_verification_deadline=14, days_till_verification_deadline=14,
verification_status=None, verification_status=None,
sku=None sku=None
...@@ -64,11 +66,13 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -64,11 +66,13 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
course_id=self.course.id, course_id=self.course.id,
mode_slug=enrollment_mode, mode_slug=enrollment_mode,
expiration_datetime=now + timedelta(days=days_till_upgrade_deadline), expiration_datetime=now + timedelta(days=days_till_upgrade_deadline),
min_price=course_min_price,
sku=sku sku=sku
) )
if enroll_user:
enrollment_mode = enrollment_mode or CourseMode.DEFAULT_MODE_SLUG
CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user, mode=enrollment_mode) CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user, mode=enrollment_mode)
else:
CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user)
if days_till_verification_deadline is not None: if days_till_verification_deadline is not None:
VerificationDeadline.objects.create( VerificationDeadline.objects.create(
...@@ -95,21 +99,36 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -95,21 +99,36 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
self.assertEqual(set(type(b) for b in blocks), set(expected_blocks)) self.assertEqual(set(type(b) for b in blocks), set(expected_blocks))
@ddt.data( @ddt.data(
# Before course starts # Verified enrollment with no photo-verification before course start
({}, (CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate)), ({}, (CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate)),
# After course end # Verified enrollment with `approved` photo-verification after course end
({'days_till_start': -10, ({'days_till_start': -10,
'days_till_end': -5, 'days_till_end': -5,
'days_till_upgrade_deadline': -6, 'days_till_upgrade_deadline': -6,
'days_till_verification_deadline': -5, 'days_till_verification_deadline': -5,
'verification_status': 'approved'}, 'verification_status': 'approved'},
(TodaysDate, CourseEndDate)), (TodaysDate, CourseEndDate)),
# No course end date # Verified enrollment with `expired` photo-verification during course run
({'days_till_start': -10,
'verification_status': 'expired'},
(TodaysDate, CourseEndDate, VerificationDeadlineDate)),
# Verified enrollment with `approved` photo-verification during course run
({'days_till_start': -10,
'verification_status': 'approved'},
(TodaysDate, CourseEndDate)),
# Audit enrollment and non-upsell course.
({'days_till_start': -10,
'days_till_upgrade_deadline': None,
'days_till_verification_deadline': None,
'course_min_price': 0,
'enrollment_mode': CourseMode.AUDIT},
(TodaysDate, CourseEndDate)),
# Verified enrollment with *NO* course end date
({'days_till_end': None}, ({'days_till_end': None},
(CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate)), (CourseStartDate, TodaysDate, VerificationDeadlineDate)),
# During course run # Verified enrollment with no photo-verification during course run
({'days_till_start': -1}, ({'days_till_start': -1},
(TodaysDate, CourseEndDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate)), (TodaysDate, CourseEndDate, VerificationDeadlineDate)),
# Verification approved # Verification approved
({'days_till_start': -10, ({'days_till_start': -10,
'days_till_upgrade_deadline': -1, 'days_till_upgrade_deadline': -1,
...@@ -117,13 +136,26 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -117,13 +136,26 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
'verification_status': 'approved'}, 'verification_status': 'approved'},
(TodaysDate, CourseEndDate)), (TodaysDate, CourseEndDate)),
# After upgrade deadline # After upgrade deadline
({'days_till_start': -10, 'days_till_upgrade_deadline': -1}, ({'days_till_start': -10,
'days_till_upgrade_deadline': -1},
(TodaysDate, CourseEndDate, VerificationDeadlineDate)), (TodaysDate, CourseEndDate, VerificationDeadlineDate)),
# After verification deadline # After verification deadline
({'days_till_start': -10, ({'days_till_start': -10,
'days_till_upgrade_deadline': -2, 'days_till_upgrade_deadline': -2,
'days_till_verification_deadline': -1}, 'days_till_verification_deadline': -1},
(TodaysDate, CourseEndDate, VerificationDeadlineDate)) (TodaysDate, CourseEndDate, VerificationDeadlineDate)),
# Un-enrolled user before course start
({'enroll_user': False},
(CourseStartDate, TodaysDate, CourseEndDate, VerifiedUpgradeDeadlineDate)),
# Un-enrolled user during course run
({'days_till_start': -1,
'enroll_user': False},
(TodaysDate, CourseEndDate, VerifiedUpgradeDeadlineDate)),
# Un-enrolled user after course end.
({'enroll_user': False,
'days_till_start': -10,
'days_till_end': -5},
(TodaysDate, CourseEndDate, VerifiedUpgradeDeadlineDate)),
) )
@ddt.unpack @ddt.unpack
def test_enabled_block_types(self, course_options, expected_blocks): def test_enabled_block_types(self, course_options, expected_blocks):
......
...@@ -720,7 +720,6 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase): ...@@ -720,7 +720,6 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase):
response = self.client.post( response = self.client.post(
url, url,
data=json.dumps([self.certificate_exception]),
content_type='application/json' content_type='application/json'
) )
# Assert Success # Assert Success
...@@ -736,24 +735,49 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase): ...@@ -736,24 +735,49 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase):
u"Certificate generation started for white listed students." 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 Test generate certificates exceptions api endpoint returns success
when called with certificate exceptions with empty 'user_id' field when calling with new certificate exception.
""" """
url = reverse( url = reverse(
'generate_certificate_exceptions', 'generate_certificate_exceptions',
kwargs={'course_id': unicode(self.course.id), 'generate_for': 'new'} kwargs={'course_id': unicode(self.course.id), 'generate_for': 'new'}
) )
# assign empty user_id response = self.client.post(
self.certificate_exception.update({'user_id': ''}) 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( response = self.client.post(
url, url,
data=json.dumps([self.certificate_exception]),
content_type='application/json' content_type='application/json'
) )
# Assert Failure # Assert Failure
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
...@@ -764,7 +788,7 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase): ...@@ -764,7 +788,7 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase):
# Assert Message # Assert Message
self.assertEqual( self.assertEqual(
res_json['message'], 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): ...@@ -3032,41 +3032,24 @@ def generate_certificate_exceptions(request, course_id, generate_for=None):
""" """
course_key = CourseKey.from_string(course_id) 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': if generate_for == 'all':
# Generate Certificates for all white listed students # Generate Certificates for all white listed students
students = User.objects.filter( students = 'all_whitelisted'
certificatewhitelist__course_id=course_key,
certificatewhitelist__whitelist=True elif generate_for == 'new':
) students = 'whitelisted_not_generated'
elif not all(users):
# Invalid data, user_id must be present for all certificate exceptions else:
# Invalid data, generate_for must be present for all certificate exceptions
return JsonResponse( return JsonResponse(
{ {
'success': False, '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 status=400
) )
else:
students = User.objects.filter(
id__in=users,
certificatewhitelist__course_id=course_key,
certificatewhitelist__whitelist=True
)
if students: instructor_task.api.generate_certificates_for_students(request, course_key, student_set=students)
# generate certificates for students if 'students' list is not empty
instructor_task.api.generate_certificates_for_students(request, course_key, students=students)
response_payload = { response_payload = {
'success': True, 'success': True,
...@@ -3275,8 +3258,10 @@ def re_validate_certificate(request, course_key, generated_certificate): ...@@ -3275,8 +3258,10 @@ def re_validate_certificate(request, course_key, generated_certificate):
certificate_invalidation.deactivate() certificate_invalidation.deactivate()
# We need to generate certificate only for a single student here # We need to generate certificate only for a single student here
students = [certificate_invalidation.generated_certificate.user] student = certificate_invalidation.generated_certificate.user
instructor_task.api.generate_certificates_for_students(request, course_key, students=students) 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): def validate_request_data_and_get_certificate(certificate_invalidation, course_key):
......
...@@ -45,6 +45,13 @@ from bulk_email.models import CourseEmail ...@@ -45,6 +45,13 @@ from bulk_email.models import CourseEmail
from util import milestones_helpers 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): def get_running_instructor_tasks(course_id):
""" """
Returns a query of InstructorTask objects of running tasks for a given course. 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): ...@@ -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) 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 Submits a task to generate certificates for given students enrolled in the course.
all students if argument 'students' is None
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 AlreadyRunningError if certificates are currently being generated.
""" Raises SpecificStudentIdMissingError if student_set is 'specific_student' and specific_student_id is 'None'
if students: """
task_type = 'generate_certificates_certain_student' if student_set:
students = [student.id for student in students] task_type = 'generate_certificates_student_set'
task_input = {'students': students} task_input = {'student_set': student_set}
if student_set == 'specific_student':
task_type = 'generate_certificates_certain_student'
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: else:
task_type = 'generate_certificates_all_student' task_type = 'generate_certificates_all_student'
task_input = {} task_input = {}
...@@ -466,22 +490,16 @@ def generate_certificates_for_students(request, course_key, students=None): # p ...@@ -466,22 +490,16 @@ def generate_certificates_for_students(request, course_key, students=None): # p
return instructor_task 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 Submits a task to regenerate certificates for given students enrolled in the course.
all students if argument 'students' is None.
Regenerate Certificate only if the status of the existing generated certificate is in 'statuses_to_regenerate' Regenerate Certificate only if the status of the existing generated certificate is in 'statuses_to_regenerate'
list passed in the arguments. list passed in the arguments.
Raises AlreadyRunningError if certificates are currently being generated. Raises AlreadyRunningError if certificates are currently being generated.
""" """
if students: task_type = 'regenerate_certificates_all_student'
task_type = 'regenerate_certificates_certain_student' task_input = {}
students = [student.id for student in students]
task_input = {'students': students}
else:
task_type = 'regenerate_certificates_all_student'
task_input = {}
task_input.update({"statuses_to_regenerate": statuses_to_regenerate}) task_input.update({"statuses_to_regenerate": statuses_to_regenerate})
task_class = generate_certificates task_class = generate_certificates
......
...@@ -1409,30 +1409,56 @@ def generate_students_certificates( ...@@ -1409,30 +1409,56 @@ def generate_students_certificates(
json column, otherwise generate certificates for all enrolled students. json column, otherwise generate certificates for all enrolled students.
""" """
start_time = time() 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
)
students = task_input.get('students', None) 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
)
if students is not None: # Whitelisted students which got certificates already.
enrolled_students = enrolled_students.filter(id__in=students) 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))
task_progress = TaskProgress(action_name, enrolled_students.count(), start_time) students_to_generate_certs_for = students_to_generate_certs_for.exclude(
id__in=certificate_generated_students_ids
)
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, students_to_generate_certs_for.count(), start_time)
current_step = {'step': 'Calculating students already have certificates'} current_step = {'step': 'Calculating students already have certificates'}
task_progress.update_task_state(extra_meta=current_step) task_progress.update_task_state(extra_meta=current_step)
statuses_to_regenerate = task_input.get('statuses_to_regenerate', []) 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 # 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: 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: if statuses_to_regenerate:
# Mark existing generated certificates as 'unavailable' before regenerating # Mark existing generated certificates as 'unavailable' before regenerating
# We need to call this method after "students_require_certificate" otherwise "students_require_certificate" # We need to call this method after "students_require_certificate" otherwise "students_require_certificate"
# would return no results. # 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) task_progress.skipped = task_progress.total - len(students_require_certs)
......
...@@ -24,6 +24,7 @@ from instructor_task.api import ( ...@@ -24,6 +24,7 @@ from instructor_task.api import (
generate_certificates_for_students, generate_certificates_for_students,
regenerate_certificates, regenerate_certificates,
submit_export_ora2_data, submit_export_ora2_data,
SpecificStudentIdMissingError,
) )
from instructor_task.api_helper import AlreadyRunningError from instructor_task.api_helper import AlreadyRunningError
...@@ -295,6 +296,18 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa ...@@ -295,6 +296,18 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
) )
self._test_resubmission(api_call) 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): def test_certificate_generation_history(self):
""" """
Tests that a new record is added whenever certificate generation/regeneration task is submitted. Tests that a new record is added whenever certificate generation/regeneration task is submitted.
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<p class="under-heading"> <p class="under-heading">
<label> <label>
<input type='radio' name='generate-exception-certificates-radio' checked="checked" value='new' aria-describedby='generate-exception-certificates-radio-new-tip'> <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> </label>
<br/> <br/>
<label> <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