def test_get_course_info_from_catalog_cached(self): """ Verify that get_course_info_from_catalog is cached We expect 2 calls to set_all_tiers in the get_course_info_from_catalog method due to: - the site_configuration api setup - the result being cached """ self.mock_access_token_response() product = create_or_update_course_entitlement('verified', 100, self.partner, 'foo-bar', 'Foo Bar Entitlement') self.mock_course_detail_endpoint( product, discovery_api_url=self.site_configuration.discovery_api_url) with patch.object( TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers: mocked_set_all_tiers.assert_not_called() _ = get_course_info_from_catalog(self.request.site, product) self.assertEqual(mocked_set_all_tiers.call_count, 2) _ = get_course_info_from_catalog(self.request.site, product) self.assertEqual(mocked_set_all_tiers.call_count, 2)
def test_get_course_run_info_from_catalog(self, course_run): """ Check to see if course info gets cached """ self.mock_access_token_response() if course_run: course = CourseFactory(partner=self.partner) product = course.create_or_update_seat('verified', None, 100) key = CourseKey.from_string(product.attr.course_key) self.mock_course_run_detail_endpoint( course, discovery_api_url=self.site_configuration.discovery_api_url) else: product = create_or_update_course_entitlement( 'verified', 100, self.partner, 'foo-bar', 'Foo Bar Entitlement') key = product.attr.UUID self.mock_course_detail_endpoint( product, discovery_api_url=self.site_configuration.discovery_api_url) cache_key = u'courses_api_detail_{}{}'.format(key, self.partner.short_code) cache_key = hashlib.md5(cache_key.encode('utf-8')).hexdigest() course_cached_response = TieredCache.get_cached_response(cache_key) self.assertFalse(course_cached_response.is_found) response = get_course_info_from_catalog(self.request.site, product) if course_run: self.assertEqual(response['title'], course.name) else: self.assertEqual(response['title'], product.title) course_cached_response = TieredCache.get_cached_response(cache_key) self.assertEqual(course_cached_response.value, response)
def test_get_course_run_info_from_catalog(self, course_run): """ Check to see if course info gets cached """ self.mock_access_token_response() if course_run: resource = "course_runs" course = CourseFactory(partner=self.partner) product = course.create_or_update_seat('verified', None, 100) key = CourseKey.from_string(product.attr.course_key) self.mock_course_run_detail_endpoint( course, discovery_api_url=self.site_configuration.discovery_api_url) else: resource = "courses" product = create_or_update_course_entitlement( 'verified', 100, self.partner, 'foo-bar', 'Foo Bar Entitlement') key = product.attr.UUID self.mock_course_detail_endpoint( discovery_api_url=self.site_configuration.discovery_api_url, course=product) cache_key = get_cache_key(site_domain=self.site.domain, resource="{}-{}".format(resource, key)) course_cached_response = TieredCache.get_cached_response(cache_key) self.assertFalse(course_cached_response.is_found) response = get_course_info_from_catalog(self.request.site, product) if course_run: self.assertEqual(response['title'], course.name) else: self.assertEqual(response['title'], product.title) course_cached_response = TieredCache.get_cached_response(cache_key) self.assertEqual(course_cached_response.value, response)
def _get_course_data(self, product): """ Return course data. Args: product (Product): A product that has course_key as attribute (seat or bulk enrollment coupon) Returns: A dictionary containing product title, course key, image URL, description, and start and end dates. Also returns course information found from catalog. """ course_data = { 'product_title': None, 'course_key': None, 'image_url': None, 'product_description': None, 'course_start': None, 'course_end': None, } course = None if product.is_seat_product: course_data['course_key'] = CourseKey.from_string( product.attr.course_key) try: course = get_course_info_from_catalog(self.request.site, product) if 'src' in course.get('image', {}): course_data['image_url'] = course['image']['src'] elif 'card_image_url' in course: course_data['image_url'] = course['card_image_url'] else: try: course_data['image_url'] = course['media']['image']['raw'] except (KeyError, TypeError): pass course_data['product_description'] = course.get( 'short_description', '') course_data['product_title'] = course.get('name') or course.get( 'title', '') # The course start/end dates are not currently used # in the default basket templates, but we are adding # the dates to the template context so that theme # template overrides can make use of them. course_data['course_start'] = self._deserialize_date( course.get('start')) course_data['course_end'] = self._deserialize_date( course.get('end')) except (ReqConnectionError, SlumberBaseException, Timeout): logger.exception( 'Failed to retrieve data from Discovery Service for course [%s].', course_data['course_key'], ) return course_data, course
def get_offers(self, request, voucher): """ Get the course offers associated with the voucher. Arguments: request (HttpRequest): Request data. voucher (Voucher): Oscar Voucher for which the offers are returned. Returns: dict: Dictionary containing a link to the next page of Course Discovery results and a List of course offers where each offer is represented as a dictionary. """ benefit = voucher.offers.first().benefit catalog_query = benefit.range.catalog_query catalog_id = benefit.range.course_catalog enterprise_catalog = benefit.range.enterprise_customer_catalog next_page = None offers = [] if catalog_id: catalog = fetch_course_catalog(request.site, catalog_id) catalog_query = catalog.get("query") if catalog else catalog_query if catalog_query or enterprise_catalog: offers, next_page = self.get_offers_from_catalog( request, voucher, catalog_query, enterprise_catalog) else: product_range = voucher.offers.first().benefit.range products = product_range.all_products() if products: product = products[0] else: raise Product.DoesNotExist course_id = product.course_id course = get_object_or_404(Course, id=course_id) stock_record = get_object_or_404(StockRecord, product__id=product.id) course_info = get_course_info_from_catalog(request.site, product) if course_info: offers.append( self.get_course_offer_data( benefit=benefit, course=course, course_info=course_info, credit_provider_price=None, multiple_credit_providers=False, is_verified=(course.type == 'verified'), product=product, stock_record=stock_record, voucher=voucher)) return {'next': next_page, 'results': offers}
def _already_enrolled_in_course_entitlement(self, seat_product, user, site): """ Check if the user is enrolled in course entitlement for given seat product. :return: True if enrolled """ course_uuid = get_course_info_from_catalog(site, seat_product)['course_uuid'] user_bought_product_ids = OrderLine.objects.filter( order__in=user.orders.all() ).values_list('product', flat=True) return Product.objects.filter( pk__in=user_bought_product_ids, attributes__code='UUID', attribute_values__value_text=course_uuid, ).exists()
def test_get_course_info_from_catalog(self): """ Check to see if course info gets cached """ course = CourseFactory() self.mock_dynamic_catalog_single_course_runs_api(course) cache_key = 'courses_api_detail_{}{}'.format(course.id, self.site.siteconfiguration.partner.short_code) cache_key = hashlib.md5(cache_key).hexdigest() cached_course = cache.get(cache_key) self.assertIsNone(cached_course) response = get_course_info_from_catalog(self.request.site, course) self.assertEqual(response['title'], course.name) cached_course = cache.get(cache_key) self.assertEqual(cached_course, response)
def _get_course_data(self, product): """ Return course data. Args: product (Product): A product that has course_key as attribute (seat or bulk enrollment coupon) Returns: Dictionary containing course name, course key, course image URL and description. """ if product.is_seat_product: course_key = CourseKey.from_string(product.attr.course_key) else: course_key = None course_name = None image_url = None short_description = None course_start = None course_end = None try: course = get_course_info_from_catalog(self.request.site, product) try: image_url = course['image']['src'] except (KeyError, TypeError): image_url = '' short_description = course.get('short_description', '') course_name = course.get('title', '') # The course start/end dates are not currently used # in the default basket templates, but we are adding # the dates to the template context so that theme # template overrides can make use of them. course_start = self._deserialize_date(course.get('start')) course_end = self._deserialize_date(course.get('end')) except (ConnectionError, SlumberBaseException, Timeout): logger.exception( 'Failed to retrieve data from Discovery Service for course [%s].', course_key) return { 'product_title': course_name, 'course_key': course_key, 'image_url': image_url, 'product_description': short_description, 'course_start': course_start, 'course_end': course_end, }
def test_get_course_info_from_catalog(self): """ Check to see if course info gets cached """ course = CourseFactory() self.mock_dynamic_catalog_single_course_runs_api(course) cache_key = 'courses_api_detail_{}{}'.format( course.id, self.site.siteconfiguration.partner.short_code) cache_hash = hashlib.md5(cache_key).hexdigest() cached_course = cache.get(cache_hash) self.assertIsNone(cached_course) response = get_course_info_from_catalog(self.request.site, course) self.assertEqual(response['title'], course.name) cached_course = cache.get(cache_hash) self.assertEqual(cached_course, response)
def get_transaction_parameters(self, basket, request=None, use_client_side_checkout=False, **kwargs): """ approval_url """ trade_id = create_trade_id(basket.id) try: course_data = get_course_info_from_catalog( request.site, basket.all_lines()[0].product) subject = body = course_data.get('title') except Exception, e: logger.exception(e) subject = body = 'buy course'
def _get_course_data(self, product): """ Return course data. Args: product (Product): A product that has course_key as attribute (seat or bulk enrollment coupon) Returns: Dictionary containing course name, course key, course image URL and description. """ course_key = CourseKey.from_string(product.attr.course_key) course_name = None image_url = None short_description = None course_start = None course_end = None try: course = get_course_info_from_catalog(self.request.site, course_key) try: image_url = course['image']['src'] except (KeyError, TypeError): image_url = '' short_description = course.get('short_description', '') course_name = course.get('title', '') # The course start/end dates are not currently used # in the default basket templates, but we are adding # the dates to the template context so that theme # template overrides can make use of them. course_start = self._deserialize_date(course.get('start')) course_end = self._deserialize_date(course.get('end')) except (ConnectionError, SlumberBaseException, Timeout): logger.exception('Failed to retrieve data from Catalog Service for course [%s].', course_key) return { 'product_title': course_name, 'course_key': course_key, 'image_url': image_url, 'product_description': short_description, 'course_start': course_start, 'course_end': course_end, }
def send_receipt_email(self, order, user, site): ''' send receipt email ''' try: lines_data = [(line, get_course_info_from_catalog(site, line.product)) for line in order.lines.all()] content = loader.render_to_string( 'edx/checkout/receipt_email.html', { 'order': order, 'lines_data': lines_data }) subject = loader.render_to_string( 'oscar/customer/emails/commtype_order_placed_subject.txt', {'order': order}) email_msg = EmailMessage(subject.strip(), content, settings.OSCAR_FROM_EMAIL, [user.email]) email_msg.content_subtype = "html" email_msg.send() except Exception, e: logger.exception(e)
def _get_course_data(self, product): """ Return course data. Args: product (Product): A product that has course_key as attribute (seat or bulk enrollment coupon) Returns: Dictionary containing course name, course key, course image URL and description. """ if product.is_seat_product: course_key = CourseKey.from_string(product.attr.course_key) else: course_key = None course_name = None image_url = None short_description = None course_start = None course_end = None course = None try: course = get_course_info_from_catalog(self.request.site, product) try: image_url = course['image']['src'] except (KeyError, TypeError): image_url = '' short_description = course.get('short_description', '') course_name = course.get('title', '') # The course start/end dates are not currently used # in the default basket templates, but we are adding # the dates to the template context so that theme # template overrides can make use of them. course_start = self._deserialize_date(course.get('start')) course_end = self._deserialize_date(course.get('end')) except (ConnectionError, SlumberBaseException, Timeout): logger.exception( 'Failed to retrieve data from Discovery Service for course [%s].', course_key) if self.request.basket.num_items == 1 and product.is_enrollment_code_product: course_key = CourseKey.from_string(product.attr.course_key) if course and course.get('marketing_url', None): course_about_url = course['marketing_url'] else: course_about_url = get_lms_course_about_url( course_key=course_key) messages.info( self.request, _('{strong_start}Purchasing just for yourself?{strong_end}{paragraph_start}If you are ' 'purchasing a single code for someone else, please continue with checkout. However, if you are the ' 'learner {link_start}go back{link_end} to enroll directly.{paragraph_end}' ).format(strong_start='<strong>', strong_end='</strong>', paragraph_start='<p>', paragraph_end='</p>', link_start='<a href="{course_about}">'.format( course_about=course_about_url), link_end='</a>'), extra_tags='safe') return { 'product_title': course_name, 'course_key': course_key, 'image_url': image_url, 'product_description': short_description, 'course_start': course_start, 'course_end': course_end, }
def get_context_data(self, **kwargs): context = super(BasketSummaryView, self).get_context_data(**kwargs) formset = context.get('formset', []) lines = context.get('line_list', []) lines_data = [] is_verification_required = is_bulk_purchase = False switch_link_text = partner_sku = '' basket = self.request.basket site = self.request.site site_configuration = site.siteconfiguration for line in lines: course_key = CourseKey.from_string(line.product.attr.course_key) course_name = None image_url = None short_description = None try: course = get_course_info_from_catalog(self.request.site, course_key) try: image_url = course['image']['src'] except (KeyError, TypeError): image_url = '' short_description = course.get('short_description', '') course_name = course.get('title', '') except (ConnectionError, SlumberBaseException, Timeout): logger.exception('Failed to retrieve data from Catalog Service for course [%s].', course_key) if self.request.site.siteconfiguration.enable_enrollment_codes: # Get variables for the switch link that toggles from enrollment codes and seat. switch_link_text, partner_sku = get_basket_switch_data(line.product) if line.product.get_product_class().name == ENROLLMENT_CODE_PRODUCT_CLASS_NAME: is_bulk_purchase = True # Iterate on message storage so all messages are marked as read. # This will hide the success messages when a user updates the quantity # for an item in the basket. list(messages.get_messages(self.request)) if line.has_discount: benefit = basket.applied_offers().values()[0].benefit benefit_value = format_benefit_value(benefit) else: benefit_value = None lines_data.append({ 'seat_type': self._determine_seat_type(line.product), 'course_name': course_name, 'course_key': course_key, 'image_url': image_url, 'course_short_description': short_description, 'benefit_value': benefit_value, 'enrollment_code': line.product.get_product_class().name == ENROLLMENT_CODE_PRODUCT_CLASS_NAME, 'line': line, }) user = self.request.user context.update({ 'analytics_data': prepare_analytics_data( user, self.request.site.siteconfiguration.segment_key, unicode(course_key) ), 'enable_client_side_checkout': False, }) if site_configuration.client_side_payment_processor \ and waffle.flag_is_active(self.request, CLIENT_SIDE_CHECKOUT_FLAG_NAME): payment_processor_class = site_configuration.get_client_side_payment_processor_class() if payment_processor_class: payment_processor = payment_processor_class(site) context.update({ 'enable_client_side_checkout': True, 'payment_form': PaymentForm(user=user, initial={'basket': basket}, label_suffix=''), 'payment_url': payment_processor.client_side_payment_url, }) else: msg = 'Unable to load client-side payment processor [{processor}] for ' \ 'site configuration [{sc}]'.format(processor=site_configuration.client_side_payment_processor, sc=site_configuration.id) raise SiteConfigurationError(msg) # Check product attributes to determine if ID verification is required for this basket try: is_verification_required = line.product.attr.id_verification_required \ and line.product.attr.certificate_type != 'credit' except AttributeError: pass context.update({ 'free_basket': context['order_total'].incl_tax == 0, 'payment_processors': site_configuration.get_payment_processors(), 'homepage_url': get_lms_url(''), 'formset_lines_data': zip(formset, lines_data), 'is_verification_required': is_verification_required, 'min_seat_quantity': 1, 'is_bulk_purchase': is_bulk_purchase, 'switch_link_text': switch_link_text, 'partner_sku': partner_sku, }) return context
def get_transaction_parameters(self, basket, request=None, use_client_side_checkout=False, **kwargs): """ """ # Get the basket, and make sure it belongs to the current user. try: basket = request.user.baskets.get(id=basket.id) except ObjectDoesNotExist: return {} try: # Freeze the basket so that it cannot be modified basket.strategy = request.strategy Applicator().apply(basket, request.user, request) # basket.freeze() if basket.total_incl_tax <= 0: return {} out_trade_no = create_trade_id(basket.id) try: course_data = get_course_info_from_catalog( request.site, basket.all_lines()[0].product) body = course_data.get('title') except Exception, e: logger.exception(e) body = 'buy course' order_price = basket.total_incl_tax total_fee = int(order_price * 100) attach_data = urljoin(get_ecommerce_url(), reverse('wechatpay:execute')) wxpayconf_pub = WxPayConf_pub() unifiedorder_pub = UnifiedOrder_pub() unifiedorder_pub.setParameter("body", body) unifiedorder_pub.setParameter("out_trade_no", out_trade_no) unifiedorder_pub.setParameter("total_fee", str(total_fee)) unifiedorder_pub.setParameter("notify_url", wxpayconf_pub.NOTIFY_URL) unifiedorder_pub.setParameter("trade_type", "NATIVE") unifiedorder_pub.setParameter("attach", attach_data) code_url = unifiedorder_pub.getCodeUrl() img = qrcode.make(code_url) buf = BytesIO() img.save(buf, format="PNG") qrcode_img = base64.b64encode(buf.getvalue()) if not PaymentProcessorResponse.objects.filter( processor_name=self.NAME, basket=basket).update(transaction_id=out_trade_no): self.record_processor_response({}, transaction_id=out_trade_no, basket=basket) parameters = { 'qrcode_img': qrcode_img, } return parameters
def is_satisfied(self, offer, basket): # pylint: disable=unused-argument """ Determines if a user is eligible for an enterprise customer offer based on their association with the enterprise customer. It also filter out the offer if the `enterprise_customer_catalog_uuid` value set on the offer condition does not match with the basket catalog value when explicitly provided by the enterprise learner. Note: Currently there is no mechanism to prioritize or apply multiple offers that may apply as opposed to disqualifying offers if the catalog doesn't explicitly match. Arguments: basket (Basket): Contains information about order line items, the current site, and the user attempting to make the purchase. Returns: bool """ if not basket.owner: # An anonymous user is never linked to any EnterpriseCustomer. return False enterprise_in_condition = str(self.enterprise_customer_uuid) enterprise_catalog = str(self.enterprise_customer_catalog_uuid) if self.enterprise_customer_catalog_uuid \ else None enterprise_name_in_condition = str(self.enterprise_customer_name) username = basket.owner.username # This variable will hold both course keys and course run identifiers. course_ids = [] for line in basket.all_lines(): if line.product.is_course_entitlement_product: try: response = get_course_info_from_catalog( basket.site, line.product) except (ReqConnectionError, KeyError, SlumberHttpBaseException, Timeout) as exc: logger.exception( '[Code Redemption Failure] Unable to apply enterprise offer because basket ' 'contains a course entitlement product but we failed to get course info from ' 'course entitlement product.' 'User: %s, Offer: %s, Message: %s, Enterprise: %s, Catalog: %s, Course UUID: %s', username, offer.id, exc, enterprise_in_condition, enterprise_catalog, line.product.attr.UUID) return False else: course_ids.append(response['key']) # Skip to the next iteration. continue course = line.product.course if not course: # Basket contains products not related to a course_run. # Only log for non-site offers to avoid noise. if offer.offer_type != ConditionalOffer.SITE: logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'the Basket contains a product not related to a course_run. ' 'User: %s, Offer: %s, Product: %s, Enterprise: %s, Catalog: %s', username, offer.id, line.product.id, enterprise_in_condition, enterprise_catalog) return False course_ids.append(course.id) courses_in_basket = ','.join(course_ids) user_enterprise = get_enterprise_id_for_user(basket.site, basket.owner) if user_enterprise and enterprise_in_condition != user_enterprise: # Learner is not linked to the EnterpriseCustomer associated with this condition. if offer.offer_type == ConditionalOffer.VOUCHER: logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because Learner\'s ' 'enterprise (%s) does not match this conditions\'s enterprise (%s). ' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s', user_enterprise, enterprise_in_condition, username, offer.id, enterprise_in_condition, enterprise_catalog, courses_in_basket) logger.info( '[Code Redemption Issue] Linking learner with the enterprise in Condition. ' 'User [%s], Enterprise [%s]', username, enterprise_in_condition) get_or_create_enterprise_customer_user( basket.site, enterprise_in_condition, username, False) msg = _( 'This coupon has been made available through {new_enterprise}. ' 'To redeem this coupon, you must first logout. When you log back in, ' 'please select {new_enterprise} as your enterprise ' 'and try again.').format( new_enterprise=enterprise_name_in_condition) messages.warning( crum.get_current_request(), msg, ) return False # Verify that the current conditional offer is related to the provided # enterprise catalog, this will also filter out offers which don't # have `enterprise_customer_catalog_uuid` value set on the condition. catalog = self._get_enterprise_catalog_uuid_from_basket(basket) if catalog: if offer.condition.enterprise_customer_catalog_uuid != catalog: logger.warning( 'Unable to apply enterprise offer %s because ' 'Enterprise catalog id on the basket (%s) ' 'does not match the catalog for this condition (%s).', offer.id, catalog, offer.condition.enterprise_customer_catalog_uuid) return False try: catalog_contains_course = catalog_contains_course_runs( basket.site, course_ids, enterprise_in_condition, enterprise_customer_catalog_uuid=enterprise_catalog) except (ReqConnectionError, KeyError, SlumberHttpBaseException, Timeout) as exc: logger.exception( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'we failed to check if course_runs exist in the catalog. ' 'User: %s, Offer: %s, Message: %s, Enterprise: %s, Catalog: %s, Courses: %s', username, offer.id, exc, enterprise_in_condition, enterprise_catalog, courses_in_basket) return False if not catalog_contains_course: # Basket contains course runs that do not exist in the EnterpriseCustomerCatalogs # associated with the EnterpriseCustomer. logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'Enterprise catalog does not contain the course(s) in this basket. ' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s', username, offer.id, enterprise_in_condition, enterprise_catalog, courses_in_basket) return False if not is_offer_max_discount_available(basket, offer): logger.warning( '[Enterprise Offer Failure] Unable to apply enterprise offer because bookings limit is consumed.' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s, BookingsLimit: %s, TotalDiscount: %s', username, offer.id, enterprise_in_condition, enterprise_catalog, courses_in_basket, offer.max_discount, offer.total_discount, ) return False if not is_offer_max_user_discount_available(basket, offer): logger.warning( '[Enterprise Offer Failure] Unable to apply enterprise offer because user bookings limit is consumed.' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s, UserBookingsLimit: %s', username, offer.id, enterprise_in_condition, enterprise_catalog, courses_in_basket, offer.max_user_discount) return False return True