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 is_eligible_for_credit(self, course_key): """ Check if a user is eligible for a credit course. Calls the LMS eligibility API endpoint and sends the username and course key query parameters and returns eligibility details for the user and course combination. Args: course_key (string): The course key for which the eligibility is checked for. Returns: A list that contains eligibility information, or empty if user is not eligible. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS eligibility API endpoint. """ query_strings = { 'username': self.username, 'course_key': course_key } try: api = EdxRestApiClient( get_lms_url('api/credit/v1/'), oauth_access_token=self.access_token ) response = api.eligibility().get(**query_strings) except (ConnectionError, SlumberBaseException, Timeout): # pragma: no cover log.exception( 'Failed to retrieve eligibility details for [%s] in course [%s]', self.username, course_key ) raise return response
def account_details(self, request): """ Returns the account details from LMS. Args: request (WSGIRequest): The request from which the LMS account API endpoint is created. Returns: A dictionary of account details. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS account API endpoint. """ try: api = EdxRestApiClient( request.site.siteconfiguration.build_lms_url('/api/user/v1'), append_slash=False, jwt=request.site.siteconfiguration.access_token ) response = api.accounts(self.username).get() return response except (ConnectionError, SlumberBaseException, Timeout): log.exception( 'Failed to retrieve account details for [%s]', self.username ) raise
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_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 _delete_program(self, program_id, jwt_token): """ With the JWT token, hit the program details URL with the patch to set the program status to "deleted". This is the delete program step """ url = '{0}/api/v1/'.format(PROGRAMS_URL_ROOT) delete_client = EdxRestApiClient(url, jwt=jwt_token) deleted_program = delete_client.programs(program_id).patch({'status': 'deleted'}) # tell the caller wither the delete is successful or not. return deleted_program['status'] == 'deleted'
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_from_code(code=code) else: voucher = None try: product = StockRecord.objects.get(partner=partner, partner_sku=sku).product course_key = product.attr.course_key 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() 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 ) return HttpResponseBadRequest(_('You are already enrolled in {course}.').format( course=product.course.name)) except StockRecord.DoesNotExist: return HttpResponseBadRequest(_('SKU [{sku}] does not exist.').format(sku=sku)) 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, ) return HttpResponseBadRequest(_('An error occurred while retrieving enrollment details. Please try again.')) purchase_info = request.strategy.fetch_for_product(product) if not purchase_info.availability.is_available_to_buy: return HttpResponseBadRequest(_('Product [{product}] not available to buy.').format(product=product.title)) prepare_basket(request, product, voucher) return HttpResponseRedirect(reverse('basket:summary'), status=303)
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_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 _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 _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 access_token(self): """ Returns an access token for this site's service user. The access token is retrieved using the current site's OAuth credentials and the client credentials grant. The token is cached for the lifetime of the token, as specified by the OAuth provider's response. The token type is JWT. Returns: str: JWT access token """ key = 'siteconfiguration_access_token_{}'.format(self.id) access_token = cache.get(key) # pylint: disable=unsubscriptable-object if not access_token: url = '{root}/access_token'.format(root=self.oauth2_provider_url) access_token, expiration_datetime = EdxRestApiClient.get_oauth_access_token( url, self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_KEY'], self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_SECRET'], token_type='jwt' ) expires = (expiration_datetime - datetime.datetime.utcnow()).seconds cache.set(key, access_token, expires) return access_token
class DiscoveryApiClient(object): """ Class for interacting with the discovery service journals endpoint """ def __init__(self): """ Initialize an authenticated Discovery service API client by using the provided user. """ catalog_integration = CatalogIntegration.current() # Client can't be used if there is no catalog integration if not (catalog_integration and catalog_integration.enabled): LOGGER.error("Unable to create DiscoveryApiClient because catalog integration not set up or enabled") return None try: user = catalog_integration.get_service_user() except ObjectDoesNotExist: LOGGER.error("Unable to retrieve catalog integration service user") return None jwt = JwtBuilder(user).build_token([]) base_url = configuration_helpers.get_value('COURSE_CATALOG_URL_BASE', settings.COURSE_CATALOG_URL_BASE) self.client = EdxRestApiClient( '{base_url}{journals_path}'.format(base_url=base_url, journals_path=JOURNALS_API_PATH), jwt=jwt ) def get_journals(self, orgs): """ get_journals from discovery, filter on orgs is supplied """ try: if orgs: response = self.client.journals.get(orgs=','.join(orgs), status='active') else: response = self.client.journals.get(status='active') LOGGER.debug('response is type=%s', type(response)) return response.get('results') except (HttpClientError, HttpServerError) as err: LOGGER.exception( 'Failed to get journals from discovery-service [%s]', err.content ) return [] def get_journal_bundles(self, uuid=''): """ get_journal_bundles from discovery on the base of uuid (optional) """ try: response = self.client.journal_bundles(uuid).get() except (HttpClientError, HttpServerError) as err: LOGGER.exception( 'Failed to get journal bundles from discovery-service [%s]', err.content ) return [] return [response] if uuid else response.get('results')
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 __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 is_verified(self, site): """ Check if a user has verified his/her identity. Calls the LMS verification status API endpoint and returns the verification status information. The status information is stored in cache, if the user is verified, until the verification expires. Args: site (Site): The site object from which the LMS account API endpoint is created. Returns: True if the user is verified, false otherwise. """ try: cache_key = 'verification_status_{username}'.format( username=self.username) cache_key = hashlib.md5(cache_key).hexdigest() verification_cached_response = TieredCache.get_cached_response( cache_key) if verification_cached_response.is_found: return verification_cached_response.value api = EdxRestApiClient( site.siteconfiguration.build_lms_url('api/user/v1/'), oauth_access_token=self.access_token) response = api.accounts(self.username).verification_status().get() verification = response.get('is_verified', False) if verification: cache_timeout = int( (parse(response.get('expiration_datetime')) - now()).total_seconds()) TieredCache.set_all_tiers(cache_key, verification, cache_timeout) return verification except HttpNotFoundError: log.debug('No verification data found for [%s]', self.username) return False except (ConnectionError, SlumberBaseException, Timeout): msg = 'Failed to retrieve verification status details for [{username}]'.format( username=self.username) log.warning(msg) return False
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_enrollment_api_client(): """ Retrieve a client of the Enrollment API. The client is authenticated using the API token from the Django settings. """ session = requests.Session() session.headers = {"X-Edx-Api-Key": settings.EDX_API_KEY} return EdxRestApiClient(settings.ENTERPRISE_ENROLLMENT_API_URL, append_slash=False, session=session)
def course_discovery_api_client(user, catalog_url): """ Return a Course Discovery API client setup with authentication for the specified user. """ if JwtBuilder is None: raise NotConnectedToOpenEdX( _("To get a Catalog API client, this package must be " "installed in an Open edX environment.")) jwt = JwtBuilder.create_jwt_for_user(user) return EdxRestApiClient(catalog_url, jwt=jwt)
def handle(self, *args, **options): # For each partner defined... partners = Partner.objects.all() # If a specific partner was indicated, filter down the set partner_code = options.get('partner_code') if partner_code: partners = partners.filter(short_code=partner_code) if not partners: raise CommandError('No partners available!') for partner in partners: access_token = options.get('access_token') token_type = options.get('token_type') if access_token and not token_type: raise CommandError('The token_type must be specified when passing in an access token!') if not access_token: logger.info('No access token provided. Retrieving access token using client_credential flow...') token_type = 'JWT' try: access_token, __ = EdxRestApiClient.get_oauth_access_token( '{root}/access_token'.format(root=partner.oidc_url_root.strip('/')), partner.oidc_key, partner.oidc_secret, token_type=token_type ) except Exception: logger.exception('No access token provided or acquired through client_credential flow.') raise loaders = [] if partner.organizations_api_url: loaders.append(OrganizationsApiDataLoader) if partner.courses_api_url: loaders.append(CoursesApiDataLoader) if partner.ecommerce_api_url: loaders.append(EcommerceApiDataLoader) if partner.marketing_site_api_url: loaders.append(DrupalApiDataLoader) if partner.programs_api_url: loaders.append(ProgramsApiDataLoader) if loaders: for loader_class in loaders: try: loader_class(partner, access_token, token_type).ingest() except Exception: # pylint: disable=broad-except logger.exception('%s failed!', loader_class.__name__)
class EnrollmentApiClient(object): def __init__(self): access_token, __ = get_access_token() self.client = EdxRestApiClient(ENROLLMENT_API_URL, jwt=access_token, append_slash=False) def get_enrollment_status(self, username, course_id): """ Retrieve the enrollment status for given user in a given course. """ param = '{username},{course_id}'.format(username=username, course_id=course_id) return self.client.enrollment(param).get()
def ecommerce_api_client(user, session=None, token_expiration=None): """ Returns an E-Commerce API client setup with authentication for the specified user. """ claims = {'tracking_context': create_tracking_context(user)} jwt = JwtBuilder(user).build_token(['email', 'profile'], expires_in=token_expiration, additional_claims=claims) return EdxRestApiClient(configuration_helpers.get_value( 'ECOMMERCE_API_URL', settings.ECOMMERCE_API_URL), jwt=jwt, session=session)
def __init__(self, user): """ Initialize an authenticated Enterprise service API client by using the provided user. """ self.user = user jwt = create_jwt_for_user(user) self.client = EdxRestApiClient( configuration_helpers.get_value('ENTERPRISE_API_URL', settings.ENTERPRISE_API_URL), jwt=jwt )
def course_catalog_api_client(self): """ Returns an API client to access the Course Catalog service. Returns: EdxRestApiClient: The client to access the Course Catalog service. """ # TODO Use URL from SiteConfiguration model. return EdxRestApiClient(settings.COURSE_CATALOG_API_URL, jwt=self.access_token)
def _hubspot_endpoint(self, hubspot_object, api_url, method, body=None, **kwargs): """ This function is responsible for all the calls of hubspot. """ client = EdxRestApiClient('/'.join([HUBSPOT_API_BASE_URL, api_url])) if method == "GET": return getattr(client, hubspot_object).get(**kwargs) if method == "POST": return getattr(client, hubspot_object).post(**kwargs) if method == "PUT": return getattr(client, hubspot_object).put(body, **kwargs)
def user_api_client(self): """ Returns an authenticated User API client. Returns: EdxRestApiClient """ return EdxRestApiClient(self.user_api_url, jwt=self.access_token, append_slash=False)
def create_video_pipeline_api_client(user, api_url): """ Returns an API client which can be used to make Video Pipeline API requests. Arguments: user(User): A requesting user. api_url(unicode): It is video pipeline's API URL. """ jwt_token = JwtBuilder(user).build_token( scopes=[], expires_in=settings.OAUTH_ID_TOKEN_EXPIRATION) return EdxRestApiClient(api_url, jwt=jwt_token)
def enterprise_catalog_api_client(self): """ Returns a REST API client for the provided enterprise catalog service Example: site.siteconfiguration.enterprise_catalog_api_client.enterprise-catalog.get() Returns: EdxRestApiClient: The client to access the Enterprise Catalog service. """ return EdxRestApiClient(self.enterprise_catalog_api_url, jwt=self.access_token)
def get_access_token(): """ Returns an access token and expiration date from the OAuth provider. Returns: (str, datetime):Tuple containing access token and expiration date. """ return EdxRestApiClient.get_oauth_access_token( OAUTH_ACCESS_TOKEN_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, token_type='jwt' )
def get_ecommerce_client(site_code=None): """ Get client for fetching data from ecommerce API Returns: EdxRestApiClient object """ ecommerce_api_root = get_configuration('ECOMMERCE_API_ROOT', site_code=site_code) signing_key = get_configuration('JWT_SECRET_KEY', site_code=site_code) issuer = get_configuration('JWT_ISSUER', site_code=site_code) service_username = get_configuration('ECOMMERCE_SERVICE_USERNAME', site_code=site_code) return EdxRestApiClient(ecommerce_api_root, signing_key=signing_key, issuer=issuer, username=service_username)
def enterprise_api_client(self): """ Constructs a Slumber-based REST API client for the provided site. Example: site.siteconfiguration.enterprise_api_client.enterprise-learner(learner.username).get() Returns: EdxRestApiClient: The client to access the Enterprise service. """ return EdxRestApiClient(self.enterprise_api_url, jwt=self.access_token)
def create_video_pipeline_api_client(user, api_client_id, api_client_secret, api_url): """ Returns an API client which can be used to make Video Pipeline API requests. Arguments: user(User): A requesting user. api_client_id(unicode): Video pipeline client id. api_client_secret(unicode): Video pipeline client secret. api_url(unicode): It is video pipeline's API URL. """ jwt_token = create_jwt_for_user(user, secret=api_client_secret, aud=api_client_id) return EdxRestApiClient(api_url, jwt=jwt_token)
def dispatch(self, request, *args, **kwargs): self.course_api_enabled = switch_is_active('enable_course_api') if self.course_api_enabled and request.user.is_authenticated: self.access_token = settings.COURSE_API_KEY or EdxRestApiClient.get_and_cache_jwt_oauth_access_token( settings.BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL, settings.BACKEND_SERVICE_EDX_OAUTH2_KEY, settings.BACKEND_SERVICE_EDX_OAUTH2_SECRET, )[0] self.course_api = CourseStructureApiClient(settings.COURSE_API_URL, self.access_token) return super(CourseAPIMixin, self).dispatch(request, *args, **kwargs)
def create_catalog_api_client(user, site=None): """Returns an API client which can be used to make Catalog API requests.""" scopes = ['email', 'profile'] expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION jwt = JwtBuilder(user).build_token(scopes, expires_in) if site: url = site.configuration.get_value('COURSE_CATALOG_API_URL') else: url = CatalogIntegration.current().get_internal_api_url() return EdxRestApiClient(url, jwt=jwt)
def _add_dynamic_discount_to_request(self, basket): # TODO: Remove as a part of REVMI-124 as this is a hacky solution # The problem is that orders are being created after payment processing, and the discount is not # saved in the database, so it needs to be calculated again in order to save the correct info to the # order. REVMI-124 will create the order before payment processing, when we have the discount context. if waffle.flag_is_active(self.request, DYNAMIC_DISCOUNT_FLAG) and basket.lines.count() == 1: # pragma: no cover pylint: disable=line-too-long discount_lms_url = get_lms_url('/api/discounts/') lms_discount_client = EdxRestApiClient(discount_lms_url, jwt=self.request.site.siteconfiguration.access_token) ck = basket.lines.first().product.course_id user_id = basket.owner.lms_user_id try: response = lms_discount_client.user(user_id).course(ck).get() self.request.POST = self.request.POST.copy() self.request.POST['discount_jwt'] = response.get('jwt') logger.info( """Received discount jwt from LMS with url: [%s], user_id: [%s], course_id: [%s], and basket_id: [%s] returned [%s]""", discount_lms_url, str(user_id), ck, basket.id, response) except (SlumberHttpBaseException, requests.exceptions.Timeout) as error: logger.warning( """Failed to receive discount jwt from LMS with url: [%s], user_id: [%s], course_id: [%s], and basket_id: [%s] returned [%s]""", discount_lms_url, str(user_id), ck, basket.id, vars(error.response) if hasattr(error, 'response') else '')
def handle(self, *args, **options): access_token = options.get('access_token') commit = options.get('commit') if access_token is None: try: access_token_url = '{}/access_token'.format( settings.SOCIAL_AUTH_EDX_OIDC_URL_ROOT.strip('/')) client_id = settings.SOCIAL_AUTH_EDX_OIDC_KEY client_secret = settings.SOCIAL_AUTH_EDX_OIDC_SECRET access_token, __ = EdxRestApiClient.get_oauth_access_token( access_token_url, client_id, client_secret) except: # pylint: disable=bare-except logger.exception( 'Unable to exchange client credentials grant for an access token.' ) return self.client = EdxRestApiClient(settings.ORGANIZATIONS_API_URL_ROOT, oauth_access_token=access_token) logger.info('Retrieving organization data from %s.', settings.ORGANIZATIONS_API_URL_ROOT) try: with transaction.atomic(): self._get_data() logger.info( 'Retrieved %d organizations from %s, %d of which were new.', self.org_count, settings.ORGANIZATIONS_API_URL_ROOT, self.new_org_count) if not commit: raise ForcedRollback( 'No data has been saved. To save data, pass the -c or --commit flags.' ) except ForcedRollback as e: logger.info(e)
def get(self, request): # lms/ecommerce has different user if 'username' in request.GET and request.user.username != request.GET.get( 'username'): logout(request) query_dict = request.GET.dict() query_dict.pop('username') redirect_url = '{path}?{query_string}'.format( path=request.path, query_string=urlencode(query_dict)) logger.info('logout user {username}'.format( username=request.GET.get('username'))) return redirect(redirect_url) partner = get_partner_for_site(request) skus = [escape(sku) for sku in request.GET.getlist('sku')] code = request.GET.get('code', None) if not skus: return HttpResponseBadRequest(_('No SKUs provided.')) products = Product.objects.filter(stockrecords__partner=partner, stockrecords__partner_sku__in=skus) if not products: return HttpResponseBadRequest( _('Products with SKU(s) [{skus}] do not exist.').format( skus=', '.join(skus))) try: lms_api = EdxRestApiClient( get_lms_url('/api/v1/vip/'), oauth_access_token=request.user.access_token, append_slash=False) # user is vip, redirect lms course about if lms_api.info().get().get('data', {}).get('status') is True: course_key = CourseKey.from_string(products[0].attr.course_key) return redirect( get_lms_course_about_url(course_key=course_key)) except Exception, e: logger.exception(e)
def is_user_already_enrolled(self, request, seat): """ Check if a user is already enrolled in the course. Calls the LMS enrollment API endpoint and sends the course ID and username query parameters and returns the status of the user's enrollment in the course. Arguments: request (WSGIRequest): the request from which the LMS enrollment API endpoint is created. seat (Product): the seat for which the check is done if the user is enrolled in. Returns: A boolean value if the user is enrolled in the course or not. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS enrollment API endpoint. """ course_key = seat.attr.course_key try: api = EdxRestApiClient( request.site.siteconfiguration.build_lms_url( '/api/enrollment/v1'), oauth_access_token=self.access_token, append_slash=False) status = api.enrollment(','.join([self.username, course_key])).get() except (ConnectionError, SlumberBaseException, Timeout) as ex: log.exception( 'Failed to retrieve enrollment details for [%s] in course [%s], because of [%s]', self.username, course_key, ex, ) raise ex seat_type = mode_for_seat(seat) if status and status.get('mode') == seat_type and status.get( 'is_active'): return True return False
def __init__(self): """ Initialize a consent service API client, authenticated using the Enterprise worker username. """ self.user = User.objects.get(username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME) jwt = JwtBuilder(self.user).build_token([]) url = configuration_helpers.get_value('ENTERPRISE_CONSENT_API_URL', settings.ENTERPRISE_CONSENT_API_URL) self.client = EdxRestApiClient( url, jwt=jwt, append_slash=False, ) self.consent_endpoint = self.client.data_sharing_consent
def __init__(self, user): """ Initialize an authenticated Consent service API client by using the provided user. """ jwt = create_jwt_for_user(user) url = configuration_helpers.get_value('ENTERPRISE_CONSENT_API_URL', settings.ENTERPRISE_CONSENT_API_URL) self.client = EdxRestApiClient( url, jwt=jwt, append_slash=False, ) self.consent_endpoint = self.client.data_sharing_consent
def post(self, request): """Handle an incoming user returned to us by Alipay after approving payment.""" resp = json.loads(request.POST['original_data'])['data'] verify_ret, payment_response = self.verify_data(resp) if not verify_ret: return Response({'result': 'fail'}) payment_id = payment_response.get('out_trade_no') basket = self._get_basket(payment_id) if not basket: return Response({'result': 'fail'}) try: lms_api = EdxRestApiClient( get_lms_url('/api/user/v1/'), oauth_access_token=basket.owner.access_token, append_slash=False) user_lang = lms_api.preferences(basket.owner.username).get() translation.activate( user_lang.get('pref-lang', settings.LANGUAGE_CODE)) except Exception, e: logger.exception(e)
def _add_dynamic_discount_to_request(self, basket): # TODO: Remove as a part of REVMI-124 as this is a hacky solution # The problem is that orders are being created after payment processing, and the discount is not # saved in the database, so it needs to be calculated again in order to save the correct info to the # order. REVMI-124 will create the order before payment processing, when we have the discount context. if waffle.flag_is_active( self.request, DYNAMIC_DISCOUNT_FLAG) and basket.lines.count() == 1: discount_lms_url = get_lms_url('/api/discounts/') lms_discount_client = EdxRestApiClient( discount_lms_url, jwt=self.request.site.siteconfiguration.access_token) ck = basket.lines.first().product.course_id user_id = basket.owner.lms_user_id try: response = lms_discount_client.user(user_id).course(ck).get() self.request.GET = self.request.GET.copy() self.request.GET['discount_jwt'] = response.get('jwt') except (SlumberHttpBaseException, Timeout) as error: logger.warning( 'Failed to get discount jwt from LMS. [%s] returned [%s]', discount_lms_url, error.response)
def is_verified(self, site): """ Check if a user has verified his/her identity. Calls the LMS verification status API endpoint and returns the verification status information. The status information is stored in cache, if the user is verified, until the verification expires. Args: site (Site): The site object from which the LMS account API endpoint is created. Returns: True if the user is verified, false otherwise. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS verification status API endpoint. """ try: cache_key = 'verification_status_{username}'.format(username=self.username) cache_key = hashlib.md5(cache_key).hexdigest() verification = cache.get(cache_key) if not verification: api = EdxRestApiClient( site.siteconfiguration.build_lms_url('api/user/v1/'), oauth_access_token=self.access_token ) response = api.accounts(self.username).verification_status().get() verification = response.get('is_verified', False) if verification: cache_timeout = int((parse(response.get('expiration_datetime')) - now()).total_seconds()) cache.set(cache_key, verification, cache_timeout) return verification except HttpNotFoundError: log.debug('No verification data found for [%s]', self.username) return False except (ConnectionError, SlumberBaseException, Timeout): msg = 'Failed to retrieve verification status details for [{username}]'.format(username=self.username) log.exception(msg) raise VerificationStatusError(msg)
def course_discovery_api_client(user): """ Return a Course Discovery API client setup with authentication for the specified user. """ if JwtBuilder is None: raise NotConnectedToOpenEdX( _("To get a Catalog API client, this package must be " "installed in an Open edX environment.")) scopes = ['email', 'profile'] expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION jwt = JwtBuilder(user).build_token(scopes, expires_in) return EdxRestApiClient(settings.COURSE_CATALOG_API_URL, jwt=jwt)
def test_get_client_credential_access_token_success(self): """ Test that the get access token method handles 200 responses and returns the access token. """ code = 200 body = {"access_token": "my-token", "expires_in": 1000} now = datetime.datetime.utcnow() expected_return = ("my-token", now + datetime.timedelta(seconds=1000)) with freeze_time(now): self._mock_auth_api(URL, code, body=body) self.assertEqual( EdxRestApiClient.get_oauth_access_token( URL, "client_id", "client_secret"), expected_return)
def ecommerce_api_client(user): """ Returns an E-Commerce API client setup with authentication for the specified user. """ jwt_auth = configuration_helpers.get_value("JWT_AUTH", settings.JWT_AUTH) return EdxRestApiClient( configuration_helpers.get_value("ECOMMERCE_API_URL", settings.ECOMMERCE_API_URL), configuration_helpers.get_value("ECOMMERCE_API_SIGNING_KEY", settings.ECOMMERCE_API_SIGNING_KEY), user.username, user.profile.name if hasattr(user, 'profile') else None, user.email, tracking_context=create_tracking_context(user), issuer=jwt_auth['JWT_ISSUER'], expires_in=jwt_auth['JWT_EXPIRATION'] )
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 is_user_already_enrolled(self, request, seat): """ Check if a user is already enrolled in the course. Calls the LMS enrollment API endpoint and sends the course ID and username query parameters and returns the status of the user's enrollment in the course. Arguments: request (WSGIRequest): the request from which the LMS enrollment API endpoint is created. seat (Product): the seat for which the check is done if the user is enrolled in. Returns: A boolean value if the user is enrolled in the course or not. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS enrollment API endpoint. """ course_key = seat.attr.course_key try: api = EdxRestApiClient( request.site.siteconfiguration.build_lms_url('/api/enrollment/v1'), oauth_access_token=self.access_token, append_slash=False ) status = api.enrollment(','.join([self.username, course_key])).get() except (ConnectionError, SlumberBaseException, Timeout) as ex: log.exception( 'Failed to retrieve enrollment details for [%s] in course [%s], Because of [%s]', self.username, course_key, ex, ) raise ex seat_type = mode_for_seat(seat) if status and status.get('mode') == seat_type and status.get('is_active'): return True return False
def _get_basket(self, basket_id): if not basket_id: return None try: basket_id = int(basket_id) basket = Basket.objects.get(id=basket_id) basket.strategy = strategy.Default() # TODO: Remove as a part of REVMI-124 as this is a hacky solution # The problem is that orders are being created after payment processing, and the discount is not # saved in the database, so it needs to be calculated again in order to save the correct info to the # order. REVMI-124 will create the order before payment processing, when we have the discount context. if waffle.flag_is_active( self.request, DYNAMIC_DISCOUNT_FLAG) and basket.lines.count() == 1: discount_lms_url = get_lms_url('/api/discounts/') lms_discount_client = EdxRestApiClient( discount_lms_url, jwt=self.request.site.siteconfiguration.access_token) ck = basket.lines.first().product.course_id user_id = self.request.user.lms_user_id try: response = lms_discount_client.user(user_id).course( ck).get() self.request.POST = self.request.POST.copy() self.request.POST['discount_jwt'] = response.get('jwt') except (SlumberHttpBaseException, requests.exceptions.Timeout) as error: logger.warning( 'Failed to get discount jwt from LMS. [%s] returned [%s]', discount_lms_url, error.response) # End TODO Applicator().apply(basket, basket.owner, self.request) logger.info('Applicator applied, basket id: [%s]', basket.id) return basket except (ValueError, ObjectDoesNotExist) as error: logger.warning('Could not get basket--error: [%s]', str(error)) return None
def journal_discovery_api_client(self): """ Returns an Journal API client to access the Discovery service. Returns: EdxRestApiClient: The client to access the Journal API in the Discovery service. """ split_url = urlsplit(self.discovery_api_url) journal_discovery_url = urlunsplit([ split_url.scheme, split_url.netloc, JOURNAL_DISCOVERY_API_PATH, split_url.query, split_url.fragment ]) return EdxRestApiClient(journal_discovery_url, jwt=self.access_token)
def test_get_client_credential_access_token_success(self): """ Test that the get access token method handles 200 responses and returns the access token. """ code = 200 body = {"access_token": "my-token", "expires_in": 1000} now = datetime.datetime.utcnow() expected_return = ("my-token", now + datetime.timedelta(seconds=1000)) with freeze_time(now): self._mock_auth_api(URL, code, body=body) self.assertEqual( EdxRestApiClient.get_oauth_access_token(URL, "client_id", "client_secret"), expected_return )
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 fulfill_order(self, order_number, site_code=None): """Fulfills an order. Arguments: order_number (str): Order number indicating which order to fulfill. Returns: None """ ecommerce_api_root = get_configuration('ECOMMERCE_API_ROOT', site_code=site_code) max_fulfillment_retries = get_configuration('MAX_FULFILLMENT_RETRIES', site_code=site_code) signing_key = get_configuration('JWT_SECRET_KEY', site_code=site_code) issuer = get_configuration('JWT_ISSUER', site_code=site_code) service_username = get_configuration('ECOMMERCE_SERVICE_USERNAME', site_code=site_code) api = EdxRestApiClient(ecommerce_api_root, signing_key=signing_key, issuer=issuer, username=service_username) try: logger.info('Requesting fulfillment of order [%s].', order_number) api.orders(order_number).fulfill.put() except exceptions.HttpClientError as exc: status_code = exc.response.status_code # pylint: disable=no-member if status_code == 406: # The order is not fulfillable. Therefore, it must be complete. logger.info('Order [%s] has already been fulfilled. Ignoring.', order_number) raise Ignore() else: # Unknown client error. Let's retry to resolve it. logger.warning( 'Fulfillment of order [%s] failed because of HttpClientError. Retrying', order_number, exc_info=True ) _retry_order(self, exc, max_fulfillment_retries, order_number) except (exceptions.HttpServerError, exceptions.Timeout) as exc: # Fulfillment failed, retry _retry_order(self, exc, max_fulfillment_retries, order_number)
def test_user_agent(self): """Make sure our custom User-Agent is getting built correctly.""" with mock.patch('socket.gethostbyname', return_value='test_hostname'): default_user_agent = user_agent() self.assertIn('python-requests', default_user_agent) self.assertIn('edx-rest-api-client/{}'.format(__version__), default_user_agent) self.assertIn('test_hostname', default_user_agent) with mock.patch('socket.gethostbyname') as mock_gethostbyname: mock_gethostbyname.side_effect = ValueError() default_user_agent = user_agent() self.assertIn('unknown_client_name', default_user_agent) with mock.patch.dict(os.environ, {'EDX_REST_API_CLIENT_NAME': "awesome_app"}): uagent = user_agent() self.assertIn('awesome_app', uagent) self.assertEqual(user_agent(), EdxRestApiClient.user_agent())
def handle(self, *args, **options): access_token = options.get('access_token') commit = options.get('commit') if access_token is None: try: access_token_url = '{}/access_token'.format( settings.SOCIAL_AUTH_EDX_OIDC_URL_ROOT.strip('/') ) client_id = settings.SOCIAL_AUTH_EDX_OIDC_KEY client_secret = settings.SOCIAL_AUTH_EDX_OIDC_SECRET access_token, __ = EdxRestApiClient.get_oauth_access_token( access_token_url, client_id, client_secret ) except: # pylint: disable=bare-except logger.exception('Unable to exchange client credentials grant for an access token.') return self.client = EdxRestApiClient(settings.ORGANIZATIONS_API_URL_ROOT, oauth_access_token=access_token) logger.info('Retrieving organization data from %s.', settings.ORGANIZATIONS_API_URL_ROOT) try: with transaction.atomic(): self._get_data() logger.info( 'Retrieved %d organizations from %s, %d of which were new.', self.org_count, settings.ORGANIZATIONS_API_URL_ROOT, self.new_org_count ) if not commit: raise ForcedRollback('No data has been saved. To save data, pass the -c or --commit flags.') except ForcedRollback as e: logger.info(e)
def get_course_catalog_api_client(site): """ Returns an API client to access the Course Catalog service. Arguments: site (Site): The site for which to retrieve settings. Returns: EdxRestApiClient: The client to access the Course Catalog service. """ access_token, __ = EdxRestApiClient.get_oauth_access_token( '{root}/access_token'.format(root=get_oauth2_provider_url()), site.siteconfiguration.oauth_settings['SOCIAL_AUTH_EDX_OIDC_KEY'], site.siteconfiguration.oauth_settings['SOCIAL_AUTH_EDX_OIDC_SECRET'], token_type='jwt' ) course_catalog_client = EdxRestApiClient( settings.COURSE_CATALOG_API_URL, jwt=access_token ) return course_catalog_client
def __init__(self): """ Initialize an authenticated Discovery service API client by using the provided user. """ catalog_integration = CatalogIntegration.current() # Client can't be used if there is no catalog integration if not (catalog_integration and catalog_integration.enabled): LOGGER.error("Unable to create DiscoveryApiClient because catalog integration not set up or enabled") return None try: user = catalog_integration.get_service_user() except ObjectDoesNotExist: LOGGER.error("Unable to retrieve catalog integration service user") return None jwt = JwtBuilder(user).build_token([]) base_url = configuration_helpers.get_value('COURSE_CATALOG_URL_BASE', settings.COURSE_CATALOG_URL_BASE) self.client = EdxRestApiClient( '{base_url}{journals_path}'.format(base_url=base_url, journals_path=JOURNALS_API_PATH), jwt=jwt )
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)
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