Commit f89a9d37 by Bertrand Marron

Add IONISx auth platform

parent d9dcd4db
...@@ -286,6 +286,12 @@ SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIME ...@@ -286,6 +286,12 @@ SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIME
##### X-Frame-Options response header settings ##### ##### X-Frame-Options response header settings #####
X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS) X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS)
##### Third-party auth options ################################################
THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH)
IONISX_AUTH = AUTH_TOKENS.get('IONISX_AUTH')
if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
MIDDLEWARE_CLASSES += ('third_party_auth.middleware.PortalSynchronizerMiddleware',)
##### ADVANCED_SECURITY_CONFIG ##### ##### ADVANCED_SECURITY_CONFIG #####
ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {}) ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {})
......
...@@ -96,6 +96,10 @@ FEATURES = { ...@@ -96,6 +96,10 @@ FEATURES = {
# Turn on/off Microsites feature # Turn on/off Microsites feature
'USE_MICROSITES': False, 'USE_MICROSITES': False,
# Turn on third-party auth. Disabled for now because full implementations are not yet available. Remember to syncdb
# if you enable this; we don't create tables by default.
'ENABLE_THIRD_PARTY_AUTH': False,
# Allow creating courses with non-ascii characters in the course id # Allow creating courses with non-ascii characters in the course id
'ALLOW_UNICODE_COURSE_ID': False, 'ALLOW_UNICODE_COURSE_ID': False,
...@@ -716,6 +720,10 @@ for app_name in OPTIONAL_APPS: ...@@ -716,6 +720,10 @@ for app_name in OPTIONAL_APPS:
continue continue
INSTALLED_APPS += (app_name,) INSTALLED_APPS += (app_name,)
# Stub for third_party_auth options.
# See common/djangoapps/third_party_auth/settings.py for configuration details.
THIRD_PARTY_AUTH = {}
### ADVANCED_SECURITY_CONFIG ### ADVANCED_SECURITY_CONFIG
# Empty by default # Empty by default
ADVANCED_SECURITY_CONFIG = {} ADVANCED_SECURITY_CONFIG = {}
......
...@@ -21,6 +21,9 @@ def run(): ...@@ -21,6 +21,9 @@ def run():
add_mimetypes() add_mimetypes()
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False):
enable_third_party_auth()
def add_mimetypes(): def add_mimetypes():
""" """
...@@ -34,3 +37,14 @@ def add_mimetypes(): ...@@ -34,3 +37,14 @@ def add_mimetypes():
mimetypes.add_type('application/x-font-opentype', '.otf') mimetypes.add_type('application/x-font-opentype', '.otf')
mimetypes.add_type('application/x-font-ttf', '.ttf') mimetypes.add_type('application/x-font-ttf', '.ttf')
mimetypes.add_type('application/font-woff', '.woff') mimetypes.add_type('application/font-woff', '.woff')
def enable_third_party_auth():
"""
Enable the use of third_party_auth, which allows users to sign in to edX
using other identity providers. For configuration details, see
common/djangoapps/third_party_auth/settings.py.
"""
from third_party_auth import settings as auth_settings
auth_settings.apply_settings(settings.THIRD_PARTY_AUTH, settings)
...@@ -49,6 +49,9 @@ urlpatterns += patterns( ...@@ -49,6 +49,9 @@ urlpatterns += patterns(
url(r'^create_account$', 'student.views.create_account', name='create_account'), url(r'^create_account$', 'student.views.create_account', name='create_account'),
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name='activate'), url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name='activate'),
url(r'^signin$', 'student.views.signin_user', name="login"),
url(r'^signup$', 'student.views.signin_user', name='signup'),
# ajax view that actually does the work # ajax view that actually does the work
url(r'^login_post$', 'student.views.login_user', name='login_post'), url(r'^login_post$', 'student.views.login_user', name='login_post'),
...@@ -62,8 +65,6 @@ urlpatterns += patterns( ...@@ -62,8 +65,6 @@ urlpatterns += patterns(
url(r'^$', 'howitworks', name='homepage'), url(r'^$', 'howitworks', name='homepage'),
url(r'^howitworks$', 'howitworks'), url(r'^howitworks$', 'howitworks'),
url(r'^signup$', 'signup', name='signup'),
url(r'^signin$', 'login_page', name='login'),
url(r'^request_course_creator$', 'request_course_creator'), url(r'^request_course_creator$', 'request_course_creator'),
url(r'^course_team/{}/(?P<email>.+)?$'.format(settings.COURSE_KEY_PATTERN), 'course_team_handler'), url(r'^course_team/{}/(?P<email>.+)?$'.format(settings.COURSE_KEY_PATTERN), 'course_team_handler'),
...@@ -133,6 +134,12 @@ if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'): ...@@ -133,6 +134,12 @@ if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
url(r'^auto_auth$', 'student.views.auto_auth'), url(r'^auto_auth$', 'student.views.auto_auth'),
) )
# Third-party auth.
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
urlpatterns += (
url(r'', include('third_party_auth.urls')),
)
if settings.DEBUG: if settings.DEBUG:
try: try:
from .urls_dev import urlpatterns as dev_urlpatterns from .urls_dev import urlpatterns as dev_urlpatterns
......
...@@ -26,7 +26,7 @@ from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbi ...@@ -26,7 +26,7 @@ from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbi
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ungettext from django.utils.translation import ungettext
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.utils.http import cookie_date, base36_to_int from django.utils.http import cookie_date, base36_to_int, urlquote
from django.utils.translation import ugettext as _, get_language from django.utils.translation import ugettext as _, get_language
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
...@@ -100,6 +100,7 @@ from util.password_policy_validators import ( ...@@ -100,6 +100,7 @@ from util.password_policy_validators import (
import third_party_auth import third_party_auth
from third_party_auth import pipeline, provider from third_party_auth import pipeline, provider
from student.helpers import auth_pipeline_urls, set_logged_in_cookie from student.helpers import auth_pipeline_urls, set_logged_in_cookie
from social.apps.django_app.default import models
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
...@@ -347,6 +348,20 @@ def signin_user(request): ...@@ -347,6 +348,20 @@ def signin_user(request):
""" """
This view will display the non-modal login form This view will display the non-modal login form
""" """
if request.user.is_authenticated():
return redirect(reverse('dashboard'))
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
# Redirect to IONISx, we don't want the registration form.
get = request.GET.copy()
if 'course_id' in request.GET:
request.session['enroll_course_id'] = request.GET.get('course_id')
get.update({ 'next': reverse('about_course', kwargs={ 'course_id': unicode(request.GET.get('course_id')) }) })
redirect_uri = reverse('social:begin', args=('portal-oauth2',))
return external_auth.views.redirect_with_get(redirect_uri, get, do_reverse=False)
if (settings.FEATURES['AUTH_USE_CERTIFICATES'] and if (settings.FEATURES['AUTH_USE_CERTIFICATES'] and
external_auth.views.ssl_get_cert_from_request(request)): external_auth.views.ssl_get_cert_from_request(request)):
# SSL login doesn't require a view, so redirect # SSL login doesn't require a view, so redirect
...@@ -356,8 +371,6 @@ def signin_user(request): ...@@ -356,8 +371,6 @@ def signin_user(request):
if settings.FEATURES.get('AUTH_USE_CAS'): if settings.FEATURES.get('AUTH_USE_CAS'):
# If CAS is enabled, redirect auth handling to there # If CAS is enabled, redirect auth handling to there
return redirect(reverse('cas-login')) return redirect(reverse('cas-login'))
if request.user.is_authenticated():
return redirect(reverse('dashboard'))
course_id = request.GET.get('course_id') course_id = request.GET.get('course_id')
context = { context = {
...@@ -376,7 +389,6 @@ def signin_user(request): ...@@ -376,7 +389,6 @@ def signin_user(request):
return render_to_response('login.html', context) return render_to_response('login.html', context)
@ensure_csrf_cookie @ensure_csrf_cookie
def register_user(request, extra_context=None): def register_user(request, extra_context=None):
""" """
...@@ -384,6 +396,18 @@ def register_user(request, extra_context=None): ...@@ -384,6 +396,18 @@ def register_user(request, extra_context=None):
""" """
if request.user.is_authenticated(): if request.user.is_authenticated():
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
# Redirect to IONISx, we don't want the registration form.
get = request.GET.copy()
if 'course_id' in request.GET:
request.session['enroll_course_id'] = request.GET.get('course_id')
get.update({ 'next': reverse('about_course', kwargs={ 'course_id': unicode(request.GET.get('course_id')) }) })
redirect_uri = reverse('social:begin', args=('portal-oauth2',))
return external_auth.views.redirect_with_get(redirect_uri, get, do_reverse=False)
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'): if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to branding to process their certificate if SSL is enabled # Redirect to branding to process their certificate if SSL is enabled
# and registration is disabled. # and registration is disabled.
...@@ -884,6 +908,13 @@ def _get_course_enrollment_domain(course_id): ...@@ -884,6 +908,13 @@ def _get_course_enrollment_domain(course_id):
@never_cache @never_cache
@ensure_csrf_cookie @ensure_csrf_cookie
def auth_with_no_login(request):
redirect_uri = reverse('social:begin', args=('portal-oauth2',))
return external_auth.views.redirect_with_get(redirect_uri, request.GET, do_reverse=False)
@never_cache
@ensure_csrf_cookie
def accounts_login(request): def accounts_login(request):
""" """
This view is mainly used as the redirect from the @login_required decorator. I don't believe that This view is mainly used as the redirect from the @login_required decorator. I don't believe that
...@@ -1157,11 +1188,7 @@ def logout_user(request): ...@@ -1157,11 +1188,7 @@ def logout_user(request):
# We do not log here, because we have a handler registered # We do not log here, because we have a handler registered
# to perform logging on successful logouts. # to perform logging on successful logouts.
logout(request) logout(request)
if settings.FEATURES.get('AUTH_USE_CAS'): response = redirect(settings.IONISX_AUTH.get('LOGOUT_URL'))
target = reverse('cas-logout')
else:
target = '/'
response = redirect(target)
response.delete_cookie( response.delete_cookie(
settings.EDXMKTG_COOKIE_NAME, settings.EDXMKTG_COOKIE_NAME,
path='/', domain=settings.SESSION_COOKIE_DOMAIN, path='/', domain=settings.SESSION_COOKIE_DOMAIN,
......
"""Middleware classes for third_party_auth.""" """Middleware classes for third_party_auth."""
import logging
import requests
from django.conf import settings
from django.contrib.auth import logout
from django.shortcuts import redirect
from django.contrib.auth.models import AnonymousUser
from student.models import UserProfile
from social.apps.django_app.default import models
from social.apps.django_app.middleware import SocialAuthExceptionMiddleware from social.apps.django_app.middleware import SocialAuthExceptionMiddleware
from . import pipeline from . import pipeline
from . import portal
log = logging.getLogger(__file__)
class ExceptionMiddleware(SocialAuthExceptionMiddleware): class ExceptionMiddleware(SocialAuthExceptionMiddleware):
"""Custom middleware that handles conditional redirection.""" """Custom middleware that handles conditional redirection."""
...@@ -23,3 +35,49 @@ class ExceptionMiddleware(SocialAuthExceptionMiddleware): ...@@ -23,3 +35,49 @@ class ExceptionMiddleware(SocialAuthExceptionMiddleware):
redirect_uri = pipeline.AUTH_DISPATCH_URLS[auth_entry] redirect_uri = pipeline.AUTH_DISPATCH_URLS[auth_entry]
return redirect_uri return redirect_uri
class PortalSynchronizerMiddleware(object):
"""Custom middleware to synchronize user status of LMS with Portal provider."""
def process_request(self, request):
if request.user.is_authenticated():
user = request.user
social_auth = models.DjangoStorage.user.get_social_auth_for_user(user)
if len(social_auth) == 1:
social_data = social_auth[0]
try:
r = requests.get(
settings.IONISX_AUTH.get('USER_DATA_URL'),
headers={'Authorization': 'Bearer {0}'.format(social_data.extra_data['access_token'])}
)
except requests.ConnectionError as err:
log.warning(err)
return
body = r.json()
if r.status_code != 200:
if body and u'error' in body and u'redirectTo' in body[u'error']:
return redirect(body[u'error'][u'redirectTo'])
else:
return logout(request)
if body:
_id = body['_id']
email = portal.get_primary_email(body['emails'])
username = body['username']
name = body['name']
if (user.email != email or user.username != body['username']):
log.info('User {} needs to be updated'.format(_id))
user.email = email
user.username = username
user.save()
if user.profile.name != body['name']:
log.info('User profile for {} needs to be updated'.format(_id))
user.profile.name = name
user.profile.save()
...@@ -62,6 +62,7 @@ import string # pylint: disable-msg=deprecated-module ...@@ -62,6 +62,7 @@ import string # pylint: disable-msg=deprecated-module
from collections import OrderedDict from collections import OrderedDict
import urllib import urllib
import analytics import analytics
import logging
from eventtracking import tracker from eventtracking import tracker
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -71,6 +72,10 @@ from django.shortcuts import redirect ...@@ -71,6 +72,10 @@ from django.shortcuts import redirect
from social.apps.django_app.default import models from social.apps.django_app.default import models
from social.exceptions import AuthException from social.exceptions import AuthException
from social.pipeline import partial from social.pipeline import partial
from django.db import IntegrityError, transaction
from student.models import (
Registration, UserProfile, create_comments_service_user
)
import student import student
from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=F0401 from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=F0401
...@@ -87,6 +92,7 @@ from logging import getLogger ...@@ -87,6 +92,7 @@ from logging import getLogger
from . import provider from . import provider
log = logging.getLogger(__file__)
# These are the query string params you can pass # These are the query string params you can pass
# to the URL that starts the authentication process. # to the URL that starts the authentication process.
...@@ -450,8 +456,23 @@ def parse_query_params(strategy, response, *args, **kwargs): ...@@ -450,8 +456,23 @@ def parse_query_params(strategy, response, *args, **kwargs):
# pylint: disable=fixme # pylint: disable=fixme
'is_login_2': auth_entry == AUTH_ENTRY_LOGIN_2, 'is_login_2': auth_entry == AUTH_ENTRY_LOGIN_2,
'is_register_2': auth_entry == AUTH_ENTRY_REGISTER_2, 'is_register_2': auth_entry == AUTH_ENTRY_REGISTER_2,
} }
def create_user_from_oauth(strategy, details, user, is_new, *args, **kwargs):
if is_new:
profile = UserProfile(user=user)
profile.name = details.get('fullname')
try:
profile.save()
except Exception:
log.error("UserProfile creation failed for user {id}.".format(id=user.id))
raise
create_comments_service_user(user)
# TODO (ECOM-369): Once the A/B test of the combined login/registration # TODO (ECOM-369): Once the A/B test of the combined login/registration
# form completes, we will be able to remove the extra login/registration # form completes, we will be able to remove the extra login/registration
# end-points. HOWEVER, users who used the new forms during the A/B # end-points. HOWEVER, users who used the new forms during the A/B
...@@ -527,6 +548,7 @@ def ensure_user_information( ...@@ -527,6 +548,7 @@ def ensure_user_information(
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER_2]) return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER_2])
@partial.partial @partial.partial
def set_logged_in_cookie(backend=None, user=None, request=None, is_api=None, *args, **kwargs): def set_logged_in_cookie(backend=None, user=None, request=None, is_api=None, *args, **kwargs):
"""This pipeline step sets the "logged in" cookie for authenticated users. """This pipeline step sets the "logged in" cookie for authenticated users.
......
from requests import HTTPError
from django.conf import settings
from social.backends.oauth import BaseOAuth2
from social.exceptions import AuthCanceled
def get_primary_email(emails):
for email in emails:
if email['primary'] is True:
return email['email']
return None
class PortalOAuth2(BaseOAuth2):
"""Portal OAuth2 authentication backend"""
auth_settings = settings.IONISX_AUTH
name = 'portal-oauth2'
ID_KEY = '_id'
AUTHORIZATION_URL = auth_settings.get('AUTHORIZATION_URL')
ACCESS_TOKEN_URL = auth_settings.get('ACCESS_TOKEN_URL')
ACCESS_TOKEN_METHOD = 'POST'
REDIRECT_STATE = False
USER_DATA_URL = auth_settings.get('USER_DATA_URL')
def get_user_details(self, response):
"""Return user details from IONISx account"""
return {
'username': response['username'],
'email': get_primary_email(response['emails']),
'fullname': response['name']
}
def user_data(self, access_token, *args, **kwargs):
"""Loads user data from service"""
return self.get_json(
self.USER_DATA_URL,
headers={'Authorization': 'Bearer {0}'.format(access_token)}
)
def process_error(self, data):
super(PortalOAuth2, self).process_error(data)
if data.get('error_code'):
raise AuthCanceled(self, data.get('error_message') or
data.get('error_code'))
...@@ -5,6 +5,7 @@ invoke the Django armature. ...@@ -5,6 +5,7 @@ invoke the Django armature.
""" """
from social.backends import google, linkedin, facebook from social.backends import google, linkedin, facebook
from . import portal
_DEFAULT_ICON_CLASS = 'icon-signin' _DEFAULT_ICON_CLASS = 'icon-signin'
...@@ -170,6 +171,26 @@ class FacebookOauth2(BaseProvider): ...@@ -170,6 +171,26 @@ class FacebookOauth2(BaseProvider):
return provider_details.get('fullname') return provider_details.get('fullname')
class PortalOauth2(BaseProvider):
"""Provider for Portal's Oauth2 auth system."""
BACKEND_CLASS = portal.PortalOAuth2
ICON_CLASS = 'icon-ionisx'
NAME = 'Portal'
SETTINGS = {
'SOCIAL_AUTH_PORTAL_OAUTH2_KEY': None,
'SOCIAL_AUTH_PORTAL_OAUTH2_SECRET': None,
}
@classmethod
def get_email(cls, provider_details):
return provider_details.get('email')
@classmethod
def get_name(cls, provider_details):
return provider_details.get('fullname')
class Registry(object): class Registry(object):
"""Singleton registry of third-party auth providers. """Singleton registry of third-party auth providers.
......
...@@ -50,7 +50,7 @@ _FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id'] ...@@ -50,7 +50,7 @@ _FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id']
_MIDDLEWARE_CLASSES = ( _MIDDLEWARE_CLASSES = (
'third_party_auth.middleware.ExceptionMiddleware', 'third_party_auth.middleware.ExceptionMiddleware',
) )
_SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/dashboard' _SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/'
_SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = '/profile' _SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = '/profile'
_SOCIAL_AUTH_DISCONNECT_REDIRECT_URL = '/profile' _SOCIAL_AUTH_DISCONNECT_REDIRECT_URL = '/profile'
...@@ -105,14 +105,13 @@ def _set_global_settings(django_settings): ...@@ -105,14 +105,13 @@ def _set_global_settings(django_settings):
# Inject our customized auth pipeline. All auth backends must work with # Inject our customized auth pipeline. All auth backends must work with
# this pipeline. # this pipeline.
django_settings.SOCIAL_AUTH_PIPELINE = ( django_settings.SOCIAL_AUTH_PIPELINE = (
'third_party_auth.pipeline.parse_query_params',
'social.pipeline.social_auth.social_details', 'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid', 'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed', 'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.social_user', 'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username', 'social.pipeline.user.get_username',
'third_party_auth.pipeline.ensure_user_information',
'social.pipeline.user.create_user', 'social.pipeline.user.create_user',
'third_party_auth.pipeline.create_user_from_oauth',
'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data', 'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details', 'social.pipeline.user.user_details',
......
...@@ -440,6 +440,10 @@ X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS) ...@@ -440,6 +440,10 @@ X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS)
##### Third-party auth options ################################################ ##### Third-party auth options ################################################
THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH) THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH)
IONISX_AUTH = AUTH_TOKENS.get('IONISX_AUTH')
if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
MIDDLEWARE_CLASSES += ('third_party_auth.middleware.PortalSynchronizerMiddleware',)
##### OAUTH2 Provider ############## ##### OAUTH2 Provider ##############
if FEATURES.get('ENABLE_OAUTH2_PROVIDER'): if FEATURES.get('ENABLE_OAUTH2_PROVIDER'):
......
...@@ -75,96 +75,6 @@ ...@@ -75,96 +75,6 @@
<section class="container dashboard" id="dashboard-main"> <section class="container dashboard" id="dashboard-main">
<section class="profile-sidebar">
<header class="profile">
<h1 class="user-name">${ user.username }</h1>
</header>
<section class="user-info">
<ul>
<li class="info--username">
<span class="title">${_("Full Name")} (<a href="#apply_name_change" rel="leanModal" class="edit-name">${_("edit")}</a>)</span> <span class="data">${ user.profile.name | h }</span>
</li>
<li class="info--email">
<span class="title">${_("Email")}
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
(<a href="#change_email" rel="leanModal" class="edit-email">${_("edit")}</a>)
% endif
</span> <span class="data">${ user.email | h }</span>
</li>
%if len(language_options) > 1:
<%include file='dashboard/_dashboard_info_language.html' />
%endif
% if third_party_auth.is_enabled():
<li class="controls--account">
<span class="title">
## Translators: this section lists all the third-party authentication providers (for example, Google and LinkedIn) the user can link with or unlink from their edX account.
${_("Connected Accounts")}
</span>
<span class="data">
<span class="third-party-auth">
% for state in provider_user_states:
<div class="auth-provider">
<div class="status">
% if state.has_account:
<i class="icon icon-link"></i> <span class="copy">${_("Linked")}</span>
% else:
<i class="icon icon-unlink"></i><span class="copy">${_("Not Linked")}</span>
% endif
</div>
<span class="provider">${state.provider.NAME}</span>
<span class="control">
% if state.has_account:
<form
action="${pipeline.get_disconnect_url(state.provider.NAME)}"
method="post"
name="${state.get_unlink_form_name()}">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<a href="#" onclick="document.${state.get_unlink_form_name()}.submit()">
## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Unlink")}
</a>
% else:
<a href="${pipeline.get_login_url(state.provider.NAME, pipeline.AUTH_ENTRY_DASHBOARD)}">
## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Link")}
</a>
% endif
</form>
</span>
</div>
% endfor
</span>
</li>
% endif
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
<li class="controls--account">
<span class="title"><a href="#password_reset_complete" rel="leanModal" id="pwd_reset_button">${_("Reset Password")}</a></span>
<form id="password_reset_form" method="post" data-remote="true" action="${reverse('password_reset')}">
<input id="id_email" type="hidden" name="email" maxlength="75" value="${user.email}" />
<!-- <input type="submit" id="pwd_reset_button" value="${_("Reset Password")}" /> -->
</form>
</li>
% endif
<%include file='dashboard/_dashboard_status_verification.html' />
<%include file='dashboard/_dashboard_reverification_sidebar.html' />
</ul>
</section>
</section>
<section class="my-courses" id="my-courses"> <section class="my-courses" id="my-courses">
<header> <header>
<h2>${_("Current Courses")}</h2> <h2>${_("Current Courses")}</h2>
......
...@@ -31,7 +31,7 @@ urlpatterns = ('', # nopep8 ...@@ -31,7 +31,7 @@ urlpatterns = ('', # nopep8
url(r'^segmentio/event$', 'track.views.segmentio.segmentio_event'), url(r'^segmentio/event$', 'track.views.segmentio.segmentio_event'),
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB? url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
url(r'^accounts/login$', 'student.views.accounts_login', name="accounts_login"), url(r'^accounts/login$', 'student.views.auth_with_no_login', name="auto_login"),
url(r'^accounts/manage_user_standing', 'student.views.manage_user_standing', url(r'^accounts/manage_user_standing', 'student.views.manage_user_standing',
name='manage_user_standing'), name='manage_user_standing'),
url(r'^accounts/disable_account_ajax$', 'student.views.disable_account_ajax', url(r'^accounts/disable_account_ajax$', 'student.views.disable_account_ajax',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment