def generate_user_certificates(student, course_key, course=None, insecure=False, generation_mode='batch'): """ It will add the add-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate to the queue, the task will have status 'error'. It also emits `edx.certificate.created` event for analytics. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. insecure - (Boolean) generation_mode - who has requested certificate generation. Its value should `batch` in case of django command and `self` if student initiated the request. """ xqueue = XQueueCertInterface() if insecure: xqueue.use_https = False generate_pdf = not has_html_certificates_enabled(course_key, course) status, cert = xqueue.add_cert(student, course_key, course=course, generate_pdf=generate_pdf) if status in [CertificateStatuses.generating, CertificateStatuses.downloadable]: emit_certificate_event('created', student, course_key, course, { 'user_id': student.id, 'course_id': unicode(course_key), 'certificate_id': cert.verify_uuid, 'enrollment_mode': cert.mode, 'generation_mode': generation_mode }) return status
def request_certificate(request): """Request the on-demand creation of a certificate for some user, course. A request doesn't imply a guarantee that such a creation will take place. We intentionally use the same machinery as is used for doing certification at the end of a course run, so that we can be sure users get graded and then if and only if they pass, do they get a certificate issued. """ if request.method == "POST": if request.user.is_authenticated(): xqci = XQueueCertInterface() username = request.user.username student = User.objects.get(username=username) course_key = SlashSeparatedCourseKey.from_deprecated_string(request.POST.get("course_id")) course = modulestore().get_course(course_key, depth=2) status = certificate_status_for_student(student, course_key)["status"] if status in [CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error]: logger.info( "Grading and certification requested for user {} in course {} via /request_certificate call".format( username, course_key ) ) status = xqci.add_cert(student, course_key, course=course) return HttpResponse(json.dumps({"add_status": status}), mimetype="application/json") return HttpResponse(json.dumps({"add_status": "ERRORANONYMOUSUSER"}), mimetype="application/json")
def handle(self, *args, **options): user = options['username'] course_id = options['course'] if not (course_id and user): raise CommandError( 'both course id and student username are required') student = None print "Fetching enrollment for student {0} in {1}".format( user, course_id) if '@' in user: student = User.objects.get(email=user, courseenrollment__course_id=course_id) else: student = User.objects.get(username=user, courseenrollment__course_id=course_id) print "Fetching course data for {0}".format(course_id) course = modulestore().get_instance( course_id, CourseDescriptor.id_to_location(course_id), depth=2) if not options['noop']: # Add the certificate request to the queue xq = XQueueCertInterface() if options['insecure']: xq.use_https = False ret = xq.regen_cert(student, course_id, course=course, forced_grade=options['grade_value'], template_file=options['template_file']) print '{0} - {1}'.format(student, ret) else: print "noop option given, skipping work queueing..."
def generate_example_certificates(course_key): """Generate example certificates for a course. Example certificates are used to validate that certificates are configured correctly for the course. Staff members can view the example certificates before enabling the self-generated certificates button for students. Several example certificates may be generated for a course. For example, if a course offers both verified and honor certificates, examples of both types of certificate will be generated. If an error occurs while starting the certificate generation job, the errors will be recorded in the database and can be retrieved using `example_certificate_status()`. Arguments: course_key (CourseKey): The course identifier. Returns: None """ xqueue = XQueueCertInterface() for cert in ExampleCertificateSet.create_example_set(course_key): xqueue.add_example_cert(cert)
def request_certificate(request): """Request the on-demand creation of a certificate for some user, course. A request doesn't imply a guarantee that such a creation will take place. We intentionally use the same machinery as is used for doing certification at the end of a course run, so that we can be sure users get graded and then if and only if they pass, do they get a certificate issued. """ if request.method == "POST": if request.user.is_authenticated(): xqci = XQueueCertInterface() username = request.user.username student = User.objects.get(username=username) course_id = request.POST.get('course_id') course = modulestore().get_instance( course_id, CourseDescriptor.id_to_location(course_id), depth=2) status = certificate_status_for_student(student, course_id)['status'] if status in [ CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error ]: logger.info( 'Grading and certification requested for user {} in course {} via /request_certificate call' .format(username, course_id)) status = xqci.add_cert(student, course_id, course=course) return HttpResponse(json.dumps({'add_status': status}), mimetype='application/json') return HttpResponse(json.dumps({'add_status': 'ERRORANONYMOUSUSER'}), mimetype='application/json')
def handle(self, *args, **options): user = options['username'] course_id = options['course'] if not (course_id and user): raise CommandError('both course id and student username are required') student = None print "Fetching enrollment for student {0} in {1}".format(user, course_id) if '@' in user: student = User.objects.get(email=user, courseenrollment__course_id=course_id) else: student = User.objects.get(username=user, courseenrollment__course_id=course_id) print "Fetching course data for {0}".format(course_id) course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2) if not options['noop']: # Add the certificate request to the queue xq = XQueueCertInterface() if options['insecure']: xq.use_https = False ret = xq.regen_cert(student, course_id, course=course) print '{0} - {1}'.format(student, ret) else: print "noop option given, skipping work queueing..."
def regenerate_user_certificates(student, course_key, course=None, forced_grade=None, template_file=None, insecure=False): """ It will add the regen-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate to the queue, the task will have status 'error'. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. grade_value - The grade string, such as "Distinction" template_file - The template file used to render this certificate insecure - (Boolean) """ xqueue = XQueueCertInterface() if insecure: xqueue.use_https = False generate_pdf = not has_html_certificates_enabled(course_key, course) return xqueue.regen_cert(student, course_key, course=course, forced_grade=forced_grade, template_file=template_file, generate_pdf=generate_pdf)
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): """Test the "add to queue" operation of the XQueue interface. """ def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() def test_add_cert_callback_url(self): with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url'])
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): """Test the "add to queue" operation of the XQueue interface. """ def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() def test_add_cert_callback_url(self): with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url'])
def regenerate_user_certificates(student, course_key, course=None, forced_grade=None, template_file=None, insecure=False): """ It will add the regen-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate to the queue, the task will have status 'error'. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. grade_value - The grade string, such as "Distinction" template_file - The template file used to render this certificate insecure - (Boolean) """ xqueue = XQueueCertInterface() if insecure: xqueue.use_https = False generate_pdf = not has_html_certificates_enabled(course_key, course) return xqueue.regen_cert( student, course_key, course=course, forced_grade=forced_grade, template_file=template_file, generate_pdf=generate_pdf )
def generate_user_certificates(student, course_key, course=None, insecure=False, generation_mode='batch', forced_grade=None): """ It will add the add-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate to the queue, the task will have status 'error'. It also emits `edx.certificate.created` event for analytics. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. insecure - (Boolean) generation_mode - who has requested certificate generation. Its value should `batch` in case of django command and `self` if student initiated the request. forced_grade - a string indicating to replace grade parameter. if present grading will be skipped. """ xqueue = XQueueCertInterface() if insecure: xqueue.use_https = False if not course: course = modulestore().get_course(course_key, depth=0) generate_pdf = not has_html_certificates_enabled(course) cert = xqueue.add_cert(student, course_key, course=course, generate_pdf=generate_pdf, forced_grade=forced_grade) # If cert_status is not present in certificate valid_statuses (for example unverified) then # add_cert returns None and raises AttributeError while accesing cert attributes. if cert is None: return if CertificateStatuses.is_passing_status(cert.status): emit_certificate_event( 'created', student, course_key, course, { 'user_id': student.id, 'course_id': unicode(course_key), 'certificate_id': cert.verify_uuid, 'enrollment_mode': cert.mode, 'generation_mode': generation_mode }) return cert.status
def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface()
def generate_user_certificates(student, course_key, course=None, insecure=False, generation_mode='batch', forced_grade=None): """ It will add the add-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate to the queue, the task will have status 'error'. It also emits `edx.certificate.created` event for analytics. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. insecure - (Boolean) generation_mode - who has requested certificate generation. Its value should `batch` in case of django command and `self` if student initiated the request. forced_grade - a string indicating to replace grade parameter. if present grading will be skipped. """ xqueue = XQueueCertInterface() if insecure: xqueue.use_https = False if not course: course = modulestore().get_course(course_key, depth=0) generate_pdf = not has_html_certificates_enabled(course) cert = xqueue.add_cert( student, course_key, course=course, generate_pdf=generate_pdf, forced_grade=forced_grade ) # If cert_status is not present in certificate valid_statuses (for example unverified) then # add_cert returns None and raises AttributeError while accesing cert attributes. if cert is None: return if CertificateStatuses.is_passing_status(cert.status): emit_certificate_event('created', student, course_key, course, { 'user_id': student.id, 'course_id': unicode(course_key), 'certificate_id': cert.verify_uuid, 'enrollment_mode': cert.mode, 'generation_mode': generation_mode }) return cert.status
def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() self.user_2 = UserFactory.create() SoftwareSecurePhotoVerificationFactory.create(user=self.user_2, status='approved')
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): """Test the "add to queue" operation of the XQueue interface. """ def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() def test_add_cert_callback_url(self): with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) def test_no_create_action_in_queue_for_html_view_certs(self): """ Tests there is no certificate create message in the queue if generate_pdf is False """ with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: self.xqueue.add_cert(self.user, self.course.id, generate_pdf=False) # Verify that add_cert method does not add message to queue self.assertFalse(mock_send.called) certificate = GeneratedCertificate.objects.get( user=self.user, course_id=self.course.id) self.assertEqual(certificate.status, CertificateStatuses.downloadable) self.assertIsNotNone(certificate.verify_uuid)
def generate_user_certificates( student, course_key, course=None, insecure=False, generation_mode="batch", forced_grade=None ): """ It will add the add-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate to the queue, the task will have status 'error'. It also emits `edx.certificate.created` event for analytics. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. insecure - (Boolean) generation_mode - who has requested certificate generation. Its value should `batch` in case of django command and `self` if student initiated the request. forced_grade - a string indicating to replace grade parameter. if present grading will be skipped. """ xqueue = XQueueCertInterface() if insecure: xqueue.use_https = False generate_pdf = not has_html_certificates_enabled(course_key, course) status, cert = xqueue.add_cert( student, course_key, course=course, generate_pdf=generate_pdf, forced_grade=forced_grade ) if status in [CertificateStatuses.generating, CertificateStatuses.downloadable]: emit_certificate_event( "created", student, course_key, course, { "user_id": student.id, "course_id": unicode(course_key), "certificate_id": cert.verify_uuid, "enrollment_mode": cert.mode, "generation_mode": generation_mode, }, ) return status
def handle(self, *args, **options): if options['course']: # try to parse out the course from the serialized form try: course_id = CourseKey.from_string(options['course']) except InvalidKeyError: print( "Course id {} could not be parsed as a CourseKey; falling back to SSCK.from_dep_str" .format(options['course'])) course_id = SlashSeparatedCourseKey.from_deprecated_string( options['course']) else: raise CommandError("You must specify a course") user = options['username'] if not (course_id and user): raise CommandError( 'both course id and student username are required') student = None print "Fetching enrollment for student {0} in {1}".format( user, course_id) if '@' in user: student = User.objects.get(email=user, courseenrollment__course_id=course_id) else: student = User.objects.get(username=user, courseenrollment__course_id=course_id) print "Fetching course data for {0}".format(course_id) course = modulestore().get_course(course_id, depth=2) if not options['noop']: # Add the certificate request to the queue xq = XQueueCertInterface() if options['insecure']: xq.use_https = False ret = xq.regen_cert(student, course_id, course=course, forced_grade=options['grade_value'], template_file=options['template_file']) print '{0} - {1}'.format(student, ret) else: print "noop option given, skipping work queueing..."
def generate_user_certificates(student, course): """ It will add the add-cert request into the xqueue. Args: student (object): user course (object): course Returns: returns status of generated certificate """ xqueue = XQueueCertInterface() ret = xqueue.add_cert(student, course.id, course=course) log.info((u"Added a certificate generation task to the XQueue " u"for student %s in course '%s'. " u"The new certificate status is '%s'."), student.id, unicode(course.id), ret) return ret
def generate_user_certificates(student, course_key, course=None): """ It will add the add-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate to the queue, the task will have status 'error'. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. """ xqueue = XQueueCertInterface() xqueue.add_cert(student, course_key, course=course)
def request_certificate(request): """Request the on-demand creation of a certificate for some user, course. A request doesn't imply a guarantee that such a creation will take place. We intentionally use the same machinery as is used for doing certification at the end of a course run, so that we can be sure users get graded and then if and only if they pass, do they get a certificate issued. """ # Memoize user information; return error if it's invalid user = None username = '' try: user = request.user username = user.username except AttributeError: return HttpResponse(json.dumps({'add_status': 'error', 'error': 'ERRORBADREQUEST'}), mimetype='application/json') # It is an error to hit this endpoint with anything but a POST if request.method != "POST": return HttpResponse(json.dumps({'add_status': 'error', 'error': 'ERRORNOPOST'}), mimetype='application/json') # It is an error to hit this endpoint as an anonymous/nonregistered user if not (user.is_authenticated() and UserProfile.has_registered(user)): return HttpResponse(json.dumps({'add_status': 'error', 'error': 'ERRORANONYMOUSUSER'}), mimetype='application/json') xq = XQueueCertInterface() student = User.objects.get(username=username) course_key = SlashSeparatedCourseKey.from_deprecated_string(request.POST.get('course_id')) course = modulestore().get_course(course_key, depth=2) title = 'None' if use_cme: titlelist = CmeUserProfile.objects.filter(user=student).values('professional_designation') if len(titlelist): title = titlelist[0]['professional_designation'] status = certificate_status_for_student(student, course_key)['status'] if status in [CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error]: log_msg = u'Grading and certification requested for user %s in course %s via /request_certificate call' logger.info(log_msg, username, course_key) status = xq.add_cert(student, course_key, course=course, title=title) return HttpResponse(json.dumps({'add_status': status, 'error': ''}), mimetype='application/json')
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): """Test the "add to queue" operation of the XQueue interface. """ def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() def test_add_cert_callback_url(self): with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) def test_no_create_action_in_queue_for_html_view_certs(self): """ Tests there is no certificate create message in the queue if generate_pdf is False """ with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: self.xqueue.add_cert(self.user, self.course.id, generate_pdf=False) # Verify that add_cert method does not add message to queue self.assertFalse(mock_send.called) certificate = GeneratedCertificate.objects.get(user=self.user, course_id=self.course.id) self.assertEqual(certificate.status, CertificateStatuses.downloadable) self.assertIsNotNone(certificate.verify_uuid)
def request_certificate(request): """Request the on-demand creation of a certificate for some user, course. A request doesn't imply a guarantee that such a creation will take place. We intentionally use the same machinery as is used for doing certification at the end of a course run, so that we can be sure users get graded and then if and only if they pass, do they get a certificate issued. """ if request.method == "POST": if request.user.is_authenticated(): xqci = XQueueCertInterface() username = request.user.username student = User.objects.get(username=username) course_id = request.POST.get('course_id') course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2) status = certificate_status_for_student(student, course_id)['status'] if status in [CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error]: logger.info('Grading and certification requested for user {} in course {} via /request_certificate call'.format(username, course_id)) status = xqci.add_cert(student, course_id, course=course) return HttpResponse(json.dumps({'add_status': status}), mimetype='application/json') return HttpResponse(json.dumps({'add_status': 'ERRORANONYMOUSUSER'}), mimetype='application/json')
def generate_user_certificates(student, course_key, course=None, insecure=False): """ It will add the add-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate to the queue, the task will have status 'error'. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. insecure - (Boolean) """ xqueue = XQueueCertInterface() if insecure: xqueue.use_https = False generate_pdf = not has_html_certificates_enabled(course_key, course) return xqueue.add_cert(student, course_key, course=course, generate_pdf=generate_pdf)
def generate_user_certificates(student, course_key, course=None): """ It will add the add-cert request into the xqueue. Args: student (User) course_key (CourseKey) Keyword Arguments: course (Course): Optionally provide the course object; if not provided it will be loaded. Returns: returns status of generated certificate """ xqueue = XQueueCertInterface() ret = xqueue.add_cert(student, course_key, course=course) log.info((u"Added a certificate generation task to the XQueue " u"for student %s in course '%s'. " u"The new certificate status is '%s'."), student.id, unicode(course_key), ret) return ret
def handle(self, *args, **options): if options['course']: # try to parse out the course from the serialized form try: course_id = CourseKey.from_string(options['course']) except InvalidKeyError: print("Course id {} could not be parsed as a CourseKey; falling back to SSCK.from_dep_str".format(options['course'])) course_id = SlashSeparatedCourseKey.from_deprecated_string(options['course']) else: raise CommandError("You must specify a course") user = options['username'] if not (course_id and user): raise CommandError('both course id and student username are required') student = None print "Fetching enrollment for student {0} in {1}".format(user, course_id) if '@' in user: student = User.objects.get(email=user, courseenrollment__course_id=course_id) else: student = User.objects.get(username=user, courseenrollment__course_id=course_id) print "Fetching course data for {0}".format(course_id) course = modulestore().get_course(course_id, depth=2) if not options['noop']: # Add the certificate request to the queue xq = XQueueCertInterface() if options['insecure']: xq.use_https = False ret = xq.regen_cert(student, course_id, course=course, forced_grade=options['grade_value'], template_file=options['template_file']) print '{0} - {1}'.format(student, ret) else: print "noop option given, skipping work queueing..."
def handle(self, *args, **options): # Will only generate a certificate if the current # status is in the unavailable state, can be set # to something else with the force flag if options['force']: valid_statuses = getattr(CertificateStatuses, options['force']) else: valid_statuses = [CertificateStatuses.unavailable] # Print update after this many students STATUS_INTERVAL = 500 if options['course']: ended_courses = [options['course']] else: # Find all courses that have ended ended_courses = [] for course_id in [course # all courses in COURSE_LISTINGS for sub in settings.COURSE_LISTINGS for course in settings.COURSE_LISTINGS[sub]]: course_loc = CourseDescriptor.id_to_location(course_id) course = modulestore().get_instance(course_id, course_loc) if course.has_ended(): ended_courses.append(course_id) for course_id in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_instance( course_id, CourseDescriptor.id_to_location(course_id), depth=2) print "Fetching enrolled students for {0}".format(course_id) enrolled_students = User.objects.filter( courseenrollment__course_id=course_id).prefetch_related( "groups").order_by('username') xq = XQueueCertInterface() total = enrolled_students.count() count = 0 start = datetime.datetime.now() for student in enrolled_students: count += 1 if count % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now() - start timeleft = diff * (total - count) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, seconds = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now() if certificate_status_for_student( student, course_id)['status'] in valid_statuses: if not options['noop']: # Add the certificate request to the queue ret = xq.add_cert(student, course_id, course=course) if ret == 'generating': print '{0} - {1}'.format(student, ret)
def handle(self, *args, **options): # Will only generate a certificate if the current # status is in the unavailable state, can be set # to something else with the force flag if options['force']: valid_statuses = getattr(CertificateStatuses, options['force']) else: valid_statuses = [CertificateStatuses.unavailable] # Print update after this many students STATUS_INTERVAL = 500 if options['course']: # try to parse out the course from the serialized form try: course = CourseKey.from_string(options['course']) except InvalidKeyError: print( "Course id {} could not be parsed as a CourseKey; falling back to SSCK.from_dep_str" .format(options['course'])) course = SlashSeparatedCourseKey.from_deprecated_string( options['course']) ended_courses = [course] else: raise CommandError("You must specify a course") for course_key in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_course(course_key, depth=2) print "Fetching enrolled students for {0}".format( course_key.to_deprecated_string()) enrolled_students = User.objects.filter( courseenrollment__course_id=course_key) xq = XQueueCertInterface() if options['insecure']: xq.use_https = False total = enrolled_students.count() count = 0 start = datetime.datetime.now(UTC) for student in enrolled_students: count += 1 if count % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now(UTC) - start timeleft = diff * (total - count) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, seconds = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now(UTC) if certificate_status_for_student( student, course_key)['status'] in valid_statuses: if not options['noop']: # Add the certificate request to the queue ret = xq.add_cert(student, course_key, course=course) if ret == 'generating': print '{0} - {1}'.format(student, ret)
def setUp(self): super(XQueueCertInterfaceExampleCertificateTest, self).setUp() self.xqueue = XQueueCertInterface()
class XQueueCertInterfaceExampleCertificateTest(TestCase): """Tests for the XQueue interface for certificate generation. """ COURSE_KEY = CourseLocator(org='test', course='test', run='test') TEMPLATE = 'test.pdf' DESCRIPTION = 'test' ERROR_MSG = 'Kaboom!' def setUp(self): super(XQueueCertInterfaceExampleCertificateTest, self).setUp() self.xqueue = XQueueCertInterface() def test_add_example_cert(self): cert = self._create_example_cert() with self._mock_xqueue() as mock_send: self.xqueue.add_example_cert(cert) # Verify that the correct payload was sent to the XQueue self._assert_queue_task(mock_send, cert) # Verify the certificate status self.assertEqual(cert.status, ExampleCertificate.STATUS_STARTED) def test_add_example_cert_error(self): cert = self._create_example_cert() with self._mock_xqueue(success=False): self.xqueue.add_example_cert(cert) # Verify the error status of the certificate self.assertEqual(cert.status, ExampleCertificate.STATUS_ERROR) self.assertIn(self.ERROR_MSG, cert.error_reason) def _create_example_cert(self): """Create an example certificate. """ cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY) return ExampleCertificate.objects.create( example_cert_set=cert_set, description=self.DESCRIPTION, template=self.TEMPLATE ) @contextmanager def _mock_xqueue(self, success=True): """Mock the XQueue method for sending a task to the queue. """ with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) if success else (1, self.ERROR_MSG) yield mock_send def _assert_queue_task(self, mock_send, cert): """Check that the task was added to the queue. """ expected_header = { 'lms_key': cert.access_key, 'lms_callback_url': 'https://edx.org/update_example_certificate?key={key}'.format(key=cert.uuid), 'queue_name': 'certificates' } expected_body = { 'action': 'create', 'username': cert.uuid, 'name': u'John Doë', 'course_id': unicode(self.COURSE_KEY), 'template_pdf': 'test.pdf', 'example_certificate': True } self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) actual_body = json.loads(kwargs['body']) self.assertEqual(expected_header, actual_header) self.assertEqual(expected_body, actual_body)
def handle(self, *args, **options): # Scrub the username from the log message cleaned_options = copy.copy(options) if 'username' in cleaned_options: cleaned_options['username'] = '******' LOGGER.info( ( u"Starting to create tasks to regenerate certificates " u"with arguments %s and options %s" ), unicode(args), unicode(cleaned_options) ) if options['course']: # try to parse out the course from the serialized form try: course_id = CourseKey.from_string(options['course']) except InvalidKeyError: LOGGER.warning( ( u"Course id %s could not be parsed as a CourseKey; " u"falling back to SlashSeparatedCourseKey.from_deprecated_string()" ), options['course'] ) course_id = SlashSeparatedCourseKey.from_deprecated_string(options['course']) else: raise CommandError("You must specify a course") user = options['username'] if not (course_id and user): raise CommandError('both course id and student username are required') student = None if '@' in user: student = User.objects.get(email=user, courseenrollment__course_id=course_id) else: student = User.objects.get(username=user, courseenrollment__course_id=course_id) course = modulestore().get_course(course_id, depth=2) if not options['noop']: LOGGER.info( ( u"Adding task to the XQueue to generate a certificate " u"for student %s in course '%s'." ), student.id, course_id ) # Add the certificate request to the queue xqueue_interface = XQueueCertInterface() if options['insecure']: xqueue_interface.use_https = False ret = xqueue_interface.regen_cert( student, course_id, course=course, forced_grade=options['grade_value'], template_file=options['template_file'] ) LOGGER.info( ( u"Added a certificate regeneration task to the XQueue " u"for student %s in course '%s'. " u"The new certificate status is '%s'." ), student.id, unicode(course_id), ret ) else: LOGGER.info( ( u"Skipping certificate generation for " u"student %s in course '%s' " u"because the noop flag is set." ), student.id, unicode(course_id) ) LOGGER.info( ( u"Finished regenerating certificates command for " u"user %s and course '%s'." ), student.id, unicode(course_id) )
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): """Test the "add to queue" operation of the XQueue interface. """ def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() self.user_2 = UserFactory.create() SoftwareSecurePhotoVerificationFactory.create(user=self.user_2, status='approved') def test_add_cert_callback_url(self): with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) def test_no_create_action_in_queue_for_html_view_certs(self): """ Tests there is no certificate create message in the queue if generate_pdf is False """ with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: self.xqueue.add_cert(self.user, self.course.id, generate_pdf=False) # Verify that add_cert method does not add message to queue self.assertFalse(mock_send.called) certificate = GeneratedCertificate.eligible_certificates.get( user=self.user, course_id=self.course.id) self.assertEqual(certificate.status, CertificateStatuses.downloadable) self.assertIsNotNone(certificate.verify_uuid) @ddt.data('honor', 'audit') def test_add_cert_with_honor_certificates(self, mode): """Test certificates generations for honor and audit modes.""" template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format( id=self.course.id) mock_send = self.add_cert_to_queue(mode) if CourseMode.is_eligible_for_certificate(mode): self.assert_certificate_generated(mock_send, mode, template_name) else: self.assert_ineligible_certificate_generated(mock_send, mode) @ddt.data('credit', 'verified') def test_add_cert_with_verified_certificates(self, mode): """Test if enrollment mode is verified or credit along with valid software-secure verification than verified certificate should be generated. """ template_name = 'certificate-template-{id.org}-{id.course}-verified.pdf'.format( id=self.course.id) mock_send = self.add_cert_to_queue(mode) self.assert_certificate_generated(mock_send, 'verified', template_name) def test_ineligible_cert_whitelisted(self): """Test that audit mode students can receive a certificate if they are whitelisted.""" # Enroll as audit CourseEnrollmentFactory(user=self.user_2, course_id=self.course.id, is_active=True, mode='audit') # Whitelist student CertificateWhitelistFactory(course_id=self.course.id, user=self.user_2) # Generate certs with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) # Assert cert generated correctly self.assertTrue(mock_send.called) certificate = GeneratedCertificate.certificate_for_student( self.user_2, self.course.id) self.assertIsNotNone(certificate) self.assertEqual(certificate.mode, 'audit') def add_cert_to_queue(self, mode): """ Dry method for course enrollment and adding request to queue. Returns a mock object containing information about the `XQueueInterface.send_to_queue` method, which can be used in other assertions. """ CourseEnrollmentFactory( user=self.user_2, course_id=self.course.id, is_active=True, mode=mode, ) with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) return mock_send def assert_certificate_generated(self, mock_send, expected_mode, expected_template_name): """ Assert that a certificate was generated with the correct mode and template type. """ # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) body = json.loads(kwargs['body']) self.assertIn(expected_template_name, body['template_pdf']) certificate = GeneratedCertificate.eligible_certificates.get( user=self.user_2, course_id=self.course.id) self.assertEqual(certificate.mode, expected_mode) def assert_ineligible_certificate_generated(self, mock_send, expected_mode): """ Assert that an ineligible certificate was generated with the correct mode. """ # Ensure the certificate was not generated self.assertFalse(mock_send.called) certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member user=self.user_2, course_id=self.course.id) self.assertFalse(certificate.eligible_for_certificate) self.assertEqual(certificate.mode, expected_mode)
def handle(self, *args, **options): LOGGER.info( ( u"Starting to create tasks for ungenerated certificates " u"with arguments %s and options %s" ), unicode(args), unicode(options) ) # Will only generate a certificate if the current # status is in the unavailable state, can be set # to something else with the force flag if options['force']: valid_statuses = [getattr(CertificateStatuses, options['force'])] else: valid_statuses = [CertificateStatuses.unavailable] # Print update after this many students STATUS_INTERVAL = 500 if options['course']: # try to parse out the course from the serialized form try: course = CourseKey.from_string(options['course']) except InvalidKeyError: LOGGER.warning( ( u"Course id %s could not be parsed as a CourseKey; " u"falling back to SlashSeparatedCourseKey.from_deprecated_string()" ), options['course'] ) course = SlashSeparatedCourseKey.from_deprecated_string(options['course']) ended_courses = [course] else: raise CommandError("You must specify a course") for course_key in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_course(course_key, depth=2) enrolled_students = User.objects.filter( courseenrollment__course_id=course_key ) xq = XQueueCertInterface() if options['insecure']: xq.use_https = False total = enrolled_students.count() count = 0 start = datetime.datetime.now(UTC) for student in enrolled_students: count += 1 if count % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now(UTC) - start timeleft = diff * (total - count) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, _seconds = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now(UTC) cert_status = certificate_status_for_student(student, course_key)['status'] LOGGER.info( ( u"Student %s has certificate status '%s' " u"in course '%s'" ), student.id, cert_status, unicode(course_key) ) if cert_status in valid_statuses: if not options['noop']: # Add the certificate request to the queue ret = xq.add_cert(student, course_key, course=course) if ret == 'generating': LOGGER.info( ( u"Added a certificate generation task to the XQueue " u"for student %s in course '%s'. " u"The new certificate status is '%s'." ), student.id, unicode(course_key), ret ) else: LOGGER.info( ( u"Skipping certificate generation for " u"student %s in course '%s' " u"because the noop flag is set." ), student.id, unicode(course_key) ) else: LOGGER.info( ( u"Skipped student %s because " u"certificate status '%s' is not in %s" ), student.id, cert_status, unicode(valid_statuses) ) LOGGER.info( ( u"Completed ungenerated certificates command " u"for course '%s'" ), unicode(course_key) )
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): """Test the "add to queue" operation of the XQueue interface. """ def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() self.user_2 = UserFactory.create() SoftwareSecurePhotoVerificationFactory.create(user=self.user_2, status='approved') def test_add_cert_callback_url(self): with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) def test_no_create_action_in_queue_for_html_view_certs(self): """ Tests there is no certificate create message in the queue if generate_pdf is False """ with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: self.xqueue.add_cert(self.user, self.course.id, generate_pdf=False) # Verify that add_cert method does not add message to queue self.assertFalse(mock_send.called) certificate = GeneratedCertificate.eligible_certificates.get( user=self.user, course_id=self.course.id) self.assertEqual(certificate.status, CertificateStatuses.downloadable) self.assertIsNotNone(certificate.verify_uuid) @ddt.data('honor', 'audit') @override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1)) def test_add_cert_with_honor_certificates(self, mode): """Test certificates generations for honor and audit modes.""" template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format( id=self.course.id) mock_send = self.add_cert_to_queue(mode) if CourseMode.is_eligible_for_certificate(mode): self.assert_certificate_generated(mock_send, mode, template_name) else: self.assert_ineligible_certificate_generated(mock_send, mode) @ddt.data('credit', 'verified') def test_add_cert_with_verified_certificates(self, mode): """Test if enrollment mode is verified or credit along with valid software-secure verification than verified certificate should be generated. """ template_name = 'certificate-template-{id.org}-{id.course}-verified.pdf'.format( id=self.course.id) mock_send = self.add_cert_to_queue(mode) self.assert_certificate_generated(mock_send, 'verified', template_name) def test_ineligible_cert_whitelisted(self): """Test that audit mode students can receive a certificate if they are whitelisted.""" # Enroll as audit CourseEnrollmentFactory(user=self.user_2, course_id=self.course.id, is_active=True, mode='audit') # Whitelist student CertificateWhitelistFactory(course_id=self.course.id, user=self.user_2) # Generate certs with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) # Assert cert generated correctly self.assertTrue(mock_send.called) certificate = GeneratedCertificate.certificate_for_student( self.user_2, self.course.id) self.assertIsNotNone(certificate) self.assertEqual(certificate.mode, 'audit') def add_cert_to_queue(self, mode): """ Dry method for course enrollment and adding request to queue. Returns a mock object containing information about the `XQueueInterface.send_to_queue` method, which can be used in other assertions. """ CourseEnrollmentFactory( user=self.user_2, course_id=self.course.id, is_active=True, mode=mode, ) with patch('courseware.grades.grade', Mock(return_value={ 'grade': 'Pass', 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) return mock_send def assert_certificate_generated(self, mock_send, expected_mode, expected_template_name): """ Assert that a certificate was generated with the correct mode and template type. """ # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) body = json.loads(kwargs['body']) self.assertIn(expected_template_name, body['template_pdf']) certificate = GeneratedCertificate.eligible_certificates.get( user=self.user_2, course_id=self.course.id) self.assertEqual(certificate.mode, expected_mode) def assert_ineligible_certificate_generated(self, mock_send, expected_mode): """ Assert that an ineligible certificate was generated with the correct mode. """ # Ensure the certificate was not generated self.assertFalse(mock_send.called) certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member user=self.user_2, course_id=self.course.id) self.assertIn(certificate.status, (CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing)) self.assertEqual(certificate.mode, expected_mode) @ddt.data( (CertificateStatuses.restricted, False), (CertificateStatuses.deleting, False), (CertificateStatuses.generating, True), (CertificateStatuses.unavailable, True), (CertificateStatuses.deleted, True), (CertificateStatuses.error, True), (CertificateStatuses.notpassing, True), (CertificateStatuses.downloadable, True), (CertificateStatuses.auditing, True), ) @ddt.unpack def test_add_cert_statuses(self, status, should_generate): """ Test that certificates can or cannot be generated with the given certificate status. """ with patch('certificates.queue.certificate_status_for_student', Mock(return_value={'status': status})): mock_send = self.add_cert_to_queue('verified') if should_generate: self.assertTrue(mock_send.called) else: self.assertFalse(mock_send.called) @ddt.data( # Eligible and should stay that way (CertificateStatuses.downloadable, datetime.now(pytz.UTC) - timedelta(days=2), 'Pass', CertificateStatuses.generating), # Ensure that certs in the wrong state can be fixed by regeneration (CertificateStatuses.downloadable, datetime.now(pytz.UTC) - timedelta(hours=1), 'Pass', CertificateStatuses.audit_passing), # Ineligible and should stay that way (CertificateStatuses.audit_passing, datetime.now(pytz.UTC) - timedelta(hours=1), 'Pass', CertificateStatuses.audit_passing), # As above (CertificateStatuses.audit_notpassing, datetime.now(pytz.UTC) - timedelta(hours=1), 'Pass', CertificateStatuses.audit_passing), # As above (CertificateStatuses.audit_notpassing, datetime.now(pytz.UTC) - timedelta(hours=1), None, CertificateStatuses.audit_notpassing), ) @ddt.unpack @override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1)) def test_regen_audit_certs_eligibility(self, status, created_date, grade, expected_status): """ Test that existing audit certificates remain eligible even if cert generation is re-run. """ # Create an existing audit enrollment and certificate CourseEnrollmentFactory( user=self.user_2, course_id=self.course.id, is_active=True, mode=CourseMode.AUDIT, ) with freezegun.freeze_time(created_date): GeneratedCertificateFactory( user=self.user_2, course_id=self.course.id, grade='1.0', status=status, mode=GeneratedCertificate.MODES.audit, ) # Run grading/cert generation again with patch('courseware.grades.grade', Mock(return_value={ 'grade': grade, 'percent': 0.75 })): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) self.assertEqual( GeneratedCertificate.objects.get(user=self.user_2, course_id=self.course.id).status, # pylint: disable=no-member expected_status)
class XQueueCertInterfaceExampleCertificateTest(TestCase): """Tests for the XQueue interface for certificate generation. """ COURSE_KEY = CourseLocator(org='test', course='test', run='test') TEMPLATE = 'test.pdf' DESCRIPTION = 'test' ERROR_MSG = 'Kaboom!' def setUp(self): super(XQueueCertInterfaceExampleCertificateTest, self).setUp() self.xqueue = XQueueCertInterface() def test_add_example_cert(self): cert = self._create_example_cert() with self._mock_xqueue() as mock_send: self.xqueue.add_example_cert(cert) # Verify that the correct payload was sent to the XQueue self._assert_queue_task(mock_send, cert) # Verify the certificate status self.assertEqual(cert.status, ExampleCertificate.STATUS_STARTED) def test_add_example_cert_error(self): cert = self._create_example_cert() with self._mock_xqueue(success=False): self.xqueue.add_example_cert(cert) # Verify the error status of the certificate self.assertEqual(cert.status, ExampleCertificate.STATUS_ERROR) self.assertIn(self.ERROR_MSG, cert.error_reason) def _create_example_cert(self): """Create an example certificate. """ cert_set = ExampleCertificateSet.objects.create( course_key=self.COURSE_KEY) return ExampleCertificate.objects.create(example_cert_set=cert_set, description=self.DESCRIPTION, template=self.TEMPLATE) @contextmanager def _mock_xqueue(self, success=True): """Mock the XQueue method for sending a task to the queue. """ with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) if success else (1, self.ERROR_MSG) yield mock_send def _assert_queue_task(self, mock_send, cert): """Check that the task was added to the queue. """ expected_header = { 'lms_key': cert.access_key, 'lms_callback_url': 'https://edx.org/update_example_certificate?key={key}'.format( key=cert.uuid), 'queue_name': 'certificates' } expected_body = { 'action': 'create', 'username': cert.uuid, 'name': u'John Doë', 'course_id': unicode(self.COURSE_KEY), 'template_pdf': 'test.pdf', 'example_certificate': True } self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) actual_body = json.loads(kwargs['body']) self.assertEqual(expected_header, actual_header) self.assertEqual(expected_body, actual_body)
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): """Test the "add to queue" operation of the XQueue interface. """ def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() self.user_2 = UserFactory.create() SoftwareSecurePhotoVerificationFactory.create(user=self.user_2, status='approved') def test_add_cert_callback_url(self): with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) def test_no_create_action_in_queue_for_html_view_certs(self): """ Tests there is no certificate create message in the queue if generate_pdf is False """ with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: self.xqueue.add_cert(self.user, self.course.id, generate_pdf=False) # Verify that add_cert method does not add message to queue self.assertFalse(mock_send.called) certificate = GeneratedCertificate.objects.get(user=self.user, course_id=self.course.id) self.assertEqual(certificate.status, CertificateStatuses.downloadable) self.assertIsNotNone(certificate.verify_uuid) @ddt.data('honor', 'audit') def test_add_cert_with_honor_certificates(self, mode): """Test certificates generations for honor and audit modes.""" template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format( id=self.course.id ) self.assert_queue_response(mode, mode, template_name) @ddt.data('credit', 'verified') def test_add_cert_with_verified_certificates(self, mode): """Test if enrollment mode is verified or credit along with valid software-secure verification than verified certificate should be generated. """ template_name = 'certificate-template-{id.org}-{id.course}-verified.pdf'.format( id=self.course.id ) self.assert_queue_response(mode, 'verified', template_name) def assert_queue_response(self, mode, expected_mode, expected_template_name): """Dry method for course enrollment and adding request to queue.""" CourseEnrollmentFactory( user=self.user_2, course_id=self.course.id, is_active=True, mode=mode, ) with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) certificate = GeneratedCertificate.objects.get(user=self.user_2, course_id=self.course.id) self.assertEqual(certificate.mode, expected_mode) body = json.loads(kwargs['body']) self.assertIn(expected_template_name, body['template_pdf'])
def setUp(self): super(XQueueCertInterfaceTest, self).setUp() self.xqueue = XQueueCertInterface()
def handle(self, *args, **options): LOGGER.info((u"Starting to create tasks for ungenerated certificates " u"with arguments %s and options %s"), unicode(args), unicode(options)) # Will only generate a certificate if the current # status is in the unavailable state, can be set # to something else with the force flag if options['force']: valid_statuses = [getattr(CertificateStatuses, options['force'])] else: valid_statuses = [CertificateStatuses.unavailable] # Print update after this many students STATUS_INTERVAL = 500 if options['course']: # try to parse out the course from the serialized form try: course = CourseKey.from_string(options['course']) except InvalidKeyError: LOGGER.warning(( u"Course id %s could not be parsed as a CourseKey; " u"falling back to SlashSeparatedCourseKey.from_deprecated_string()" ), options['course']) course = SlashSeparatedCourseKey.from_deprecated_string( options['course']) ended_courses = [course] else: raise CommandError("You must specify a course") for course_key in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_course(course_key, depth=2) enrolled_students = User.objects.filter( courseenrollment__course_id=course_key) xq = XQueueCertInterface() if options['insecure']: xq.use_https = False total = enrolled_students.count() count = 0 start = datetime.datetime.now(UTC) for student in enrolled_students: count += 1 if count % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now(UTC) - start timeleft = diff * (total - count) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, _seconds = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now(UTC) cert_status = certificate_status_for_student( student, course_key)['status'] LOGGER.info((u"Student %s has certificate status '%s' " u"in course '%s'"), student.id, cert_status, unicode(course_key)) if cert_status in valid_statuses: if not options['noop']: # Add the certificate request to the queue ret = xq.add_cert(student, course_key, course=course) if ret == 'generating': LOGGER.info(( u"Added a certificate generation task to the XQueue " u"for student %s in course '%s'. " u"The new certificate status is '%s'."), student.id, unicode(course_key), ret) else: LOGGER.info((u"Skipping certificate generation for " u"student %s in course '%s' " u"because the noop flag is set."), student.id, unicode(course_key)) else: LOGGER.info((u"Skipped student %s because " u"certificate status '%s' is not in %s"), student.id, cert_status, unicode(valid_statuses)) LOGGER.info((u"Completed ungenerated certificates command " u"for course '%s'"), unicode(course_key))
def handle(self, *args, **options): # Will only generate a certificate if the current # status is in the unavailable state, can be set # to something else with the force flag if options['force']: valid_statuses = getattr(CertificateStatuses, options['force']) else: valid_statuses = [CertificateStatuses.unavailable] # Print update after this many students STATUS_INTERVAL = 500 if options['course']: ended_courses = [options['course']] else: # Find all courses that have ended ended_courses = [] for course_id in [ course # all courses in COURSE_LISTINGS for sub in settings.COURSE_LISTINGS for course in settings.COURSE_LISTINGS[sub] ]: course_loc = CourseDescriptor.id_to_location(course_id) course = modulestore().get_instance(course_id, course_loc) if course.has_ended(): ended_courses.append(course_id) for course_id in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_instance( course_id, CourseDescriptor.id_to_location(course_id), depth=2) print "Fetching enrolled students for {0}".format(course_id) enrolled_students = User.objects.filter( courseenrollment__course_id=course_id) xq = XQueueCertInterface() if options['insecure']: xq.use_https = False total = enrolled_students.count() count = 0 start = datetime.datetime.now(UTC) for student in enrolled_students: count += 1 if count % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now(UTC) - start timeleft = diff * (total - count) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, seconds = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now(UTC) if certificate_status_for_student( student, course_id)['status'] in valid_statuses: if not options['noop']: # Add the certificate request to the queue ret = xq.add_cert(student, course_id, course=course) if ret == 'generating': print '{0} - {1}'.format(student, ret)
def handle(self, *args, **options): # Will only generate a certificate if the current # status is in the unavailable state, can be set # to something else with the force flag if options['force']: valid_statuses = getattr(CertificateStatuses, options['force']) else: valid_statuses = [CertificateStatuses.unavailable] # Print update after this many students STATUS_INTERVAL = 500 if options['course']: # try to parse out the course from the serialized form try: course = CourseKey.from_string(options['course']) except InvalidKeyError: print("Course id {} could not be parsed as a CourseKey; falling back to SSCK.from_dep_str".format(options['course'])) course = SlashSeparatedCourseKey.from_deprecated_string(options['course']) ended_courses = [course] else: raise CommandError("You must specify a course") for course_key in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_course(course_key, depth=2) print "Fetching enrolled students for {0}".format(course_key.to_deprecated_string()) enrolled_students = User.objects.filter( courseenrollment__course_id=course_key) xq = XQueueCertInterface() if options['insecure']: xq.use_https = False total = enrolled_students.count() count = 0 start = datetime.datetime.now(UTC) for student in enrolled_students: count += 1 if count % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now(UTC) - start timeleft = diff * (total - count) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, seconds = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now(UTC) if certificate_status_for_student( student, course_key)['status'] in valid_statuses: if not options['noop']: # Add the certificate request to the queue ret = xq.add_cert(student, course_key, course=course) if ret == 'generating': print '{0} - {1}'.format(student, ret)
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): """Test the "add to queue" operation of the XQueue interface. """ def setUp(self): super(XQueueCertInterfaceAddCertificateTest, self).setUp() self.user = UserFactory.create() self.course = CourseFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, mode="honor", ) self.xqueue = XQueueCertInterface() self.user_2 = UserFactory.create() SoftwareSecurePhotoVerificationFactory.create(user=self.user_2, status='approved') def test_add_cert_callback_url(self): with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user, self.course.id) # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) def test_no_create_action_in_queue_for_html_view_certs(self): """ Tests there is no certificate create message in the queue if generate_pdf is False """ with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: self.xqueue.add_cert(self.user, self.course.id, generate_pdf=False) # Verify that add_cert method does not add message to queue self.assertFalse(mock_send.called) certificate = GeneratedCertificate.eligible_certificates.get(user=self.user, course_id=self.course.id) self.assertEqual(certificate.status, CertificateStatuses.downloadable) self.assertIsNotNone(certificate.verify_uuid) @ddt.data('honor', 'audit') def test_add_cert_with_honor_certificates(self, mode): """Test certificates generations for honor and audit modes.""" template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format( id=self.course.id ) mock_send = self.add_cert_to_queue(mode) if CourseMode.is_eligible_for_certificate(mode): self.assert_certificate_generated(mock_send, mode, template_name) else: self.assert_ineligible_certificate_generated(mock_send, mode) @ddt.data('credit', 'verified') def test_add_cert_with_verified_certificates(self, mode): """Test if enrollment mode is verified or credit along with valid software-secure verification than verified certificate should be generated. """ template_name = 'certificate-template-{id.org}-{id.course}-verified.pdf'.format( id=self.course.id ) mock_send = self.add_cert_to_queue(mode) self.assert_certificate_generated(mock_send, 'verified', template_name) def test_ineligible_cert_whitelisted(self): """Test that audit mode students can receive a certificate if they are whitelisted.""" # Enroll as audit CourseEnrollmentFactory( user=self.user_2, course_id=self.course.id, is_active=True, mode='audit' ) # Whitelist student CertificateWhitelistFactory(course_id=self.course.id, user=self.user_2) # Generate certs with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) # Assert cert generated correctly self.assertTrue(mock_send.called) certificate = GeneratedCertificate.certificate_for_student(self.user_2, self.course.id) self.assertIsNotNone(certificate) self.assertEqual(certificate.mode, 'audit') def add_cert_to_queue(self, mode): """ Dry method for course enrollment and adding request to queue. Returns a mock object containing information about the `XQueueInterface.send_to_queue` method, which can be used in other assertions. """ CourseEnrollmentFactory( user=self.user_2, course_id=self.course.id, is_active=True, mode=mode, ) with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) return mock_send def assert_certificate_generated(self, mock_send, expected_mode, expected_template_name): """ Assert that a certificate was generated with the correct mode and template type. """ # Verify that the task was sent to the queue with the correct callback URL self.assertTrue(mock_send.called) __, kwargs = mock_send.call_args_list[0] actual_header = json.loads(kwargs['header']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) body = json.loads(kwargs['body']) self.assertIn(expected_template_name, body['template_pdf']) certificate = GeneratedCertificate.eligible_certificates.get(user=self.user_2, course_id=self.course.id) self.assertEqual(certificate.mode, expected_mode) def assert_ineligible_certificate_generated(self, mock_send, expected_mode): """ Assert that an ineligible certificate was generated with the correct mode. """ # Ensure the certificate was not generated self.assertFalse(mock_send.called) certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member user=self.user_2, course_id=self.course.id ) self.assertIn(certificate.status, (CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing)) self.assertEqual(certificate.mode, expected_mode) @ddt.data( (CertificateStatuses.restricted, False), (CertificateStatuses.deleting, False), (CertificateStatuses.generating, True), (CertificateStatuses.unavailable, True), (CertificateStatuses.deleted, True), (CertificateStatuses.error, True), (CertificateStatuses.notpassing, True), (CertificateStatuses.downloadable, True), (CertificateStatuses.auditing, True), ) @ddt.unpack def test_add_cert_statuses(self, status, should_generate): """ Test that certificates can or cannot be generated with the given certificate status. """ with patch('certificates.queue.certificate_status_for_student', Mock(return_value={'status': status})): mock_send = self.add_cert_to_queue('verified') if should_generate: self.assertTrue(mock_send.called) else: self.assertFalse(mock_send.called) @ddt.data( (CertificateStatuses.downloadable, 'Pass', CertificateStatuses.generating), (CertificateStatuses.audit_passing, 'Pass', CertificateStatuses.audit_passing), (CertificateStatuses.audit_notpassing, 'Pass', CertificateStatuses.audit_passing), (CertificateStatuses.audit_notpassing, None, CertificateStatuses.audit_notpassing), ) @ddt.unpack def test_regen_audit_certs_eligibility(self, status, grade, expected_status): """ Test that existing audit certificates remain eligible even if cert generation is re-run. """ # Create an existing audit enrollment and certificate CourseEnrollmentFactory( user=self.user_2, course_id=self.course.id, is_active=True, mode=CourseMode.AUDIT, ) GeneratedCertificateFactory( user=self.user_2, course_id=self.course.id, grade='1.0', status=status, mode=GeneratedCertificate.MODES.audit, ) # Run grading/cert generation again with patch('courseware.grades.grade', Mock(return_value={'grade': grade, 'percent': 0.75})): with patch.object(XQueueInterface, 'send_to_queue') as mock_send: mock_send.return_value = (0, None) self.xqueue.add_cert(self.user_2, self.course.id) self.assertEqual( GeneratedCertificate.objects.get(user=self.user_2, course_id=self.course.id).status, # pylint: disable=no-member expected_status )