def _publish_creditcourse(self, course_id, access_token): """Creates or updates a CreditCourse object on the LMS.""" api = EdxRestApiClient(get_lms_url('api/credit/v1/'), oauth_access_token=access_token, timeout=self.timeout) data = {'course_key': course_id, 'enabled': True} api.courses(course_id).put(data)
def _publish_creditcourse(self, course_id, access_token): """Creates or updates a CreditCourse object on the LMS.""" api = EdxRestApiClient( get_lms_url('api/credit/v1/'), oauth_access_token=access_token, timeout=self.timeout ) data = { 'course_key': course_id, 'enabled': True } api.courses(course_id).put(data)
def refresh_all_course_api_data(cls, access_token): course_api_url = settings.COURSES_API_URL client = EdxRestApiClient(course_api_url, oauth_access_token=access_token) count = None page = 1 logger.info('Refreshing course api data from %s....', course_api_url) while page: # TODO Update API to not require username? response = client.courses().get(page=page, page_size=50, username='******') count = response['pagination']['count'] results = response['results'] logger.info('Retrieved %d courses...', len(results)) if response['pagination']['next']: page += 1 else: page = None for body in results: Course(body['id']).update(body) logger.info('Retrieved %d courses from %s.', count, course_api_url)
def get_context_data(self, **kwargs): context = super(CouponOfferView, self).get_context_data(**kwargs) code = self.request.GET.get('code', None) if code is not None: voucher, product = get_voucher(code=code) valid_voucher, msg = voucher_is_valid(voucher, product, self.request) if valid_voucher: api = EdxRestApiClient( get_lms_url('api/courses/v1/'), ) try: course = api.courses(product.course_id).get() except SlumberHttpBaseException as e: logger.exception('Could not get course information. [%s]', e) return { 'error': _('Could not get course information. [{error}]'.format(error=e)) } course['image_url'] = get_lms_url(course['media']['course_image']['uri']) stock_records = voucher.offers.first().benefit.range.catalog.stock_records.first() context.update({ 'course': course, 'code': code, 'price': stock_records.price_excl_tax, 'verified': (product.attr.certificate_type is 'verified') }) return context return { 'error': msg } return { 'error': _('This coupon code is invalid.') }
def refresh_all(cls, access_token): """ Refresh all course data. Args: access_token (str): OAuth access token Returns: None """ client = EdxRestApiClient(settings.ECOMMERCE_API_URL, oauth_access_token=access_token) logger.info('Refreshing course data from %s....', settings.ECOMMERCE_API_URL) count = None page = 1 while page: response = client.courses().get(include_products=True, page=page, page_size=50) count = response['count'] results = response['results'] logger.info('Retrieved %d courses...', len(results)) if response['next']: page += 1 else: page = None for body in results: Course(body['id'], body).save() logger.info('Retrieved %d courses.', count)
def build_content_table(content_api_url, lms_url, access_token): """ Use Course Discovery API to build dict of course runs """ logger.info("Retrieving course content, url=" + content_api_url) client = EdxRestApiClient(content_api_url, jwt=access_token) page = 1 content_table = {} # read the courses and create a Sailthru content item for each course_run while page: # get a page of courses response = client.courses().get(limit=500, offset=(page-1)*500) results = response['results'] if response['next']: page += 1 else: page = None for course in results: for course_run in course['course_runs']: content_table[course_run['key']] = create_sailthru_content(course, course_run, lms_url) logger.info('Retrieved %d course runs.', len(content_table)) return content_table
class CatalogApiService(object): """The service to interface with edX catalog API""" def __init__(self, access_token, oauth_host, oauth_key, oauth_secret, api_url_root): self.access_token = access_token if not access_token: logger.info( 'No access token provided. Retrieving access token using client_credential flow...' ) try: self.access_token, expires = EdxRestApiClient.get_oauth_access_token( '{root}/access_token'.format(root=oauth_host), oauth_key, oauth_secret, token_type='jwt') except Exception: logger.exception( 'No access token provided or acquired through client_credential flow.' ) raise logger.info('Token retrieved: %s', access_token) self.api_client = EdxRestApiClient(api_url_root, jwt=self.access_token) self._programs_dictionary = {} def _get_resource_from_api(self, api_endpoint, page_size, **kwargs): page = 0 results = [] while page >= 0: response = api_endpoint.get(limit=page_size, offset=(page * page_size), **kwargs) if response.get('next'): page += 1 else: page = -1 results.extend(response.get('results')) return results def get_courses(self): logger.debug('Get Courses called') return self._get_resource_from_api(self.api_client.courses(), COURSES_PAGE_SIZE, marketable=1) def get_program_dictionary(self): if not self._programs_dictionary: program_array = self._get_resource_from_api( self.api_client.programs(), PROGRAMS_PAGE_SIZE, marketable=1, published_course_runs_only=1) for program in program_array: self._programs_dictionary[program['uuid']] = program return self._programs_dictionary
def get_course_info_from_lms(course_key): """ Get course information from LMS via the course api and cache """ api = EdxRestApiClient(get_lms_url('api/courses/v1/')) cache_key = 'courses_api_detail_{}'.format(course_key) cache_hash = hashlib.md5(cache_key).hexdigest() course = cache.get(cache_hash) if not course: # pragma: no cover course = api.courses(course_key).get() cache.set(cache_hash, course, settings.COURSES_API_CACHE_TIMEOUT) return course
def get_course_info_from_lms(course_key): """ Get course information from LMS via the course api and cache """ api = EdxRestApiClient(get_lms_url('api/courses/v1/')) cache_key = 'courses_api_detail_{}'.format(course_key) cache_hash = hashlib.md5(cache_key.encode('utf-8')).hexdigest() course = cache.get(cache_hash) if not course: # pragma: no cover course = api.courses(course_key).get() cache.set(cache_hash, course, settings.COURSES_API_CACHE_TIMEOUT) return course
def get_context_data(self, **kwargs): context = super(CouponOfferView, self).get_context_data(**kwargs) footer = get_lms_footer() code = self.request.GET.get('code', None) if code is not None: voucher, product = get_voucher(code=code) valid_voucher, msg = voucher_is_valid(voucher, product, self.request) if valid_voucher: api = EdxRestApiClient(get_lms_url('api/courses/v1/'), ) try: course = api.courses(product.course_id).get() except SlumberHttpBaseException as e: logger.exception('Could not get course information. [%s]', e) return { 'error': _('Could not get course information. [{error}]'.format( error=e)), 'footer': footer } course['image_url'] = get_lms_url( course['media']['course_image']['uri']) stock_records = voucher.offers.first( ).benefit.range.catalog.stock_records.first() benefit_type = voucher.offers.first().benefit.type benefit_value = voucher.offers.first().benefit.value price = stock_records.price_excl_tax if benefit_type == 'Percentage': new_price = price - (price * (benefit_value / 100)) else: new_price = price - benefit_value if new_price < 0: new_price = 0.00 context.update({ 'benefit_type': benefit_type, 'benefit_value': benefit_value, 'course': course, 'code': code, 'price': price, 'new_price': "%.2f" % new_price, 'verified': (product.attr.certificate_type == 'verified'), 'footer': footer }) return context return {'error': msg, 'footer': footer} return {'error': _('This coupon code is invalid.'), 'footer': footer}
def get_context_data(self, **kwargs): context = super(CouponOfferView, self).get_context_data(**kwargs) footer = get_lms_footer() code = self.request.GET.get('code', None) if code is not None: voucher, product = get_voucher_from_code(code=code) valid_voucher, msg = voucher_is_valid(voucher, product, self.request) if valid_voucher: api = EdxRestApiClient( get_lms_url('api/courses/v1/'), ) try: course = api.courses(product.course_id).get() except SlumberHttpBaseException as e: logger.exception('Could not get course information. [%s]', e) return { 'error': _('Could not get course information. [{error}]'.format(error=e)), 'footer': footer } course['image_url'] = get_lms_url(course['media']['course_image']['uri']) benefit = voucher.offers.first().benefit stock_record = benefit.range.catalog.stock_records.first() price = stock_record.price_excl_tax context.update(get_voucher_discount_info(benefit, price)) if benefit.type == 'Percentage': new_price = price - (price * (benefit.value / 100)) else: new_price = price - benefit.value if new_price < 0: new_price = Decimal(0) context.update({ 'benefit': benefit, 'course': course, 'code': code, 'is_discount_value_percentage': benefit.type == 'Percentage', 'is_enrollment_code': benefit.type == Benefit.PERCENTAGE and benefit.value == 100.00, 'discount_value': "%.2f" % (price - new_price), 'price': price, 'new_price': "%.2f" % new_price, 'verified': (product.attr.certificate_type == 'verified'), 'verification_deadline': product.course.verification_deadline, 'footer': footer }) return context return { 'error': msg, 'footer': footer } return { 'error': _('This coupon code is invalid.'), 'footer': footer }
class CatalogApiService(object): """The service to interface with edX catalog API""" def __init__(self, access_token, oauth_host, oauth_key, oauth_secret, api_url_root): self.access_token = access_token if not access_token: logger.info('No access token provided. Retrieving access token using client_credential flow...') try: self.access_token, expires = EdxRestApiClient.get_oauth_access_token( '{root}/access_token'.format(root=oauth_host), oauth_key, oauth_secret, token_type='jwt' ) except Exception: logger.exception('No access token provided or acquired through client_credential flow.') raise logger.info('Token retrieved: %s', access_token) self.api_client = EdxRestApiClient(api_url_root, jwt=self.access_token) self._programs_dictionary = {} def _get_resource_from_api(self, api_endpoint, page_size, **kwargs): page = 0 results = [] while page >= 0: response = api_endpoint.get(limit=page_size, offset=(page * page_size), **kwargs) if response.get('next'): page += 1 else: page = -1 results.extend(response.get('results')) return results def get_courses(self): logger.debug('Get Courses called') return self._get_resource_from_api(self.api_client.courses(), COURSES_PAGE_SIZE, marketable=1) def get_program_dictionary(self): if not self._programs_dictionary: program_array = self._get_resource_from_api( self.api_client.programs(), PROGRAMS_PAGE_SIZE, marketable=1, published_course_runs_only=1 ) for program in program_array: self._programs_dictionary[program['uuid']] = program return self._programs_dictionary
def _get_courses_enrollment_info(self): """ Retrieve the enrollment information for all the courses. Returns: Dictionary representing the key-value pair (course_key, enrollment_end) of course. """ def _parse_response(api_response): response_data = api_response.get('results', []) # Map course_id with enrollment end date. courses_enrollment = dict( (course_info['course_id'], course_info['enrollment_end']) for course_info in response_data ) return courses_enrollment, api_response['pagination'].get('next', None) querystring = {'page_size': 50} api = EdxRestApiClient(get_lms_url('api/courses/v1/')) course_enrollments = {} page = 0 throttling_attempts = 0 next_page = True while next_page: page += 1 querystring['page'] = page try: response = api.courses().get(**querystring) throttling_attempts = 0 except HttpClientError as exc: # this is a known limitation; If we get HTTP429, we need to pause execution for a few seconds # before re-requesting the data. raise any other errors if exc.response.status_code == 429 and throttling_attempts < self.max_tries: logger.warning( 'API calls are being rate-limited. Waiting for [%d] seconds before retrying...', self.pause_time ) time.sleep(self.pause_time) page -= 1 throttling_attempts += 1 logger.info('Retrying [%d]...', throttling_attempts) continue else: raise enrollment_info, next_page = _parse_response(response) course_enrollments.update(enrollment_info) return course_enrollments
def refresh(cls, course_id, access_token): """ Refresh the course data from the raw data sources. Args: course_id (str): Course ID access_token (str): OAuth access token Returns: Course """ client = EdxRestApiClient(settings.ECOMMERCE_API_URL, oauth_access_token=access_token) body = client.courses(course_id).get(include_products=True) course = Course(course_id, body) course.save() return course
def _query_course_structure_api(self): """Get course name from the Course Structure API.""" api_client = EdxRestApiClient( self.site_configuration.build_lms_url('/api/course_structure/v0/'), jwt=self.site_configuration.access_token) data = api_client.courses(self.course.id).get() logger.debug(data) course_name = data.get('name') if course_name is None: message = 'Aborting migration. No name is available for {}.'.format( self.course.id) logger.error(message) raise Exception(message) # A course without entries in the LMS CourseModes table must be an honor course, meaning # it has no verification deadline. course_verification_deadline = None return course_name.strip(), course_verification_deadline
def get_context_data(self, **kwargs): context = super(BasketSummaryView, self).get_context_data(**kwargs) lines = context.get('line_list', []) api = EdxRestApiClient(get_lms_url('api/courses/v1/')) for line in lines: course_id = line.product.course_id # Get each course type so we can display to the user at checkout. try: line.certificate_type = get_certificate_type_display_value(line.product.attr.certificate_type) except ValueError: line.certificate_type = None cache_key = 'courses_api_detail_{}'.format(course_id) cache_hash = hashlib.md5(cache_key).hexdigest() try: course = cache.get(cache_hash) if not course: course = api.courses(course_id).get() course['image_url'] = get_lms_url(course['media']['course_image']['uri']) cache.set(cache_hash, course, settings.COURSES_API_CACHE_TIMEOUT) line.course = course except (ConnectionError, SlumberBaseException, Timeout): logger.exception('Failed to retrieve data from Course API for course [%s].', course_id) if line.has_discount: line.discount_percentage = line.discount_value / line.unit_price_incl_tax * Decimal(100) else: line.discount_percentage = 0 context.update({ 'payment_processors': self.get_payment_processors(), 'homepage_url': get_lms_url(''), 'footer': get_lms_footer(), 'lines': lines, 'faq_url': get_lms_url('') + '/verified-certificate', }) return context
def refresh_all_ecommerce_data(cls, access_token): ecommerce_api_url = settings.ECOMMERCE_API_URL client = EdxRestApiClient(ecommerce_api_url, oauth_access_token=access_token) count = None page = 1 logger.info('Refreshing ecommerce data from %s....', ecommerce_api_url) while page: response = client.courses().get(include_products=True, page=page, page_size=50) count = response['count'] results = response['results'] logger.info('Retrieved %d courses...', len(results)) if response['next']: page += 1 else: page = None for body in results: Course(body['id']).update(body) logger.info('Retrieved %d courses from %s.', count, ecommerce_api_url)
def get_context_data(self, **kwargs): context = super(CouponOfferView, self).get_context_data(**kwargs) code = self.request.GET.get('code', None) if code is not None: voucher, product = get_voucher(code=code) valid_voucher, msg = voucher_is_valid(voucher, product, self.request) if valid_voucher: api = EdxRestApiClient(get_lms_url('api/courses/v1/'), ) try: course = api.courses(product.course_id).get() except SlumberHttpBaseException as e: logger.exception('Could not get course information. [%s]', e) return { 'error': _('Could not get course information. [{error}]'.format( error=e)) } course['image_url'] = get_lms_url( course['media']['course_image']['uri']) stock_records = voucher.offers.first( ).benefit.range.catalog.stock_records.first() context.update({ 'course': course, 'code': code, 'price': stock_records.price_excl_tax, 'verified': (product.attr.certificate_type is 'verified') }) return context return {'error': msg} return {'error': _('This coupon code is invalid.')}
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 = [] api = EdxRestApiClient(get_lms_url('api/courses/v1/')) is_verification_required = False for line in lines: course_key = CourseKey.from_string(line.product.attr.course_key) cache_key = 'courses_api_detail_{}'.format(course_key) cache_hash = hashlib.md5(cache_key).hexdigest() course_name = None image_url = None short_description = None try: course = cache.get(cache_hash) if not course: course = api.courses(course_key).get() cache.set(cache_hash, course, settings.COURSES_API_CACHE_TIMEOUT) image_url = get_lms_url(course['media']['course_image']['uri']) short_description = course['short_description'] course_name = course['name'] except (ConnectionError, SlumberBaseException, Timeout): logger.exception('Failed to retrieve data from Course API for course [%s].', course_key) if line.has_discount: benefit = self.request.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, }) context.update({ 'analytics_data': prepare_analytics_data( self.request.user, self.request.site.siteconfiguration.segment_key, unicode(course_key) ), }) # Check product attributes to determine if ID verification is required for this basket try: is_verification_required = is_verification_required or line.product.attr.id_verification_required except AttributeError: pass context.update({ 'free_basket': context['order_total'].incl_tax == 0, 'payment_processors': self.request.site.siteconfiguration.get_payment_processors(), 'homepage_url': get_lms_url(''), 'formset_lines_data': zip(formset, lines_data), 'is_verification_required': is_verification_required, }) return context
def process_load(args, sc, lms_url, temp_file): """ Process load command """ # Try to get edX access token if not supplied access_token = args.access_token if not access_token: logger.info( 'No access token provided. Retrieving access token using client_credential flow...' ) try: access_token, expires = EdxRestApiClient.get_oauth_access_token( '{root}/access_token'.format(root=args.oauth_host), args.oauth_key, args.oauth_secret, token_type='jwt') except Exception: logger.exception( 'No access token provided or acquired through client_credential flow.' ) raise logger.info('Token retrieved: %s', access_token) # use programs api to build table of course runs that are part of xseries series_table = load_series_table() # load any fixups fixups = load_fixups(args.fixups) logger.info(fixups) client = EdxRestApiClient(args.content_api_url, jwt=access_token) count = None page = 1 course_runs = 0 # read the courses and create a Sailthru content item for each course_run within the course while page: # get a page of courses response = client.courses().get(limit=500, offset=(page - 1) * 500) count = response['count'] results = response['results'] if response['next']: page += 1 else: page = None for course in results: for course_run in course['course_runs']: sailthru_content = create_sailthru_content( course, course_run, series_table, lms_url, fixups) if sailthru_content: course_runs += 1 if sc: try: response = sc.api_post('content', sailthru_content) except SailthruClientError as exc: logger.exception( "Exception attempting to update Sailthru", exc) # wait 10 seconds and retry time.sleep(10) response = sc.api_post('content', sailthru_content) if not response.is_ok(): logger.error( "Error code %d connecting to Sailthru content api: %s", response.json['error'], response.json['errormsg']) return logger.info( "Course: %s, Course_run: %s saved in Sailthru.", course['key'], course_run['key']) elif temp_file: json.dump(sailthru_content, temp_file) temp_file.write('\n') logger.info( "Course: %s, Course_run: %s being updated.", course['key'], course_run['key']) else: logger.info(sailthru_content) logger.info('Retrieved %d courses.', count) logger.info('Saved %d course runs in Sailthru.', course_runs)
def process_load(args, sc, lms_url, test): """ Process load command :param sc: Sailthru client :return: """ # Try to get edX access token if not supplied access_token = args.access_token if not access_token: logger.info('No access token provided. Retrieving access token using client_credential flow...') try: access_token, __ = EdxRestApiClient.get_oauth_access_token( '{root}/access_token'.format(root=args.oauth_host), args.oauth_key, args.oauth_seret ) except Exception: logger.exception('No access token provided or acquired through client_credential flow.') raise # logger.info('Token retrieved: %s', access_token) # use programs api to build table of course runs that are part of xseries series_table = load_series_table() client = EdxRestApiClient(args.content_api_url, oauth_access_token=access_token) count = None page = 1 course_runs = 0 # read the courses and create a Sailthru content item for each course_run within the course while page: # get a page of courses response = client.courses().get(limit=20, offset=(page-1)*20) count = response['count'] results = response['results'] if response['next']: page += 1 else: page = None for course in results: for course_run in course['course_runs']: sailthru_content = create_sailthru_content(course, course_run, series_table, lms_url) if sailthru_content: course_runs += 1 if not test: response = sc.api_post('content', sailthru_content) if not response.is_ok(): logger.error("Error code %d connecting to Sailthru content api: %s", response.json['error'], response.json['errormsg']) return logger.info("Course: %s, Course_run: %s saved in Sailthru.", course['key'], course_run['key']) else: logger.info(sailthru_content) logger.info('Retrieved %d courses.', count) logger.info('Saved %d course runs in Sailthru.', course_runs)
def get_context_data(self, **kwargs): context = super(CouponOfferView, self).get_context_data(**kwargs) code = self.request.GET.get('code', None) if code is not None: try: voucher, product = get_voucher_from_code(code=code) except Voucher.DoesNotExist: return { 'error': _('Coupon does not exist'), } except exceptions.ProductNotFoundError: return { 'error': _('The voucher is not applicable to your current basket.'), } valid_voucher, msg = voucher_is_valid(voucher, product, self.request) if valid_voucher: api = EdxRestApiClient(get_lms_url('api/courses/v1/'), ) try: course = api.courses(product.course_id).get() except SlumberHttpBaseException as e: logger.exception('Could not get course information. [%s]', e) return { 'error': _('Could not get course information. [{error}]'.format( error=e)), } course['image_url'] = get_lms_url( course['media']['course_image']['uri']) benefit = voucher.offers.first().benefit # Note (multi-courses): fix this to work for all stock records / courses. if benefit.range.catalog: stock_record = benefit.range.catalog.stock_records.first() else: stock_record = StockRecord.objects.get( product=benefit.range.included_products.first()) price = stock_record.price_excl_tax benefit_value = format_benefit_value(benefit) if benefit.type == 'Percentage': new_price = price - (price * (benefit.value / 100)) else: new_price = price - benefit.value if new_price < 0: new_price = Decimal(0) context.update({ 'analytics_data': prepare_analytics_data( self.request.user, self.request.site.siteconfiguration.segment_key, product.course_id), 'benefit_value': benefit_value, 'course': course, 'code': code, 'is_discount_value_percentage': benefit.type == 'Percentage', 'is_enrollment_code': benefit.type == Benefit.PERCENTAGE and benefit.value == 100.00, 'discount_value': "%.2f" % (price - new_price), 'price': price, 'new_price': "%.2f" % new_price, 'verified': (product.attr.certificate_type == 'verified'), 'verification_deadline': product.course.verification_deadline, }) return context return { 'error': msg, } return { 'error': _('This coupon code is invalid.'), }
class CatalogApiService(object): """The service to interface with edX catalog API""" def __init__(self, access_token, oauth_host, oauth_key, oauth_secret, api_url_root): self.access_token = access_token if not access_token: logger.info( 'No access token provided. Retrieving access token using client_credential flow...' ) try: access_token_url = '{}/access_token'.format(oauth_host) self.access_token, __ = EdxRestApiClient.get_oauth_access_token( access_token_url, oauth_key, oauth_secret, token_type='jwt') except Exception: logger.exception( 'No access token provided or acquired through client_credential flow.' ) raise logger.info('Retrieved access token.') self.api_client = EdxRestApiClient(api_url_root, jwt=self.access_token) self._programs_dictionary = {} def _get_resource_from_api(self, api_endpoint, **kwargs): results = [] while True: response = api_endpoint.get(**kwargs) next_url = response.get('next') results.extend(response.get('results')) if next_url: parsed_url = urlparse(next_url) query_string_dict = parse_qs(parsed_url.query) kwargs = query_string_dict else: break return results def get_courses(self): logger.debug('Get Courses called') return self._get_resource_from_api(self.api_client.courses(), page=1, page_size=COURSES_PAGE_SIZE, exclude_utm=1) def get_marketable_only_course_runs_keys(self): logger.debug('Get marketable only course_runs called') courses = self._get_resource_from_api( self.api_client.courses(), page=1, page_size=COURSES_PAGE_SIZE, exclude_utm=1, marketable_course_runs_only=1, ) course_run_keys = [] for course in courses: course_runs = course.get('course_runs', []) for course_run in course_runs: course_run_keys.append(course_run.get('key')) logging.debug('Retrieved {} marketable course runs'.format( len(course_run_keys))) return course_run_keys def get_program_dictionary(self): if not self._programs_dictionary: program_array = self._get_resource_from_api( self.api_client.programs(), page=1, page_size=PROGRAMS_PAGE_SIZE, marketable_course_runs_only=1, marketable=1) for program in program_array: self._programs_dictionary[program['uuid']] = program return self._programs_dictionary