Commit 2667f705 by Usman Khalid Committed by Andy Armstrong

Enable third party auth from account settings page.

TNL-1535
parent a378995f
......@@ -117,6 +117,7 @@ AUTH_EMAIL_OPT_IN_KEY = 'email_opt_in'
AUTH_ENTRY_DASHBOARD = 'dashboard'
AUTH_ENTRY_LOGIN = 'login'
AUTH_ENTRY_REGISTER = 'register'
AUTH_ENTRY_ACCOUNT_SETTINGS = 'account_settings'
# This is left-over from an A/B test
# of the new combined login/registration page (ECOM-369)
......@@ -145,6 +146,7 @@ AUTH_DISPATCH_URLS = {
AUTH_ENTRY_DASHBOARD: '/dashboard',
AUTH_ENTRY_LOGIN: '/login',
AUTH_ENTRY_REGISTER: '/register',
AUTH_ENTRY_ACCOUNT_SETTINGS: '/account/settings',
# This is left-over from an A/B test
# of the new combined login/registration page (ECOM-369)
......@@ -159,6 +161,7 @@ _AUTH_ENTRY_CHOICES = frozenset([
AUTH_ENTRY_DASHBOARD,
AUTH_ENTRY_LOGIN,
AUTH_ENTRY_REGISTER,
AUTH_ENTRY_ACCOUNT_SETTINGS,
# This is left-over from an A/B test
# of the new combined login/registration page (ECOM-369)
......
......@@ -255,3 +255,17 @@ class AccountSettingsPageTest(WebAppTest):
u'',
[u'Pushto', u''],
)
def test_connected_accounts(self):
"""
Test that fields for third party auth providers exist.
Currently there is no way to test the whole authentication process
because that would require accounts with the providers.
"""
for field_id, title, link_title in [
['auth-facebook', 'Facebook', 'Link'],
['auth-google', 'Google', 'Link'],
]:
self.assertEqual(self.account_settings_page.title_for_field(field_id), title)
self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title)
......@@ -9,11 +9,14 @@ import json
import mock
import ddt
import markupsafe
from django.test import TestCase
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core import mail
from django.contrib import messages
from django.contrib.messages.middleware import MessageMiddleware
from django.test import TestCase
from django.test.utils import override_settings
from django.test.client import RequestFactory
from embargo.test_utils import restrict_course
from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account
......@@ -517,14 +520,24 @@ class AccountSettingsViewTest(TestCase):
'preferred_language',
]
@mock.patch("django.conf.settings.MESSAGE_STORAGE", 'django.contrib.messages.storage.cookie.CookieStorage')
def setUp(self):
super(AccountSettingsViewTest, self).setUp()
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.request = RequestFactory()
self.request.user = self.user
# Python-social saves auth failure notifcations in Django messages.
# See pipeline.get_duplicate_provider() for details.
self.request.COOKIES = {}
MessageMiddleware().process_request(self.request)
messages.error(self.request, 'Facebook is already in use.', extra_tags='Auth facebook')
def test_context(self):
context = account_settings_context(self.user)
context = account_settings_context(self.request)
user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username})
self.assertEqual(context['user_accounts_api_url'], user_accounts_api_url)
......@@ -535,6 +548,17 @@ class AccountSettingsViewTest(TestCase):
for attribute in self.FIELDS:
self.assertIn(attribute, context['fields'])
self.assertEqual(
context['user_accounts_api_url'], reverse("accounts_api", kwargs={'username': self.user.username})
)
self.assertEqual(
context['user_preferences_api_url'], reverse('preferences_api', kwargs={'username': self.user.username})
)
self.assertEqual(context['duplicate_provider'].BACKEND_CLASS.name, 'facebook')
self.assertEqual(context['auth']['providers'][0]['name'], 'Facebook')
self.assertEqual(context['auth']['providers'][1]['name'], 'Google')
def test_view(self):
view_path = reverse('account_settings')
......
......@@ -5,6 +5,7 @@ import json
from ipware.ip import get_ip
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import (
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
......@@ -35,6 +36,7 @@ from student.views import (
)
from student_account.helpers import auth_pipeline_urls
import third_party_auth
from third_party_auth import pipeline
from util.bad_request_rate_limiter import BadRequestRateLimiter
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
......@@ -321,19 +323,21 @@ def account_settings(request):
GET /account/settings
"""
return render_to_response('student_account/account_settings.html', account_settings_context(request.user))
return render_to_response('student_account/account_settings.html', account_settings_context(request))
def account_settings_context(user):
def account_settings_context(request):
""" Context for the account settings page.
Args:
user (User): The user for whom the context is required.
request: The request object.
Returns:
dict
"""
user = request.user
country_options = [
(country_code, unicode(country_name))
for country_code, country_name in sorted(
......@@ -344,8 +348,8 @@ def account_settings_context(user):
year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS]
context = {
'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}),
'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}),
'auth': {},
'duplicate_provider': None,
'fields': {
'country': {
'options': country_options,
......@@ -362,7 +366,33 @@ def account_settings_context(user):
}, 'preferred_language': {
'options': settings.ALL_LANGUAGES,
}
}
},
'platform_name': settings.PLATFORM_NAME,
'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}),
'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}),
}
if third_party_auth.is_enabled():
# If the account on the third party provider is already connected with another edX account,
# we display a message to the user.
context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request))
auth_states = pipeline.get_provider_user_states(user)
context['auth']['providers'] = [{
'name': state.provider.NAME, # The name of the provider e.g. Facebook
'connected': state.has_account, # Whether the user's edX account is connected with the provider.
# If the user is not connected, they should be directed to this page to authenticate
# with the particular provider.
'connect_url': pipeline.get_login_url(
state.provider.NAME,
pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS,
# The url the user should be directed to after the auth process has completed.
redirect_url=reverse('account_settings'),
),
# If the user is connected, sending a POST request to this url removes the connection
# information for this provider from their edX account.
'disconnect_url': pipeline.get_disconnect_url(state.provider.NAME),
} for state in auth_states]
return context
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
'js/spec/views/fields_helpers',
'js/spec/student_account/helpers',
'js/spec/student_account/account_settings_fields_helpers',
'js/student_account/views/account_settings_factory',
'js/student_account/views/account_settings_view'
],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers, Helpers, AccountSettingsPage, AccountSettingsView) {
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers, Helpers,
AccountSettingsFieldViewSpecHelpers, AccountSettingsPage, AccountSettingsView) {
'use strict';
describe("edx.user.AccountSettingsFactory", function () {
......@@ -27,6 +29,23 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
}
};
var AUTH_DATA = {
'providers': [
{
'name': "Network 1",
'connected': true,
'connect_url': 'yetanother1.com/auth/connect',
'disconnect_url': 'yetanother1.com/auth/disconnect'
},
{
'name': "Network 2",
'connected': true,
'connect_url': 'yetanother2.com/auth/connect',
'disconnect_url': 'yetanother2.com/auth/disconnect'
}
]
}
var requests;
beforeEach(function () {
......@@ -43,7 +62,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
requests = AjaxHelpers.requests(this);
var context = AccountSettingsPage(
FIELDS_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
);
var accountSettingsView = context.accountSettingsView;
......@@ -67,7 +86,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
requests = AjaxHelpers.requests(this);
var context = AccountSettingsPage(
FIELDS_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
);
var accountSettingsView = context.accountSettingsView;
......@@ -99,7 +118,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
requests = AjaxHelpers.requests(this);
var context = AccountSettingsPage(
FIELDS_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
);
var accountSettingsView = context.accountSettingsView;
......@@ -120,7 +139,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
requests = AjaxHelpers.requests(this);
var context = AccountSettingsPage(
FIELDS_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
);
var accountSettingsView = context.accountSettingsView;
......@@ -161,6 +180,13 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
}, requests);
}
var section2Fields = sectionsData[2].fields;
expect(section2Fields.length).toBe(2);
for (var i = 0; i < section2Fields.length; i++) {
var view = section2Fields[i].view;
AccountSettingsFieldViewSpecHelpers.verifyAuthField(view, view.options, requests);
}
});
});
});
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
'js/spec/views/fields_helpers',
'string_utils'],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers) {
'use strict';
var verifyAuthField = function (view, data, requests) {
var selector = '.u-field-value > a';
spyOn(view, 'redirect_to');
FieldViewsSpecHelpers.expectTitleAndMessageToBe(view, data.title, data.helpMessage);
expect(view.$(selector).text().trim()).toBe('Unlink');
view.$(selector).click();
FieldViewsSpecHelpers.expectMessageContains(view, 'Unlinking');
AjaxHelpers.expectRequest(requests, 'POST', data.disconnectUrl);
AjaxHelpers.respondWithNoContent(requests);
expect(view.$(selector).text().trim()).toBe('Link');
FieldViewsSpecHelpers.expectMessageContains(view, 'Successfully unlinked.');
view.$(selector).click();
FieldViewsSpecHelpers.expectMessageContains(view, 'Linking');
expect(view.redirect_to).toHaveBeenCalledWith(data.connectUrl);
}
return {
verifyAuthField: verifyAuthField
};
});
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
'js/views/fields',
'js/spec/views/fields_helpers',
'js/spec/student_account/account_settings_fields_helpers',
'js/student_account/views/account_settings_fields',
'js/student_account/models/user_account_model',
'string_utils'],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews, FieldViewsSpecHelpers, AccountSettingsFieldViews, UserAccountModel) {
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews, FieldViewsSpecHelpers,
AccountSettingsFieldViewSpecHelpers, AccountSettingsFieldViews, UserAccountModel) {
'use strict';
describe("edx.AccountSettingsFieldViews", function () {
......@@ -92,5 +94,21 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
AjaxHelpers.respondWithNoContent(requests);
});
it("correctly links and unlinks from AuthFieldView", function() {
requests = AjaxHelpers.requests(this);
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, {
title: 'Yet another social network',
helpMessage: '',
valueAttribute: 'auth-yet-another',
connected: true,
connectUrl: 'yetanother.com/auth/connect',
disconnectUrl: 'yetanother.com/auth/disconnect'
});
var view = new AccountSettingsFieldViews.AuthFieldView(fieldData).render();
AccountSettingsFieldViewSpecHelpers.verifyAuthField(view, fieldData, requests);
});
});
});
......@@ -10,7 +10,7 @@
], function (gettext, $, _, Backbone, FieldViews, UserAccountModel, UserPreferencesModel,
AccountSettingsFieldViews, AccountSettingsView) {
return function (fieldsData, userAccountsApiUrl, userPreferencesApiUrl) {
return function (fieldsData, authData, userAccountsApiUrl, userPreferencesApiUrl) {
var accountSettingsElement = $('.wrapper-account-settings');
......@@ -117,30 +117,26 @@
}
]
},
{
];
if (_.isArray(authData.providers)) {
var accountsSectionData = {
title: gettext('Connected Accounts'),
fields: [
{
view: new FieldViews.LinkFieldView({
model: userAccountModel,
title: gettext('Facebook'),
valueAttribute: 'auth-facebook',
linkTitle: gettext('Link'),
helpMessage: gettext('Coming soon')
})
},
{
view: new FieldViews.LinkFieldView({
model: userAccountModel,
title: gettext('Google'),
valueAttribute: 'auth-google',
linkTitle: gettext('Link'),
helpMessage: gettext('Coming soon')
fields: _.map(authData.providers, function(provider) {
return {
'view': new AccountSettingsFieldViews.AuthFieldView({
title: provider['name'],
valueAttribute: 'auth-' + provider['name'].toLowerCase(),
helpMessage: '',
connected: provider['connected'],
connectUrl: provider['connect_url'],
disconnectUrl: provider['disconnect_url']
})
}
]
}
];
})
};
sectionsData.push(accountsSectionData);
}
var accountSettingsView = new AccountSettingsView({
el: accountSettingsElement,
......
......@@ -99,6 +99,74 @@
attributes[this.options.valueAttribute] = value;
this.saveAttributes(attributes);
}
});
AccountSettingsFieldViews.AuthFieldView = FieldViews.LinkFieldView.extend({
initialize: function (options) {
this._super(options);
_.bindAll(this, 'redirect_to', 'disconnect', 'successMessage', 'inProgressMessage');
},
render: function () {
this.$el.html(this.template({
id: this.options.valueAttribute,
title: this.options.title,
linkTitle: this.options.connected ? gettext('Unlink') : gettext('Link'),
linkHref: '',
message: this.helpMessage
}));
return this;
},
linkClicked: function (event) {
event.preventDefault();
this.showInProgressMessage();
if (this.options.connected) {
this.disconnect();
} else {
// Direct the user to the providers site to start the authentication process.
// See python-social-auth docs for more information.
this.redirect_to(this.options.connectUrl);
}
},
redirect_to: function (url) {
window.location.href = url;
},
disconnect: function () {
var data = {};
// Disconnects the provider from the user's edX account.
// See python-social-auth docs for more information.
var view = this;
$.ajax({
type: 'POST',
url: this.options.disconnectUrl,
data: data,
dataType: 'html',
success: function (data, status, xhr) {
view.options.connected = false;
view.render();
view.showSuccessMessage();
},
error: function (xhr, status, error) {
view.showErrorMessage(xhr);
}
});
},
inProgressMessage: function() {
return this.indicators['inProgress'] + (this.options.connected ? gettext('Unlinking') : gettext('Linking'));
},
successMessage: function() {
return this.indicators['success'] + gettext('Successfully unlinked.');
},
});
return AccountSettingsFieldViews;
......
......@@ -22,6 +22,12 @@
% endfor
</%block>
% if duplicate_provider:
<section>
<%include file='/dashboard/_dashboard_third_party_error.html' />
</section>
% endif
<div class="wrapper-account-settings"></div>
<%block name="headextra">
......@@ -30,8 +36,13 @@
<script>
(function (require) {
require(['js/student_account/views/account_settings_factory'], function(setupAccountSettingsSection) {
var fieldsData = ${ json.dumps(fields) };
setupAccountSettingsSection(fieldsData, '${user_accounts_api_url}', '${user_preferences_api_url}');
var authData = ${ json.dumps(auth) };
setupAccountSettingsSection(
fieldsData, authData, '${user_accounts_api_url}', '${user_preferences_api_url}'
);
});
}).call(this, require || RequireJS.require);
</script>
......
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