Skip to content

Commit

Permalink
feat: job to assign skills to Degreed courses (#1958)
Browse files Browse the repository at this point in the history
  • Loading branch information
sameenfatima78 authored Dec 5, 2023
1 parent bb02232 commit bae4c25
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Change Log
Unreleased
----------

[4.8.5]
--------
feat: Added a management command to assign skills to Degreed courses

[4.8.4]
--------
fix: changed relative resumeCourseRunUrl to an absolute URL
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.8.4"
__version__ = "4.8.5"
1 change: 1 addition & 0 deletions integrated_channels/degreed2/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ class Degreed2Config(AppConfig):
oauth_api_path = "/oauth/token"
courses_api_path = "/api/v2/content/courses"
completions_api_path = "/api/v2/completions"
skill_api_path = "api/v2/content/{contentId}/relationships/skills"
47 changes: 47 additions & 0 deletions integrated_channels/degreed2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def __init__(self, enterprise_configuration):
self.oauth_api_path = app_config.oauth_api_path
self.courses_api_path = app_config.courses_api_path
self.completions_api_path = app_config.completions_api_path
self.skill_api_path = app_config.skill_api_path
# to log without having to pass channel_name, ent_customer_uuid each time
self.make_log_msg = lambda course_key, message, lms_user_id=None: generate_formatted_log(
self.enterprise_configuration.channel_code(),
Expand All @@ -72,6 +73,12 @@ def get_courses_url(self):
def get_completions_url(self):
return urljoin(self.enterprise_configuration.degreed_base_url, self.completions_api_path)

def get_course_skills_url(self, course_key):
return urljoin(
self.enterprise_configuration.degreed_base_url,
self.skill_api_path.format(contentId=course_key)
)

def create_assessment_reporting(self, user_id, payload):
"""
Not implemented yet.
Expand Down Expand Up @@ -197,6 +204,46 @@ def fetch_degreed_course_id(self, external_id):
f'Degreed2: Attempted to find degreed course id but failed, external id was {external_id}'
f', Response from Degreed was {response_body}')

def assign_course_skills(self, course_id, serialized_data):
"""
Assign skills to a course.
Args:
serialized_data: JSON-encoded object containing skills metadata.
Raises:
ClientError:
If degreed course id doesn't exist.
If Degreed course skills API request fails.
"""

degreed_course_id = self.fetch_degreed_course_id(course_id)
if not degreed_course_id:
raise ClientError(f'Degreed2: Cannot find course via external-id {course_id}')

course_skills_url = self.get_course_skills_url(degreed_course_id)
LOGGER.info(self.make_log_msg(course_id, f'Attempting to assign course skills {course_skills_url}'))
try:
status_code, response_body = self._patch(course_skills_url, serialized_data, self.CONTENT_WRITE_SCOPE)
if status_code == 201:
LOGGER.info(
self.make_log_msg(
course_id,
f'Succesfully assigned skills to course {course_id}')
)
elif status_code >= 400:
raise ClientError(
f'Degreed2APIClient failed to assign skills to course {course_id}.'
f'Failed with status_code={status_code} and response={response_body}',
)
except requests.exceptions.RequestException as exc:
raise ClientError(
'Degreed2APIClient request to assign skills failed: {error} {message}'.format(
error=exc.__class__.__name__,
message=str(exc)
)
) from exc

def create_content_metadata(self, serialized_data):
"""
Create content metadata using the Degreed course content API.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Assign skills to degreed courses
"""
from logging import getLogger

from requests.exceptions import ConnectionError, RequestException, Timeout # pylint: disable=redefined-builtin

from django.contrib import auth
from django.core.management.base import BaseCommand, CommandError

from enterprise.api_client.enterprise_catalog import EnterpriseCatalogApiClient
from integrated_channels.degreed2.client import Degreed2APIClient
from integrated_channels.exceptions import ClientError
from integrated_channels.integrated_channel.management.commands import IntegratedChannelCommandMixin
from integrated_channels.utils import generate_formatted_log

User = auth.get_user_model()
LOGGER = getLogger(__name__)


class Command(IntegratedChannelCommandMixin, BaseCommand):
"""
Add skill metadata to existing Degreed courses.
./manage.py lms assign_skills_to_degreed_courses
"""

def add_arguments(self, parser):
"""
Add required arguments to the parser.
"""
parser.add_argument(
'--catalog_user',
dest='catalog_user',
required=True,
metavar='ENTERPRISE_CATALOG_API_USERNAME',
help='Use this user to access the Enterprise Catalog API.'
)
super().add_arguments(parser)

def _prepare_json_payload_for_skills_endpoint(self, course_skills):
"""
Prepares a json payload for skills in the Degreed expected format.
"""
course_skills_json = []
for skill in course_skills:
skill_data = {"type": "skills", "id": skill}
course_skills_json.append(skill_data)
return {
"data": course_skills_json
}

def handle(self, *args, **options):
"""
Update all existing Degreed courses to assign skills metadata.
"""
options['channel'] = 'DEGREED2'
username = options['catalog_user']

try:
user = User.objects.get(username=username)
except User.DoesNotExist as no_user_error:
raise CommandError('A user with the username {} was not found.'.format(username)) from no_user_error

enterprise_catalog_client = EnterpriseCatalogApiClient(user)

for degreed_channel_config in self.get_integrated_channels(options):
enterprise_customer = degreed_channel_config.enterprise_customer
enterprise_customer_catalogs = degreed_channel_config.customer_catalogs_to_transmit or \
enterprise_customer.enterprise_customer_catalogs.all()
try:
content_metadata_in_catalogs = enterprise_catalog_client.get_content_metadata(
enterprise_customer,
enterprise_customer_catalogs
)
except (RequestException, ConnectionError, Timeout) as exc:
LOGGER.exception(
'Failed to retrieve enterprise catalogs content metadata due to: [%s]', str(exc)
)
continue

degreed_client = Degreed2APIClient(degreed_channel_config)

for content_item in content_metadata_in_catalogs:

course_id = content_item.get('key', [])
course_skills = content_item.get('skill_names', [])
json_payload = self._prepare_json_payload_for_skills_endpoint(course_skills)

# assign skills metadata to degreed course by first fetching degreed course id
try:
degreed_client.assign_course_skills(course_id, json_payload)
except ClientError as error:
LOGGER.error(
generate_formatted_log(
degreed_channel_config.channel_code(),
degreed_channel_config.enterprise_customer.uuid,
None,
None,
f'Degreed2APIClient assign_course_skills failed for course {course_id} '
f'with message: {error.message}'
)
)
except RequestException as error:
LOGGER.error(
generate_formatted_log(
degreed_channel_config.channel_code(),
degreed_channel_config.enterprise_customer.uuid,
None,
None,
f'Degreed2APIClient request to assign skills failed with message: {error.message}'
)
)

0 comments on commit bae4c25

Please sign in to comment.