Merge pull request #10304 from edx/zub/story/ecom-2578-basic-programs-api-client-setup
basic programs api setup and dashboard integration
This commit is contained in:
@@ -28,7 +28,6 @@ from django.shortcuts import redirect
|
||||
from django.utils.translation import ungettext
|
||||
from django.utils.http import base36_to_int
|
||||
from django.utils.translation import ugettext as _, get_language
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
from django.db.models.signals import post_save
|
||||
@@ -123,6 +122,8 @@ from notification_prefs.views import enable_notifications
|
||||
|
||||
# Note that this lives in openedx, so this dependency should be refactored.
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangoapps.programs.views import get_course_programs_for_dashboard
|
||||
from openedx.core.djangoapps.programs.utils import is_student_dashboard_programs_enabled
|
||||
|
||||
|
||||
log = logging.getLogger("edx.student")
|
||||
@@ -573,6 +574,13 @@ def dashboard(request):
|
||||
and has_access(request.user, 'view_courseware_with_prerequisites', enrollment.course_overview)
|
||||
)
|
||||
|
||||
# get the programs associated with courses being displayed.
|
||||
# pass this along in template context in order to render additional
|
||||
# program-related information on the dashboard view.
|
||||
course_programs = {}
|
||||
if is_student_dashboard_programs_enabled():
|
||||
course_programs = get_course_programs_for_dashboard(user, show_courseware_links_for)
|
||||
|
||||
# Construct a dictionary of course mode information
|
||||
# used to render the course list. We re-use the course modes dict
|
||||
# we loaded earlier to avoid hitting the database.
|
||||
@@ -693,6 +701,7 @@ def dashboard(request):
|
||||
'order_history_list': order_history_list,
|
||||
'courses_requirements_not_met': courses_requirements_not_met,
|
||||
'nav_hidden': True,
|
||||
'course_programs': course_programs,
|
||||
}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""
|
||||
Decorators related to edXNotes.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from edxnotes.helpers import (
|
||||
get_edxnotes_id_token,
|
||||
get_public_endpoint,
|
||||
get_id_token,
|
||||
get_token_url,
|
||||
generate_uid,
|
||||
is_feature_enabled,
|
||||
@@ -43,7 +46,7 @@ def edxnotes(cls):
|
||||
# Use camelCase to name keys.
|
||||
"usageId": unicode(self.scope_ids.usage_id).encode("utf-8"),
|
||||
"courseId": unicode(self.runtime.course_id).encode("utf-8"),
|
||||
"token": get_id_token(self.runtime.get_real_user(self.runtime.anonymous_student_id)),
|
||||
"token": get_edxnotes_id_token(self.runtime.get_real_user(self.runtime.anonymous_student_id)),
|
||||
"tokenUrl": get_token_url(self.runtime.course_id),
|
||||
"endpoint": get_public_endpoint(),
|
||||
"debug": settings.DEBUG,
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
"""
|
||||
Helper methods related to EdxNotes.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
from uuid import uuid4
|
||||
from json import JSONEncoder
|
||||
from uuid import uuid4
|
||||
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from courseware.access import has_access
|
||||
from courseware.views import get_current_child
|
||||
from dateutil.parser import parse as dateutil_parse
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from edxnotes.exceptions import EdxNotesParseError, EdxNotesServiceUnavailable
|
||||
from capa.util import sanitize_html
|
||||
from courseware.views import get_current_child
|
||||
from courseware.access import has_access
|
||||
from openedx.core.djangoapps.util.helpers import get_id_token
|
||||
from student.models import anonymous_id_for_user
|
||||
from util.date_utils import get_default_time_display
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from util.date_utils import get_default_time_display
|
||||
from dateutil.parser import parse as dateutil_parse
|
||||
from provider.oauth2.models import AccessToken, Client
|
||||
import oauth2_provider.oidc as oidc
|
||||
from provider.utils import now
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from .exceptions import EdxNotesParseError, EdxNotesServiceUnavailable
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
HIGHLIGHT_TAG = "span"
|
||||
HIGHLIGHT_CLASS = "note-highlight"
|
||||
# OAuth2 Client name for edxnotes
|
||||
CLIENT_NAME = "edx-notes"
|
||||
|
||||
|
||||
class NoteJSONEncoder(JSONEncoder):
|
||||
@@ -43,27 +47,11 @@ class NoteJSONEncoder(JSONEncoder):
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def get_id_token(user):
|
||||
def get_edxnotes_id_token(user):
|
||||
"""
|
||||
Generates JWT ID-Token, using or creating user's OAuth access token.
|
||||
Returns generated ID Token for edxnotes.
|
||||
"""
|
||||
try:
|
||||
client = Client.objects.get(name="edx-notes")
|
||||
except Client.DoesNotExist:
|
||||
raise ImproperlyConfigured("OAuth2 Client with name 'edx-notes' is not present in the DB")
|
||||
try:
|
||||
access_token = AccessToken.objects.get(
|
||||
client=client,
|
||||
user=user,
|
||||
expires__gt=now()
|
||||
)
|
||||
except AccessToken.DoesNotExist:
|
||||
access_token = AccessToken(client=client, user=user)
|
||||
access_token.save()
|
||||
|
||||
id_token = oidc.id_token(access_token)
|
||||
secret = id_token.access_token.client.client_secret
|
||||
return id_token.encode(secret)
|
||||
return get_id_token(user, CLIENT_NAME)
|
||||
|
||||
|
||||
def get_token_url(course_id):
|
||||
@@ -97,7 +85,7 @@ def send_request(user, course_id, path="", query_string=None):
|
||||
response = requests.get(
|
||||
url,
|
||||
headers={
|
||||
"x-annotator-auth-token": get_id_token(user)
|
||||
"x-annotator-auth-token": get_edxnotes_id_token(user)
|
||||
},
|
||||
params=params
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ class EdxNotesDecoratorTest(ModuleStoreTestCase):
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_EDXNOTES': True})
|
||||
@patch("edxnotes.decorators.get_public_endpoint")
|
||||
@patch("edxnotes.decorators.get_token_url")
|
||||
@patch("edxnotes.decorators.get_id_token")
|
||||
@patch("edxnotes.decorators.get_edxnotes_id_token")
|
||||
@patch("edxnotes.decorators.generate_uid")
|
||||
def test_edxnotes_enabled(self, mock_generate_uid, mock_get_id_token, mock_get_token_url, mock_get_endpoint):
|
||||
"""
|
||||
@@ -691,7 +691,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
|
||||
@override_settings(EDXNOTES_PUBLIC_API="http://example.com")
|
||||
@override_settings(EDXNOTES_INTERNAL_API="http://example.com")
|
||||
@patch("edxnotes.helpers.anonymous_id_for_user")
|
||||
@patch("edxnotes.helpers.get_id_token")
|
||||
@patch("edxnotes.helpers.get_edxnotes_id_token")
|
||||
@patch("edxnotes.helpers.requests.get")
|
||||
def test_send_request_with_query_string(self, mock_get, mock_get_id_token, mock_anonymous_id_for_user):
|
||||
"""
|
||||
@@ -720,7 +720,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
|
||||
@override_settings(EDXNOTES_PUBLIC_API="http://example.com")
|
||||
@override_settings(EDXNOTES_INTERNAL_API="http://example.com")
|
||||
@patch("edxnotes.helpers.anonymous_id_for_user")
|
||||
@patch("edxnotes.helpers.get_id_token")
|
||||
@patch("edxnotes.helpers.get_edxnotes_id_token")
|
||||
@patch("edxnotes.helpers.requests.get")
|
||||
def test_send_request_without_query_string(self, mock_get, mock_get_id_token, mock_anonymous_id_for_user):
|
||||
"""
|
||||
|
||||
@@ -15,13 +15,14 @@ from courseware.module_render import get_module_for_descriptor
|
||||
from util.json_request import JsonResponse, JsonResponseBadRequest
|
||||
from edxnotes.exceptions import EdxNotesParseError, EdxNotesServiceUnavailable
|
||||
from edxnotes.helpers import (
|
||||
get_edxnotes_id_token,
|
||||
get_notes,
|
||||
get_id_token,
|
||||
is_feature_enabled,
|
||||
search,
|
||||
get_course_position,
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -94,7 +95,7 @@ def get_token(request, course_id):
|
||||
"""
|
||||
Get JWT ID-Token, in case you need new one.
|
||||
"""
|
||||
return HttpResponse(get_id_token(request.user), content_type='text/plain')
|
||||
return HttpResponse(get_edxnotes_id_token(request.user), content_type='text/plain')
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Platform support for Programs.
|
||||
|
||||
This package is a thin wrapper around interactions with the Programs service,
|
||||
supporting learner- and author-facing features involving that service
|
||||
if and only if the service is deployed in the Open edX installation.
|
||||
|
||||
To ensure maximum separation of concerns, and a minimum of interdependencies,
|
||||
this package should be kept small, thin, and stateless.
|
||||
"""
|
||||
|
||||
27
openedx/core/djangoapps/programs/tests/mixins.py
Normal file
27
openedx/core/djangoapps/programs/tests/mixins.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Broadly-useful mixins for use in automated tests.
|
||||
"""
|
||||
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
|
||||
|
||||
class ProgramsApiConfigMixin(object):
|
||||
"""
|
||||
Programs api configuration utility methods for testing.
|
||||
"""
|
||||
|
||||
INTERNAL_URL = "http://internal/"
|
||||
PUBLIC_URL = "http://public/"
|
||||
|
||||
DEFAULTS = dict(
|
||||
internal_service_url=INTERNAL_URL,
|
||||
public_service_url=PUBLIC_URL,
|
||||
api_version_number=1,
|
||||
)
|
||||
|
||||
def create_config(self, **kwargs):
|
||||
"""
|
||||
DRY helper. Create a new ProgramsApiConfig with self.DEFAULTS, updated
|
||||
with any kwarg overrides.
|
||||
"""
|
||||
ProgramsApiConfig(**dict(self.DEFAULTS, **kwargs)).save()
|
||||
@@ -2,34 +2,20 @@
|
||||
Tests for models supporting Program-related functionality.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
|
||||
|
||||
@patch('config_models.models.cache.get', return_value=None) # during tests, make every cache get a miss.
|
||||
class ProgramsApiConfigTest(TestCase):
|
||||
class ProgramsApiConfigTest(ProgramsApiConfigMixin, TestCase):
|
||||
"""
|
||||
Tests for the ProgramsApiConfig model.
|
||||
"""
|
||||
|
||||
INTERNAL_URL = "http://internal/"
|
||||
PUBLIC_URL = "http://public/"
|
||||
|
||||
DEFAULTS = dict(
|
||||
internal_service_url=INTERNAL_URL,
|
||||
public_service_url=PUBLIC_URL,
|
||||
api_version_number=1,
|
||||
)
|
||||
|
||||
def create_config(self, **kwargs):
|
||||
"""
|
||||
DRY helper. Create a new ProgramsApiConfig with self.DEFAULTS, updated
|
||||
with any kwarg overrides.
|
||||
"""
|
||||
ProgramsApiConfig(**dict(self.DEFAULTS, **kwargs)).save()
|
||||
|
||||
def test_default_state(self, _mock_cache):
|
||||
"""
|
||||
Ensure the config stores empty values when no data has been inserted,
|
||||
|
||||
208
openedx/core/djangoapps/programs/tests/test_views.py
Normal file
208
openedx/core/djangoapps/programs/tests/test_views.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
Tests for the Programs.
|
||||
"""
|
||||
|
||||
from mock import patch
|
||||
from provider.oauth2.models import Client
|
||||
from provider.constants import CONFIDENTIAL
|
||||
from unittest import skipUnless
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
|
||||
from openedx.core.djangoapps.programs.views import get_course_programs_for_dashboard
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
|
||||
"""
|
||||
Tests for the Programs views.
|
||||
"""
|
||||
|
||||
def setUp(self, **kwargs): # pylint: disable=unused-argument
|
||||
super(TestGetXSeriesPrograms, self).setUp()
|
||||
self.create_config(enabled=True, enable_student_dashboard=True)
|
||||
Client.objects.get_or_create(name="programs", client_type=CONFIDENTIAL)
|
||||
self.user = UserFactory()
|
||||
self.programs_api_response = {
|
||||
"results": [
|
||||
{
|
||||
'category': 'xseries',
|
||||
'status': 'active',
|
||||
'subtitle': 'Dummy program 1 for testing',
|
||||
'name': 'First Program',
|
||||
'course_codes': [
|
||||
{
|
||||
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
|
||||
'display_name': 'Demo XSeries Program 1',
|
||||
'key': 'TEST_A',
|
||||
'marketing_slug': 'fake-marketing-slug-xseries-1',
|
||||
'run_modes': [
|
||||
{'sku': '', 'mode_slug': 'ABC_1', 'course_key': 'edX/DemoX_1/Run_1'},
|
||||
{'sku': '', 'mode_slug': 'ABC_2', 'course_key': 'edX/DemoX_2/Run_2'},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'xseries',
|
||||
'status': 'active',
|
||||
'subtitle': 'Dummy program 2 for testing',
|
||||
'name': 'Second Program',
|
||||
'course_codes': [
|
||||
{
|
||||
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
|
||||
'display_name': 'Demo XSeries Program 2',
|
||||
'key': 'TEST_B',
|
||||
'marketing_slug': 'fake-marketing-slug-xseries-2',
|
||||
'run_modes': [
|
||||
{'sku': '', 'mode_slug': 'XYZ_1', 'course_key': 'edX/Program/Program_Run'},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def test_get_course_programs_with_valid_user_and_courses(self):
|
||||
""" Test that the method 'get_course_programs_for_dashboard' returns
|
||||
only matching courses from the xseries programs in the expected format.
|
||||
"""
|
||||
# mock the request call
|
||||
with patch('slumber.Resource.get') as mock_get:
|
||||
mock_get.return_value = self.programs_api_response
|
||||
|
||||
# first test with user having multiple courses in a single xseries
|
||||
programs = get_course_programs_for_dashboard(
|
||||
self.user,
|
||||
['edX/DemoX_1/Run_1', 'edX/DemoX_2/Run_2', 'valid/edX/Course']
|
||||
)
|
||||
expected_output = {
|
||||
'edX/DemoX_1/Run_1': {
|
||||
'category': 'xseries',
|
||||
'status': 'active',
|
||||
'course_codes': [
|
||||
{
|
||||
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
|
||||
'marketing_slug': 'fake-marketing-slug-xseries-1',
|
||||
'display_name': 'Demo XSeries Program 1',
|
||||
'key': 'TEST_A',
|
||||
'run_modes': [
|
||||
{'sku': '', 'mode_slug': 'ABC_1', 'course_key': 'edX/DemoX_1/Run_1'},
|
||||
{'sku': '', 'mode_slug': 'ABC_2', 'course_key': 'edX/DemoX_2/Run_2'},
|
||||
]
|
||||
}
|
||||
],
|
||||
'subtitle': 'Dummy program 1 for testing',
|
||||
'name': 'First Program'
|
||||
},
|
||||
'edX/DemoX_2/Run_2': {
|
||||
'category': 'xseries',
|
||||
'status': 'active',
|
||||
'course_codes': [
|
||||
{
|
||||
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
|
||||
'marketing_slug': 'fake-marketing-slug-xseries-1',
|
||||
'display_name': 'Demo XSeries Program 1',
|
||||
'key': 'TEST_A',
|
||||
'run_modes': [
|
||||
{'sku': '', 'mode_slug': 'ABC_1', 'course_key': 'edX/DemoX_1/Run_1'},
|
||||
{'sku': '', 'mode_slug': 'ABC_2', 'course_key': 'edX/DemoX_2/Run_2'},
|
||||
]
|
||||
}
|
||||
],
|
||||
'subtitle': 'Dummy program 1 for testing',
|
||||
'name': 'First Program'
|
||||
},
|
||||
}
|
||||
self.assertTrue(mock_get.called)
|
||||
self.assertEqual(expected_output, programs)
|
||||
self.assertEqual(sorted(programs.keys()), ['edX/DemoX_1/Run_1', 'edX/DemoX_2/Run_2'])
|
||||
|
||||
# now test with user having multiple courses across two different
|
||||
# xseries
|
||||
mock_get.reset_mock()
|
||||
programs = get_course_programs_for_dashboard(
|
||||
self.user,
|
||||
['edX/DemoX_1/Run_1', 'edX/DemoX_2/Run_2', 'edX/Program/Program_Run', 'valid/edX/Course']
|
||||
)
|
||||
expected_output['edX/Program/Program_Run'] = {
|
||||
'category': 'xseries',
|
||||
'status': 'active',
|
||||
'course_codes': [
|
||||
{
|
||||
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
|
||||
'marketing_slug': 'fake-marketing-slug-xseries-2',
|
||||
'display_name': 'Demo XSeries Program 2',
|
||||
'key': 'TEST_B',
|
||||
'run_modes': [
|
||||
{'sku': '', 'mode_slug': 'XYZ_1', 'course_key': 'edX/Program/Program_Run'},
|
||||
]
|
||||
}
|
||||
],
|
||||
'subtitle': 'Dummy program 2 for testing',
|
||||
'name': 'Second Program'
|
||||
}
|
||||
self.assertTrue(mock_get.called)
|
||||
self.assertEqual(expected_output, programs)
|
||||
self.assertEqual(
|
||||
sorted(programs.keys()),
|
||||
['edX/DemoX_1/Run_1', 'edX/DemoX_2/Run_2', 'edX/Program/Program_Run']
|
||||
)
|
||||
|
||||
def test_get_course_programs_with_api_client_exception(self):
|
||||
""" Test that the method 'get_course_programs_for_dashboard' returns
|
||||
empty dictionary in case of an exception coming from patching slumber
|
||||
based client 'programs_api_client'.
|
||||
"""
|
||||
# mock the request call
|
||||
with patch('edx_rest_api_client.client.EdxRestApiClient.__init__') as mock_init:
|
||||
# test output in case of any exception
|
||||
mock_init.side_effect = Exception('exc')
|
||||
programs = get_course_programs_for_dashboard(
|
||||
self.user,
|
||||
['edX/DemoX_1/Run_1', 'valid/edX/Course']
|
||||
)
|
||||
self.assertTrue(mock_init.called)
|
||||
self.assertEqual(programs, {})
|
||||
|
||||
def test_get_course_programs_with_exception(self):
|
||||
""" Test that the method 'get_course_programs_for_dashboard' returns
|
||||
empty dictionary in case of exception while accessing programs service.
|
||||
"""
|
||||
# mock the request call
|
||||
with patch('slumber.Resource.get') as mock_get:
|
||||
# test output in case of any exception
|
||||
mock_get.side_effect = Exception('exc')
|
||||
programs = get_course_programs_for_dashboard(
|
||||
self.user,
|
||||
['edX/DemoX_1/Run_1', 'valid/edX/Course']
|
||||
)
|
||||
self.assertTrue(mock_get.called)
|
||||
self.assertEqual(programs, {})
|
||||
|
||||
def test_get_course_programs_with_non_existing_courses(self):
|
||||
""" Test that the method 'get_course_programs_for_dashboard' returns
|
||||
only those program courses which exists in the programs api response.
|
||||
"""
|
||||
# mock the request call
|
||||
with patch('slumber.Resource.get') as mock_get:
|
||||
mock_get.return_value = self.programs_api_response
|
||||
self.assertEqual(
|
||||
get_course_programs_for_dashboard(self.user, ['invalid/edX/Course']), {}
|
||||
)
|
||||
self.assertTrue(mock_get.called)
|
||||
|
||||
def test_get_course_programs_with_empty_response(self):
|
||||
""" Test that the method 'get_course_programs_for_dashboard' returns
|
||||
empty dict if programs rest api client returns empty response.
|
||||
"""
|
||||
# mock the request call
|
||||
with patch('slumber.Resource.get') as mock_get:
|
||||
mock_get.return_value = {}
|
||||
self.assertEqual(
|
||||
get_course_programs_for_dashboard(self.user, ['edX/DemoX/Run']), {}
|
||||
)
|
||||
self.assertTrue(mock_get.called)
|
||||
22
openedx/core/djangoapps/programs/utils.py
Normal file
22
openedx/core/djangoapps/programs/utils.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Helper methods for Programs.
|
||||
"""
|
||||
from edx_rest_api_client.client import EdxRestApiClient
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
|
||||
|
||||
def is_student_dashboard_programs_enabled(): # pylint: disable=invalid-name
|
||||
""" Returns a Boolean indicating whether LMS dashboard functionality
|
||||
related to Programs should be enabled or not.
|
||||
"""
|
||||
return ProgramsApiConfig.current().is_student_dashboard_enabled
|
||||
|
||||
|
||||
def programs_api_client(api_url, jwt_access_token):
|
||||
""" Returns an Programs API client setup with authentication for the
|
||||
specified user.
|
||||
"""
|
||||
return EdxRestApiClient(
|
||||
api_url,
|
||||
jwt=jwt_access_token
|
||||
)
|
||||
68
openedx/core/djangoapps/programs/views.py
Normal file
68
openedx/core/djangoapps/programs/views.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Main views and method related to the Programs.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from openedx.core.djangoapps.util.helpers import get_id_token
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs.utils import programs_api_client, is_student_dashboard_programs_enabled
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
# OAuth2 Client name for programs
|
||||
CLIENT_NAME = "programs"
|
||||
|
||||
|
||||
def get_course_programs_for_dashboard(user, course_keys): # pylint: disable=invalid-name
|
||||
""" Return all programs related to a user.
|
||||
|
||||
Given a user and an iterable of course keys, find all
|
||||
the programs relevant to the user's dashboard and return them in a
|
||||
dictionary keyed by the course_key.
|
||||
|
||||
Arguments:
|
||||
user (user object): Currently logged-in User for which we need to get
|
||||
JWT ID-Token
|
||||
course_keys (list): List of course keys in which user is enrolled
|
||||
|
||||
Returns:
|
||||
Dictionary response containing programs or None
|
||||
"""
|
||||
course_programs = {}
|
||||
if not is_student_dashboard_programs_enabled():
|
||||
log.warning("Programs service for student dashboard is disabled.")
|
||||
return course_programs
|
||||
|
||||
# unicode-ify the course keys for efficient lookup
|
||||
course_keys = map(unicode, course_keys)
|
||||
|
||||
# get programs slumber-based client 'EdxRestApiClient'
|
||||
try:
|
||||
api_client = programs_api_client(ProgramsApiConfig.current().internal_api_url, get_id_token(user, CLIENT_NAME))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception('Failed to initialize the Programs API client.')
|
||||
return course_programs
|
||||
|
||||
# get programs from api client
|
||||
try:
|
||||
response = api_client.programs.get()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception('Failed to retrieve programs from the Programs API.')
|
||||
return course_programs
|
||||
|
||||
programs = response.get('results', [])
|
||||
if not programs:
|
||||
log.warning("No programs found for the user '%s'.", user.id)
|
||||
return course_programs
|
||||
|
||||
# reindex the result from pgm -> course code -> course run
|
||||
# to
|
||||
# course run -> program, ignoring course runs not present in the dashboard enrollments
|
||||
for program in programs:
|
||||
for course_code in program['course_codes']:
|
||||
for run in course_code['run_modes']:
|
||||
if run['course_key'] in course_keys:
|
||||
course_programs[run['course_key']] = program
|
||||
|
||||
return course_programs
|
||||
48
openedx/core/djangoapps/util/helpers.py
Normal file
48
openedx/core/djangoapps/util/helpers.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Common helpers methods for django apps.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from provider.oauth2.models import AccessToken, Client
|
||||
from provider.utils import now
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_id_token(user, client_name):
|
||||
"""Generates a JWT ID-Token, using or creating user's OAuth access token.
|
||||
|
||||
Arguments:
|
||||
user (User Object): User for which we need to get JWT ID-Token
|
||||
client_name (unicode): Name of the OAuth2 Client
|
||||
|
||||
Returns:
|
||||
String containing the signed JWT value or raise the exception
|
||||
'ImproperlyConfigured'
|
||||
"""
|
||||
# TODO: there's a circular import problem somewhere which is why we do the oidc import inside of this function.
|
||||
import oauth2_provider.oidc as oidc
|
||||
|
||||
try:
|
||||
client = Client.objects.get(name=client_name)
|
||||
except Client.DoesNotExist:
|
||||
raise ImproperlyConfigured("OAuth2 Client with name '%s' is not present in the DB" % client_name)
|
||||
|
||||
access_tokens = AccessToken.objects.filter(
|
||||
client=client,
|
||||
user__username=user.username,
|
||||
expires__gt=now()
|
||||
).order_by('-expires')
|
||||
|
||||
if access_tokens:
|
||||
access_token = access_tokens[0]
|
||||
else:
|
||||
access_token = AccessToken.objects.create(client=client, user=user)
|
||||
|
||||
id_token = oidc.id_token(access_token)
|
||||
secret = id_token.access_token.client.client_secret
|
||||
return id_token.encode(secret)
|
||||
0
openedx/core/djangoapps/util/tests/__init__.py
Normal file
0
openedx/core/djangoapps/util/tests/__init__.py
Normal file
45
openedx/core/djangoapps/util/tests/test_helpers.py
Normal file
45
openedx/core/djangoapps/util/tests/test_helpers.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Tests for the helper methods.
|
||||
"""
|
||||
|
||||
import jwt
|
||||
from oauth2_provider.tests.factories import ClientFactory
|
||||
from provider.oauth2.models import AccessToken, Client
|
||||
from unittest import skipUnless
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
|
||||
from openedx.core.djangoapps.util.helpers import get_id_token
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class GetIdTokenTest(TestCase):
|
||||
"""
|
||||
Tests for then helper method 'get_id_token'.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.client_name = "edx-dummy-client"
|
||||
ClientFactory(name=self.client_name)
|
||||
super(GetIdTokenTest, self).setUp()
|
||||
self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
|
||||
self.client.login(username=self.user.username, password="edx")
|
||||
|
||||
def test_get_id_token(self):
|
||||
"""
|
||||
Test generation of ID Token.
|
||||
"""
|
||||
# test that a user with no ID Token gets a valid token on calling the
|
||||
# method 'get_id_token' against a client
|
||||
self.assertEqual(AccessToken.objects.all().count(), 0)
|
||||
client = Client.objects.get(name=self.client_name)
|
||||
first_token = get_id_token(self.user, self.client_name)
|
||||
self.assertEqual(AccessToken.objects.all().count(), 1)
|
||||
jwt.decode(first_token, client.client_secret, audience=client.client_id)
|
||||
|
||||
# test that a user with existing ID Token gets the same token instead
|
||||
# of a new generated token
|
||||
second_token = get_id_token(self.user, self.client_name)
|
||||
self.assertEqual(AccessToken.objects.all().count(), 1)
|
||||
self.assertEqual(first_token, second_token)
|
||||
@@ -30,7 +30,7 @@ django-storages==1.1.5
|
||||
django-method-override==0.1.0
|
||||
djangorestframework>=3.1,<3.2
|
||||
django==1.4.22
|
||||
edx-rest-api-client==1.2.0
|
||||
edx-rest-api-client==1.2.1
|
||||
elasticsearch==0.4.5
|
||||
facebook-sdk==0.4.0
|
||||
feedparser==5.1.3
|
||||
|
||||
Reference in New Issue
Block a user