Skip to content

Commit

Permalink
feat: add management command for course prompts
Browse files Browse the repository at this point in the history
  • Loading branch information
alangsto committed Oct 18, 2023
1 parent 278bf81 commit 9253edc
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 49 deletions.
Empty file.
Empty file.
121 changes: 121 additions & 0 deletions learning_assistant/management/commands/set_course_prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
Django management command to generate course prompts.
"""
import json
import logging
from posixpath import join as urljoin

import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from edx_rest_api_client.auth import SuppliedJwtAuth
from edx_rest_api_client.client import USER_AGENT
from opaque_keys.edx.keys import CourseKey

try:
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
except ImportError:
create_jwt_for_user = None

from learning_assistant.models import CoursePrompt

logger = logging.getLogger(__name__)


class Command(BaseCommand):
"""
Django Management command to create a set of course prompts
"""

def add_arguments(self, parser):

# list of course ids
parser.add_argument(
'--course_ids',
dest='course_ids',
help='comma separated list of course_ids to generate',
)

# pre-message
parser.add_argument(
'--pre_message',
dest='pre_message',
help='message to prepend to course topics',
)

parser.add_argument(
'--skills_descriptor',
dest='skills_descriptor',
help='message that describes skill structure'
)

# post-message
parser.add_argument(
'--post_message',
dest='post_message',
help='message to append to course topics',
)

# service username
parser.add_argument(
'-u',
'--username',
dest='username',
help='username for authenticating calls to discovery API'
)

@staticmethod
def _get_discovery_api_client(username):
"""
Returns an API client which can be used to make Catalog API requests.
"""
User = get_user_model()
service_user = User.objects.get(username=username)
jwt = create_jwt_for_user(service_user)
client = requests.Session()
client.headers.update({'User-Agent': USER_AGENT})
client.auth = SuppliedJwtAuth(jwt)

return client

def handle(self, *args, **options):
"""
Management command entry point
"""
course_ids = options['course_ids']
pre_message = options['pre_message']
skills_descriptor = options['skills_descriptor']
post_message = options['post_message']
service_username = options['username']

client = self._get_discovery_api_client(service_username)

course_ids_list = course_ids.split(',')
for course_run_id in course_ids_list:
course_key = CourseKey.from_string(course_run_id)

# discovery API requires course keys, not course run keys
course_id = f'{course_key.org}+{course_key.course}'

url = urljoin(
settings.DISCOVERY_BASE_URL,
'api/v1/courses/{course_id}'.format(course_id=course_id)
)
response_data = client.get(url).data
title = response_data['title']
skill_names = response_data['skill_names']

# create restructured dictionary with data
course_dict = {'title': title, 'topics': skill_names}

# append descriptor message and decode json dict into a string
skills_message = skills_descriptor + json.dumps(course_dict)

# finally, create list of prompt messages and save
prompt_messages = [pre_message, skills_message, post_message]
CoursePrompt.objects.update_or_create(
course_id=course_run_id, json_prompt_content=prompt_messages
)

logger.info('Updated course prompt for course_run_id=%s', course_run_id)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
Tests for the set_course_prompts management command.
"""
import json
from posixpath import join as urljoin
from unittest.mock import MagicMock, patch

from django.conf import settings
from django.core.management import call_command
from django.test import TestCase
from rest_framework.response import Response

from learning_assistant.models import CoursePrompt


class SetCoursePromptsTests(TestCase):
"""Test set_course_prompts command"""
command = 'set_course_prompts'

def setUp(self):
self.pre_message = 'This is the first message'
self.skills_descriptor = 'These are the skills: '
self.post_message = 'This message comes after'
self.service_username = 'lms_worker'
self.course_ids = 'course-v1:edx+test+23,course-v1:edx+test+24'
self.course_title = 'Intro to Testing'
self.skill_names = ['Testing', 'Computers', 'Coding']

def get_mock_discovery_response(self):
"""
Create scaled down mock of discovery response
"""
response_data = {
'title': self.course_title,
'skill_names': self.skill_names
}
return Response(data=response_data)

@patch('learning_assistant.management.commands.set_course_prompts.Command._get_discovery_api_client')
def test_course_prompts_created(self, mock_get_discovery_client):
"""
Assert that course prompts are created by calling management command.
"""
mock_client = MagicMock()
mock_get_discovery_client.return_value = mock_client
mock_client.get.return_value = self.get_mock_discovery_response()

call_command(
self.command,
course_ids=self.course_ids,
pre_message=self.pre_message,
skills_descriptor=self.skills_descriptor,
post_message=self.post_message,
username=self.service_username,
)

# assert that discovery api was called with course id, not course run id
expected_url = urljoin(
settings.DISCOVERY_BASE_URL,
'api/v1/courses/{course_id}'.format(course_id='edx+test')
)
mock_client.get.assert_any_call(expected_url)
mock_client.get.assert_called()

# assert that number of prompts created is equivalent to number of courses passed in to command
prompts = CoursePrompt.objects.filter()
self.assertEqual(len(prompts), len(self.course_ids.split(',')))

# assert structure of prompt
course_prompt = prompts[0].json_prompt_content
self.assertEqual(len(course_prompt), 3)

