Files
edx-platform/openedx/core/djangoapps/ccxcon/api.py
2019-07-10 19:57:32 +05:00

161 lines
5.2 KiB
Python

"""
Module containing API functions for the CCXCon
"""
from __future__ import absolute_import
import logging
import six
import six.moves.urllib.parse # pylint: disable=import-error
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(u'Master Course with key "%s" not found', six.text_type(course_key))
return
if not course.enable_ccx:
log.debug(u'ccx not enabled for course key "%s"', six.text_type(course_key))
return
if not course.ccx_connector:
log.debug(u'ccx connector not defined for course key "%s"', six.text_type(course_key))
return
if not is_valid_url(course.ccx_connector):
log.error(
u'ccx connector URL "%s" for course key "%s" is not a valid URL.',
course.ccx_connector, six.text_type(course_key)
)
return
# get the oauth credential for this URL
try:
ccxcon = CCXCon.objects.get(url=course.ccx_connector)
except CCXCon.DoesNotExist:
log.error(u'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=six.moves.urllib.parse.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': six.text_type(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 = six.moves.urllib.parse.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(u'Server returned error Status: %s, Content: %s', resp.status_code, resp.content)
if resp.status_code >= 400:
log.error(u"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(u'Server returned unexpected status code %s', resp.status_code)
else:
log.debug(u'Request successful. Status: %s, Content: %s', resp.status_code, resp.content)