Add context for navigation states added find course to dashboard sidebar and included check for context that Will adds in PR removed nav_course_search context due to design change so replaced with nav_hidden Removed rwd_header.js and all references as no longer being used. Wrapped Find Courses in dashboard sidebar in if statement
224 lines
9.3 KiB
Python
224 lines
9.3 KiB
Python
""" Commerce views. """
|
|
import logging
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from ecommerce_api_client import exceptions
|
|
from opaque_keys import InvalidKeyError
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from rest_framework.authentication import SessionAuthentication
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.status import HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT
|
|
from rest_framework.views import APIView
|
|
|
|
from commerce import ecommerce_api_client
|
|
from commerce.constants import Messages
|
|
from commerce.exceptions import InvalidResponseError
|
|
from commerce.http import DetailResponse, InternalRequestErrorResponse
|
|
from commerce.utils import audit_log
|
|
from course_modes.models import CourseMode
|
|
from courseware import courses
|
|
from edxmako.shortcuts import render_to_response
|
|
from enrollment.api import add_enrollment
|
|
from enrollment.views import EnrollmentCrossDomainSessionAuth
|
|
from embargo import api as embargo_api
|
|
from microsite_configuration import microsite
|
|
from student.models import CourseEnrollment
|
|
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
|
|
from util.json_request import JsonResponse
|
|
from verify_student.models import SoftwareSecurePhotoVerification
|
|
from shoppingcart.processors.CyberSource2 import is_user_payment_error
|
|
from django.utils.translation import ugettext as _
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class BasketsView(APIView):
|
|
""" Creates a basket with a course seat and enrolls users. """
|
|
|
|
# LMS utilizes User.user_is_active to indicate email verification, not whether an account is active. Sigh!
|
|
authentication_classes = (EnrollmentCrossDomainSessionAuth, OAuth2AuthenticationAllowInactiveUser)
|
|
permission_classes = (IsAuthenticated,)
|
|
|
|
def _is_data_valid(self, request):
|
|
"""
|
|
Validates the data posted to the view.
|
|
|
|
Arguments
|
|
request -- HTTP request
|
|
|
|
Returns
|
|
Tuple (data_is_valid, course_key, error_msg)
|
|
"""
|
|
course_id = request.DATA.get('course_id')
|
|
|
|
if not course_id:
|
|
return False, None, u'Field course_id is missing.'
|
|
|
|
try:
|
|
course_key = CourseKey.from_string(course_id)
|
|
courses.get_course(course_key)
|
|
except (InvalidKeyError, ValueError)as ex:
|
|
log.exception(u'Unable to locate course matching %s.', course_id)
|
|
return False, None, ex.message
|
|
|
|
return True, course_key, None
|
|
|
|
def _enroll(self, course_key, user):
|
|
""" Enroll the user in the course. """
|
|
add_enrollment(user.username, unicode(course_key))
|
|
|
|
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
|
"""
|
|
Attempt to create the basket and enroll the user.
|
|
"""
|
|
user = request.user
|
|
valid, course_key, error = self._is_data_valid(request)
|
|
if not valid:
|
|
return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)
|
|
|
|
embargo_response = embargo_api.get_embargo_response(request, course_key, user)
|
|
|
|
if embargo_response:
|
|
return embargo_response
|
|
|
|
# Don't do anything if an enrollment already exists
|
|
course_id = unicode(course_key)
|
|
enrollment = CourseEnrollment.get_enrollment(user, course_key)
|
|
if enrollment and enrollment.is_active:
|
|
msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
|
|
return DetailResponse(msg, status=HTTP_409_CONFLICT)
|
|
|
|
# If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS
|
|
# redirects to track selection.
|
|
honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)
|
|
|
|
if not honor_mode:
|
|
msg = Messages.NO_HONOR_MODE.format(course_id=course_id)
|
|
return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
|
|
elif not honor_mode.sku:
|
|
# If there are no course modes with SKUs, enroll the user without contacting the external API.
|
|
msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=CourseMode.HONOR, course_id=course_id,
|
|
username=user.username)
|
|
log.debug(msg)
|
|
self._enroll(course_key, user)
|
|
return DetailResponse(msg)
|
|
|
|
# Setup the API
|
|
|
|
try:
|
|
api = ecommerce_api_client(user)
|
|
except ValueError:
|
|
self._enroll(course_key, user)
|
|
msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key))
|
|
log.debug(msg)
|
|
return DetailResponse(msg)
|
|
|
|
# Make the API call
|
|
try:
|
|
response_data = api.baskets.post({
|
|
'products': [{'sku': honor_mode.sku}],
|
|
'checkout': True,
|
|
})
|
|
|
|
payment_data = response_data["payment_data"]
|
|
if payment_data:
|
|
# Pass data to the client to begin the payment flow.
|
|
return JsonResponse(payment_data)
|
|
elif response_data['order']:
|
|
# The order was completed immediately because there is no charge.
|
|
msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number'])
|
|
log.debug(msg)
|
|
return DetailResponse(msg)
|
|
else:
|
|
msg = u'Unexpected response from basket endpoint.'
|
|
log.error(
|
|
msg + u' Could not enroll user %(username)s in course %(course_id)s.',
|
|
{'username': user.id, 'course_id': course_id},
|
|
)
|
|
raise InvalidResponseError(msg)
|
|
except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
|
|
log.exception(ex.message)
|
|
return InternalRequestErrorResponse(ex.message)
|
|
finally:
|
|
audit_log(
|
|
'checkout_requested',
|
|
course_id=course_id,
|
|
mode=honor_mode.slug,
|
|
processor_name=None,
|
|
user_id=user.id
|
|
)
|
|
|
|
|
|
@csrf_exempt
|
|
def checkout_cancel(_request):
|
|
""" Checkout/payment cancellation view. """
|
|
context = {'payment_support_email': microsite.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL)}
|
|
return render_to_response("commerce/checkout_cancel.html", context)
|
|
|
|
|
|
@csrf_exempt
|
|
@login_required
|
|
def checkout_receipt(request):
|
|
""" Receipt view. """
|
|
|
|
page_title = _('Receipt')
|
|
is_payment_complete = True
|
|
payment_support_email = microsite.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL)
|
|
payment_support_link = '<a href=\"mailto:{email}\">{email}</a>'.format(email=payment_support_email)
|
|
|
|
is_cybersource = all(k in request.POST for k in ('signed_field_names', 'decision', 'reason_code'))
|
|
if is_cybersource and request.POST['decision'] != 'ACCEPT':
|
|
# Cybersource may redirect users to this view if it couldn't recover
|
|
# from an error while capturing payment info.
|
|
is_payment_complete = False
|
|
page_title = _('Payment Failed')
|
|
reason_code = request.POST['reason_code']
|
|
# if the problem was with the info submitted by the user, we present more detailed messages.
|
|
if is_user_payment_error(reason_code):
|
|
error_summary = _("There was a problem with this transaction. You have not been charged.")
|
|
error_text = _(
|
|
"Make sure your information is correct, or try again with a different card or another form of payment."
|
|
)
|
|
else:
|
|
error_summary = _("A system error occurred while processing your payment. You have not been charged.")
|
|
error_text = _("Please wait a few minutes and then try again.")
|
|
for_help_text = _("For help, contact {payment_support_link}.").format(payment_support_link=payment_support_link)
|
|
else:
|
|
# if anything goes wrong rendering the receipt, it indicates a problem fetching order data.
|
|
error_summary = _("An error occurred while creating your receipt.")
|
|
error_text = None # nothing particularly helpful to say if this happens.
|
|
for_help_text = _(
|
|
"If your course does not appear on your dashboard, contact {payment_support_link}."
|
|
).format(payment_support_link=payment_support_link)
|
|
|
|
context = {
|
|
'page_title': page_title,
|
|
'is_payment_complete': is_payment_complete,
|
|
'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME),
|
|
'verified': SoftwareSecurePhotoVerification.verification_valid_or_pending(request.user).exists(),
|
|
'error_summary': error_summary,
|
|
'error_text': error_text,
|
|
'for_help_text': for_help_text,
|
|
'payment_support_email': payment_support_email,
|
|
'nav_hidden': True,
|
|
}
|
|
return render_to_response('commerce/checkout_receipt.html', context)
|
|
|
|
|
|
class BasketOrderView(APIView):
|
|
""" Retrieve the order associated with a basket. """
|
|
|
|
authentication_classes = (SessionAuthentication,)
|
|
permission_classes = (IsAuthenticated,)
|
|
|
|
def get(self, request, *_args, **kwargs):
|
|
""" HTTP handler. """
|
|
try:
|
|
order = ecommerce_api_client(request.user).baskets(kwargs['basket_id']).order.get()
|
|
return JsonResponse(order)
|
|
except exceptions.HttpNotFoundError:
|
|
return JsonResponse(status=404)
|