diff --git a/cms/djangoapps/contentstore/views/tests/test_programs.py b/cms/djangoapps/contentstore/views/tests/test_programs.py
index fc5f2df2d2..1f3042170e 100644
--- a/cms/djangoapps/contentstore/views/tests/test_programs.py
+++ b/cms/djangoapps/contentstore/views/tests/test_programs.py
@@ -5,7 +5,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse
import httpretty
import mock
-from oauth2_provider.tests.factories import ClientFactory
+from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
diff --git a/cms/envs/common.py b/cms/envs/common.py
index face8e67d9..71389ff714 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -876,9 +876,12 @@ INSTALLED_APPS = (
# Self-paced course configuration
'openedx.core.djangoapps.self_paced',
- # OAuth2 Provider
+ # django-oauth2-provider (deprecated)
'provider',
'provider.oauth2',
+ 'edx_oauth2_provider',
+
+ # django-oauth-toolkit
'oauth2_provider',
# These are apps that aren't strictly needed by Studio, but are imported by
diff --git a/common/djangoapps/auth_exchange/forms.py b/common/djangoapps/auth_exchange/forms.py
index cad3db1e1b..8caf61799d 100644
--- a/common/djangoapps/auth_exchange/forms.py
+++ b/common/djangoapps/auth_exchange/forms.py
@@ -3,11 +3,12 @@ Forms to support third-party to first-party OAuth 2.0 access token exchange
"""
from django.contrib.auth.models import User
from django.forms import CharField
-from oauth2_provider.constants import SCOPE_NAMES
+from edx_oauth2_provider.constants import SCOPE_NAMES
import provider.constants
from provider.forms import OAuthForm, OAuthValidationError
from provider.oauth2.forms import ScopeChoiceField, ScopeMixin
from provider.oauth2.models import Client
+from oauth2_provider.models import Application
from requests import HTTPError
from social.backends import oauth as social_oauth
from social.exceptions import AuthException
@@ -21,9 +22,10 @@ class AccessTokenExchangeForm(ScopeMixin, OAuthForm):
scope = ScopeChoiceField(choices=SCOPE_NAMES, required=False)
client_id = CharField(required=False)
- def __init__(self, request, *args, **kwargs):
+ def __init__(self, request, oauth2_adapter, *args, **kwargs):
super(AccessTokenExchangeForm, self).__init__(*args, **kwargs)
self.request = request
+ self.oauth2_adapter = oauth2_adapter
def _require_oauth_field(self, field_name):
"""
@@ -68,15 +70,15 @@ class AccessTokenExchangeForm(ScopeMixin, OAuthForm):
client_id = self.cleaned_data["client_id"]
try:
- client = Client.objects.get(client_id=client_id)
- except Client.DoesNotExist:
+ client = self.oauth2_adapter.get_client(client_id=client_id)
+ except (Client.DoesNotExist, Application.DoesNotExist):
raise OAuthValidationError(
{
"error": "invalid_client",
"error_description": "{} is not a valid client_id".format(client_id),
}
)
- if client.client_type != provider.constants.PUBLIC:
+ if client.client_type not in [provider.constants.PUBLIC, Application.CLIENT_PUBLIC]:
raise OAuthValidationError(
{
# invalid_client isn't really the right code, but this mirrors
diff --git a/common/djangoapps/auth_exchange/tests/mixins.py b/common/djangoapps/auth_exchange/tests/mixins.py
new file mode 100644
index 0000000000..450f789657
--- /dev/null
+++ b/common/djangoapps/auth_exchange/tests/mixins.py
@@ -0,0 +1,111 @@
+"""
+Mixins to facilitate testing OAuth connections to Django-OAuth-Toolkit or
+Django-OAuth2-Provider.
+"""
+
+# pylint: disable=protected-access
+
+from unittest import skip, expectedFailure
+from django.test.client import RequestFactory
+
+from lms.djangoapps.oauth_dispatch import adapters
+from lms.djangoapps.oauth_dispatch.tests.constants import DUMMY_REDIRECT_URL
+
+from ..views import DOTAccessTokenExchangeView
+
+
+class DOPAdapterMixin(object):
+ """
+ Mixin to rewire existing tests to use django-oauth2-provider (DOP) backend
+
+ Overwrites self.client_id, self.access_token, self.oauth2_adapter
+ """
+ client_id = 'dop_test_client_id'
+ access_token = 'dop_test_access_token'
+ oauth2_adapter = adapters.DOPAdapter()
+
+ def create_public_client(self, user, client_id=None):
+ """
+ Create an oauth client application that is public.
+ """
+ return self.oauth2_adapter.create_public_client(
+ name='Test Public Client',
+ user=user,
+ client_id=client_id,
+ redirect_uri=DUMMY_REDIRECT_URL,
+ )
+
+ def create_confidential_client(self, user, client_id=None):
+ """
+ Create an oauth client application that is confidential.
+ """
+ return self.oauth2_adapter.create_confidential_client(
+ name='Test Confidential Client',
+ user=user,
+ client_id=client_id,
+ redirect_uri=DUMMY_REDIRECT_URL,
+ )
+
+ def get_token_response_keys(self):
+ """
+ Return the set of keys provided when requesting an access token
+ """
+ return {'access_token', 'token_type', 'expires_in', 'scope'}
+
+
+class DOTAdapterMixin(object):
+ """
+ Mixin to rewire existing tests to use django-oauth-toolkit (DOT) backend
+
+ Overwrites self.client_id, self.access_token, self.oauth2_adapter
+ """
+
+ client_id = 'dot_test_client_id'
+ access_token = 'dot_test_access_token'
+ oauth2_adapter = adapters.DOTAdapter()
+
+ def create_public_client(self, user, client_id=None):
+ """
+ Create an oauth client application that is public.
+ """
+ return self.oauth2_adapter.create_public_client(
+ name='Test Public Application',
+ user=user,
+ client_id=client_id,
+ redirect_uri=DUMMY_REDIRECT_URL,
+ )
+
+ def create_confidential_client(self, user, client_id=None):
+ """
+ Create an oauth client application that is confidential.
+ """
+ return self.oauth2_adapter.create_confidential_client(
+ name='Test Confidential Application',
+ user=user,
+ client_id=client_id,
+ redirect_uri=DUMMY_REDIRECT_URL,
+ )
+
+ def get_token_response_keys(self):
+ """
+ Return the set of keys provided when requesting an access token
+ """
+ return {'access_token', 'refresh_token', 'token_type', 'expires_in', 'scope'}
+
+ def test_get_method(self):
+ # Dispatch routes all get methods to DOP, so we test this on the view
+ request_factory = RequestFactory()
+ request = request_factory.get('/oauth2/exchange_access_token/')
+ request.session = {}
+ view = DOTAccessTokenExchangeView.as_view()
+ response = view(request, backend='facebook')
+ self.assertEqual(response.status_code, 400)
+
+ @expectedFailure
+ def test_single_access_token(self):
+ # TODO: Single access tokens not supported yet for DOT (See MA-2122)
+ super(DOTAdapterMixin, self).test_single_access_token()
+
+ @skip("Not supported yet (See MA-2123)")
+ def test_scopes(self):
+ super(DOTAdapterMixin, self).test_scopes()
diff --git a/common/djangoapps/auth_exchange/tests/test_forms.py b/common/djangoapps/auth_exchange/tests/test_forms.py
index ffeffb4e22..490628f868 100644
--- a/common/djangoapps/auth_exchange/tests/test_forms.py
+++ b/common/djangoapps/auth_exchange/tests/test_forms.py
@@ -12,10 +12,12 @@ import httpretty
from provider import scope
import social.apps.django_app.utils as social_utils
-from auth_exchange.forms import AccessTokenExchangeForm
-from auth_exchange.tests.utils import AccessTokenExchangeTestMixin
from third_party_auth.tests.utils import ThirdPartyOAuthTestMixinFacebook, ThirdPartyOAuthTestMixinGoogle
+from ..forms import AccessTokenExchangeForm
+from .utils import AccessTokenExchangeTestMixin
+from .mixins import DOPAdapterMixin, DOTAdapterMixin
+
class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
"""
@@ -31,7 +33,7 @@ class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
self.request.backend = social_utils.load_backend(self.request.social_strategy, self.BACKEND, redirect_uri)
def _assert_error(self, data, expected_error, expected_error_description):
- form = AccessTokenExchangeForm(request=self.request, data=data)
+ form = AccessTokenExchangeForm(request=self.request, oauth2_adapter=self.oauth2_adapter, data=data)
self.assertEqual(
form.errors,
{"error": expected_error, "error_description": expected_error_description}
@@ -39,7 +41,7 @@ class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
self.assertNotIn("partial_pipeline", self.request.session)
def _assert_success(self, data, expected_scopes):
- form = AccessTokenExchangeForm(request=self.request, data=data)
+ form = AccessTokenExchangeForm(request=self.request, oauth2_adapter=self.oauth2_adapter, data=data)
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data["user"], self.user)
self.assertEqual(form.cleaned_data["client"], self.oauth_client)
@@ -49,13 +51,15 @@ class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
# This is necessary because cms does not implement third party auth
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
@httpretty.activate
-class AccessTokenExchangeFormTestFacebook(
+class DOPAccessTokenExchangeFormTestFacebook(
+ DOPAdapterMixin,
AccessTokenExchangeFormTest,
ThirdPartyOAuthTestMixinFacebook,
- TestCase
+ TestCase,
):
"""
- Tests for AccessTokenExchangeForm used with Facebook
+ Tests for AccessTokenExchangeForm used with Facebook, tested against
+ django-oauth2-provider (DOP).
"""
pass
@@ -63,12 +67,46 @@ class AccessTokenExchangeFormTestFacebook(
# This is necessary because cms does not implement third party auth
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
@httpretty.activate
-class AccessTokenExchangeFormTestGoogle(
+class DOTAccessTokenExchangeFormTestFacebook(
+ DOTAdapterMixin,
AccessTokenExchangeFormTest,
- ThirdPartyOAuthTestMixinGoogle,
- TestCase
+ ThirdPartyOAuthTestMixinFacebook,
+ TestCase,
):
"""
- Tests for AccessTokenExchangeForm used with Google
+ Tests for AccessTokenExchangeForm used with Facebook, tested against
+ django-oauth-toolkit (DOT).
+ """
+ pass
+
+
+# This is necessary because cms does not implement third party auth
+@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
+@httpretty.activate
+class DOPAccessTokenExchangeFormTestGoogle(
+ DOPAdapterMixin,
+ AccessTokenExchangeFormTest,
+ ThirdPartyOAuthTestMixinGoogle,
+ TestCase,
+):
+ """
+ Tests for AccessTokenExchangeForm used with Google, tested against
+ django-oauth2-provider (DOP).
+ """
+ pass
+
+
+# This is necessary because cms does not implement third party auth
+@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
+@httpretty.activate
+class DOTAccessTokenExchangeFormTestGoogle(
+ DOTAdapterMixin,
+ AccessTokenExchangeFormTest,
+ ThirdPartyOAuthTestMixinGoogle,
+ TestCase,
+):
+ """
+ Tests for AccessTokenExchangeForm used with Google, tested against
+ django-oauth-toolkit (DOT).
"""
pass
diff --git a/common/djangoapps/auth_exchange/tests/test_views.py b/common/djangoapps/auth_exchange/tests/test_views.py
index 5c0b7503f9..60c687960c 100644
--- a/common/djangoapps/auth_exchange/tests/test_views.py
+++ b/common/djangoapps/auth_exchange/tests/test_views.py
@@ -1,25 +1,30 @@
-# pylint: disable=no-member
"""
Tests for OAuth token exchange views
"""
+
+# pylint: disable=no-member
+
from datetime import timedelta
import json
import mock
import unittest
+import ddt
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import TestCase
import httpretty
import provider.constants
-from provider import scope
from provider.oauth2.models import AccessToken, Client
+from rest_framework.test import APIClient
-from auth_exchange.tests.utils import AccessTokenExchangeTestMixin
from student.tests.factories import UserFactory
from third_party_auth.tests.utils import ThirdPartyOAuthTestMixinFacebook, ThirdPartyOAuthTestMixinGoogle
+from .mixins import DOPAdapterMixin, DOTAdapterMixin
+from .utils import AccessTokenExchangeTestMixin
+@ddt.ddt
class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
"""
Mixin that defines test cases for AccessTokenExchangeView
@@ -27,33 +32,34 @@ class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
def setUp(self):
super(AccessTokenExchangeViewTest, self).setUp()
self.url = reverse("exchange_access_token", kwargs={"backend": self.BACKEND})
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
def _assert_error(self, data, expected_error, expected_error_description):
- response = self.client.post(self.url, data)
+ response = self.csrf_client.post(self.url, data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response["Content-Type"], "application/json")
self.assertEqual(
json.loads(response.content),
- {"error": expected_error, "error_description": expected_error_description}
+ {u"error": expected_error, u"error_description": expected_error_description}
)
self.assertNotIn("partial_pipeline", self.client.session)
def _assert_success(self, data, expected_scopes):
- response = self.client.post(self.url, data)
+ response = self.csrf_client.post(self.url, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "application/json")
content = json.loads(response.content)
- self.assertEqual(set(content.keys()), {"access_token", "token_type", "expires_in", "scope"})
+ self.assertEqual(set(content.keys()), self.get_token_response_keys())
self.assertEqual(content["token_type"], "Bearer")
self.assertLessEqual(
timedelta(seconds=int(content["expires_in"])),
provider.constants.EXPIRE_DELTA_PUBLIC
)
- self.assertEqual(content["scope"], " ".join(expected_scopes))
- token = AccessToken.objects.get(token=content["access_token"])
+ self.assertEqual(content["scope"], self.oauth2_adapter.normalize_scopes(expected_scopes))
+ token = self.oauth2_adapter.get_access_token(token_string=content["access_token"])
self.assertEqual(token.user, self.user)
- self.assertEqual(token.client, self.oauth_client)
- self.assertEqual(scope.to_names(token.scope), expected_scopes)
+ self.assertEqual(self.oauth2_adapter.get_client_for_token(token), self.oauth_client)
+ self.assertEqual(self.oauth2_adapter.get_token_scope_names(token), expected_scopes)
def test_single_access_token(self):
def extract_token(response):
@@ -64,16 +70,15 @@ class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
self._setup_provider_response(success=True)
for single_access_token in [True, False]:
- with mock.patch(
- "auth_exchange.views.constants.SINGLE_ACCESS_TOKEN",
- single_access_token
- ):
+ with mock.patch("auth_exchange.views.constants.SINGLE_ACCESS_TOKEN", single_access_token):
first_response = self.client.post(self.url, self.data)
second_response = self.client.post(self.url, self.data)
- self.assertEqual(
- extract_token(first_response) == extract_token(second_response),
- single_access_token
- )
+ self.assertEqual(first_response.status_code, 200)
+ self.assertEqual(second_response.status_code, 200)
+ self.assertEqual(
+ extract_token(first_response) == extract_token(second_response),
+ single_access_token
+ )
def test_get_method(self):
response = self.client.get(self.url, self.data)
@@ -95,10 +100,11 @@ class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
# This is necessary because cms does not implement third party auth
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
@httpretty.activate
-class AccessTokenExchangeViewTestFacebook(
+class DOPAccessTokenExchangeViewTestFacebook(
+ DOPAdapterMixin,
AccessTokenExchangeViewTest,
ThirdPartyOAuthTestMixinFacebook,
- TestCase
+ TestCase,
):
"""
Tests for AccessTokenExchangeView used with Facebook
@@ -106,16 +112,48 @@ class AccessTokenExchangeViewTestFacebook(
pass
+@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
+@httpretty.activate
+class DOTAccessTokenExchangeViewTestFacebook(
+ DOTAdapterMixin,
+ AccessTokenExchangeViewTest,
+ ThirdPartyOAuthTestMixinFacebook,
+ TestCase,
+):
+ """
+ Rerun AccessTokenExchangeViewTestFacebook tests against DOT backend
+ """
+ pass
+
+
# This is necessary because cms does not implement third party auth
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
@httpretty.activate
-class AccessTokenExchangeViewTestGoogle(
+class DOPAccessTokenExchangeViewTestGoogle(
+ DOPAdapterMixin,
AccessTokenExchangeViewTest,
ThirdPartyOAuthTestMixinGoogle,
- TestCase
+ TestCase,
):
"""
- Tests for AccessTokenExchangeView used with Google
+ Tests for AccessTokenExchangeView used with Google using
+ django-oauth2-provider backend.
+ """
+ pass
+
+
+# This is necessary because cms does not implement third party auth
+@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
+@httpretty.activate
+class DOTAccessTokenExchangeViewTestGoogle(
+ DOTAdapterMixin,
+ AccessTokenExchangeViewTest,
+ ThirdPartyOAuthTestMixinGoogle,
+ TestCase,
+):
+ """
+ Tests for AccessTokenExchangeView used with Google using
+ django-oauth-toolkit backend.
"""
pass
diff --git a/common/djangoapps/auth_exchange/tests/utils.py b/common/djangoapps/auth_exchange/tests/utils.py
index 60608f292f..2629557c08 100644
--- a/common/djangoapps/auth_exchange/tests/utils.py
+++ b/common/djangoapps/auth_exchange/tests/utils.py
@@ -1,9 +1,8 @@
"""
Test utilities for OAuth access token exchange
"""
-import provider.constants
-from social.apps.django_app.default.models import UserSocialAuth
+from social.apps.django_app.default.models import UserSocialAuth
from third_party_auth.tests.utils import ThirdPartyOAuthTestMixin
@@ -37,6 +36,12 @@ class AccessTokenExchangeTestMixin(ThirdPartyOAuthTestMixin):
"""
raise NotImplementedError()
+ def _create_client(self):
+ """
+ Create an oauth2 client application using class defaults.
+ """
+ return self.create_public_client(self.user, self.client_id)
+
def test_minimal(self):
self._setup_provider_response(success=True)
self._assert_success(self.data, expected_scopes=[])
@@ -61,12 +66,12 @@ class AccessTokenExchangeTestMixin(ThirdPartyOAuthTestMixin):
)
def test_confidential_client(self):
- self.oauth_client.client_type = provider.constants.CONFIDENTIAL
- self.oauth_client.save()
+ self.data['client_id'] += '_confidential'
+ self.oauth_client = self.create_confidential_client(self.user, self.data['client_id'])
self._assert_error(
self.data,
"invalid_client",
- "test_client_id is not a public client"
+ "{}_confidential is not a public client".format(self.client_id),
)
def test_inactive_user(self):
diff --git a/common/djangoapps/auth_exchange/views.py b/common/djangoapps/auth_exchange/views.py
index abd31de9ec..6669e489c5 100644
--- a/common/djangoapps/auth_exchange/views.py
+++ b/common/djangoapps/auth_exchange/views.py
@@ -1,4 +1,3 @@
-# pylint: disable=abstract-method
"""
Views to support exchange of authentication credentials.
The following are currently implemented:
@@ -7,36 +6,52 @@ The following are currently implemented:
2. LoginWithAccessTokenView:
1st party (open-edx) OAuth 2.0 access token -> session cookie
"""
+
+# pylint: disable=abstract-method
+
from django.conf import settings
from django.contrib.auth import login
import django.contrib.auth as auth
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
+from edx_oauth2_provider.constants import SCOPE_VALUE_DICT
+from oauth2_provider.settings import oauth2_settings
+from oauth2_provider.views.base import TokenView as DOTAccessTokenView
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
from provider import constants
-from provider.oauth2.views import AccessTokenView as AccessTokenView
+from provider.oauth2.views import AccessTokenView as DOPAccessTokenView
from rest_framework import permissions
+from rest_framework.response import Response
from rest_framework.views import APIView
import social.apps.django_app.utils as social_utils
from auth_exchange.forms import AccessTokenExchangeForm
+from lms.djangoapps.oauth_dispatch import adapters
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
-class AccessTokenExchangeView(AccessTokenView):
+class AccessTokenExchangeBase(APIView):
"""
- View for token exchange from 3rd party OAuth access token to 1st party OAuth access token
+ View for token exchange from 3rd party OAuth access token to 1st party
+ OAuth access token.
"""
@method_decorator(csrf_exempt)
@method_decorator(social_utils.strategy("social:complete"))
def dispatch(self, *args, **kwargs):
- return super(AccessTokenExchangeView, self).dispatch(*args, **kwargs)
+ return super(AccessTokenExchangeBase, self).dispatch(*args, **kwargs)
def get(self, request, _backend): # pylint: disable=arguments-differ
- return super(AccessTokenExchangeView, self).get(request)
+ """
+ Pass through GET requests without the _backend
+ """
+ return super(AccessTokenExchangeBase, self).get(request)
def post(self, request, _backend): # pylint: disable=arguments-differ
- form = AccessTokenExchangeForm(request=request, data=request.POST)
+ """
+ Handle POST requests to get a first-party access token.
+ """
+ form = AccessTokenExchangeForm(request=request, oauth2_adapter=self.oauth2_adapter, data=request.POST) # pylint: disable=no-member
if not form.is_valid():
return self.error_response(form.errors)
@@ -44,12 +59,89 @@ class AccessTokenExchangeView(AccessTokenView):
scope = form.cleaned_data["scope"]
client = form.cleaned_data["client"]
+ return self.exchange_access_token(request, user, scope, client)
+
+ def exchange_access_token(self, request, user, scope, client):
+ """
+ Exchange third party credentials for an edx access token, and return a
+ serialized access token response.
+ """
if constants.SINGLE_ACCESS_TOKEN:
- edx_access_token = self.get_access_token(request, user, scope, client)
+ edx_access_token = self.get_access_token(request, user, scope, client) # pylint: disable=no-member
else:
edx_access_token = self.create_access_token(request, user, scope, client)
+ return self.access_token_response(edx_access_token) # pylint: disable=no-member
- return self.access_token_response(edx_access_token)
+
+class DOPAccessTokenExchangeView(AccessTokenExchangeBase, DOPAccessTokenView):
+ """
+ View for token exchange from 3rd party OAuth access token to 1st party
+ OAuth access token. Uses django-oauth2-provider (DOP) to manage access
+ tokens.
+ """
+
+ oauth2_adapter = adapters.DOPAdapter()
+
+
+class DOTAccessTokenExchangeView(AccessTokenExchangeBase, DOTAccessTokenView):
+ """
+ View for token exchange from 3rd party OAuth access token to 1st party
+ OAuth access token. Uses django-oauth-toolkit (DOT) to manage access
+ tokens.
+ """
+
+ oauth2_adapter = adapters.DOTAdapter()
+
+ def get(self, request, _backend):
+ return Response(status=400, data={
+ 'error': 'invalid_request',
+ 'error_description': 'Only POST requests allowed.',
+ })
+
+ def get_access_token(self, request, user, scope, client):
+ """
+ TODO: MA-2122: Reusing access tokens is not yet supported for DOT.
+ Just return a new access token.
+ """
+ return self.create_access_token(request, user, scope, client)
+
+ def create_access_token(self, request, user, scope, client):
+ """
+ Create and return a new access token.
+ """
+ _days = 24 * 60 * 60
+ token_generator = BearerToken(
+ expires_in=settings.OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS * _days,
+ request_validator=oauth2_settings.OAUTH2_VALIDATOR_CLASS(),
+ )
+ self._populate_create_access_token_request(request, user, scope, client)
+ return token_generator.create_token(request, refresh_token=True)
+
+ def access_token_response(self, token):
+ """
+ Wrap an access token in an appropriate response
+ """
+ return Response(data=token)
+
+ def _populate_create_access_token_request(self, request, user, scope, client):
+ """
+ django-oauth-toolkit expects certain non-standard attributes to
+ be present on the request object. This function modifies the
+ request object to match these expectations
+ """
+ request.user = user
+ request.scopes = [SCOPE_VALUE_DICT[scope]]
+ request.client = client
+ request.state = None
+ request.refresh_token = None
+ request.extra_credentials = None
+ request.grant_type = client.authorization_grant_type
+
+ def error_response(self, form_errors):
+ """
+ Return an error response consisting of the errors in the form
+ """
+ return Response(status=400, data=form_errors)
class LoginWithAccessTokenView(APIView):
diff --git a/common/djangoapps/third_party_auth/tests/utils.py b/common/djangoapps/third_party_auth/tests/utils.py
index cce2edd59b..588d23dce9 100644
--- a/common/djangoapps/third_party_auth/tests/utils.py
+++ b/common/djangoapps/third_party_auth/tests/utils.py
@@ -22,23 +22,30 @@ class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin):
USER_URL: The URL of the endpoint that the backend retrieves user data from
UID_FIELD: The field in the user data that the backend uses as the user id
"""
+ social_uid = "test_social_uid"
+ access_token = "test_access_token"
+ client_id = "test_client_id"
+
def setUp(self, create_user=True):
super(ThirdPartyOAuthTestMixin, self).setUp()
- self.social_uid = "test_social_uid"
- self.access_token = "test_access_token"
- self.client_id = "test_client_id"
- self.oauth_client = Client.objects.create(
- client_id=self.client_id,
- client_type=PUBLIC
- )
if create_user:
self.user = UserFactory()
UserSocialAuth.objects.create(user=self.user, provider=self.BACKEND, uid=self.social_uid)
+ self.oauth_client = self._create_client()
if self.BACKEND == 'google-oauth2':
self.configure_google_provider(enabled=True)
elif self.BACKEND == 'facebook':
self.configure_facebook_provider(enabled=True)
+ def _create_client(self):
+ """
+ Create an OAuth2 client application
+ """
+ return Client.objects.create(
+ client_id=self.client_id,
+ client_type=PUBLIC,
+ )
+
def _setup_provider_response(self, success=False, email=''):
"""
Register a mock response for the third party user information endpoint;
@@ -65,7 +72,7 @@ class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin):
self.USER_URL,
body=body,
status=status,
- content_type="application/json"
+ content_type="application/json",
)
diff --git a/common/test/db_cache/bok_choy_data_default.json b/common/test/db_cache/bok_choy_data_default.json
index aebe847da7..1eece3c36e 100644
--- a/common/test/db_cache/bok_choy_data_default.json
+++ b/common/test/db_cache/bok_choy_data_default.json
@@ -1 +1 @@
-[{"fields": {"model": "permission", "app_label": "auth"}, "model": "contenttypes.contenttype", "pk": 1}, {"fields": {"model": "group", "app_label": "auth"}, "model": "contenttypes.contenttype", "pk": 2}, {"fields": {"model": "user", "app_label": "auth"}, "model": "contenttypes.contenttype", "pk": 3}, {"fields": {"model": "contenttype", "app_label": "contenttypes"}, "model": "contenttypes.contenttype", "pk": 4}, {"fields": {"model": "session", "app_label": "sessions"}, "model": "contenttypes.contenttype", "pk": 5}, {"fields": {"model": "site", "app_label": "sites"}, "model": "contenttypes.contenttype", "pk": 6}, {"fields": {"model": "taskmeta", "app_label": "djcelery"}, "model": "contenttypes.contenttype", "pk": 7}, {"fields": {"model": "tasksetmeta", "app_label": "djcelery"}, "model": "contenttypes.contenttype", "pk": 8}, {"fields": {"model": "intervalschedule", "app_label": "djcelery"}, "model": "contenttypes.contenttype", "pk": 9}, {"fields": {"model": "crontabschedule", "app_label": "djcelery"}, "model": "contenttypes.contenttype", "pk": 10}, {"fields": {"model": "periodictasks", "app_label": "djcelery"}, "model": "contenttypes.contenttype", "pk": 11}, {"fields": {"model": "periodictask", "app_label": "djcelery"}, "model": "contenttypes.contenttype", "pk": 12}, {"fields": {"model": "workerstate", "app_label": "djcelery"}, "model": "contenttypes.contenttype", "pk": 13}, {"fields": {"model": "taskstate", "app_label": "djcelery"}, "model": "contenttypes.contenttype", "pk": 14}, {"fields": {"model": "globalstatusmessage", "app_label": "status"}, "model": "contenttypes.contenttype", "pk": 15}, {"fields": {"model": "coursemessage", "app_label": "status"}, "model": "contenttypes.contenttype", "pk": 16}, {"fields": {"model": "assetbaseurlconfig", "app_label": "static_replace"}, "model": "contenttypes.contenttype", "pk": 17}, {"fields": {"model": "assetexcludedextensionsconfig", "app_label": "static_replace"}, "model": "contenttypes.contenttype", "pk": 18}, {"fields": {"model": "courseassetcachettlconfig", "app_label": "contentserver"}, "model": "contenttypes.contenttype", "pk": 19}, {"fields": {"model": "studentmodule", "app_label": "courseware"}, "model": "contenttypes.contenttype", "pk": 20}, {"fields": {"model": "studentmodulehistory", "app_label": "courseware"}, "model": "contenttypes.contenttype", "pk": 21}, {"fields": {"model": "xmoduleuserstatesummaryfield", "app_label": "courseware"}, "model": "contenttypes.contenttype", "pk": 22}, {"fields": {"model": "xmodulestudentprefsfield", "app_label": "courseware"}, "model": "contenttypes.contenttype", "pk": 23}, {"fields": {"model": "xmodulestudentinfofield", "app_label": "courseware"}, "model": "contenttypes.contenttype", "pk": 24}, {"fields": {"model": "offlinecomputedgrade", "app_label": "courseware"}, "model": "contenttypes.contenttype", "pk": 25}, {"fields": {"model": "offlinecomputedgradelog", "app_label": "courseware"}, "model": "contenttypes.contenttype", "pk": 26}, {"fields": {"model": "studentfieldoverride", "app_label": "courseware"}, "model": "contenttypes.contenttype", "pk": 27}, {"fields": {"model": "anonymoususerid", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 28}, {"fields": {"model": "userstanding", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 29}, {"fields": {"model": "userprofile", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 30}, {"fields": {"model": "usersignupsource", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 31}, {"fields": {"model": "usertestgroup", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 32}, {"fields": {"model": "registration", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 33}, {"fields": {"model": "pendingnamechange", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 34}, {"fields": {"model": "pendingemailchange", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 35}, {"fields": {"model": "passwordhistory", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 36}, {"fields": {"model": "loginfailures", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 37}, {"fields": {"model": "historicalcourseenrollment", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 38}, {"fields": {"model": "courseenrollment", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 39}, {"fields": {"model": "manualenrollmentaudit", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 40}, {"fields": {"model": "courseenrollmentallowed", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 41}, {"fields": {"model": "courseaccessrole", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 42}, {"fields": {"model": "dashboardconfiguration", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 43}, {"fields": {"model": "linkedinaddtoprofileconfiguration", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 44}, {"fields": {"model": "entranceexamconfiguration", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 45}, {"fields": {"model": "languageproficiency", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 46}, {"fields": {"model": "courseenrollmentattribute", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 47}, {"fields": {"model": "enrollmentrefundconfiguration", "app_label": "student"}, "model": "contenttypes.contenttype", "pk": 48}, {"fields": {"model": "trackinglog", "app_label": "track"}, "model": "contenttypes.contenttype", "pk": 49}, {"fields": {"model": "ratelimitconfiguration", "app_label": "util"}, "model": "contenttypes.contenttype", "pk": 50}, {"fields": {"model": "certificatewhitelist", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 51}, {"fields": {"model": "generatedcertificate", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 52}, {"fields": {"model": "certificategenerationhistory", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 53}, {"fields": {"model": "certificateinvalidation", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 54}, {"fields": {"model": "examplecertificateset", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 55}, {"fields": {"model": "examplecertificate", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 56}, {"fields": {"model": "certificategenerationcoursesetting", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 57}, {"fields": {"model": "certificategenerationconfiguration", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 58}, {"fields": {"model": "certificatehtmlviewconfiguration", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 59}, {"fields": {"model": "badgeassertion", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 60}, {"fields": {"model": "badgeimageconfiguration", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 61}, {"fields": {"model": "certificatetemplate", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 62}, {"fields": {"model": "certificatetemplateasset", "app_label": "certificates"}, "model": "contenttypes.contenttype", "pk": 63}, {"fields": {"model": "instructortask", "app_label": "instructor_task"}, "model": "contenttypes.contenttype", "pk": 64}, {"fields": {"model": "courseusergroup", "app_label": "course_groups"}, "model": "contenttypes.contenttype", "pk": 65}, {"fields": {"model": "cohortmembership", "app_label": "course_groups"}, "model": "contenttypes.contenttype", "pk": 66}, {"fields": {"model": "courseusergrouppartitiongroup", "app_label": "course_groups"}, "model": "contenttypes.contenttype", "pk": 67}, {"fields": {"model": "coursecohortssettings", "app_label": "course_groups"}, "model": "contenttypes.contenttype", "pk": 68}, {"fields": {"model": "coursecohort", "app_label": "course_groups"}, "model": "contenttypes.contenttype", "pk": 69}, {"fields": {"model": "courseemail", "app_label": "bulk_email"}, "model": "contenttypes.contenttype", "pk": 70}, {"fields": {"model": "optout", "app_label": "bulk_email"}, "model": "contenttypes.contenttype", "pk": 71}, {"fields": {"model": "courseemailtemplate", "app_label": "bulk_email"}, "model": "contenttypes.contenttype", "pk": 72}, {"fields": {"model": "courseauthorization", "app_label": "bulk_email"}, "model": "contenttypes.contenttype", "pk": 73}, {"fields": {"model": "brandinginfoconfig", "app_label": "branding"}, "model": "contenttypes.contenttype", "pk": 74}, {"fields": {"model": "brandingapiconfig", "app_label": "branding"}, "model": "contenttypes.contenttype", "pk": 75}, {"fields": {"model": "externalauthmap", "app_label": "external_auth"}, "model": "contenttypes.contenttype", "pk": 76}, {"fields": {"model": "nonce", "app_label": "django_openid_auth"}, "model": "contenttypes.contenttype", "pk": 77}, {"fields": {"model": "association", "app_label": "django_openid_auth"}, "model": "contenttypes.contenttype", "pk": 78}, {"fields": {"model": "useropenid", "app_label": "django_openid_auth"}, "model": "contenttypes.contenttype", "pk": 79}, {"fields": {"model": "client", "app_label": "oauth2"}, "model": "contenttypes.contenttype", "pk": 80}, {"fields": {"model": "grant", "app_label": "oauth2"}, "model": "contenttypes.contenttype", "pk": 81}, {"fields": {"model": "accesstoken", "app_label": "oauth2"}, "model": "contenttypes.contenttype", "pk": 82}, {"fields": {"model": "refreshtoken", "app_label": "oauth2"}, "model": "contenttypes.contenttype", "pk": 83}, {"fields": {"model": "trustedclient", "app_label": "oauth2_provider"}, "model": "contenttypes.contenttype", "pk": 84}, {"fields": {"model": "oauth2providerconfig", "app_label": "third_party_auth"}, "model": "contenttypes.contenttype", "pk": 85}, {"fields": {"model": "samlproviderconfig", "app_label": "third_party_auth"}, "model": "contenttypes.contenttype", "pk": 86}, {"fields": {"model": "samlconfiguration", "app_label": "third_party_auth"}, "model": "contenttypes.contenttype", "pk": 87}, {"fields": {"model": "samlproviderdata", "app_label": "third_party_auth"}, "model": "contenttypes.contenttype", "pk": 88}, {"fields": {"model": "ltiproviderconfig", "app_label": "third_party_auth"}, "model": "contenttypes.contenttype", "pk": 89}, {"fields": {"model": "providerapipermissions", "app_label": "third_party_auth"}, "model": "contenttypes.contenttype", "pk": 90}, {"fields": {"model": "nonce", "app_label": "oauth_provider"}, "model": "contenttypes.contenttype", "pk": 91}, {"fields": {"model": "scope", "app_label": "oauth_provider"}, "model": "contenttypes.contenttype", "pk": 92}, {"fields": {"model": "consumer", "app_label": "oauth_provider"}, "model": "contenttypes.contenttype", "pk": 93}, {"fields": {"model": "token", "app_label": "oauth_provider"}, "model": "contenttypes.contenttype", "pk": 94}, {"fields": {"model": "resource", "app_label": "oauth_provider"}, "model": "contenttypes.contenttype", "pk": 95}, {"fields": {"model": "article", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 96}, {"fields": {"model": "articleforobject", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 97}, {"fields": {"model": "articlerevision", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 98}, {"fields": {"model": "urlpath", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 99}, {"fields": {"model": "articleplugin", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 100}, {"fields": {"model": "reusableplugin", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 101}, {"fields": {"model": "simpleplugin", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 102}, {"fields": {"model": "revisionplugin", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 103}, {"fields": {"model": "revisionpluginrevision", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 104}, {"fields": {"model": "image", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 105}, {"fields": {"model": "imagerevision", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 106}, {"fields": {"model": "attachment", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 107}, {"fields": {"model": "attachmentrevision", "app_label": "wiki"}, "model": "contenttypes.contenttype", "pk": 108}, {"fields": {"model": "notificationtype", "app_label": "django_notify"}, "model": "contenttypes.contenttype", "pk": 109}, {"fields": {"model": "settings", "app_label": "django_notify"}, "model": "contenttypes.contenttype", "pk": 110}, {"fields": {"model": "subscription", "app_label": "django_notify"}, "model": "contenttypes.contenttype", "pk": 111}, {"fields": {"model": "notification", "app_label": "django_notify"}, "model": "contenttypes.contenttype", "pk": 112}, {"fields": {"model": "logentry", "app_label": "admin"}, "model": "contenttypes.contenttype", "pk": 113}, {"fields": {"model": "role", "app_label": "django_comment_common"}, "model": "contenttypes.contenttype", "pk": 114}, {"fields": {"model": "permission", "app_label": "django_comment_common"}, "model": "contenttypes.contenttype", "pk": 115}, {"fields": {"model": "note", "app_label": "notes"}, "model": "contenttypes.contenttype", "pk": 116}, {"fields": {"model": "splashconfig", "app_label": "splash"}, "model": "contenttypes.contenttype", "pk": 117}, {"fields": {"model": "userpreference", "app_label": "user_api"}, "model": "contenttypes.contenttype", "pk": 118}, {"fields": {"model": "usercoursetag", "app_label": "user_api"}, "model": "contenttypes.contenttype", "pk": 119}, {"fields": {"model": "userorgtag", "app_label": "user_api"}, "model": "contenttypes.contenttype", "pk": 120}, {"fields": {"model": "order", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 121}, {"fields": {"model": "orderitem", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 122}, {"fields": {"model": "invoice", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 123}, {"fields": {"model": "invoicetransaction", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 124}, {"fields": {"model": "invoiceitem", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 125}, {"fields": {"model": "courseregistrationcodeinvoiceitem", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 126}, {"fields": {"model": "invoicehistory", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 127}, {"fields": {"model": "courseregistrationcode", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 128}, {"fields": {"model": "registrationcoderedemption", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 129}, {"fields": {"model": "coupon", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 130}, {"fields": {"model": "couponredemption", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 131}, {"fields": {"model": "paidcourseregistration", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 132}, {"fields": {"model": "courseregcodeitem", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 133}, {"fields": {"model": "courseregcodeitemannotation", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 134}, {"fields": {"model": "paidcourseregistrationannotation", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 135}, {"fields": {"model": "certificateitem", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 136}, {"fields": {"model": "donationconfiguration", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 137}, {"fields": {"model": "donation", "app_label": "shoppingcart"}, "model": "contenttypes.contenttype", "pk": 138}, {"fields": {"model": "coursemode", "app_label": "course_modes"}, "model": "contenttypes.contenttype", "pk": 139}, {"fields": {"model": "coursemodesarchive", "app_label": "course_modes"}, "model": "contenttypes.contenttype", "pk": 140}, {"fields": {"model": "coursemodeexpirationconfig", "app_label": "course_modes"}, "model": "contenttypes.contenttype", "pk": 141}, {"fields": {"model": "softwaresecurephotoverification", "app_label": "verify_student"}, "model": "contenttypes.contenttype", "pk": 142}, {"fields": {"model": "historicalverificationdeadline", "app_label": "verify_student"}, "model": "contenttypes.contenttype", "pk": 143}, {"fields": {"model": "verificationdeadline", "app_label": "verify_student"}, "model": "contenttypes.contenttype", "pk": 144}, {"fields": {"model": "verificationcheckpoint", "app_label": "verify_student"}, "model": "contenttypes.contenttype", "pk": 145}, {"fields": {"model": "verificationstatus", "app_label": "verify_student"}, "model": "contenttypes.contenttype", "pk": 146}, {"fields": {"model": "incoursereverificationconfiguration", "app_label": "verify_student"}, "model": "contenttypes.contenttype", "pk": 147}, {"fields": {"model": "icrvstatusemailsconfiguration", "app_label": "verify_student"}, "model": "contenttypes.contenttype", "pk": 148}, {"fields": {"model": "skippedreverification", "app_label": "verify_student"}, "model": "contenttypes.contenttype", "pk": 149}, {"fields": {"model": "darklangconfig", "app_label": "dark_lang"}, "model": "contenttypes.contenttype", "pk": 150}, {"fields": {"model": "microsite", "app_label": "microsite_configuration"}, "model": "contenttypes.contenttype", "pk": 151}, {"fields": {"model": "micrositehistory", "app_label": "microsite_configuration"}, "model": "contenttypes.contenttype", "pk": 152}, {"fields": {"model": "historicalmicrositeorganizationmapping", "app_label": "microsite_configuration"}, "model": "contenttypes.contenttype", "pk": 153}, {"fields": {"model": "micrositeorganizationmapping", "app_label": "microsite_configuration"}, "model": "contenttypes.contenttype", "pk": 154}, {"fields": {"model": "historicalmicrositetemplate", "app_label": "microsite_configuration"}, "model": "contenttypes.contenttype", "pk": 155}, {"fields": {"model": "micrositetemplate", "app_label": "microsite_configuration"}, "model": "contenttypes.contenttype", "pk": 156}, {"fields": {"model": "whitelistedrssurl", "app_label": "rss_proxy"}, "model": "contenttypes.contenttype", "pk": 157}, {"fields": {"model": "embargoedcourse", "app_label": "embargo"}, "model": "contenttypes.contenttype", "pk": 158}, {"fields": {"model": "embargoedstate", "app_label": "embargo"}, "model": "contenttypes.contenttype", "pk": 159}, {"fields": {"model": "restrictedcourse", "app_label": "embargo"}, "model": "contenttypes.contenttype", "pk": 160}, {"fields": {"model": "country", "app_label": "embargo"}, "model": "contenttypes.contenttype", "pk": 161}, {"fields": {"model": "countryaccessrule", "app_label": "embargo"}, "model": "contenttypes.contenttype", "pk": 162}, {"fields": {"model": "courseaccessrulehistory", "app_label": "embargo"}, "model": "contenttypes.contenttype", "pk": 163}, {"fields": {"model": "ipfilter", "app_label": "embargo"}, "model": "contenttypes.contenttype", "pk": 164}, {"fields": {"model": "coursererunstate", "app_label": "course_action_state"}, "model": "contenttypes.contenttype", "pk": 165}, {"fields": {"model": "mobileapiconfig", "app_label": "mobile_api"}, "model": "contenttypes.contenttype", "pk": 166}, {"fields": {"model": "usersocialauth", "app_label": "default"}, "model": "contenttypes.contenttype", "pk": 167}, {"fields": {"model": "nonce", "app_label": "default"}, "model": "contenttypes.contenttype", "pk": 168}, {"fields": {"model": "association", "app_label": "default"}, "model": "contenttypes.contenttype", "pk": 169}, {"fields": {"model": "code", "app_label": "default"}, "model": "contenttypes.contenttype", "pk": 170}, {"fields": {"model": "surveyform", "app_label": "survey"}, "model": "contenttypes.contenttype", "pk": 171}, {"fields": {"model": "surveyanswer", "app_label": "survey"}, "model": "contenttypes.contenttype", "pk": 172}, {"fields": {"model": "xblockasidesconfig", "app_label": "lms_xblock"}, "model": "contenttypes.contenttype", "pk": 173}, {"fields": {"model": "courseoverview", "app_label": "course_overviews"}, "model": "contenttypes.contenttype", "pk": 174}, {"fields": {"model": "courseoverviewtab", "app_label": "course_overviews"}, "model": "contenttypes.contenttype", "pk": 175}, {"fields": {"model": "courseoverviewimageset", "app_label": "course_overviews"}, "model": "contenttypes.contenttype", "pk": 176}, {"fields": {"model": "courseoverviewimageconfig", "app_label": "course_overviews"}, "model": "contenttypes.contenttype", "pk": 177}, {"fields": {"model": "coursestructure", "app_label": "course_structures"}, "model": "contenttypes.contenttype", "pk": 178}, {"fields": {"model": "corsmodel", "app_label": "corsheaders"}, "model": "contenttypes.contenttype", "pk": 179}, {"fields": {"model": "xdomainproxyconfiguration", "app_label": "cors_csrf"}, "model": "contenttypes.contenttype", "pk": 180}, {"fields": {"model": "commerceconfiguration", "app_label": "commerce"}, "model": "contenttypes.contenttype", "pk": 181}, {"fields": {"model": "creditprovider", "app_label": "credit"}, "model": "contenttypes.contenttype", "pk": 182}, {"fields": {"model": "creditcourse", "app_label": "credit"}, "model": "contenttypes.contenttype", "pk": 183}, {"fields": {"model": "creditrequirement", "app_label": "credit"}, "model": "contenttypes.contenttype", "pk": 184}, {"fields": {"model": "historicalcreditrequirementstatus", "app_label": "credit"}, "model": "contenttypes.contenttype", "pk": 185}, {"fields": {"model": "creditrequirementstatus", "app_label": "credit"}, "model": "contenttypes.contenttype", "pk": 186}, {"fields": {"model": "crediteligibility", "app_label": "credit"}, "model": "contenttypes.contenttype", "pk": 187}, {"fields": {"model": "historicalcreditrequest", "app_label": "credit"}, "model": "contenttypes.contenttype", "pk": 188}, {"fields": {"model": "creditrequest", "app_label": "credit"}, "model": "contenttypes.contenttype", "pk": 189}, {"fields": {"model": "courseteam", "app_label": "teams"}, "model": "contenttypes.contenttype", "pk": 190}, {"fields": {"model": "courseteammembership", "app_label": "teams"}, "model": "contenttypes.contenttype", "pk": 191}, {"fields": {"model": "xblockdisableconfig", "app_label": "xblock_django"}, "model": "contenttypes.contenttype", "pk": 192}, {"fields": {"model": "bookmark", "app_label": "bookmarks"}, "model": "contenttypes.contenttype", "pk": 193}, {"fields": {"model": "xblockcache", "app_label": "bookmarks"}, "model": "contenttypes.contenttype", "pk": 194}, {"fields": {"model": "programsapiconfig", "app_label": "programs"}, "model": "contenttypes.contenttype", "pk": 195}, {"fields": {"model": "selfpacedconfiguration", "app_label": "self_paced"}, "model": "contenttypes.contenttype", "pk": 196}, {"fields": {"model": "kvstore", "app_label": "thumbnail"}, "model": "contenttypes.contenttype", "pk": 197}, {"fields": {"model": "credentialsapiconfig", "app_label": "credentials"}, "model": "contenttypes.contenttype", "pk": 198}, {"fields": {"model": "milestone", "app_label": "milestones"}, "model": "contenttypes.contenttype", "pk": 199}, {"fields": {"model": "milestonerelationshiptype", "app_label": "milestones"}, "model": "contenttypes.contenttype", "pk": 200}, {"fields": {"model": "coursemilestone", "app_label": "milestones"}, "model": "contenttypes.contenttype", "pk": 201}, {"fields": {"model": "coursecontentmilestone", "app_label": "milestones"}, "model": "contenttypes.contenttype", "pk": 202}, {"fields": {"model": "usermilestone", "app_label": "milestones"}, "model": "contenttypes.contenttype", "pk": 203}, {"fields": {"model": "studentitem", "app_label": "submissions"}, "model": "contenttypes.contenttype", "pk": 204}, {"fields": {"model": "submission", "app_label": "submissions"}, "model": "contenttypes.contenttype", "pk": 205}, {"fields": {"model": "score", "app_label": "submissions"}, "model": "contenttypes.contenttype", "pk": 206}, {"fields": {"model": "scoresummary", "app_label": "submissions"}, "model": "contenttypes.contenttype", "pk": 207}, {"fields": {"model": "scoreannotation", "app_label": "submissions"}, "model": "contenttypes.contenttype", "pk": 208}, {"fields": {"model": "rubric", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 209}, {"fields": {"model": "criterion", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 210}, {"fields": {"model": "criterionoption", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 211}, {"fields": {"model": "assessment", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 212}, {"fields": {"model": "assessmentpart", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 213}, {"fields": {"model": "assessmentfeedbackoption", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 214}, {"fields": {"model": "assessmentfeedback", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 215}, {"fields": {"model": "peerworkflow", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 216}, {"fields": {"model": "peerworkflowitem", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 217}, {"fields": {"model": "trainingexample", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 218}, {"fields": {"model": "studenttrainingworkflow", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 219}, {"fields": {"model": "studenttrainingworkflowitem", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 220}, {"fields": {"model": "aiclassifierset", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 221}, {"fields": {"model": "aiclassifier", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 222}, {"fields": {"model": "aitrainingworkflow", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 223}, {"fields": {"model": "aigradingworkflow", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 224}, {"fields": {"model": "staffworkflow", "app_label": "assessment"}, "model": "contenttypes.contenttype", "pk": 225}, {"fields": {"model": "assessmentworkflow", "app_label": "workflow"}, "model": "contenttypes.contenttype", "pk": 226}, {"fields": {"model": "assessmentworkflowstep", "app_label": "workflow"}, "model": "contenttypes.contenttype", "pk": 227}, {"fields": {"model": "assessmentworkflowcancellation", "app_label": "workflow"}, "model": "contenttypes.contenttype", "pk": 228}, {"fields": {"model": "profile", "app_label": "edxval"}, "model": "contenttypes.contenttype", "pk": 229}, {"fields": {"model": "video", "app_label": "edxval"}, "model": "contenttypes.contenttype", "pk": 230}, {"fields": {"model": "coursevideo", "app_label": "edxval"}, "model": "contenttypes.contenttype", "pk": 231}, {"fields": {"model": "encodedvideo", "app_label": "edxval"}, "model": "contenttypes.contenttype", "pk": 232}, {"fields": {"model": "subtitle", "app_label": "edxval"}, "model": "contenttypes.contenttype", "pk": 233}, {"fields": {"model": "proctoredexam", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 234}, {"fields": {"model": "proctoredexamreviewpolicy", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 235}, {"fields": {"model": "proctoredexamreviewpolicyhistory", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 236}, {"fields": {"model": "proctoredexamstudentattempt", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 237}, {"fields": {"model": "proctoredexamstudentattempthistory", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 238}, {"fields": {"model": "proctoredexamstudentallowance", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 239}, {"fields": {"model": "proctoredexamstudentallowancehistory", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 240}, {"fields": {"model": "proctoredexamsoftwaresecurereview", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 241}, {"fields": {"model": "proctoredexamsoftwaresecurereviewhistory", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 242}, {"fields": {"model": "proctoredexamsoftwaresecurecomment", "app_label": "edx_proctoring"}, "model": "contenttypes.contenttype", "pk": 243}, {"fields": {"model": "organization", "app_label": "organizations"}, "model": "contenttypes.contenttype", "pk": 244}, {"fields": {"model": "organizationcourse", "app_label": "organizations"}, "model": "contenttypes.contenttype", "pk": 245}, {"fields": {"model": "studentmodulehistoryextended", "app_label": "coursewarehistoryextended"}, "model": "contenttypes.contenttype", "pk": 246}, {"fields": {"model": "videouploadconfig", "app_label": "contentstore"}, "model": "contenttypes.contenttype", "pk": 247}, {"fields": {"model": "pushnotificationconfig", "app_label": "contentstore"}, "model": "contenttypes.contenttype", "pk": 248}, {"fields": {"model": "coursecreator", "app_label": "course_creators"}, "model": "contenttypes.contenttype", "pk": 249}, {"fields": {"model": "studioconfig", "app_label": "xblock_config"}, "model": "contenttypes.contenttype", "pk": 250}, {"fields": {"domain": "example.com", "name": "example.com"}, "model": "sites.site", "pk": 1}, {"fields": {"default": false, "mode": "honor", "icon": "badges/honor.png"}, "model": "certificates.badgeimageconfiguration", "pk": 1}, {"fields": {"default": false, "mode": "verified", "icon": "badges/verified.png"}, "model": "certificates.badgeimageconfiguration", "pk": 2}, {"fields": {"default": false, "mode": "professional", "icon": "badges/professional.png"}, "model": "certificates.badgeimageconfiguration", "pk": 3}, {"fields": {"plain_template": "{course_title}\n\n{{message_body}}\r\n----\r\nCopyright 2013 edX, All rights reserved.\r\n----\r\nConnect with edX:\r\nFacebook (http://facebook.com/edxonline)\r\nTwitter (http://twitter.com/edxonline)\r\nGoogle+ (https://plus.google.com/108235383044095082735)\r\nMeetup (http://www.meetup.com/edX-Communities/)\r\n----\r\nThis email was automatically sent from {platform_name}.\r\nYou are receiving this email at address {email} because you are enrolled in {course_title}\r\n(URL: {course_url} ).\r\nTo stop receiving email like this, update your course email settings at {email_settings_url}.\r\n", "html_template": "
Update from {course_title}