skills_message = self.skills_descriptor + json.dumps({'title': self.course_title, 'topics': self.skill_names})
expected_response = [self.pre_message, skills_message, self.post_message]
self.assertEqual(course_prompt, expected_response)
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ Django # Web application framework
django-model-utils
djangorestframework
edx-drf-extensions
edx-rest-api-client
edx-opaque-keys
23 changes: 17 additions & 6 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ click==8.1.7
# via edx-django-utils
cryptography==41.0.4
# via pyjwt
django==3.2.21
django==3.2.22
# via
# -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
# -r requirements/base.in
Expand All @@ -45,27 +45,33 @@ djangorestframework==3.14.0
drf-jwt==1.19.2
# via edx-drf-extensions
edx-django-utils==5.7.0
# via edx-drf-extensions
edx-drf-extensions==8.10.0
# via
# edx-drf-extensions
# edx-rest-api-client
edx-drf-extensions==8.12.0
# via -r requirements/base.in
edx-opaque-keys==2.5.1
# via
# -r requirements/base.in
# edx-drf-extensions
edx-rest-api-client==5.6.1
# via -r requirements/base.in
idna==3.4
# via requests
newrelic==9.1.0
# via edx-django-utils
pbr==5.11.1
# via stevedore
psutil==5.9.5
psutil==5.9.6
# via edx-django-utils
pycparser==2.21
# via cffi
pyjwt[crypto]==2.8.0
# via
# drf-jwt
# edx-drf-extensions
# edx-rest-api-client
# pyjwt
pymongo==3.13.0
# via edx-opaque-keys
pynacl==1.5.0
Expand All @@ -75,9 +81,14 @@ pytz==2023.3.post1
# django
# djangorestframework
requests==2.31.0
# via edx-drf-extensions
# via
# edx-drf-extensions
# edx-rest-api-client
# slumber
semantic-version==2.10.0
# via edx-drf-extensions
slumber==0.7.1
# via edx-rest-api-client
sqlparse==0.4.4
# via django
stevedore==5.1.0
Expand All @@ -88,5 +99,5 @@ typing-extensions==4.8.0
# via
# asgiref
# edx-opaque-keys
urllib3==2.0.6
urllib3==2.0.7
# via requests
2 changes: 1 addition & 1 deletion requirements/ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ tox==3.28.0
# tox-battery
tox-battery==0.6.2
# via -r requirements/ci.in
urllib3==2.0.6
urllib3==2.0.7
# via requests
virtualenv==20.24.5
# via tox
28 changes: 21 additions & 7 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,15 @@ coverage[toml]==7.3.2
# -r requirements/ci.txt
# -r requirements/quality.txt
# codecov
# coverage
# pytest-cov
cryptography==41.0.4
# via
# -r requirements/quality.txt
# pyjwt
ddt==1.6.0
# via -r requirements/quality.txt
diff-cover==7.7.0
diff-cover==8.0.0
# via -r requirements/dev.in
dill==0.3.7
# via
Expand All @@ -75,7 +76,7 @@ distlib==0.3.7
# via
# -r requirements/ci.txt
# virtualenv
django==3.2.21
django==3.2.22
# via
# -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
# -r requirements/quality.txt
Expand Down Expand Up @@ -111,16 +112,19 @@ edx-django-utils==5.7.0
# via
# -r requirements/quality.txt
# edx-drf-extensions
edx-drf-extensions==8.10.0
# edx-rest-api-client
edx-drf-extensions==8.12.0
# via -r requirements/quality.txt
edx-i18n-tools==1.2.0
edx-i18n-tools==1.3.0
# via -r requirements/dev.in
edx-lint==5.3.4
# via -r requirements/quality.txt
edx-opaque-keys==2.5.1
# via
# -r requirements/quality.txt
# edx-drf-extensions
edx-rest-api-client==5.6.1
# via -r requirements/quality.txt
exceptiongroup==1.1.3
# via
# -r requirements/quality.txt
Expand Down Expand Up @@ -156,6 +160,8 @@ lazy-object-proxy==1.9.0
# via
# -r requirements/quality.txt
# astroid
lxml==4.9.3
# via edx-i18n-tools
markupsafe==2.1.3
# via
# -r requirements/quality.txt
Expand Down Expand Up @@ -199,15 +205,15 @@ pluggy==1.3.0
# tox
polib==1.2.0
# via edx-i18n-tools
psutil==5.9.5
psutil==5.9.6
# via
# -r requirements/quality.txt
# edx-django-utils
py==1.11.0
# via
# -r requirements/ci.txt
# tox
pycodestyle==2.11.0
pycodestyle==2.11.1
# via -r requirements/quality.txt
pycparser==2.21
# via
Expand All @@ -222,6 +228,8 @@ pyjwt[crypto]==2.8.0
# -r requirements/quality.txt
# drf-jwt
# edx-drf-extensions
# edx-rest-api-client
# pyjwt
pylint==2.17.7
# via
# -r requirements/quality.txt
Expand Down Expand Up @@ -284,7 +292,9 @@ requests==2.31.0
# -r requirements/quality.txt
# codecov
# edx-drf-extensions
# edx-rest-api-client
# responses
# slumber
responses==0.23.3
# via -r requirements/quality.txt
semantic-version==2.10.0
Expand All @@ -297,6 +307,10 @@ six==1.16.0
# -r requirements/quality.txt
# edx-lint
# tox
slumber==0.7.1
# via
# -r requirements/quality.txt
# edx-rest-api-client
snowballstemmer==2.2.0
# via
# -r requirements/quality.txt
Expand Down Expand Up @@ -349,7 +363,7 @@ typing-extensions==4.8.0
# astroid
# edx-opaque-keys
# pylint
urllib3==2.0.6
urllib3==2.0.7
# via
# -r requirements/ci.txt
# -r requirements/quality.txt
Expand Down
Loading

0 comments on commit 9253edc

Please sign in to comment.