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 <mrey@teltek.es>

* Implement OAuth2 tokens flow for BadgrBackend

* Use Badgr v2 API

Co-authored-by: mrey <mrey@teltek.es>
This commit is contained in:
Maari
2021-06-03 15:46:21 +03:00
committed by GitHub
parent 7a96588b15
commit a0165e88b2
7 changed files with 330 additions and 56 deletions

View File

@@ -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)

View File

@@ -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']}

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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."

View File

@@ -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):
"""

View File

@@ -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.