def test_no_product(self): """ Verify that an exception is raised if there is no product. """ voucher = VoucherFactory(code='NOPRODUCT') offer = ConditionalOfferFactory() voucher.offers.add(offer) with self.assertRaises(exceptions.ProductNotFoundError): get_voucher_and_products_from_code(code='NOPRODUCT')
def test_no_product(self): """ Verify that an exception is raised if there is no product. """ voucher = VoucherFactory(code='NOPRODUCT') offer = ConditionalOfferFactory() voucher.offers.add(offer) with self.assertRaises(exceptions.ProductNotFoundError): get_voucher_and_products_from_code(code='NOPRODUCT')
def test_get_offers_for_multiple_courses_voucher(self): """ Verify that the course offers data is returned for a multiple courses voucher. """ course, seat = self.create_course_and_seat() self.mock_dynamic_catalog_course_runs_api(query='*:*', course_run=course) new_range, __ = Range.objects.get_or_create(catalog_query='*:*') new_range.add_product(seat) voucher, __ = prepare_voucher(_range=new_range, benefit_value=10) voucher, products = get_voucher_and_products_from_code(voucher.code) benefit = voucher.offers.first().benefit request = self.prepare_offers_listing_request(voucher.code) offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher) first_offer = offers[0] self.assertEqual(len(offers), 1) self.assertDictEqual(first_offer, { 'benefit': { 'type': benefit.type, 'value': benefit.value }, 'contains_verified': False, 'course_start_date': '2016-05-01T00:00:00Z', 'id': course.id, 'image_url': 'path/to/the/course/image', 'organization': CourseKey.from_string(course.id).org, 'seat_type': course.type, 'stockrecords': serializers.StockRecordSerializer(seat.stockrecords.first()).data, 'title': course.name, 'voucher_end_date': voucher.end_datetime })
def offers(self, request): """ Preview the courses offered by the voucher. Paginated Response containing the list of course offers will be returned. --- parameters: - name: code description: Voucher code required: true type: string paramType: query multiple: false """ code = request.GET.get('code', '') try: voucher, products = get_voucher_and_products_from_code(code) except Voucher.DoesNotExist: logger.error('Voucher with code %s not found.', code) return Response(status=status.HTTP_400_BAD_REQUEST) except exceptions.ProductNotFoundError: logger.error('No product(s) are associated with this code.') return Response(status=status.HTTP_400_BAD_REQUEST) offers = self.get_offers(products, request, voucher) page = self.paginate_queryset(offers) return self.get_paginated_response(page)
def test_get_offers_for_single_course_voucher(self): """ Verify that the course offers data is returned for a single course voucher. """ course, seat = self.create_course_and_seat() new_range = RangeFactory(products=[seat, ]) voucher, __ = prepare_voucher(_range=new_range, benefit_value=10) voucher, products = get_voucher_and_products_from_code(voucher.code) benefit = voucher.offers.first().benefit request = self.prepare_offers_listing_request(voucher.code) self.mock_course_api_response(course=course) offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher) first_offer = offers[0] self.assertEqual(len(offers), 1) self.assertDictEqual(first_offer, { 'benefit': { 'type': benefit.type, 'value': benefit.value }, 'contains_verified': True, 'course_start_date': '2013-02-05T05:00:00Z', 'id': course.id, 'image_url': get_lms_url('/asset-v1:test+test+test+type@asset+block@images_course_image.jpg'), 'organization': CourseKey.from_string(course.id).org, 'seat_type': course.type, 'stockrecords': serializers.StockRecordSerializer(seat.stockrecords.first()).data, 'title': course.name, 'voucher_end_date': voucher.end_datetime })
def offers(self, request): """ Preview the courses offered by the voucher. Paginated Response containing the list of course offers will be returned. --- parameters: - name: code description: Voucher code required: true type: string paramType: query multiple: false """ code = request.GET.get('code', '') try: voucher, products = get_voucher_and_products_from_code(code) except Voucher.DoesNotExist: logger.error('Voucher with code %s not found.', code) return Response(status=status.HTTP_400_BAD_REQUEST) except exceptions.ProductNotFoundError: logger.error('No product(s) are associated with this code.') return Response(status=status.HTTP_400_BAD_REQUEST) offers = self.get_offers(products, request, voucher) page = self.paginate_queryset(offers) return self.get_paginated_response(page)
def test_get_voucher_and_products_from_code(self): """ Verify that get_voucher_and_products_from_code() returns products and voucher. """ original_voucher, original_product = prepare_voucher(code=COUPON_CODE) voucher, products = get_voucher_and_products_from_code(code=COUPON_CODE) self.assertIsNotNone(voucher) self.assertEqual(voucher, original_voucher) self.assertEqual(voucher.code, COUPON_CODE) self.assertEqual(len(products), 1) self.assertEqual(products[0], original_product)
def test_get_voucher_and_products_from_code(self): """ Verify that get_voucher_and_products_from_code() returns products and voucher. """ original_voucher, original_product = prepare_voucher(code=COUPON_CODE) voucher, products = get_voucher_and_products_from_code(code=COUPON_CODE) self.assertIsNotNone(voucher) self.assertEqual(voucher, original_voucher) self.assertEqual(voucher.code, COUPON_CODE) self.assertEqual(len(products), 1) self.assertEqual(products[0], original_product)
def test_voucher_offers_listing_catalog_query(self): """ Verify the endpoint returns offers data for single product range. """ course, seat = self.create_course_and_seat() self.mock_dynamic_catalog_course_runs_api(query='*:*', course_run=course) new_range, __ = Range.objects.get_or_create(catalog_query='*:*') new_range.add_product(seat) voucher, __ = prepare_voucher(_range=new_range) voucher, __ = get_voucher_and_products_from_code(voucher.code) request = self.prepare_offers_listing_request(voucher.code) response = self.endpointView(request) self.assertEqual(response.status_code, 200) self.assertGreater(len(response.data), 0)
def get(self, request): partner = get_partner_for_site(request) sku = request.GET.get('sku', None) code = request.GET.get('code', None) if not sku: return HttpResponseBadRequest(_('No SKU provided.')) if code: voucher, __ = get_voucher_and_products_from_code(code=code) else: voucher = None try: product = StockRecord.objects.get(partner=partner, partner_sku=sku).product except StockRecord.DoesNotExist: return HttpResponseBadRequest( _('SKU [{sku}] does not exist.').format(sku=sku)) # If the product isn't available then there's no reason to continue with the basket addition purchase_info = request.strategy.fetch_for_product(product) if not purchase_info.availability.is_available_to_buy: msg = _('Product [{product}] not available to buy.').format( product=product.title) return HttpResponseBadRequest(msg) # If the product is not an Enrollment Code, we check to see if the user is already # enrolled to prevent double-enrollment and/or accidental coupon usage if product.get_product_class( ).name != ENROLLMENT_CODE_PRODUCT_CLASS_NAME: try: if request.user.is_user_already_enrolled(request, product): logger.warning( 'User [%s] attempted to repurchase the [%s] seat of course [%s]', request.user.username, mode_for_seat(product), product.attr.course_key) msg = _('You are already enrolled in {course}.').format( course=product.course.name) return HttpResponseBadRequest(msg) except (ConnectionError, SlumberBaseException, Timeout): msg = _( 'An error occurred while retrieving enrollment details. Please try again.' ) return HttpResponseBadRequest(msg) # At this point we're either adding an Enrollment Code product to the basket, # or the user is adding a Seat product for which they are not already enrolled prepare_basket(request, product, voucher) return HttpResponseRedirect(reverse('basket:summary'), status=303)
def test_voucher_offers_listing_catalog_query(self): """ Verify the endpoint returns offers data for single product range. """ course, seat = self.create_course_and_seat() self.mock_dynamic_catalog_course_runs_api(query='*:*', course_run=course) new_range, __ = Range.objects.get_or_create(catalog_query='*:*') new_range.add_product(seat) voucher, __ = prepare_voucher(_range=new_range) voucher, __ = get_voucher_and_products_from_code(voucher.code) request = self.prepare_offers_listing_request(voucher.code) response = self.endpointView(request) self.assertEqual(response.status_code, 200) self.assertGreater(len(response.data), 0)
def test_omitting_unavailable_seats(self): """ Verify an unavailable seat is omitted from offer page results. """ course1, seat1 = self.create_course_and_seat() course2, seat2 = self.create_course_and_seat() course_run_info = { 'count': 2, 'next': 'path/to/the/next/page', 'results': [{ 'key': course1.id, 'title': course1.name, 'start': '2016-05-01T00:00:00Z', 'image': { 'src': 'path/to/the/course/image' } }, { 'key': course2.id, 'title': course2.name, 'start': '2016-05-01T00:00:00Z', 'image': { 'src': 'path/to/the/course/image' } }] } self.mock_dynamic_catalog_course_runs_api( query='*:*', course_run_info=course_run_info) new_range, __ = Range.objects.get_or_create(catalog_query='*:*') new_range.add_product(seat1) new_range.add_product(seat2) voucher, __ = prepare_voucher(_range=new_range) voucher, products = get_voucher_and_products_from_code(voucher.code) factory = APIRequestFactory() request = factory.get('/?code={}&page_size=6'.format(voucher.code)) request.site = self.site request.strategy = DefaultStrategy() offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)['results'] self.assertEqual(len(offers), 2) products[1].expires = pytz.utc.localize(datetime.datetime.min) offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)['results'] self.assertEqual(len(offers), 1)
def get(self, request): partner = get_partner_for_site(request) sku = request.GET.get('sku', None) code = request.GET.get('code', None) if not sku: return HttpResponseBadRequest(_('No SKU provided.')) if code: voucher, __ = get_voucher_and_products_from_code(code=code) else: voucher = None try: product = StockRecord.objects.get(partner=partner, partner_sku=sku).product except StockRecord.DoesNotExist: return HttpResponseBadRequest(_('SKU [{sku}] does not exist.').format(sku=sku)) # If the product isn't available then there's no reason to continue with the basket addition purchase_info = request.strategy.fetch_for_product(product) if not purchase_info.availability.is_available_to_buy: msg = _('Product [{product}] not available to buy.').format(product=product.title) return HttpResponseBadRequest(msg) # If the product is not an Enrollment Code, we check to see if the user is already # enrolled to prevent double-enrollment and/or accidental coupon usage if product.get_product_class().name != ENROLLMENT_CODE_PRODUCT_CLASS_NAME: try: if request.user.is_user_already_enrolled(request, product): logger.warning( 'User [%s] attempted to repurchase the [%s] seat of course [%s]', request.user.username, mode_for_seat(product), product.attr.course_key ) msg = _('You are already enrolled in {course}.').format(course=product.course.name) return HttpResponseBadRequest(msg) except (ConnectionError, SlumberBaseException, Timeout): msg = _('An error occurred while retrieving enrollment details. Please try again.') return HttpResponseBadRequest(msg) # At this point we're either adding an Enrollment Code product to the basket, # or the user is adding a Seat product for which they are not already enrolled prepare_basket(request, product, voucher) return HttpResponseRedirect(reverse('basket:summary'), status=303)
def test_voucher_offers_listing_catalog_query_exception(self, return_value, method): """ Verify the endpoint returns status 200 and an empty list of course offers when all product Courses and Stock Records are not found and range has a catalog query """ course, seat = self.create_course_and_seat() self.mock_dynamic_catalog_course_runs_api(query='*:*', course_run=course) new_range, __ = Range.objects.get_or_create(catalog_query='*:*') new_range.add_product(seat) voucher, __ = prepare_voucher(_range=new_range) voucher, products = get_voucher_and_products_from_code(voucher.code) request = self.prepare_offers_listing_request(voucher.code) with mock.patch(method, mock.Mock(return_value=return_value)): offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher) self.assertEqual(len(offers), 0)
def test_get_offers_for_single_course_voucher(self): """ Verify that the course offers data is returned for a single course voucher. """ course, seat = self.create_course_and_seat() new_range = RangeFactory(products=[ seat, ]) voucher, __ = prepare_voucher(_range=new_range, benefit_value=10) voucher, products = get_voucher_and_products_from_code(voucher.code) benefit = voucher.offers.first().benefit request = self.prepare_offers_listing_request(voucher.code) self.mock_course_api_response(course=course) offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher) first_offer = offers[0] self.assertEqual(len(offers), 1) self.assertDictEqual( first_offer, { 'benefit': { 'type': benefit.type, 'value': benefit.value }, 'contains_verified': True, 'course_start_date': '2013-02-05T05:00:00Z', 'id': course.id, 'image_url': get_lms_url( '/asset-v1:test+test+test+type@asset+block@images_course_image.jpg' ), 'organization': CourseKey.from_string(course.id).org, 'seat_type': course.type, 'stockrecords': serializers.StockRecordSerializer( seat.stockrecords.first()).data, 'title': course.name, 'voucher_end_date': voucher.end_datetime })
def offers(self, request): """ Preview the courses offered by the voucher. Paginated Response containing the list of course offers will be returned. --- parameters: - name: code description: Voucher code required: true type: string paramType: query multiple: false """ code = request.GET.get('code', '') try: voucher, products = get_voucher_and_products_from_code(code) except Voucher.DoesNotExist: logger.error('Voucher with code %s not found.', code) return Response(status=status.HTTP_400_BAD_REQUEST) except exceptions.ProductNotFoundError: logger.error('No product(s) are associated with this code.') return Response(status=status.HTTP_400_BAD_REQUEST) query = voucher.offers.first().benefit.range.catalog_query if query: cache_key = 'voucher_offers_{}'.format(query) else: cache_key = 'voucher_offers_{}'.format(voucher.id) cache_hash = hashlib.md5(cache_key).hexdigest() offers = cache.get(cache_hash) if not offers: try: offers = self.get_offers(products, request, voucher) except (ConnectionError, SlumberBaseException, Timeout): logger.error('Could not get course information.') return Response(status=status.HTTP_400_BAD_REQUEST) except Http404: logger.error('Could not get information for product %s.', products[0].title) return Response(status=status.HTTP_404_NOT_FOUND) cache.set(cache_hash, offers, settings.COURSES_API_CACHE_TIMEOUT) page = self.paginate_queryset(offers) return self.get_paginated_response(page)
def test_get_offers(self): """ Verify that the course offers data is returned. """ course, seat = self.create_course_and_seat() new_range = RangeFactory(products=[ seat, ]) voucher, __ = prepare_voucher(_range=new_range, benefit_value=10) voucher, products = get_voucher_and_products_from_code(voucher.code) benefit = voucher.offers.first().benefit request = self.prepare_offers_listing_request(voucher.code) self.mock_dynamic_catalog_course_runs_api( course_run=course, query=new_range.catalog_query) offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher) first_offer = offers[0] self.assertEqual(len(offers), 1) self.assertDictEqual( first_offer, { 'benefit': { 'type': benefit.type, 'value': benefit.value }, 'contains_verified': True, 'course_start_date': '2016-05-01T00:00:00Z', 'id': course.id, 'image_url': 'path/to/the/course/image', 'organization': CourseKey.from_string(course.id).org, 'seat_type': course.type, 'stockrecords': serializers.StockRecordSerializer( seat.stockrecords.first()).data, 'title': course.name, 'voucher_end_date': voucher.end_datetime })
def offers(self, request): """ Preview the courses offered by the voucher. Paginated Response containing the list of course offers will be returned. --- parameters: - name: code description: Voucher code required: true type: string paramType: query multiple: false """ code = request.GET.get('code', '') try: voucher, products = get_voucher_and_products_from_code(code) except Voucher.DoesNotExist: logger.error('Voucher with code %s not found.', code) return Response(status=status.HTTP_400_BAD_REQUEST) except exceptions.ProductNotFoundError: logger.error('No product(s) are associated with this code.') return Response(status=status.HTTP_400_BAD_REQUEST) try: offers_data = self.get_offers(products, request, voucher) except (ConnectionError, SlumberBaseException, Timeout): logger.error('Could not get course information.') return Response(status=status.HTTP_400_BAD_REQUEST) except Http404: logger.error('Could not get information for product %s.', products[0].title) return Response(status=status.HTTP_404_NOT_FOUND) next_page = offers_data['next'] if next_page: next_page_query = urlparse(next_page).query offers_data['next'] = '{path}?{query}&code={code}'.format( code=code, path=request.path, query=next_page_query, ) return Response(data=offers_data)
def offers(self, request): """ Preview the courses offered by the voucher. Paginated Response containing the list of course offers will be returned. --- parameters: - name: code description: Voucher code required: true type: string paramType: query multiple: false """ code = request.GET.get('code', '') try: voucher, products = get_voucher_and_products_from_code(code) except Voucher.DoesNotExist: logger.error('Voucher with code %s not found.', code) return Response(status=status.HTTP_400_BAD_REQUEST) except exceptions.ProductNotFoundError: logger.error('No product(s) are associated with this code.') return Response(status=status.HTTP_400_BAD_REQUEST) try: offers_data = self.get_offers(products, request, voucher) except (ConnectionError, SlumberBaseException, Timeout): logger.error('Could not get course information.') return Response(status=status.HTTP_400_BAD_REQUEST) except Http404: logger.error('Could not get information for product %s.', products[0].title) return Response(status=status.HTTP_404_NOT_FOUND) next_page = offers_data['next'] if next_page: next_page_query = urlparse(next_page).query offers_data['next'] = '{path}?{query}&code={code}'.format( code=code, path=request.path, query=next_page_query, ) return Response(data=offers_data)
def test_voucher_offers_listing_catalog_query_exception( self, return_value, method): """ Verify the endpoint returns status 200 and an empty list of course offers when all product Courses and Stock Records are not found and range has a catalog query """ course, seat = self.create_course_and_seat() self.mock_dynamic_catalog_course_runs_api(query='*:*', course_run=course) new_range, __ = Range.objects.get_or_create(catalog_query='*:*') new_range.add_product(seat) voucher, __ = prepare_voucher(_range=new_range) voucher, products = get_voucher_and_products_from_code(voucher.code) request = self.prepare_offers_listing_request(voucher.code) with mock.patch(method, mock.Mock(return_value=return_value)): offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher) self.assertEqual(len(offers), 0)
def test_omitting_unavailable_seats(self): """ Verify an unavailable seat is omitted from offer page results. """ course1, seat1 = self.create_course_and_seat() course2, seat2 = self.create_course_and_seat() course_run_info = { 'count': 2, 'results': [{ 'key': course1.id, 'title': course1.name, 'start': '2016-05-01T00:00:00Z', 'image': { 'src': 'path/to/the/course/image' } }, { 'key': course2.id, 'title': course2.name, 'start': '2016-05-01T00:00:00Z', 'image': { 'src': 'path/to/the/course/image' } }] } self.mock_dynamic_catalog_course_runs_api(query='*:*', course_run_info=course_run_info) new_range, __ = Range.objects.get_or_create(catalog_query='*:*') new_range.add_product(seat1) new_range.add_product(seat2) voucher, __ = prepare_voucher(_range=new_range) voucher, products = get_voucher_and_products_from_code(voucher.code) factory = APIRequestFactory() request = factory.get('/?code={}&page_size=6'.format(voucher.code)) request.site = self.site request.strategy = DefaultStrategy() offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher) self.assertEqual(len(offers), 2) products[1].expires = pytz.utc.localize(datetime.datetime.min) offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher) self.assertEqual(len(offers), 1)
def test_get_non_existing_voucher(self): """ Verify that get_voucher_and_products_from_code() raises exception for a non-existing voucher. """ with self.assertRaises(Voucher.DoesNotExist): get_voucher_and_products_from_code(code='INVALID')
def test_get_non_existing_voucher(self): """ Verify that get_voucher_and_products_from_code() raises exception for a non-existing voucher. """ with self.assertRaises(Voucher.DoesNotExist): get_voucher_and_products_from_code(code='INVALID')
def get(self, request): partner = get_partner_for_site(request) sku = request.GET.get('sku', None) code = request.GET.get('code', None) if not sku: return HttpResponseBadRequest(_('No SKU provided.')) if code: voucher, __ = get_voucher_and_products_from_code(code=code) else: voucher = None try: product = StockRecord.objects.get(partner=partner, partner_sku=sku).product except StockRecord.DoesNotExist: return HttpResponseBadRequest(_('SKU [{sku}] does not exist.').format(sku=sku)) # If the product isn't available then there's no reason to continue with the basket addition purchase_info = request.strategy.fetch_for_product(product) if not purchase_info.availability.is_available_to_buy: msg = _('Product [{product}] not available to buy.').format(product=product.title) return HttpResponseBadRequest(msg) # If the product is not an Enrollment Code, we check to see if the user is already # enrolled to prevent double-enrollment and/or accidental coupon usage if product.get_product_class().name != ENROLLMENT_CODE_PRODUCT_CLASS_NAME: course_key = product.attr.course_key # Submit a query to the LMS Enrollment API try: api = EdxRestApiClient( get_lms_enrollment_base_api_url(), oauth_access_token=request.user.access_token, append_slash=False ) logger.debug( 'Getting enrollment information for [%s] in [%s].', request.user.username, course_key ) status = api.enrollment(','.join([request.user.username, course_key])).get() except (ConnectionError, SlumberBaseException, Timeout) as ex: logger.exception( 'Failed to retrieve enrollment details for [%s] in course [%s], Because of [%s]', request.user.username, course_key, ex, ) msg = _('An error occurred while retrieving enrollment details. Please try again.') return HttpResponseBadRequest(msg) # Enrollment API response received, now perform the actual enrollment check username = request.user.username seat_type = mode_for_seat(product) if status and status.get('mode') == seat_type and status.get('is_active'): logger.warning( 'User [%s] attempted to repurchase the [%s] seat of course [%s]', username, seat_type, course_key ) msg = _('You are already enrolled in {course}.').format(course=product.course.name) return HttpResponseBadRequest(msg) # At this point we're either adding an Enrollment Code product to the basket, # or the user is adding a Seat product for which they are not already enrolled prepare_basket(request, product, voucher) return HttpResponseRedirect(reverse('basket:summary'), status=303)