""" Module containing API functions for the CCXCon """ import logging import urlparse from django.core.exceptions import ValidationError from django.core.validators import URLValidator from django.http import Http404 from oauthlib.oauth2 import BackendApplicationClient from requests_oauthlib import OAuth2Session from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED from lms.djangoapps.courseware.courses import get_course_by_id from openedx.core.djangoapps.models.course_details import CourseDetails from student.models import anonymous_id_for_user from student.roles import CourseInstructorRole from .models import CCXCon log = logging.getLogger(__name__) CCXCON_COURSEXS_URL = '/api/v1/coursexs/' CCXCON_TOKEN_URL = '/o/token/' CCXCON_REQUEST_TIMEOUT = 30 class CCXConnServerError(Exception): """ Custom exception to be raised in case there is any issue with the request to the server """ def is_valid_url(url): """ Helper function used to check if a string is a valid url. Args: url (str): the url string to be validated Returns: bool: whether the url is valid or not """ validate = URLValidator() try: validate(url) return True except ValidationError: return False def get_oauth_client(server_token_url, client_id, client_secret): """ Function that creates an oauth client and fetches a token. It intentionally doesn't handle errors. Args: server_token_url (str): server URL where to get an authentication token client_id (str): oauth client ID client_secret (str): oauth client secret Returns: OAuth2Session: an instance of OAuth2Session with a token """ if not is_valid_url(server_token_url): return client = BackendApplicationClient(client_id=client_id) oauth_ccxcon = OAuth2Session(client=client) oauth_ccxcon.fetch_token( token_url=server_token_url, client_id=client_id, client_secret=client_secret, timeout=CCXCON_REQUEST_TIMEOUT ) return oauth_ccxcon def course_info_to_ccxcon(course_key): """ Function that gathers informations about the course and makes a post request to a CCXCon with the data. Args: course_key (CourseLocator): the master course key """ try: course = get_course_by_id(course_key) except Http404: log.error('Master Course with key "%s" not found', unicode(course_key)) return if not course.enable_ccx: log.debug('ccx not enabled for course key "%s"', unicode(course_key)) return if not course.ccx_connector: log.debug('ccx connector not defined for course key "%s"', unicode(course_key)) return if not is_valid_url(course.ccx_connector): log.error( 'ccx connector URL "%s" for course key "%s" is not a valid URL.', course.ccx_connector, unicode(course_key) ) return # get the oauth credential for this URL try: ccxcon = CCXCon.objects.get(url=course.ccx_connector) except CCXCon.DoesNotExist: log.error('ccx connector Oauth credentials not configured for URL "%s".', course.ccx_connector) return # get an oauth client with a valid token oauth_ccxcon = get_oauth_client( server_token_url=urlparse.urljoin(course.ccx_connector, CCXCON_TOKEN_URL), client_id=ccxcon.oauth_client_id, client_secret=ccxcon.oauth_client_secret ) # get the entire list of instructors course_instructors = CourseInstructorRole(course.id).users_with_role() # get anonymous ids for each of them course_instructors_ids = [anonymous_id_for_user(user, course_key) for user in course_instructors] # extract the course details course_details = CourseDetails.fetch(course_key) payload = { 'course_id': unicode(course_key), 'title': course.display_name, 'author_name': None, 'overview': course_details.overview, 'description': course_details.short_description, 'image_url': course_details.course_image_asset_path, 'instructors': course_instructors_ids } headers = {'content-type': 'application/json'} # make the POST request add_course_url = urlparse.urljoin(course.ccx_connector, CCXCON_COURSEXS_URL) resp = oauth_ccxcon.post( url=add_course_url, json=payload, headers=headers, timeout=CCXCON_REQUEST_TIMEOUT ) if resp.status_code >= 500: raise CCXConnServerError('Server returned error Status: %s, Content: %s', resp.status_code, resp.content) if resp.status_code >= 400: log.error("Error creating course on ccxcon. Status: %s, Content: %s", resp.status_code, resp.content) # this API performs a POST request both for POST and PATCH, but the POST returns 201 and the PATCH returns 200 elif resp.status_code != HTTP_200_OK and resp.status_code != HTTP_201_CREATED: log.error('Server returned unexpected status code %s', resp.status_code) else: log.debug('Request successful. Status: %s, Content: %s', resp.status_code, resp.content)