def create_credit_request(course_key, provider_id, username): """ Initiate a request for credit from a credit provider. This will return the parameters that the user's browser will need to POST to the credit provider. It does NOT calculate the signature. Only users who are eligible for credit (have satisfied all credit requirements) are allowed to make requests. A provider can be configured either with *integration enabled* or not. If automatic integration is disabled, this method will simply return a URL to the credit provider and method set to "GET", so the student can visit the URL and request credit directly. No database record will be created to track these requests. If automatic integration *is* enabled, then this will also return the parameters that the user's browser will need to POST to the credit provider. These parameters will be digitally signed using a secret key shared with the credit provider. A database record will be created to track the request with a 32-character UUID. The returned dictionary can be used by the user's browser to send a POST request to the credit provider. If a pending request already exists, this function should return a request description with the same UUID. (Other parameters, such as the user's full name may be different than the original request). If a completed request (either accepted or rejected) already exists, this function will raise an exception. Users are not allowed to make additional requests once a request has been completed. Arguments: course_key (CourseKey): The identifier for the course. provider_id (str): The identifier of the credit provider. username (str): The user initiating the request. Returns: dict Raises: UserIsNotEligible: The user has not satisfied eligibility requirements for credit. CreditProviderNotConfigured: The credit provider has not been configured for this course. RequestAlreadyCompleted: The user has already submitted a request and received a response from the credit provider. Example Usage: >>> create_credit_request(course.id, "hogwarts", "ron") { "url": "https://credit.example.com/request", "method": "POST", "parameters": { "request_uuid": "557168d0f7664fe59097106c67c3f847", "timestamp": 1434631630, "course_org": "HogwartsX", "course_num": "Potions101", "course_run": "1T2015", "final_grade": "0.95", "user_username": "******", "user_email": "*****@*****.**", "user_full_name": "Ron Weasley", "user_mailing_address": "", "user_country": "US", "signature": "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI=" } } """ try: user_eligibility = CreditEligibility.objects.select_related('course').get( username=username, course__course_key=course_key ) credit_course = user_eligibility.course credit_provider = CreditProvider.objects.get(provider_id=provider_id) except CreditEligibility.DoesNotExist: log.warning( u'User "%s" tried to initiate a request for credit in course "%s", ' u'but the user is not eligible for credit', username, course_key ) raise UserIsNotEligible except CreditProvider.DoesNotExist: log.error(u'Credit provider with ID "%s" has not been configured.', provider_id) raise CreditProviderNotConfigured # Check if we've enabled automatic integration with the credit # provider. If not, we'll show the user a link to a URL # where the user can request credit directly from the provider. # Note that we do NOT track these requests in our database, # since the state would always be "pending" (we never hear back). if not credit_provider.enable_integration: return { "url": credit_provider.provider_url, "method": "GET", "parameters": {} } else: # If automatic credit integration is enabled, then try # to retrieve the shared signature *before* creating the request. # That way, if there's a misconfiguration, we won't have requests # in our system that we know weren't sent to the provider. shared_secret_key = get_shared_secret_key(credit_provider.provider_id) if shared_secret_key is None: msg = u'Credit provider with ID "{provider_id}" does not have a secret key configured.'.format( provider_id=credit_provider.provider_id ) log.error(msg) raise CreditProviderNotConfigured(msg) # Initiate a new request if one has not already been created credit_request, created = CreditRequest.objects.get_or_create( course=credit_course, provider=credit_provider, username=username, ) # Check whether we've already gotten a response for a request, # If so, we're not allowed to issue any further requests. # Skip checking the status if we know that we just created this record. if not created and credit_request.status != "pending": log.warning( ( u'Cannot initiate credit request because the request with UUID "%s" ' u'exists with status "%s"' ), credit_request.uuid, credit_request.status ) raise RequestAlreadyCompleted if created: credit_request.uuid = uuid.uuid4().hex # Retrieve user account and profile info user = User.objects.select_related('profile').get(username=username) # Retrieve the final grade from the eligibility table try: final_grade = CreditRequirementStatus.objects.get( username=username, requirement__namespace="grade", requirement__name="grade", requirement__course__course_key=course_key, status="satisfied" ).reason["final_grade"] # NOTE (CCB): Limiting the grade to seven characters is a hack for ASU. if len(unicode(final_grade)) > 7: final_grade = u'{:.5f}'.format(final_grade) else: final_grade = unicode(final_grade) except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError): msg = 'Could not retrieve final grade from the credit eligibility table for ' \ 'user [{user_id}] in course [{course_key}].'.format(user_id=user.id, course_key=course_key) log.exception(msg) raise UserIsNotEligible(msg) # Getting the students's enrollment date course_enrollment = CourseEnrollment.get_enrollment(user, course_key) enrollment_date = course_enrollment.created if course_enrollment else "" # Getting the student's course completion date completion_date = get_last_exam_completion_date(course_key, username) parameters = { "request_uuid": credit_request.uuid, "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)), "course_org": course_key.org, "course_num": course_key.course, "course_run": course_key.run, "enrollment_timestamp": to_timestamp(enrollment_date) if enrollment_date else "", "course_completion_timestamp": to_timestamp(completion_date) if completion_date else "", "final_grade": final_grade, "user_username": user.username, "user_email": user.email, "user_full_name": user.profile.name, "user_mailing_address": "", "user_country": ( user.profile.country.code if user.profile.country.code is not None else "" ), } credit_request.parameters = parameters credit_request.save() if created: log.info(u'Created new request for credit with UUID "%s"', credit_request.uuid) else: log.info( u'Updated request for credit with UUID "%s" so the user can re-issue the request', credit_request.uuid ) # Sign the parameters using a secret key we share with the credit provider. parameters["signature"] = signature(parameters, shared_secret_key) return { "url": credit_provider.provider_url, "method": "POST", "parameters": parameters }
def create_credit_request(course_key, provider_id, username): """ Initiate a request for credit from a credit provider. This will return the parameters that the user's browser will need to POST to the credit provider. It does NOT calculate the signature. Only users who are eligible for credit (have satisfied all credit requirements) are allowed to make requests. A provider can be configured either with *integration enabled* or not. If automatic integration is disabled, this method will simply return a URL to the credit provider and method set to "GET", so the student can visit the URL and request credit directly. No database record will be created to track these requests. If automatic integration *is* enabled, then this will also return the parameters that the user's browser will need to POST to the credit provider. These parameters will be digitally signed using a secret key shared with the credit provider. A database record will be created to track the request with a 32-character UUID. The returned dictionary can be used by the user's browser to send a POST request to the credit provider. If a pending request already exists, this function should return a request description with the same UUID. (Other parameters, such as the user's full name may be different than the original request). If a completed request (either accepted or rejected) already exists, this function will raise an exception. Users are not allowed to make additional requests once a request has been completed. Arguments: course_key (CourseKey): The identifier for the course. provider_id (str): The identifier of the credit provider. username (str): The user initiating the request. Returns: dict Raises: UserIsNotEligible: The user has not satisfied eligibility requirements for credit. CreditProviderNotConfigured: The credit provider has not been configured for this course. RequestAlreadyCompleted: The user has already submitted a request and received a response from the credit provider. Example Usage: >>> create_credit_request(course.id, "hogwarts", "ron") { "url": "https://credit.example.com/request", "method": "POST", "parameters": { "request_uuid": "557168d0f7664fe59097106c67c3f847", "timestamp": 1434631630, "course_org": "HogwartsX", "course_num": "Potions101", "course_run": "1T2015", "final_grade": "0.95", "user_username": "******", "user_email": "*****@*****.**", "user_full_name": "Ron Weasley", "user_mailing_address": "", "user_country": "US", "signature": "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI=" } } """ try: user_eligibility = CreditEligibility.objects.select_related( 'course').get(username=username, course__course_key=course_key) credit_course = user_eligibility.course credit_provider = CreditProvider.objects.get(provider_id=provider_id) except CreditEligibility.DoesNotExist: log.warning( u'User "%s" tried to initiate a request for credit in course "%s", ' u'but the user is not eligible for credit', username, course_key) raise UserIsNotEligible except CreditProvider.DoesNotExist: log.error(u'Credit provider with ID "%s" has not been configured.', provider_id) raise CreditProviderNotConfigured # Check if we've enabled automatic integration with the credit # provider. If not, we'll show the user a link to a URL # where the user can request credit directly from the provider. # Note that we do NOT track these requests in our database, # since the state would always be "pending" (we never hear back). if not credit_provider.enable_integration: return { "url": credit_provider.provider_url, "method": "GET", "parameters": {} } else: # If automatic credit integration is enabled, then try # to retrieve the shared signature *before* creating the request. # That way, if there's a misconfiguration, we won't have requests # in our system that we know weren't sent to the provider. shared_secret_key = get_shared_secret_key(credit_provider.provider_id) if shared_secret_key is None: msg = u'Credit provider with ID "{provider_id}" does not have a secret key configured.'.format( provider_id=credit_provider.provider_id) log.error(msg) raise CreditProviderNotConfigured(msg) # Initiate a new request if one has not already been created credit_request, created = CreditRequest.objects.get_or_create( course=credit_course, provider=credit_provider, username=username, ) # Check whether we've already gotten a response for a request, # If so, we're not allowed to issue any further requests. # Skip checking the status if we know that we just created this record. if not created and credit_request.status != "pending": log.warning(( u'Cannot initiate credit request because the request with UUID "%s" ' u'exists with status "%s"'), credit_request.uuid, credit_request.status) raise RequestAlreadyCompleted if created: credit_request.uuid = uuid.uuid4().hex # Retrieve user account and profile info user = User.objects.select_related('profile').get(username=username) # Retrieve the final grade from the eligibility table try: final_grade = CreditRequirementStatus.objects.get( username=username, requirement__namespace="grade", requirement__name="grade", requirement__course__course_key=course_key, status="satisfied").reason["final_grade"] # NOTE (CCB): Limiting the grade to seven characters is a hack for ASU. if len(unicode(final_grade)) > 7: final_grade = u'{:.5f}'.format(final_grade) else: final_grade = unicode(final_grade) except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError): msg = 'Could not retrieve final grade from the credit eligibility table for ' \ 'user [{user_id}] in course [{course_key}].'.format(user_id=user.id, course_key=course_key) log.exception(msg) raise UserIsNotEligible(msg) # Getting the students's enrollment date course_enrollment = CourseEnrollment.get_enrollment(user, course_key) enrollment_date = course_enrollment.created if course_enrollment else "" # Getting the student's course completion date completion_date = get_last_exam_completion_date(course_key, username) parameters = { "request_uuid": credit_request.uuid, "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)), "course_org": course_key.org, "course_num": course_key.course, "course_run": course_key.run, "enrollment_timestamp": to_timestamp(enrollment_date) if enrollment_date else "", "course_completion_timestamp": to_timestamp(completion_date) if completion_date else "", "final_grade": final_grade, "user_username": user.username, "user_email": user.email, "user_full_name": user.profile.name, "user_mailing_address": "", "user_country": (user.profile.country.code if user.profile.country.code is not None else ""), } credit_request.parameters = parameters credit_request.save() if created: log.info(u'Created new request for credit with UUID "%s"', credit_request.uuid) else: log.info( u'Updated request for credit with UUID "%s" so the user can re-issue the request', credit_request.uuid) # Sign the parameters using a secret key we share with the credit provider. parameters["signature"] = signature(parameters, shared_secret_key) return { "url": credit_provider.provider_url, "method": "POST", "parameters": parameters }