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 = CourseKey.from_string(request.POST.get('course_id')) course = modulestore().get_course(course_key, depth=2) status = certificate_status_for_student(student, course_key)['status'] if can_generate_certificate_task(student, course_key): log.info( f'{course_key} is using V2 course certificates. Attempt will be made to generate a V2 ' f'certificate for user {student.id}.') generate_certificate_task(student, course_key) elif status in [ CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error ]: log_msg = '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) return HttpResponse(json.dumps({'add_status': status}), content_type='application/json') # pylint: disable=http-response-with-content-type-json, http-response-with-json-dumps return HttpResponse(json.dumps({'add_status': 'ERRORANONYMOUSUSER'}), content_type='application/json') # pylint: disable=http-response-with-content-type-json, http-response-with-json-dumps
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, uuid 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, 'is_unverified': True if current_status['status'] == CertificateStatuses.unverified else False, 'download_url': None, 'uuid': None, } may_view_certificate = CourseOverview.get_from_id(course_key).may_certify() if current_status['status'] == CertificateStatuses.downloadable and may_view_certificate: response_data['is_downloadable'] = True response_data['download_url'] = current_status['download_url'] or get_certificate_url(student.id, course_key) response_data['uuid'] = current_status['uuid'] return response_data
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 = CourseKey.from_string(request.POST.get('course_id')) course = get_course(course_key) 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}), 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 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, course_id=self.course.id, ), 'uuid': cert_status['uuid'] })
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, uuid 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, 'is_unverified': True if current_status['status'] == CertificateStatuses.unverified else False, 'download_url': None, 'uuid': None, } may_view_certificate = CourseOverview.get_from_id(course_key).may_certify() if current_status['status'] == CertificateStatuses.downloadable and may_view_certificate: response_data['is_downloadable'] = True response_data['download_url'] = current_status['download_url'] or get_certificate_url(student.id, course_key) response_data['uuid'] = current_status['uuid'] return response_data
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 get_has_cert(self, model): user = self.context['request'].user if user.is_authenticated(): cert_status = certificate_status_for_student(user, model.id)['status'] return cert_status == 'downloadable' else: return False
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 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 can_view_course(cls, user, course_id): is_vip = cls.is_vip(user) cert_status = certificate_status_for_student(user, course_id)['status'] normal_enroll = CourseEnrollment.get_enrollment(user, course_id) vip_enroll = VIPCourseEnrollment.objects.filter(user=user, course_id=course_id, is_active=True).exists() is_buyed = normal_enroll and vip_enroll return not (is_vip == False and cert_status != 'downloadable' and is_buyed != False)
def test_with_downloadable_web_cert(self): cert_status = certificate_status_for_student(self.student, self.course.id) assert certificate_downloadable_status(self.student, self.course.id) ==\ {'is_downloadable': True, 'is_generating': False, 'is_unverified': False, 'download_url': f'/certificates/{cert_status["uuid"]}', 'is_pdf_certificate': False, 'uuid': cert_status['uuid']}
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, uuid 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, # pylint: disable=simplifiable-if-expression CertificateStatuses.error ] else False, 'is_unverified': True if current_status['status'] == CertificateStatuses.unverified else False, # pylint: disable=simplifiable-if-expression 'download_url': None, 'uuid': None, } course_overview = CourseOverview.get_from_id(course_key) if (not certificates_viewable_for_course(course_overview) and (current_status['status'] in CertificateStatuses.PASSED_STATUSES) and course_overview.certificate_available_date): response_data['earned_but_not_available'] = True may_view_certificate = course_overview.may_certify() if current_status[ 'status'] == CertificateStatuses.downloadable and may_view_certificate: response_data['is_downloadable'] = True response_data['download_url'] = current_status[ 'download_url'] or get_certificate_url(student.id, course_key, current_status['uuid']) response_data['is_pdf_certificate'] = bool( current_status['download_url']) response_data['uuid'] = current_status['uuid'] return response_data
def test_with_downloadable_web_cert(self): CourseEnrollment.enroll(self.student, self.course.id, mode='honor') self._setup_course_certificate() with mock_passing_grade(): generate_user_certificates(self.student, self.course.id) cert_status = certificate_status_for_student(self.student, self.course.id) assert certificate_downloadable_status(self.student, self.course.id) ==\ {'is_downloadable': True, 'is_generating': False, 'is_unverified': False, 'download_url': f'/certificates/{cert_status["uuid"]}', 'is_pdf_certificate': False, 'uuid': cert_status['uuid']}
def issue(request, course_id): student = request.user # Extracts CourseLocator Object from course ID. course_key = locator.CourseLocator.from_string(course_id) # Extract the CourseOverview object of the course. course = modulestore().get_course(course_key) # Check the status of GeneratedCertificate in XQueue. certificate_status = \ certificate_status_for_student(student, course.id)['status'] # init with a fail, if it didn't match the conditions for issue. template = 'edraak_certificates/fail.html' # If user didn't issue or pass certificate, or an error happened # in the generation. if certificate_status in [CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error]: # If the course is certifiable and the user has passed it. if is_certificate_allowed(student, course) and \ is_student_pass(student, course_id): forced_grade = None if has_access(student, 'staff', course): forced_grade = "Pass" # generate the certificate generate_user_certificates(student, course.id, course=course, forced_grade=forced_grade) template = 'edraak_certificates/issue.html' elif certificate_status == CertificateStatuses.downloadable: cert = GeneratedCertificate.objects.get( user_id=student.id, course_id=course_key, status=CertificateStatuses.downloadable ) return render_cert_by_uuid(request, cert.verify_uuid) return render_to_response(template, { 'course_id': course_id, 'user': student, 'cert_course': course, 'is_staff': has_access(student, 'staff', course), 'studio_url': settings.CMS_BASE })
def cert_info(user, course_overview): """ Get the certificate info needed to render the dashboard section for the given student and course. Arguments: user (User): A user. course_overview (CourseOverview): A course. Returns: See _cert_info """ return _cert_info( user, course_overview, certificate_status_for_student(user, course_overview.id), )
def test_with_downloadable_web_cert(self): CourseEnrollment.enroll(self.student, self.course.id, mode='honor') self._setup_course_certificate() with 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, course_id=self.course.id, ), 'uuid': cert_status['uuid'] } )
def cert_info(user, course_overview): """ Get the certificate info needed to render the dashboard section for the given student and course. Arguments: user (User): A user. course_overview (CourseOverview): A course. Returns: dict: A dictionary with keys: 'status': one of 'generating', 'downloadable', 'notpassing', 'processing', 'restricted', 'unavailable', or 'certificate_earned_but_not_available' 'download_url': url, only present if show_download_url is True 'show_survey_button': bool 'survey_url': url, only if show_survey_button is True 'grade': if status is not 'processing' 'can_unenroll': if status allows for unenrollment """ return _cert_info(user, course_overview, certificate_status_for_student(user, course_overview.id))
def download(request, course_id): user = request.user # Extracts CourseLocator Object from course ID. course_key = locator.CourseLocator.from_string(course_id) # Extract the CourseOverview object of the course. course = modulestore().get_course(course_key) certificate_status = \ certificate_status_for_student(user, course.id)['status'] if certificate_status == CertificateStatuses.downloadable or is_student_pass(user, course_id): pdf_file = generate_certificate(request, course_id) file_size = os.path.getsize(pdf_file.name) wrapper = FileWrapper(pdf_file) # `application/octet-stream` is to force download response = HttpResponse(wrapper, content_type='application/octet-stream') response['Content-Length'] = file_size response['Content-Disposition'] = "attachment; filename=Edraak-Certificate.pdf" return response else: return redirect(reverse('dashboard'))
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 = CourseKey.from_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' log.info(log_msg, username, course_key) status = generate_user_certificates(student, course_key, course=course) return HttpResponse(json.dumps({'add_status': status}), content_type='application/json') return HttpResponse(json.dumps({'add_status': 'ERRORANONYMOUSUSER'}), content_type='application/json')
def cert_info(user, course_overview): """ Get the certificate info needed to render the dashboard section for the given student and course. Arguments: user (User): A user. course_overview (CourseOverview): A course. Returns: dict: A dictionary with keys: 'status': one of 'generating', 'downloadable', 'notpassing', 'processing', 'restricted', 'unavailable', or 'certificate_earned_but_not_available' 'download_url': url, only present if show_download_url is True 'show_survey_button': bool 'survey_url': url, only if show_survey_button is True 'grade': if status is not 'processing' 'can_unenroll': if status allows for unenrollment """ return _cert_info( user, course_overview, certificate_status_for_student(user, course_overview.id) )
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(u"{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 add_cert(self, student, course_id, 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 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( ("Cannot create certificate generation task for user %s " "in the course '%s'; " "certificates are not allowed for CCX courses."), student.id, str(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, status.unverified, ] cert_status_dict = certificate_status_for_student(student, course_id) cert_status = cert_status_dict.get('status') download_url = cert_status_dict.get('download_url') cert = None if download_url: self._log_pdf_cert_generation_discontinued_warning( student.id, course_id, cert_status, download_url) return None if cert_status not in valid_statuses: LOGGER.warning( ("Cannot create certificate generation task for user %s " "in the course '%s'; " "the certificate status '%s' is not one of %s."), student.id, str(course_id), cert_status, str(valid_statuses)) return None 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().read(student, course_key=course_id) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( student, course_id) mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES user_is_verified = IDVerificationService.user_is_verified(student) cert_mode = enrollment_mode is_eligible_for_certificate = modes_api.is_eligible_for_certificate( enrollment_mode, cert_status) if is_whitelisted and not is_eligible_for_certificate: # check if audit certificates are enabled for audit mode is_eligible_for_certificate = enrollment_mode != CourseMode.AUDIT or \ not settings.FEATURES['DISABLE_AUDIT_CERTIFICATES'] unverified = False # For credit mode generate verified certificate if cert_mode in (CourseMode.CREDIT_MODE, CourseMode.MASTERS): 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(( "Certificate generated for student %s in the course: %s with template: %s. " "given template: %s, " "user is verified: %s, " "mode is verified: %s," "generate_pdf is: %s"), student.username, str(course_id), template_pdf, template_file, user_is_verified, mode_is_verified, generate_pdf) cert, __ = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) 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 passing = False try: grade_contents = lxml.html.fromstring( grade_contents).text_content() passing = True except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info(("Could not retrieve grade for student %s " "in the course '%s' " "because an exception occurred while parsing the " "grade contents '%s' as HTML. " "The exception was: '%s'"), student.id, str(course_id), grade_contents, str(exc)) # Check if the student is whitelisted if is_whitelisted: LOGGER.info("Student %s is whitelisted in '%s'", student.id, str(course_id)) passing = True # 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 = status.audit_passing if passing else status.audit_notpassing cert.save() LOGGER.info( "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( ("Student %s does not have a grade for '%s', " "so their certificate status has been set to '%s'. " "No certificate generation task was sent to the XQueue."), student.id, str(course_id), cert.status) return cert if unverified: cert.status = status.unverified cert.save() LOGGER.info( ("User %s has a verified enrollment in course %s " "but is missing ID verification. " "Certificate status has been set to unverified"), student.id, str(course_id), ) return cert # Finally, generate the certificate and send it off. return self._generate_cert(cert, student, grade_contents, template_pdf, generate_pdf)
def to_representation(self, course_overview): course_id = unicode(course_overview.id) request = self.context.get('request') return { # identifiers 'id': course_id, 'name': course_overview.display_name, 'number': course_overview.display_number_with_default, 'org': course_overview.display_org_with_default, # dates 'start': course_overview.start, 'start_display': course_overview.start_display, 'start_type': course_overview.start_type, 'end': course_overview.end, # notification info 'subscription_id': course_overview.clean_id(padding_char='_'), # access info 'courseware_access': has_access(request.user, 'load_mobile', course_overview).to_json(), # various URLs # course_image is sent in both new and old formats # (within media to be compatible with the new Course API) 'media': { 'course_image': { 'uri': course_overview.course_image_url, 'name': 'Course Image', } }, 'course_image': course_overview.course_image_url, 'course_about': get_link_for_about_page(course_overview), 'course_sharing_utm_parameters': get_encoded_course_sharing_utm_params(), 'course_updates': reverse( 'course-updates-list', kwargs={'course_id': course_id}, request=request, ), 'course_handouts': reverse( 'course-handouts-list', kwargs={'course_id': course_id}, request=request, ), 'discussion_url': reverse( 'discussion_course', kwargs={'course_id': course_id}, request=request, ) if course_overview.is_discussion_tab_enabled() else None, 'video_outline': reverse( 'video-summary-list', kwargs={'course_id': course_id}, request=request, ), 'is_vip': VIPInfo.is_vip(request.user), 'is_normal_enroll': not VIPCourseEnrollment.objects.filter( user=request.user, course_id=course_overview.id, is_active=True).exists(), 'has_cert': certificate_status_for_student( request.user, course_overview.id)['status'] == 'downloadable', }
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 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: ''' rade 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 " + str(section_key)) if not description: description = "course_description" is_whitelisted = self.whitelist.filter( user=student, course_id=course_id, whitelist=True).exists() grade = CourseGradeFactory().read(student, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( student, course_id ) mode_is_verified = ( enrollment_mode == GeneratedCertificate.MODES.verified ) cert_mode = GeneratedCertificate.MODES.honor if forced_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 # convert percent to points as an integer grade_contents = int(grade.percent * 100) 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 defined_status == "generating": approve = False else: approve = True # check to see if this is a BETA course course_name = course_name.strip() if course_name.startswith("BETA") or course_name.startswith("Beta") or course_name.startswith("beta"): course_name = course_name[4:].strip() grade_into_string = grade.letter_grade payload = { "credential": { "name": course_name, "group_name": course_name, "description": description, "achievement_id": contents['course_id'], "course_link": "/courses/" + contents['course_id'] + "/about", "approve": approve, "template_name": contents['course_id'], "grade": contents['grade'], "recipient": { "name": contents['name'], "email": student.email } } } 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 handle(self, *args, **options): LOGGER.info(("Starting to create tasks for ungenerated certificates " "with arguments %s and options %s"), str(args), str(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: 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( f"{count}/{total} completed ~{hours:02}:{minutes:02}m remaining" ) start = datetime.datetime.now(UTC) cert_status = certificate_status_for_student( student, course_key)['status'] LOGGER.info(("Student %s has certificate status '%s' " "in course '%s'"), student.id, cert_status, str(course_key)) if cert_status in valid_statuses: if not options['noop']: # Add the certificate request to the queue generate_user_certificates( student, course_key, insecure=options['insecure']) LOGGER.info( f"Added a certificate generation task to the XQueue for student {student.id} in " f"course {course_key}.") else: LOGGER.info(("Skipping certificate generation for " "student %s in course '%s' " "because the noop flag is set."), student.id, str(course_key)) else: LOGGER.info(("Skipped student %s because " "certificate status '%s' is not in %s"), student.id, cert_status, str(valid_statuses)) LOGGER.info(("Completed ungenerated certificates command " "for course '%s'"), str(course_key))
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, status.unverified, ] cert_status_dict = certificate_status_for_student(student, course_id) cert_status = cert_status_dict.get('status') download_url = cert_status_dict.get('download_url') cert = None if download_url: self._log_pdf_cert_generation_discontinued_warning( student.id, course_id, cert_status, download_url ) return 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().read(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 = IDVerificationService.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 in (CourseMode.CREDIT_MODE, CourseMode.MASTERS): 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," u"generate_pdf is: %s" ), student.username, unicode(course_id), template_pdf, template_file, user_is_verified, mode_is_verified, generate_pdf ) cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) 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 = status.audit_passing if passing else status.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 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, status.unverified, ] 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().read(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," u"generate_pdf is: %s" ), student.username, unicode(course_id), template_pdf, template_file, user_is_verified, mode_is_verified, generate_pdf ) cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) 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 = status.audit_passing if passing else status.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)