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(): 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 ]: 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 = generate_user_certificates(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 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 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 get_certificate(self, model): """Returns the information about the user's certificate in the course.""" certificate_info = certificate_status_for_student(model.user, model.course_id) if certificate_info["status"] == CertificateStatuses.downloadable: return {"url": certificate_info["download_url"]} else: return {}
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(): 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) designation = None if 'openedx.stanford.djangoapps.register_cme' in settings.INSTALLED_APPS: designation = ExtraInfo.lookup_professional_designation(student) 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' log.info(log_msg, username, course_key) status = generate_user_certificates(student, course_key, course=course, designation=designation) return HttpResponse(json.dumps({'add_status': status}), content_type='application/json') return HttpResponse(json.dumps({'add_status': 'ERRORANONYMOUSUSER'}), content_type='application/json')
def test_with_downloadable_web_cert(self): CourseEnrollment.enroll(self.student, self.course.id, mode='honor') self._setup_course_certificate() with self._mock_passing_grade(): certs_api.generate_user_certificates(self.student, self.course.id) cert_status = certificate_status_for_student(self.student, self.course.id) self.assertEqual( certs_api.certificate_downloadable_status(self.student, self.course.id), { 'is_downloadable': True, 'is_generating': False, 'is_unverified': False, 'download_url': '/certificates/user/{user_id}/course/{course_id}'.format( user_id=self.student.id, # pylint: disable=no-member course_id=self.course.id, ), 'uuid': cert_status['uuid'] })
def test_certificate_status_for_student(self): student = UserFactory() course = CourseFactory.create(org='edx', number='verified', display_name='Verified Course') certificate_status = certificate_status_for_student(student, course.id) self.assertEqual(certificate_status['status'], CertificateStatuses.unavailable) self.assertEqual(certificate_status['mode'], GeneratedCertificate.MODES.honor)
def certificate_downloadable_status(student, course_key): """ Check the student existing certificates against a given course. if status is not generating and not downloadable or error then user can view the generate button. Args: student (user object): logged-in user course_key (CourseKey): ID associated with the course Returns: Dict containing student passed status also download url for cert if available """ current_status = certificate_status_for_student(student, course_key) # If the certificate status is an error user should view that status is "generating". # On the back-end, need to monitor those errors and re-submit the task. response_data = { 'is_downloadable': False, 'is_generating': True if current_status['status'] in [ CertificateStatuses.generating, CertificateStatuses.error ] else False, 'download_url': None } if current_status['status'] == CertificateStatuses.downloadable: response_data['is_downloadable'] = True response_data['download_url'] = current_status['download_url'] return response_data
def certificate_downloadable_status(student, course_key): """ Check the student existing certificates against a given course. if status is not generating and not downloadable or error then user can view the generate button. Args: student (user object): logged-in user course_key (CourseKey): ID associated with the course Returns: Dict containing student passed status also download url for cert if available """ current_status = certificate_status_for_student(student, course_key) # If the certificate status is an error user should view that status is "generating". # On the back-end, need to monitor those errors and re-submit the task. response_data = { 'is_downloadable': False, 'is_generating': True if current_status['status'] in [CertificateStatuses.generating, CertificateStatuses.error] else False, 'download_url': None } if current_status['status'] == CertificateStatuses.downloadable: response_data['is_downloadable'] = True response_data['download_url'] = current_status['download_url'] return response_data
def get_certificate(self, model): """Returns the information about the user's certificate in the course.""" certificate_info = certificate_status_for_student(model.user, model.course_id) if certificate_info['status'] == CertificateStatuses.downloadable: return { "url": certificate_info['download_url'], } else: return {}
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] 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") if options['api_key']: api_key = options['api_key'] else: raise CommandError("You must give a api_key, if don't have one visit: https://accredible.com/issuer/sign_up") if options['styling']: if options['styling'] == 'True': new_status = "generating" else: raise CommandError("You must give true if want to do no styling, no any other argument") else: new_status = "downloadable" 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 = CertificateGeneration(api_key=api_key) total = enrolled_students.count() print "Total number of students: " + str(total) for student in enrolled_students: if certificate_status_for_student( student, course_key)['status'] in valid_statuses: ret = xq.add_cert(student, course_key, new_status, course=course) print ret
def generate_certificate(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): """ Generate a certificate for graduated students """ course = modulestore().get_course(course_id, depth=2) course_key = get_course_key(str(course_id)) course_display_name = unicode(course.display_name).encode('utf-8') university = University.objects.get(code=course.location.org) certificate_base_filename = "attestation_suivi_" + (course_id.to_deprecated_string().replace('/', '_')) + '_' start_time = time() start_date = datetime.now(UTC) status_interval = 1 enrolled_students = get_enrolled_students(course_id) teachers = get_teachers_list_from_course(course_id.to_deprecated_string()) task_progress = TaskProgress(action_name, enrolled_students.count(), start_time) # generate a test certificate test_certificate = create_test_certificate(course, course_key, university) all_status = {status.notpassing: 0, status.error: 0, status.downloadable: 0, 'test_certificate_filename' : test_certificate.filename} for count, student in enumerate(enrolled_students): task_progress.attempted += 1 if task_progress.attempted % status_interval == 0: task_progress.update_task_state(extra_meta=all_status) if certificate_status_for_student(student, course_id)['status'] != status.downloadable: if university.certificate_logo: logo_path = os.path.join(university.certificate_logo.url, university.certificate_logo.path) else: logo_path = None student_status = generate_fun_certificate(student, course_id, course_display_name, course, teachers, university.name, logo_path, certificate_base_filename, False, False, False) if student_status: all_status[student_status] += 1 task_progress.succeeded += 1 else: task_progress.failed += 1 else: all_status[status.downloadable] += 1 return task_progress.update_task_state(extra_meta=all_status)
def handle(self, *args, **options): course_key_string = args[0] course_id = CourseKey.from_string(course_key_string) student_ids = enrolled_proctoru_students(course_id) reports = utils_proctorU_api.get_reports_from_ids(course_id.course, course_id.run, student_ids=student_ids) for student in reports: user = User.objects.get(username=student) course_enrollment = CourseEnrollment.objects.get(course_id=course_id, user=user) generated_certificate = certificate_status_for_student(user, course_id) print("Previous certificate status: {}".format(generated_certificate.get("status"))) print("Mode for student {}: {}".format(student, course_enrollment.mode)) print("reports for: {} -- is OK PU: {}".format(student, utils_proctorU_api.is_proctoru_ok(reports[student]))) pprint(reports[student])
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 len(options['teachers']) > 4: raise CommandError("Too many teachers. Certificate can not include more than four names.") if not options['course']: raise CommandError("--course argument is mandatory") if options['grade'] and float(options['grade']) > 1: raise CommandError('grades range from 0 to 1') try: ended_courses = [CourseKey.from_string(options['course'])] except InvalidKeyError: raise CommandError("Course id {} could not be parsed as a CourseKey;".format(options['course'])) for course_id in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_course(course_id, depth=2) course_display_name = unicode(course.display_name).encode('utf-8') university = University.objects.get(code=course.location.org) certificate_base_filename = "attestation_suivi_" + (course_id.to_deprecated_string().replace('/', '_')) + '_' print "Fetching enrolled students for {0} ()".format(course_id) enrolled_students = get_enrolled_students(course_id, options['user']) total = enrolled_students.count() print "Course has {0} enrolled students".format(total) stats = { status.notpassing: 0, status.error: 0, status.downloadable: 0 } start = datetime.datetime.now(UTC) for count, student in enumerate(enrolled_students): start = print_progress(count, total, start) if options['force'] or (certificate_status_for_student(student, course_id)['status'] != status.downloadable): if university.certificate_logo: logo_path = os.path.join(university.certificate_logo.url, university.certificate_logo.path) else: logo_path = None new_status = generate_fun_certificate( student, course_id, course_display_name, course, options['teachers'], university.name, logo_path, certificate_base_filename, options['ignore_grades'], options['grade'], options['fail']) stats[new_status] += 1 pprint(stats)
def cert_info(user, course): """ Get the certificate info needed to render the dashboard section for the given student and course. Returns a dictionary with keys: 'status': one of 'generating', 'ready', 'notpassing', 'processing', 'restricted' 'show_download_url': bool 'download_url': url, only present if show_download_url is True 'show_disabled_download_button': bool -- true if state is 'generating' 'show_survey_button': bool 'survey_url': url, only if show_survey_button is True 'grade': if status is not 'processing' """ if not course.has_ended(): return {} return _cert_info(user, course, certificate_status_for_student(user, course.id))
def test_with_downloadable_web_cert(self): CourseEnrollment.enroll(self.student, self.course.id, mode="honor") self._setup_course_certificate() with self._mock_passing_grade(): certs_api.generate_user_certificates(self.student, self.course.id) cert_status = certificate_status_for_student(self.student, self.course.id) self.assertEqual( certs_api.certificate_downloadable_status(self.student, self.course.id), { "is_downloadable": True, "is_generating": False, "download_url": "/certificates/user/{user_id}/course/{course_id}".format( user_id=self.student.id, course_id=self.course.id # pylint: disable=no-member ), "uuid": cert_status["uuid"], }, )
def test_with_downloadable_web_cert(self): CourseEnrollment.enroll(self.student, self.course.id, mode='honor') self._setup_course_certificate() with self._mock_passing_grade(): certs_api.generate_user_certificates(self.student, self.course.id) cert_status = certificate_status_for_student(self.student, self.course.id) self.assertEqual( certs_api.certificate_downloadable_status(self.student, self.course.id), { 'is_downloadable': True, 'is_generating': False, 'download_url': '/certificates/user/{user_id}/course/{course_id}'.format( user_id=self.student.id, # pylint: disable=no-member course_id=self.course.id, ), 'uuid': cert_status['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. """ # 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')
def handle(self, *args, **options): course_key_string = args[0] course_id = CourseKey.from_string(course_key_string) student_ids = enrolled_proctoru_students(course_id) reports = utils_proctorU_api.get_reports_from_ids( course_id.course, course_id.run, student_ids=student_ids) for student in reports: user = User.objects.get(username=student) course_enrollment = CourseEnrollment.objects.get( course_id=course_id, user=user) generated_certificate = certificate_status_for_student( user, course_id) print("Previous certificate status: {}".format( generated_certificate.get("status"))) print("Mode for student {}: {}".format(student, course_enrollment.mode)) print("reports for: {} -- is OK PU: {}".format( student, utils_proctorU_api.is_proctoru_ok(reports[student]))) pprint(reports[student])
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(): # Enter your api key here xqci = CertificateGeneration(api_key=settings.APPSEMBLER_FEATURES['ACCREDIBLE_API_KEY']) 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 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 add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title='None', generate_pdf=True): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. generate_pdf - Boolean should a message be sent in queue to generate certificate PDF Will change the certificate status to 'generating' or `downloadable` in case of web view certificates. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status and newly created certificate instance """ valid_statuses = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing, status.downloadable ] cert_status = certificate_status_for_student(student, course_id)['status'] new_status = cert_status cert = None if cert_status not in valid_statuses: LOGGER.warning( ( u"Cannot create certificate generation task for user %s " u"in the course '%s'; " u"the certificate status '%s' is not one of %s." ), student.id, unicode(course_id), cert_status, unicode(valid_statuses) ) else: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = modulestore().get_course(course_id, depth=0) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed self.request.user = student self.request.session = {} course_name = course.display_name or unicode(course_id) is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, self.request, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) cert_mode = enrollment_mode # For credit mode generate verified certificate if cert_mode == CourseMode.CREDIT_MODE: cert_mode = CourseMode.VERIFIED if mode_is_verified and user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id) elif mode_is_verified and not user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) cert_mode = GeneratedCertificate.MODES.honor else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) if forced_grade: grade['grade'] = forced_grade cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile_name cert.download_url = '' # Strip HTML from grade range label grade_contents = grade.get('grade', None) try: grade_contents = lxml.html.fromstring(grade_contents).text_content() except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info( ( u"Could not retrieve grade for student %s " u"in the course '%s' " u"because an exception occurred while parsing the " u"grade contents '%s' as HTML. " u"The exception was: '%s'" ), student.id, unicode(course_id), grade_contents, unicode(exc) ) # Despite blowing up the xml parser, bad values here are fine grade_contents = None if is_whitelisted or grade_contents is not None: if is_whitelisted: LOGGER.info( u"Student %s is whitelisted in '%s'", student.id, unicode(course_id) ) # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue if self.restricted.filter(user=student).exists(): new_status = status.restricted cert.status = new_status cert.save() LOGGER.info( ( u"Student %s is in the embargoed country restricted " u"list, so their certificate status has been set to '%s' " u"for the course '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, new_status, unicode(course_id) ) else: key = make_hashkey(random.random()) cert.key = key contents = { 'action': 'create', 'username': student.username, 'course_id': unicode(course_id), 'course_name': course_name, 'name': profile_name, 'grade': grade_contents, 'template_pdf': template_pdf, } if template_file: contents['template_pdf'] = template_file if generate_pdf: new_status = status.generating else: new_status = status.downloadable cert.verify_uuid = uuid4().hex cert.status = new_status cert.save() if generate_pdf: try: self._send_to_xqueue(contents, key) except XQueueAddToQueueError as exc: new_status = ExampleCertificate.STATUS_ERROR cert.status = new_status cert.error_reason = unicode(exc) cert.save() LOGGER.critical( ( u"Could not add certificate task to XQueue. " u"The course was '%s' and the student was '%s'." u"The certificate task status has been marked as 'error' " u"and can be re-submitted with a management command." ), course_id, student.id ) else: LOGGER.info( ( u"The certificate status has been set to '%s'. " u"Sent a certificate grading task to the XQueue " u"with the key '%s'. " ), new_status, key ) else: new_status = status.notpassing cert.status = new_status cert.save() LOGGER.info( ( u"Student %s does not have a grade for '%s', " u"so their certificate status has been set to '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, unicode(course_id), new_status ) return new_status, cert
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 add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title='None'): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. Will change the certificate status to 'generating'. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status """ valid_statuses = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing ] cert_status = certificate_status_for_student(student, course_id)['status'] new_status = cert_status if cert_status not in valid_statuses: LOGGER.warning( ( u"Cannot create certificate generation task for user %s " u"in the course '%s'; " u"the certificate status '%s' is not one of %s." ), student.id, unicode(course_id), cert_status, unicode(valid_statuses) ) else: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = courses.get_course_by_id(course_id) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed self.request.user = student self.request.session = {} course_name = course.display_name or course_id.to_deprecated_string() is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, self.request, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student) cert_mode = enrollment_mode if (mode_is_verified and user_is_verified and user_is_reverified): template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id) elif (mode_is_verified and not (user_is_verified and user_is_reverified)): template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) cert_mode = GeneratedCertificate.MODES.honor else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) if forced_grade: grade['grade'] = forced_grade cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile_name # Strip HTML from grade range label grade_contents = grade.get('grade', None) try: grade_contents = lxml.html.fromstring(grade_contents).text_content() except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info( ( u"Could not retrieve grade for student %s " u"in the course '%s' " u"because an exception occurred while parsing the " u"grade contents '%s' as HTML. " u"The exception was: '%s'" ), student.id, unicode(course_id), grade_contents, unicode(exc) ) # Despite blowing up the xml parser, bad values here are fine grade_contents = None if is_whitelisted or grade_contents is not None: if is_whitelisted: LOGGER.info( u"Student %s is whitelisted in '%s'", student.id, unicode(course_id) ) # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue if self.restricted.filter(user=student).exists(): new_status = status.restricted cert.status = new_status cert.save() LOGGER.info( ( u"Student %s is in the embargoed country restricted " u"list, so their certificate status has been set to '%s' " u"for the course '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, new_status, unicode(course_id) ) else: key = make_hashkey(random.random()) cert.key = key contents = { 'action': 'create', 'username': student.username, 'course_id': course_id.to_deprecated_string(), 'course_name': course_name, 'name': profile_name, 'grade': grade_contents, 'template_pdf': template_pdf, } if template_file: contents['template_pdf'] = template_file new_status = status.generating cert.status = new_status cert.save() self._send_to_xqueue(contents, key) LOGGER.info( ( u"The certificate status has been set to '%s'. " u"Sent a certificate grading task to the XQueue " u"with the key '%s'. " ), key, new_status ) else: cert_status = status.notpassing cert.status = cert_status cert.save() LOGGER.info( ( u"Student %s does not have a grade for '%s', " u"so their certificate status has been set to '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, unicode(course_id), cert_status ) return new_status
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 ) 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 = generate_user_certificates( student, course_key, course=course, insecure=options['insecure'] ) 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 len(options['teachers']) > 4: print "Too many teachers. Certificate can not include more than four names." return # Print update after this many students STATUS_INTERVAL = 100 if options['course']: try: ended_courses = [CourseKey.from_string(options['course'])] except InvalidKeyError: print("Course id {} could not be parsed as a CourseKey;".format(options['course'])) return if options['grade'] and float(options['grade']) > 1: raise CommandError('grades range from 0 to 1') for course_id in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_course(course_id, depth=2) course_display_name = unicode(course.display_name).encode('utf-8') university = University.objects.get(code=course.location.org) certificate_base_filename = "attestation_suivi_" + (course_id.to_deprecated_string().replace('/','_')) + '_'; print "Fetching enrolled students for {0} ()".format(course_id, course_display_name) if options['user'] is None: enrolled_students = User.objects.filter( courseenrollment__course_id=course_id, profile__isnull=False).prefetch_related( "groups").order_by('username') else: user = options['user'] if '@' in user: enrolled_students = User.objects.filter(email=user, courseenrollment__course_id=course_id, profile__isnull=False) else: enrolled_students = User.objects.filter(username=user, courseenrollment__course_id=course_id, profile__isnull=False) total = enrolled_students.count() print "Course has {0} enrolled students".format(total) count = 0 stats = { status.notpassing : 0, status.error : 0, status.downloadable : 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 options['force'] or (certificate_status_for_student(student, course_id)['status'] != status.downloadable): if university.certificate_logo: logo_path = os.path.join(university.certificate_logo.url, university.certificate_logo.path) else: logo_path = None new_status = generate_fun_certificate(student, course_id, course_display_name, course, options['teachers'], university.name, logo_path, certificate_base_filename, options['ignore_grades'], options['grade']) stats[new_status] += 1 pprint(stats)
def add_cert(self, student, course_id, defined_status="downloadable", course=None, forced_grade=None, template_file=None, title='None'): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. Will change the certificate status to 'generating' or 'downloadable'. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status """ VALID_STATUSES = [status.generating, status.unavailable, status.deleted, status.error, status.notpassing] cert_status = certificate_status_for_student(student, course_id)['status'] new_status = cert_status if cert_status in VALID_STATUSES: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = courses.get_course_by_id(course_id) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed self.request.user = student self.request.session = {} course_name = course.display_name or course_id.to_deprecated_string() description = '' for section_key in ['short_description', 'description','overview']: loc = loc = course.location.replace(category='about', name=section_key) try: if modulestore().get_item(loc).data: description = modulestore().get_item(loc).data break except: print "this course don't have " +section_key if not description: description = "course_description" is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, self.request, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student) cert_mode = enrollment_mode if (mode_is_verified and not (user_is_verified and user_is_reverified)): cert_mode = GeneratedCertificate.MODES.honor if forced_grade: grade['grade'] = forced_grade cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile_name # Strip HTML from grade range label grade_contents = grade.get('grade', None) try: grade_contents = lxml.html.fromstring(grade_contents).text_content() except (TypeError, XMLSyntaxError, ParserError) as e: # Despite blowing up the xml parser, bad values here are fine grade_contents = None if is_whitelisted or grade_contents is not None: # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue print grade_contents if self.restricted.filter(user=student).exists(): new_status = status.restricted cert.status = new_status cert.save() else: contents = { 'action': 'create', 'username': student.username, 'course_id': course_id.to_deprecated_string(), 'course_name': course_name, 'name': profile_name, 'grade': grade_contents } if cert_mode == "verified": seal_image = "https://s3.amazonaws.com/accredible_api_style_preferences/signature_images/37/original/open-uri20141023-10175-q5szt4" elif cert_mode == "honor": seal_image = "https://s3.amazonaws.com/accredible_api_organizations/images/8/medium/data?1414086141" else: seal_image = None if defined_status == "generating": approve = False else: approve = True grade_into_string = ''.join('{}{}'.format(key, val) for key, val in grade.items()) payload = {"credential": { "name": course_name, "description": description, "achievement_id": contents['course_id'] , "course_link": "/courses/" +contents['course_id'] + "/about", "approve": approve, "grade": grade_contents, "recipient": {"name": contents['name'], "email": student.email}, "style_preference": {"distinction_url": seal_image},"evidence_items": [{"description": "Course Transcript", "category": "transcript", "string_object": json.dumps(grade["section_breakdown"])}, {"description": "Final Grade", "category": "grade", "string_object": grade['percent']}]}} payload = json.dumps(payload) r = requests.post('https://api.accredible.com/v1/credentials', payload, headers={'Authorization':'Token token=' + self.api_key, 'Content-Type':'application/json'}) if r.status_code == 200: json_response = r.json() cert.status = defined_status cert.key = json_response["credential"]["id"] if 'private' in json_response: cert.download_url = "https://wwww.accredible.com/" + str(json_response["credential"]["id"]) + "?key" + str(json_response["private_key"]) else: cert.download_url = "https://www.accredible.com/" + str(cert.key) cert.save() else: new_status = "errors" else: cert_status = status.notpassing cert.status = cert_status cert.save() return new_status
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, generate_pdf=True): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. generate_pdf - Boolean should a message be sent in queue to generate certificate PDF Will change the certificate status to 'generating' or `downloadable` in case of web view certificates. The course must not be a CCX. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the newly created certificate instance """ if hasattr(course_id, 'ccx'): LOGGER.warning( ( u"Cannot create certificate generation task for user %s " u"in the course '%s'; " u"certificates are not allowed for CCX courses." ), student.id, unicode(course_id) ) return None valid_statuses = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing, status.downloadable, status.auditing, status.audit_passing, status.audit_notpassing, ] cert_status = certificate_status_for_student(student, course_id)['status'] cert = None if cert_status not in valid_statuses: LOGGER.warning( ( u"Cannot create certificate generation task for user %s " u"in the course '%s'; " u"the certificate status '%s' is not one of %s." ), student.id, unicode(course_id), cert_status, unicode(valid_statuses) ) return None # The caller can optionally pass a course in to avoid # re-fetching it from Mongo. If they have not provided one, # get it from the modulestore. if course is None: course = modulestore().get_course(course_id, depth=0) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed for access control in grading. self.request.user = student self.request.session = {} is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() course_grade = CourseGradeFactory().create(student, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) cert_mode = enrollment_mode is_eligible_for_certificate = is_whitelisted or CourseMode.is_eligible_for_certificate(enrollment_mode) unverified = False # For credit mode generate verified certificate if cert_mode == CourseMode.CREDIT_MODE: cert_mode = CourseMode.VERIFIED if template_file is not None: template_pdf = template_file elif mode_is_verified and user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id) elif mode_is_verified and not user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) if CourseMode.mode_for_course(course_id, CourseMode.HONOR): cert_mode = GeneratedCertificate.MODES.honor else: unverified = True else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) LOGGER.info( ( u"Certificate generated for student %s in the course: %s with template: %s. " u"given template: %s, " u"user is verified: %s, " u"mode is verified: %s" ), student.username, unicode(course_id), template_pdf, template_file, user_is_verified, mode_is_verified ) cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) # pylint: disable=no-member cert.mode = cert_mode cert.user = student cert.grade = course_grade.percent cert.course_id = course_id cert.name = profile_name cert.download_url = '' # Strip HTML from grade range label grade_contents = forced_grade or course_grade.letter_grade try: grade_contents = lxml.html.fromstring(grade_contents).text_content() passing = True except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info( ( u"Could not retrieve grade for student %s " u"in the course '%s' " u"because an exception occurred while parsing the " u"grade contents '%s' as HTML. " u"The exception was: '%s'" ), student.id, unicode(course_id), grade_contents, unicode(exc) ) # Log if the student is whitelisted if is_whitelisted: LOGGER.info( u"Student %s is whitelisted in '%s'", student.id, unicode(course_id) ) passing = True else: passing = False # If this user's enrollment is not eligible to receive a # certificate, mark it as such for reporting and # analytics. Only do this if the certificate is new, or # already marked as ineligible -- we don't want to mark # existing audit certs as ineligible. cutoff = settings.AUDIT_CERT_CUTOFF_DATE if (cutoff and cert.created_date >= cutoff) and not is_eligible_for_certificate: cert.status = CertificateStatuses.audit_passing if passing else CertificateStatuses.audit_notpassing cert.save() LOGGER.info( u"Student %s with enrollment mode %s is not eligible for a certificate.", student.id, enrollment_mode ) return cert # If they are not passing, short-circuit and don't generate cert elif not passing: cert.status = status.notpassing cert.save() LOGGER.info( ( u"Student %s does not have a grade for '%s', " u"so their certificate status has been set to '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, unicode(course_id), cert.status ) return cert # Check to see whether the student is on the the embargoed # country restricted list. If so, they should not receive a # certificate -- set their status to restricted and log it. if self.restricted.filter(user=student).exists(): cert.status = status.restricted cert.save() LOGGER.info( ( u"Student %s is in the embargoed country restricted " u"list, so their certificate status has been set to '%s' " u"for the course '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, cert.status, unicode(course_id) ) return cert if unverified: cert.status = status.unverified cert.save() LOGGER.info( ( u"User %s has a verified enrollment in course %s " u"but is missing ID verification. " u"Certificate status has been set to unverified" ), student.id, unicode(course_id), ) return cert # Finally, generate the certificate and send it off. return self._generate_cert(cert, course, student, grade_contents, template_pdf, generate_pdf)
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 add_cert(self, student, course_id, course=None): """ Arguments: student - User.object course_id - courseenrollment.course_id (string) Request a new certificate for a student. Will change the certificate status to 'generating'. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status """ VALID_STATUSES = [status.generating, status.unavailable, status.deleted, status.error, status.notpassing] cert_status = certificate_status_for_student(student, course_id)['status'] new_status = cert_status if cert_status in VALID_STATUSES: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = courses.get_course_by_id(course_id) profile = UserProfile.objects.get(user=student) # Needed self.request.user = student self.request.session = {} grade = grades.grade(student, self.request, course) is_whitelisted = self.whitelist.filter( user=student, course_id=course_id, whitelist=True).exists() enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id) mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student) org = course_id.split('/')[0] course_num = course_id.split('/')[1] cert_mode = enrollment_mode if (mode_is_verified and user_is_verified and user_is_reverified): template_pdf = "certificate-template-{0}-{1}-verified.pdf".format( org, course_num) elif (mode_is_verified and not (user_is_verified and user_is_reverified)): template_pdf = "certificate-template-{0}-{1}.pdf".format( org, course_num) cert_mode = GeneratedCertificate.MODES.honor else: # honor code and audit students template_pdf = "certificate-template-{0}-{1}.pdf".format( org, course_num) cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile.name if is_whitelisted or grade['grade'] is not None: # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue if self.restricted.filter(user=student).exists(): new_status = status.restricted cert.status = new_status cert.save() else: key = make_hashkey(random.random()) cert.key = key contents = { 'action': 'create', 'username': student.username, 'course_id': course_id, 'name': profile.name, 'grade': grade['grade'], 'template_pdf': template_pdf, } new_status = status.generating cert.status = new_status cert.save() self._send_to_xqueue(contents, key) else: new_status = status.notpassing cert.status = new_status cert.save() return new_status
def handle(self, *args, **options): LOGGER.info( ( u"Starting to create tasks for ungenerated certificates " u"with arguments %s and options %s" ), text_type(args), text_type(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 course = CourseKey.from_string(options['course']) ended_courses = [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 ) 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, text_type(course_key) ) if cert_status in valid_statuses: if not options['noop']: # Add the certificate request to the queue ret = generate_user_certificates( student, course_key, course=course, insecure=options['insecure'] ) 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, text_type(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, text_type(course_key) ) else: LOGGER.info( ( u"Skipped student %s because " u"certificate status '%s' is not in %s" ), student.id, cert_status, text_type(valid_statuses) ) LOGGER.info( ( u"Completed ungenerated certificates command " u"for course '%s'" ), text_type(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] 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") if options['api_key']: api_key = options['api_key'] else: raise CommandError( "You must give a api_key, if don't have one visit: https://accredible.com/issuer/sign_up" ) if options['styling']: if options['styling'] == 'True': new_status = "generating" else: raise CommandError( "You must give true if want to do no styling, no any other argument" ) else: new_status = "downloadable" 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 = CertificateGeneration(api_key=api_key) total = enrolled_students.count() print "Total number of students: " + str(total) for student in enrolled_students: if certificate_status_for_student( student, course_key)['status'] in valid_statuses: ret = xq.add_cert(student, course_key, new_status, course=course) print ret
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title='None'): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. Will change the certificate status to 'generating'. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status """ VALID_STATUSES = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing ] cert_status = certificate_status_for_student(student, course_id)['status'] new_status = cert_status if cert_status in VALID_STATUSES: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = courses.get_course_by_id(course_id) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed self.request.user = student self.request.session = {} course_name = course.display_name or course_id.to_deprecated_string( ) is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, self.request, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( student, course_id) mode_is_verified = ( enrollment_mode == GeneratedCertificate.MODES.verified) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified( student) user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all( course_id, student) cert_mode = enrollment_mode if (mode_is_verified and user_is_verified and user_is_reverified): template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format( id=course_id) elif (mode_is_verified and not (user_is_verified and user_is_reverified)): template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) cert_mode = GeneratedCertificate.MODES.honor else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) if forced_grade: grade['grade'] = forced_grade cert, __ = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile_name # Strip HTML from grade range label grade_contents = grade.get('grade', None) try: grade_contents = lxml.html.fromstring( grade_contents).text_content() except (TypeError, XMLSyntaxError, ParserError) as e: # Despite blowing up the xml parser, bad values here are fine grade_contents = None if is_whitelisted or grade_contents is not None: # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue if self.restricted.filter(user=student).exists(): new_status = status.restricted cert.status = new_status cert.save() else: key = make_hashkey(random.random()) cert.key = key contents = { 'action': 'create', 'username': student.username, 'course_id': course_id.to_deprecated_string(), 'course_name': course_name, 'name': profile_name, 'grade': grade_contents, 'template_pdf': template_pdf, } if template_file: contents['template_pdf'] = template_file new_status = status.generating cert.status = new_status cert.save() self._send_to_xqueue(contents, key) else: cert_status = status.notpassing cert.status = cert_status cert.save() return new_status
def add_cert(self, student, course_id, course=None): """ Arguments: student - User.object course_id - courseenrollment.course_id (string) Request a new certificate for a student. Will change the certificate status to 'generating'. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status """ VALID_STATUSES = [status.generating, status.unavailable, status.deleted, status.error, status.notpassing] cert_status = certificate_status_for_student( student, course_id)['status'] if cert_status in VALID_STATUSES: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = courses.get_course_by_id(course_id) profile = UserProfile.objects.get(user=student) cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) grade = grades.grade(student, self.request, course) is_whitelisted = self.whitelist.filter( user=student, course_id=course_id, whitelist=True).exists() if is_whitelisted or grade['grade'] is not None: key = make_hashkey(random.random()) cert.grade = grade['percent'] cert.user = student cert.course_id = course_id cert.key = key cert.name = profile.name # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue if self.restricted.filter(user=student).exists(): cert.status = status.restricted else: contents = { 'action': 'create', 'username': student.username, 'course_id': course_id, 'name': profile.name, } cert.status = status.generating self._send_to_xqueue(contents, key) cert.save() else: cert_status = status.notpassing cert.grade = grade['percent'] cert.status = cert_status cert.user = student cert.course_id = course_id cert.name = profile.name cert.save() return cert_status
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title="None"): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. Will change the certificate status to 'generating'. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status """ VALID_STATUSES = [status.generating, status.unavailable, status.deleted, status.error, status.notpassing] cert_status = certificate_status_for_student(student, course_id)["status"] new_status = cert_status if cert_status in VALID_STATUSES: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = courses.get_course_by_id(course_id) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed self.request.user = student self.request.session = {} course_name = course.display_name or course_id.to_deprecated_string() is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, self.request, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) mode_is_verified = enrollment_mode == GeneratedCertificate.MODES.verified user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student) cert_mode = enrollment_mode if mode_is_verified and user_is_verified and user_is_reverified: template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id) elif mode_is_verified and not (user_is_verified and user_is_reverified): template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) cert_mode = GeneratedCertificate.MODES.honor else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) if forced_grade: grade["grade"] = forced_grade cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = grade["percent"] cert.course_id = course_id cert.name = profile_name # Strip HTML from grade range label grade_contents = grade.get("grade", None) try: grade_contents = lxml.html.fromstring(grade_contents).text_content() except (TypeError, XMLSyntaxError, ParserError) as e: # Despite blowing up the xml parser, bad values here are fine grade_contents = None if is_whitelisted or grade_contents is not None: # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue if self.restricted.filter(user=student).exists(): new_status = status.restricted cert.status = new_status cert.save() else: key = make_hashkey(random.random()) cert.key = key contents = { "action": "create", "username": student.username, "course_id": course_id.to_deprecated_string(), "course_name": course_name, "name": profile_name, "grade": grade_contents, "template_pdf": template_pdf, } if template_file: contents["template_pdf"] = template_file new_status = status.generating cert.status = new_status cert.save() self._send_to_xqueue(contents, key) else: cert_status = status.notpassing cert.status = cert_status cert.save() return new_status
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 add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title='None', generate_pdf=True): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. generate_pdf - Boolean should a message be sent in queue to generate certificate PDF Will change the certificate status to 'generating' or `downloadable` in case of web view certificates. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status and newly created certificate instance """ valid_statuses = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing, status.downloadable ] cert_status = certificate_status_for_student(student, course_id)['status'] new_status = cert_status cert = None if cert_status not in valid_statuses: LOGGER.warning( (u"Cannot create certificate generation task for user %s " u"in the course '%s'; " u"the certificate status '%s' is not one of %s."), student.id, unicode(course_id), cert_status, unicode(valid_statuses)) else: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = modulestore().get_course(course_id, depth=0) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed self.request.user = student self.request.session = {} course_name = course.display_name or unicode(course_id) is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, self.request, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( student, course_id) mode_is_verified = ( enrollment_mode == GeneratedCertificate.MODES.verified) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified( student) cert_mode = enrollment_mode if mode_is_verified and user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format( id=course_id) elif mode_is_verified and not user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) cert_mode = GeneratedCertificate.MODES.honor else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) if forced_grade: grade['grade'] = forced_grade cert, __ = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile_name cert.download_url = '' # Strip HTML from grade range label grade_contents = grade.get('grade', None) try: grade_contents = lxml.html.fromstring( grade_contents).text_content() except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info( (u"Could not retrieve grade for student %s " u"in the course '%s' " u"because an exception occurred while parsing the " u"grade contents '%s' as HTML. " u"The exception was: '%s'"), student.id, unicode(course_id), grade_contents, unicode(exc)) # Despite blowing up the xml parser, bad values here are fine grade_contents = None if is_whitelisted or grade_contents is not None: if is_whitelisted: LOGGER.info(u"Student %s is whitelisted in '%s'", student.id, unicode(course_id)) # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue if self.restricted.filter(user=student).exists(): new_status = status.restricted cert.status = new_status cert.save() LOGGER.info(( u"Student %s is in the embargoed country restricted " u"list, so their certificate status has been set to '%s' " u"for the course '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, new_status, unicode(course_id)) else: key = make_hashkey(random.random()) cert.key = key contents = { 'action': 'create', 'username': student.username, 'course_id': unicode(course_id), 'course_name': course_name, 'name': profile_name, 'grade': grade_contents, 'template_pdf': template_pdf, } if template_file: contents['template_pdf'] = template_file if generate_pdf: new_status = status.generating else: new_status = status.downloadable cert.verify_uuid = uuid4().hex cert.status = new_status cert.save() if generate_pdf: try: self._send_to_xqueue(contents, key) except XQueueAddToQueueError as exc: new_status = ExampleCertificate.STATUS_ERROR cert.status = new_status cert.error_reason = unicode(exc) cert.save() LOGGER.critical(( u"Could not add certificate task to XQueue. " u"The course was '%s' and the student was '%s'." u"The certificate task status has been marked as 'error' " u"and can be re-submitted with a management command." ), course_id, student.id) else: LOGGER.info(( u"The certificate status has been set to '%s'. " u"Sent a certificate grading task to the XQueue " u"with the key '%s'. "), new_status, key) else: new_status = status.notpassing cert.status = new_status cert.save() LOGGER.info(( u"Student %s does not have a grade for '%s', " u"so their certificate status has been set to '%s'. " u"No certificate generation task was sent to the XQueue."), student.id, unicode(course_id), new_status) return new_status, cert
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)