From a0165e88b2d3ac37651f415690183187d628e676 Mon Sep 17 00:00:00 2001 From: Maari <56252537+mrtmm@users.noreply.github.com> Date: Thu, 3 Jun 2021 15:46:21 +0300 Subject: [PATCH] Badgr integration updates (#27181) * Badgr integration fix Badges are no longer created on the Badgr side with a given 'slug'. Instead, a slug(v1) or an entityId(v2) will be generated for each badge on the Badgr side and we will need to use that value to check if a certain badge matching a BadgeClass on our side exists and/or to create assertions for it. This commit introduces a new field to the badgeclass: 'badgr_server_slug' by cherry-picking the following commit from 3309aab2a2eb00d28c5ca3d3145c8dddb15e6159 - TTK-18543: fix Badgr Server connection (https://github.com/teltek/edx-platform/pull/46) This commit also modifies the cherry-picked commit by making the newly added field optional since the BadgeClass is not neccessarily always used with the Badgr backend. Co-authored-by: mrey * Implement OAuth2 tokens flow for BadgrBackend * Use Badgr v2 API Co-authored-by: mrey --- lms/djangoapps/badges/backends/badgr.py | 179 +++++++++++++++--- .../backends/tests/test_badgr_backend.py | 150 ++++++++++++--- .../0004_badgeclass_badgr_server_slug.py | 19 ++ lms/djangoapps/badges/models.py | 1 + lms/djangoapps/badges/tests/factories.py | 1 + lms/djangoapps/badges/tests/test_models.py | 6 +- lms/envs/common.py | 30 ++- 7 files changed, 330 insertions(+), 56 deletions(-) create mode 100644 lms/djangoapps/badges/migrations/0004_badgeclass_badgr_server_slug.py diff --git a/lms/djangoapps/badges/backends/badgr.py b/lms/djangoapps/badges/backends/badgr.py index 5cec5b0e0f..d58eb89925 100644 --- a/lms/djangoapps/badges/backends/badgr.py +++ b/lms/djangoapps/badges/backends/badgr.py @@ -3,17 +3,23 @@ Badge Awarding backend for Badgr-Server. """ +import base64 +import datetime +import json import hashlib import logging import mimetypes import requests +from cryptography.fernet import Fernet from django.conf import settings from django.core.exceptions import ImproperlyConfigured from eventtracking import tracker from lazy import lazy # lint-amnesty, pylint: disable=no-name-in-module from requests.packages.urllib3.exceptions import HTTPError # lint-amnesty, pylint: disable=import-error +from edx_django_utils.cache import TieredCache + from lms.djangoapps.badges.backends.base import BadgeBackend from lms.djangoapps.badges.models import BadgeAssertion @@ -29,28 +35,37 @@ class BadgrBackend(BadgeBackend): def __init__(self): super().__init__() - if not settings.BADGR_API_TOKEN: - raise ImproperlyConfigured("BADGR_API_TOKEN not set.") + if None in (settings.BADGR_USERNAME, + settings.BADGR_PASSWORD, + settings.BADGR_TOKENS_CACHE_KEY, + settings.BADGR_ISSUER_SLUG, + settings.BADGR_BASE_URL): + error_msg = ( + "One or more of the required settings are not defined. " + "Required settings: BADGR_USERNAME, BADGR_PASSWORD, " + "BADGR_TOKENS_CACHE_KEY, BADGR_ISSUER_SLUG, BADGR_BASE_URL.") + LOGGER.error(error_msg) + raise ImproperlyConfigured(error_msg) @lazy def _base_url(self): """ - Base URL for all API requests. + Base URL for API requests that contain the issuer slug. """ - return f"{settings.BADGR_BASE_URL}/v1/issuer/issuers/{settings.BADGR_ISSUER_SLUG}" + return f"{settings.BADGR_BASE_URL}/v2/issuers/{settings.BADGR_ISSUER_SLUG}" @lazy def _badge_create_url(self): """ URL for generating a new Badge specification """ - return f"{self._base_url}/badges" + return f"{self._base_url}/badgeclasses" def _badge_url(self, slug): """ Get the URL for a course's badge in a given mode. """ - return f"{self._badge_create_url}/{slug}" + return f"{settings.BADGR_BASE_URL}/v2/badgeclasses/{slug}" def _assertion_url(self, slug): """ @@ -102,18 +117,28 @@ class BadgrBackend(BadgeBackend): "Could not determine content-type of image! Make sure it is a properly named .png file. " "Filename was: {}".format(image.name) ) - files = {'image': (image.name, image, content_type)} - data = { - 'name': badge_class.display_name, - 'criteria': badge_class.criteria, - 'slug': self._slugify(badge_class), - 'description': badge_class.description, - } - result = requests.post( - self._badge_create_url, headers=self._get_headers(), data=data, files=files, - timeout=settings.BADGR_TIMEOUT - ) - self._log_if_raised(result, data) + with open(image.path, 'rb') as image_file: + files = {'image': (image.name, image_file, content_type)} + data = { + 'name': badge_class.display_name, + 'criteriaUrl': badge_class.criteria, + 'description': badge_class.description, + } + result = requests.post( + self._badge_create_url, headers=self._get_headers(), + data=data, files=files, timeout=settings.BADGR_TIMEOUT) + self._log_if_raised(result, data) + try: + result_json = result.json() + badgr_badge_class = result_json['result'][0] + badgr_server_slug = badgr_badge_class.get('entityId') + badge_class.badgr_server_slug = badgr_server_slug + badge_class.save() + except Exception as excep: # pylint: disable=broad-except + LOGGER.error( + 'Error on saving Badgr Server Slug of badge_class slug ' + '"{0}" with response json "{1}" : {2}'.format( + badge_class.slug, result.json(), excep)) def _send_assertion_created_event(self, user, assertion): """ @@ -123,6 +148,7 @@ class BadgrBackend(BadgeBackend): 'edx.badge.assertion.created', { 'user_id': user.id, 'badge_slug': assertion.badge_class.slug, + 'badge_badgr_server_slug': assertion.badge_class.badgr_server_slug, 'badge_name': assertion.badge_class.display_name, 'issuing_component': assertion.badge_class.issuing_component, 'course_id': str(assertion.badge_class.course_id), @@ -139,11 +165,20 @@ class BadgrBackend(BadgeBackend): Register an assertion with the Badgr server for a particular user for a specific class. """ data = { - 'email': user.email, - 'evidence': evidence_url, + "recipient": { + "identity": user.email, + "type": "email" + }, + "evidence": [ + { + "url": evidence_url + } + ] } response = requests.post( - self._assertion_url(self._slugify(badge_class)), headers=self._get_headers(), data=data, + self._assertion_url(badge_class.badgr_server_slug), + headers=self._get_headers(), + json=data, timeout=settings.BADGR_TIMEOUT ) self._log_if_raised(response, data) @@ -157,17 +192,113 @@ class BadgrBackend(BadgeBackend): return assertion @staticmethod - def _get_headers(): + def _fernet_setup(): + """ + Set up the Fernet class for encrypting/decrypting tokens. + Fernet keys must always be URL-safe base64 encoded 32-byte binary + strings. Use the SECRET_KEY for creating the encryption key. + """ + fernet_key = base64.urlsafe_b64encode( + settings.SECRET_KEY.ljust(64).encode('utf-8')[:32] + ) + return Fernet(fernet_key) + + def _encrypt_token(self, token): + """ + Encrypt a token + """ + fernet = self._fernet_setup() + return fernet.encrypt(token.encode('utf-8')) + + def _decrypt_token(self, token): + """ + Decrypt a token + """ + fernet = self._fernet_setup() + return fernet.decrypt(token).decode() + + def _get_and_cache_oauth_tokens(self, refresh_token=None): + """ + Get or renew OAuth tokens. If a refresh_token is provided, + use it to renew tokens, otherwise create new ones. + Once tokens are created/renewed, encrypt the values and cache them. + """ + data = { + 'username': settings.BADGR_USERNAME, + 'password': settings.BADGR_PASSWORD, + } + if refresh_token: + data = { + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token + } + + oauth_url = "{}/o/token".format(settings.BADGR_BASE_URL) + + response = requests.post( + oauth_url, data=data, timeout=settings.BADGR_TIMEOUT + ) + self._log_if_raised(response, data) + try: + data = response.json() + result = { + 'access_token': self._encrypt_token(data['access_token']), + 'refresh_token': self._encrypt_token(data['refresh_token']), + 'expires_at': datetime.datetime.utcnow() + datetime.timedelta( + seconds=data['expires_in']) + } + # The refresh_token is long-lived, we want to be able to retrieve + # it from cache as long as possible. + # Set the cache timeout to None so the cache key never expires + # (https://docs.djangoproject.com/en/2.2/topics/cache/#cache-arguments) + TieredCache.set_all_tiers( + settings.BADGR_TOKENS_CACHE_KEY, result, None) + return result + except (KeyError, json.decoder.JSONDecodeError) as json_error: + raise requests.RequestException(response=response) from json_error + + def _get_access_token(self): + """ + Get an access token from cache if one is present and valid. If a + token is cached but expired, renew it. If all fails or a token has + not yet been cached, create a new one. + """ + tokens = {} + cached_response = TieredCache.get_cached_response( + settings.BADGR_TOKENS_CACHE_KEY) + if cached_response.is_found: + cached_tokens = cached_response.value + # add a 5 seconds buffer to the cutoff timestamp to make sure + # the token will not expire while in use + expiry_cutoff = ( + datetime.datetime.utcnow() + datetime.timedelta(seconds=5)) + if cached_tokens.get('expires_at') > expiry_cutoff: + tokens = cached_tokens + else: + # renew the tokens with the cached `refresh_token` + refresh_token = self._decrypt_token(cached_tokens.get( + 'refresh_token')) + tokens = self._get_and_cache_oauth_tokens( + refresh_token=refresh_token) + + # if no tokens are cached or something went wrong with + # retreiving/renewing them, go and create new tokens + if not tokens: + tokens = self._get_and_cache_oauth_tokens() + return self._decrypt_token(tokens.get('access_token')) + + def _get_headers(self): """ Headers to send along with the request-- used for authentication. """ - return {'Authorization': f'Token {settings.BADGR_API_TOKEN}'} + access_token = self._get_access_token() + return {'Authorization': u'Bearer {}'.format(access_token)} def _ensure_badge_created(self, badge_class): """ Verify a badge has been created for this badge class, and create it if not. """ - slug = self._slugify(badge_class) + slug = badge_class.badgr_server_slug if slug in BadgrBackend.badges: return response = requests.get(self._badge_url(slug), headers=self._get_headers(), timeout=settings.BADGR_TIMEOUT) diff --git a/lms/djangoapps/badges/backends/tests/test_badgr_backend.py b/lms/djangoapps/badges/backends/tests/test_badgr_backend.py index f970d6f5ef..78bf2dfc6a 100644 --- a/lms/djangoapps/badges/backends/tests/test_badgr_backend.py +++ b/lms/djangoapps/badges/backends/tests/test_badgr_backend.py @@ -2,15 +2,16 @@ Tests for BadgrBackend """ - -from datetime import datetime +import datetime from unittest.mock import Mock, call, patch +import json import ddt -from django.db.models.fields.files import ImageFieldFile +import httpretty from django.test.utils import override_settings from lazy.lazy import lazy # lint-amnesty, pylint: disable=no-name-in-module +from edx_django_utils.cache import TieredCache from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from common.djangoapps.track.tests import EventTrackingTestCase from lms.djangoapps.badges.backends.badgr import BadgrBackend @@ -21,18 +22,22 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory BADGR_SETTINGS = { - 'BADGR_API_TOKEN': '12345', 'BADGR_BASE_URL': 'https://example.com', 'BADGR_ISSUER_SLUG': 'test-issuer', + 'BADGR_USERNAME': 'example@example.com', + 'BADGR_PASSWORD': 'password', + 'BADGR_TOKENS_CACHE_KEY': 'badgr-test-cache-key' } # Should be the hashed result of test_slug as the slug, and test_component as the component EXAMPLE_SLUG = '15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a' +BADGR_SERVER_SLUG = 'test_badgr_server_slug' # pylint: disable=protected-access @ddt.ddt @override_settings(**BADGR_SETTINGS) +@httpretty.activate class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): """ Tests the BadgeHandler object @@ -46,8 +51,8 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): # Need key to be deterministic to test slugs. self.course = CourseFactory.create( org='edX', course='course_test', run='test_run', display_name='Badged', - start=datetime(year=2015, month=5, day=19), - end=datetime(year=2015, month=5, day=20) + start=datetime.datetime(year=2015, month=5, day=19), + end=datetime.datetime(year=2015, month=5, day=20) ) self.user = UserFactory.create(email='example@example.com') CourseEnrollmentFactory.create(user=self.user, course_id=self.course.location.course_key, mode='honor') @@ -58,6 +63,8 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): course_id=self.course.location.course_key, issuing_component='' ) self.no_course_badge_class = BadgeClassFactory.create() + TieredCache.dangerous_clear_all_tiers() + httpretty.httpretty.reset() @lazy def handler(self): @@ -67,29 +74,38 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): """ return BadgrBackend() + def _mock_badgr_tokens_api(self, result): + assert httpretty.is_enabled() + responses = [httpretty.Response(body=json.dumps(result), + content_type='application/json')] + httpretty.register_uri(httpretty.POST, + 'https://example.com/o/token', + responses=responses) + def test_urls(self): """ Make sure the handler generates the correct URLs for different API tasks. """ - assert self.handler._base_url == 'https://example.com/v1/issuer/issuers/test-issuer' + assert self.handler._base_url == 'https://example.com/v2/issuers/test-issuer' # lint-amnesty, pylint: disable=no-member - assert self.handler._badge_create_url == 'https://example.com/v1/issuer/issuers/test-issuer/badges' + assert self.handler._badge_create_url == 'https://example.com/v2/issuers/test-issuer/badgeclasses' # lint-amnesty, pylint: disable=no-member assert self.handler._badge_url('test_slug_here') ==\ - 'https://example.com/v1/issuer/issuers/test-issuer/badges/test_slug_here' + 'https://example.com/v2/badgeclasses/test_slug_here' assert self.handler._assertion_url('another_test_slug') ==\ - 'https://example.com/v1/issuer/issuers/test-issuer/badges/another_test_slug/assertions' + 'https://example.com/v2/badgeclasses/another_test_slug/assertions' def check_headers(self, headers): """ Verify the a headers dict from a requests call matches the proper auth info. """ - assert headers == {'Authorization': 'Token 12345'} + assert headers == {'Authorization': 'Bearer 12345'} def test_get_headers(self): """ Check to make sure the handler generates appropriate HTTP headers. """ + self.handler._get_access_token = Mock(return_value='12345') self.check_headers(self.handler._get_headers()) # lint-amnesty, pylint: disable=no-member @patch('requests.post') @@ -97,24 +113,23 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): """ Verify badge spec creation works. """ + self.handler._get_access_token = Mock(return_value='12345') self.handler._create_badge(self.badge_class) args, kwargs = post.call_args - assert args[0] == 'https://example.com/v1/issuer/issuers/test-issuer/badges' + assert args[0] == 'https://example.com/v2/issuers/test-issuer/badgeclasses' assert kwargs['files']['image'][0] == self.badge_class.image.name - assert isinstance(kwargs['files']['image'][1], ImageFieldFile) assert kwargs['files']['image'][2] == 'image/png' self.check_headers(kwargs['headers']) assert kwargs['data'] ==\ {'name': 'Test Badge', - 'slug': EXAMPLE_SLUG, - 'criteria': 'https://example.com/syllabus', + 'criteriaUrl': 'https://example.com/syllabus', 'description': "Yay! It's a test badge."} def test_ensure_badge_created_cache(self): """ Make sure ensure_badge_created doesn't call create_badge if we know the badge is already there. """ - BadgrBackend.badges.append(EXAMPLE_SLUG) + BadgrBackend.badges.append(BADGR_SERVER_SLUG) self.handler._create_badge = Mock() self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member assert not self.handler._create_badge.called @@ -135,13 +150,16 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): response.status_code = 200 get.return_value = response assert 'test_componenttest_slug' not in BadgrBackend.badges + self.handler._get_access_token = Mock(return_value='12345') self.handler._create_badge = Mock() self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member assert get.called args, kwargs = get.call_args - assert args[0] == ('https://example.com/v1/issuer/issuers/test-issuer/badges/' + EXAMPLE_SLUG) + assert args[0] == ( + 'https://example.com/v2/badgeclasses/' + + BADGR_SERVER_SLUG) self.check_headers(kwargs['headers']) - assert EXAMPLE_SLUG in BadgrBackend.badges + assert BADGR_SERVER_SLUG in BadgrBackend.badges assert not self.handler._create_badge.called @patch('requests.get') @@ -149,12 +167,13 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): response = Mock() response.status_code = 404 get.return_value = response - assert EXAMPLE_SLUG not in BadgrBackend.badges + assert BADGR_SERVER_SLUG not in BadgrBackend.badges + self.handler._get_access_token = Mock(return_value='12345') self.handler._create_badge = Mock() self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member assert self.handler._create_badge.called assert self.handler._create_badge.call_args == call(self.badge_class) - assert EXAMPLE_SLUG in BadgrBackend.badges + assert BADGR_SERVER_SLUG in BadgrBackend.badges @patch('requests.post') def test_badge_creation_event(self, post): @@ -162,21 +181,26 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): 'json': {'id': 'http://www.example.com/example'}, 'image': 'http://www.example.com/example.png', 'badge': 'test_assertion_slug', - 'issuer': 'https://example.com/v1/issuer/issuers/test-issuer', + 'issuer': 'https://example.com/v2/issuers/test-issuer', } response = Mock() response.json.return_value = result post.return_value = response self.recreate_tracker() + self.handler._get_access_token = Mock(return_value='12345') self.handler._create_assertion(self.badge_class, self.user, 'https://example.com/irrefutable_proof') # lint-amnesty, pylint: disable=no-member args, kwargs = post.call_args - assert args[0] == (('https://example.com/v1/issuer/issuers/test-issuer/badges/' + EXAMPLE_SLUG) + '/assertions') + assert args[0] == (( + 'https://example.com/v2/badgeclasses/' + + BADGR_SERVER_SLUG) + + '/assertions') self.check_headers(kwargs['headers']) assertion = BadgeAssertion.objects.get(user=self.user, badge_class__course_id=self.course.location.course_key) assert assertion.data == result assert assertion.image_url == 'http://www.example.com/example.png' assert assertion.assertion_url == 'http://www.example.com/example' - assert kwargs['data'] == {'email': 'example@example.com', 'evidence': 'https://example.com/irrefutable_proof'} + assert kwargs['json'] == {"recipient": {"identity": 'example@example.com', "type": "email"}, + "evidence": [{"url": 'https://example.com/irrefutable_proof'}]} assert_event_matches({ 'name': 'edx.badge.assertion.created', 'data': { @@ -186,9 +210,87 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): 'assertion_id': assertion.id, 'badge_name': 'Test Badge', 'badge_slug': 'test_slug', + 'badge_badgr_server_slug': BADGR_SERVER_SLUG, 'issuing_component': 'test_component', 'assertion_image_url': 'http://www.example.com/example.png', 'assertion_json_url': 'http://www.example.com/example', - 'issuer': 'https://example.com/v1/issuer/issuers/test-issuer', + 'issuer': 'https://example.com/v2/issuers/test-issuer', } }, self.get_event()) + + def test_get_new_tokens(self): + result = { + 'access_token': '12345', + 'refresh_token': '67890', + 'expires_in': 86400, + } + self._mock_badgr_tokens_api(result) + self.handler._get_and_cache_oauth_tokens() + assert 'o/token' in httpretty.httpretty.last_request.path + assert httpretty.httpretty.last_request.parsed_body == { + 'username': ['example@example.com'], + 'password': ['password']} + + def test_renew_tokens(self): + result = { + 'access_token': '12345', + 'refresh_token': '67890', + 'expires_in': 86400, + } + self._mock_badgr_tokens_api(result) + self.handler._get_and_cache_oauth_tokens(refresh_token='67890') + assert 'o/token' in httpretty.httpretty.last_request.path + assert httpretty.httpretty.last_request.parsed_body == { + 'grant_type': ['refresh_token'], + 'refresh_token': ['67890']} + + def test_get_access_token_from_cache_valid(self): + encrypted_access_token = self.handler._encrypt_token('12345') + encrypted_refresh_token = self.handler._encrypt_token('67890') + tokens = { + 'access_token': encrypted_access_token, + 'refresh_token': encrypted_refresh_token, + 'expires_at': datetime.datetime.utcnow() + datetime.timedelta(seconds=20) + } + TieredCache.set_all_tiers('badgr-test-cache-key', tokens, None) + + access_token = self.handler._get_access_token() + assert access_token == self.handler._decrypt_token( + tokens.get('access_token')) + + def test_get_access_token_from_cache_expired(self): + encrypted_access_token = self.handler._encrypt_token('12345') + encrypted_refresh_token = self.handler._encrypt_token('67890') + tokens = { + 'access_token': encrypted_access_token, + 'refresh_token': encrypted_refresh_token, + 'expires_at': datetime.datetime.utcnow() + } + TieredCache.set_all_tiers('badgr-test-cache-key', tokens, None) + result = { + 'access_token': '12345', + 'refresh_token': '67890', + 'expires_in': 86400, + } + self._mock_badgr_tokens_api(result) + access_token = self.handler._get_access_token() + assert access_token == result.get('access_token') + assert 'o/token' in httpretty.httpretty.last_request.path + assert httpretty.httpretty.last_request.parsed_body == { + 'grant_type': ['refresh_token'], + 'refresh_token': [self.handler._decrypt_token( + tokens.get('refresh_token'))]} + + def test_get_access_token_from_cache_none(self): + result = { + 'access_token': '12345', + 'refresh_token': '67890', + 'expires_in': 86400, + } + self._mock_badgr_tokens_api(result) + access_token = self.handler._get_access_token() + assert access_token == result.get('access_token') + assert 'o/token' in httpretty.httpretty.last_request.path + assert httpretty.httpretty.last_request.parsed_body == { + 'username': ['example@example.com'], + 'password': ['password']} diff --git a/lms/djangoapps/badges/migrations/0004_badgeclass_badgr_server_slug.py b/lms/djangoapps/badges/migrations/0004_badgeclass_badgr_server_slug.py new file mode 100644 index 0000000000..7b1f16934c --- /dev/null +++ b/lms/djangoapps/badges/migrations/0004_badgeclass_badgr_server_slug.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('badges', '0003_schema__add_event_configuration'), + ] + + operations = [ + migrations.AddField( + model_name='badgeclass', + name='badgr_server_slug', + field=models.SlugField(blank=True, default='', max_length=255), + ), + ] diff --git a/lms/djangoapps/badges/models.py b/lms/djangoapps/badges/models.py index fcfd7c01c9..0d0bdc6f27 100644 --- a/lms/djangoapps/badges/models.py +++ b/lms/djangoapps/badges/models.py @@ -56,6 +56,7 @@ class BadgeClass(models.Model): .. no_pii: """ slug = models.SlugField(max_length=255, validators=[validate_lowercase]) + badgr_server_slug = models.SlugField(max_length=255, default='', blank=True) issuing_component = models.SlugField(max_length=50, default='', blank=True, validators=[validate_lowercase]) display_name = models.CharField(max_length=255) course_id = CourseKeyField(max_length=255, blank=True, default=None) diff --git a/lms/djangoapps/badges/tests/factories.py b/lms/djangoapps/badges/tests/factories.py index e4425a6afc..f84053e329 100644 --- a/lms/djangoapps/badges/tests/factories.py +++ b/lms/djangoapps/badges/tests/factories.py @@ -48,6 +48,7 @@ class BadgeClassFactory(factory.django.DjangoModelFactory): model = BadgeClass slug = 'test_slug' + badgr_server_slug = 'test_badgr_server_slug' issuing_component = 'test_component' display_name = 'Test Badge' description = "Yay! It's a test badge." diff --git a/lms/djangoapps/badges/tests/test_models.py b/lms/djangoapps/badges/tests/test_models.py index e6469dc6a5..256bf8972b 100644 --- a/lms/djangoapps/badges/tests/test_models.py +++ b/lms/djangoapps/badges/tests/test_models.py @@ -209,7 +209,11 @@ class BadgeClassTest(ModuleStoreTestCase): assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=user) assert list(badge_class.get_for_user(user)) == [assertion] - @override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.badgr.BadgrBackend', BADGR_API_TOKEN='test') + @override_settings( + BADGING_BACKEND='lms.djangoapps.badges.backends.badgr.BadgrBackend', + BADGR_USERNAME='example@example.com', + BADGR_PASSWORD='password', + BADGR_TOKENS_CACHE_KEY='badgr-test-cache-key') @patch('lms.djangoapps.badges.backends.badgr.BadgrBackend.award') def test_award(self, mock_award): """ diff --git a/lms/envs/common.py b/lms/envs/common.py index 69902fb76f..6946d5c8a8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3403,13 +3403,6 @@ CERT_NAME_LONG = "Certificate of Achievement" # .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context. BADGING_BACKEND = 'lms.djangoapps.badges.backends.badgr.BadgrBackend' -# .. setting_name: BADGR_API_TOKEN -# .. setting_default: None -# .. setting_description: The API token string for Badgr. You should be able to create this via Badgr's settings. See -# https://github.com/concentricsky/badgr-server for details on setting up Badgr. -# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context. -BADGR_API_TOKEN = None - # .. setting_name: BADGR_BASE_URL # .. setting_default: 'http://localhost:8005' # .. setting_description: The base URL for the Badgr server. @@ -3424,6 +3417,29 @@ BADGR_BASE_URL = "http://localhost:8005" # .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context. BADGR_ISSUER_SLUG = "example-issuer" +# .. setting_name: BADGR_USERNAME +# .. setting_default: None +# .. setting_description: The username for Badgr. You should set up an issuer application with Badgr +# (https://badgr.org/app-developers/). The username and password will then be used to create or renew +# OAuth2 tokens. +# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context. +BADGR_USERNAME = None + +# .. setting_name: BADGR_PASSWORD +# .. setting_default: None +# .. setting_description: The password for Badgr. You should set up an issuer application with Badgr +# (https://badgr.org/app-developers/). The username and password will then be used to create or renew +# OAuth2 tokens. +# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context. +BADGR_PASSWORD = None + +# .. setting_name: BADGR_TOKENS_CACHE_KEY +# .. setting_default: None +# .. setting_description: The cache key for Badgr API tokens. Once created, the tokens will be stored in cache. +# Define the key here for setting and retrieveing the tokens. +# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context. +BADGR_TOKENS_CACHE_KEY = None + # .. setting_name: BADGR_TIMEOUT # .. setting_default: 10 # .. setting_description: Number of seconds to wait on the badging server when contacting it before giving up.