Commit c7d41833 by Julia Hansbrough

Merge pull request #4915 from edx/flowerhack/redo-third-party

Facebook auth support.
parents b8049c01 fd925e89
......@@ -46,7 +46,8 @@ from student.models import (
Registration, UserProfile, PendingNameChange,
PendingEmailChange, CourseEnrollment, unique_id_for_user,
CourseEnrollmentAllowed, UserStanding, LoginFailures,
create_comments_service_user, PasswordHistory, UserSignupSource
create_comments_service_user, PasswordHistory, UserSignupSource,
anonymous_id_for_user
)
from student.forms import PasswordResetFormNoActive
......@@ -92,6 +93,9 @@ from util.password_policy_validators import (
from third_party_auth import pipeline, provider
from xmodule.error_module import ErrorDescriptor
import analytics
from eventtracking import tracker
log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
......@@ -381,6 +385,10 @@ def register_user(request, extra_context=None):
'username': '',
}
# We save this so, later on, we can determine what course motivated a user's signup
# if they actually complete the registration process
request.session['registration_course_id'] = context['course_id']
if extra_context is not None:
context.update(extra_context)
......@@ -951,6 +959,31 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
if LoginFailures.is_feature_enabled():
LoginFailures.clear_lockout_counter(user)
# Track the user's sign in
if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'):
tracking_context = tracker.get_tracker().resolve_context()
analytics.identify(anonymous_id_for_user(user, None), {
'email': email,
'username': username,
})
# If the user entered the flow via a specific course page, we track that
registration_course_id = request.session.get('registration_course_id')
analytics.track(
user.id,
"edx.bi.user.account.authenticated",
{
'category': "conversion",
'label': registration_course_id
},
context={
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
)
request.session['registration_course_id'] = None
if user is not None and user.is_active:
try:
# We do not log here, because we have a handler registered
......@@ -1398,6 +1431,33 @@ def create_account(request, post_override=None): # pylint: disable-msg=too-many
(user, profile, registration) = ret
dog_stats_api.increment("common.student.account_created")
email = post_vars['email']
# Track the user's registration
if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'):
tracking_context = tracker.get_tracker().resolve_context()
analytics.identify(anonymous_id_for_user(user, None), {
email: email,
username: username,
})
registration_course_id = request.session.get('registration_course_id')
analytics.track(
user.id,
"edx.bi.user.account.registered",
{
"category": "conversion",
"label": registration_course_id
},
context={
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
)
request.session['registration_course_id'] = None
create_comments_service_user(user)
context = {
......
......@@ -4,7 +4,7 @@ Loaded by Django's settings mechanism. Consequently, this module must not
invoke the Django armature.
"""
from social.backends import google, linkedin
from social.backends import google, linkedin, facebook
_DEFAULT_ICON_CLASS = 'icon-signin'
......@@ -150,6 +150,26 @@ class LinkedInOauth2(BaseProvider):
return provider_details.get('fullname')
class FacebookOauth2(BaseProvider):
"""Provider for LinkedIn's Oauth2 auth system."""
BACKEND_CLASS = facebook.FacebookOAuth2
ICON_CLASS = 'icon-facebook'
NAME = 'Facebook'
SETTINGS = {
'SOCIAL_AUTH_FACEBOOK_KEY': None,
'SOCIAL_AUTH_FACEBOOK_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):
"""Singleton registry of third-party auth providers.
......
......@@ -282,7 +282,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
def assert_register_response_before_pipeline_looks_correct(self, response):
"""Asserts a GET of /register not in the pipeline looks correct."""
self.assertEqual(200, response.status_code)
self.assertIn('Sign in with ' + self.PROVIDER_CLASS.NAME, response.content)
self.assertIn('Sign up with ' + self.PROVIDER_CLASS.NAME, response.content)
self.assert_signin_button_looks_functional(response.content, pipeline.AUTH_ENTRY_REGISTER)
def assert_signin_button_looks_functional(self, content, auth_entry):
......
......@@ -91,75 +91,7 @@ window.parseQueryString = function(queryString) {
return parameters
};
// Check if the user recently enrolled in a course by looking at a referral URL
window.checkRecentEnrollment = function(referrer) {
var enrolledIn = null;
// Check if the referrer URL contains a query string
if (referrer.indexOf("?") > -1) {
referrerQueryString = referrer.split("?")[1];
} else {
referrerQueryString = "";
}
if (referrerQueryString != "") {
// Convert a non-empty query string into a key/value object
var referrerParameters = window.parseQueryString(referrerQueryString);
if ("course_id" in referrerParameters && "enrollment_action" in referrerParameters) {
if (referrerParameters.enrollment_action == "enroll") {
enrolledIn = referrerParameters.course_id;
}
}
}
return enrolledIn
};
window.assessUserSignIn = function(parameters, userID, email, username) {
// Check if the user has logged in to enroll in a course - designed for when "Register" button registers users on click (currently, this could indicate a course registration when there may not have yet been one)
var enrolledIn = window.checkRecentEnrollment(document.referrer);
// Check if the user has just registered
if (parameters.signin == "initial") {
window.trackAccountRegistration(enrolledIn, userID, email, username);
} else {
window.trackReturningUserSignIn(enrolledIn, userID, email, username);
}
};
window.trackAccountRegistration = function(enrolledIn, userID, email, username) {
// Alias the user's anonymous history with the user's new identity (for Mixpanel)
analytics.alias(userID);
// Map the user's activity to their newly assigned ID
analytics.identify(userID, {
email: email,
username: username
});
// Track the user's account creation
analytics.track("edx.bi.user.account.registered", {
category: "conversion",
label: enrolledIn != null ? enrolledIn : "none"
});
};
window.trackReturningUserSignIn = function(enrolledIn, userID, email, username) {
// Map the user's activity to their assigned ID
analytics.identify(userID, {
email: email,
username: username
});
// Track the user's sign in
analytics.track("edx.bi.user.account.authenticated", {
category: "conversion",
label: enrolledIn != null ? enrolledIn : "none"
});
};
window.identifyUser = function(userID, email, username) {
// If the signin parameter isn't present but the query string is non-empty, map the user's activity to their assigned ID
analytics.identify(userID, {
email: email,
username: username
......
......@@ -44,12 +44,6 @@ FEEDBACK_SUBMISSION_EMAIL = "dummy@example.com"
WIKI_ENABLED = True
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",
local_loglevel="DEBUG",
dev_env=True,
debug=True)
DJFS = {
'type': 'osfs',
'directory_root': 'lms/static/djpyfs',
......
......@@ -37,7 +37,7 @@ def run():
# Initialize Segment.io analytics module. Flushes first time a message is received and
# every 50 messages thereafter, or if 10 seconds have passed since last flush
if settings.FEATURES.get('SEGMENT_IO_LMS') and settings.SEGMENT_IO_LMS_KEY:
if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'):
analytics.init(settings.SEGMENT_IO_LMS_KEY, flush_at=50)
......
......@@ -237,6 +237,54 @@
}
}
// blue secondary button outline style
%btn-secondary-blue-outline {
@extend %t-action2;
@extend %btn;
@extend %btn-edged;
box-shadow: none;
border: 1px solid $m-blue-d3;
padding: ($baseline/2) $baseline;
background: transparent;
color: $m-blue-d3;
&:hover, &:active, &:focus {
box-shadow: 0 2px 1px 0 $m-blue-d4;
background: $m-blue-d1;
color: $white;
}
&.current, &.active {
box-shadow: inset 0 2px 1px 1px $m-blue-d2;
background: $m-blue;
color: $m-blue-d2;
&:hover, &:active, &:focus {
box-shadow: inset 0 2px 1px 1px $m-blue-d3;
color: $m-blue-d3;
}
}
&.disabled, &[disabled] {
box-shadow: none;
}
}
// grey secondary button outline style
%btn-secondary-grey-outline {
@extend %btn-secondary-blue-outline;
border: 1px solid $gray-l4;
&:hover, &:active, &:focus {
box-shadow: none;
border: 1px solid $m-blue-d3;
}
&.disabled, &[disabled] {
box-shadow: none;
}
}
// ====================
// application: canned actions
......
......@@ -230,6 +230,21 @@
margin: 0 0 ($baseline/4) 0;
}
}
.cta-login {
h3.title,
.instructions {
display: inline-block;
margin-bottom: 0;
}
.cta-login-action {
@extend %btn-secondary-grey-outline;
padding: ($baseline/10) ($baseline*.75);
margin-left: ($baseline/4);
}
}
}
// forms
......@@ -275,6 +290,17 @@
}
}
.group-form-personalinformation {
.field-education-level,
.field-gender,
.field-yob {
display: inline-block;
vertical-align: top;
margin-bottom: 0;
}
}
// individual fields
.field {
margin: 0 0 $baseline 0;
......@@ -304,6 +330,16 @@
font-size: em(13);
}
&.password {
position: relative;
.tip {
position: absolute;
top: 0;
right: 0;
}
}
input, textarea {
width: 100%;
margin: 0;
......@@ -432,9 +468,7 @@
}
.action-primary {
float: left;
width: flex-grid(8,8);
margin-right: flex-gutter(0);
}
.action-secondary {
......@@ -452,16 +486,71 @@
}
// forms - third-party auth
.form-third-party-auth {
// UI: deco - divider
.deco-divider {
position: relative;
display: block;
margin: ($baseline*1.5) 0;
border-top: ($baseline/5) solid $m-gray-l4;
.copy {
@extend %t-copy-lead1;
@extend %t-weight4;
position: absolute;
top: -($baseline);
left: 43%;
padding: ($baseline/4) ($baseline*1.5);
background: white;
text-align: center;
color: $m-gray-l2;
}
}
// downplay required note
.instructions .note {
@extend %t-copy-sub2;
display: block;
font-weight: normal;
color: $gray;
}
.form-actions.form-third-party-auth {
width: flex-grid(8,8);
margin-bottom: $baseline;
button {
margin-right: $baseline;
button[type="submit"] {
@extend %btn-secondary-blue-outline;
width: flex-grid(4,8);
margin-right: ($baseline/2);
.icon {
color: inherit;
margin-right: $baseline/2;
}
&:last-child {
margin-right: 0;
}
&.button-Google:hover {
box-shadow: 0 2px 1px 0 #8D3024;
background-color: #dd4b39;
border: 1px solid #A5382B;
}
&.button-Facebook:hover {
box-shadow: 0 2px 1px 0 #30487C;
background-color: #3b5998;
border: 1px solid #263A62;
}
&.button-LinkedIn:hover {
box-shadow: 0 2px 1px 0 #005D8E;
background-color: #0077b5;
border: 1px solid #06527D;
}
}
}
......@@ -536,7 +625,6 @@
.introduction {
header {
height: 120px;
border-bottom: 1px solid $m-gray;
background: transparent $login-banner-image 0 0 no-repeat;
}
}
......@@ -548,7 +636,6 @@
.introduction {
header {
height: 120px;
border-bottom: 1px solid $m-gray;
background: transparent $register-banner-image 0 0 no-repeat;
}
}
......
......@@ -110,17 +110,40 @@
.third-party-auth {
color: inherit;
font-weight: inherit;
}
.control {
float: right;
}
.auth-provider {
width: flex-grid(12);
display: block;
margin-top: ($baseline/4);
.status {
width: flex-grid(1);
display: inline-block;
color: $gray-l2;
.icon-link {
color: $base-font-color;
}
.icon {
margin-top: 4px;
.copy {
@extend %text-sr;
}
}
.provider {
display: inline;
width: flex-grid(9);
display: inline-block;
}
.control {
width: flex-grid(2);
display: inline-block;
text-align: right;
a:link, a:visited {
@extend %t-copy-sub2;
}
}
}
}
......
......@@ -198,7 +198,7 @@
% if duplicate_provider:
<section class="dashboard-banner third-party-auth">
## Translators: this message is displayed when a user tries to link their account with a third-party authentication provider (for example, Google or LinkedIn) with a given edX account, but their third-party account is already associated with another edX account. provider_name is the name of the third-party authentication provider, and platform_name is the name of the edX deployment.
${_('The selected {provider_name} account is already linked to another {platform_name} account. Please {link_start}log out{link_end}, then log in with your {provider_name} account.').format(link_end='</a>', link_start='<a href="%s">' % logout_url, provider_name='<strong>%s</strong>' % duplicate_provider.NAME, platform_name=platform_name)}
<p>${_('The {provider_name} account you selected is already linked to another {platform_name} account.').format(provider_name='<strong>%s</strong>' % duplicate_provider.NAME, platform_name=platform_name)}
</section>
% endif
......@@ -226,22 +226,23 @@
% if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
<li class="controls--account">
<span class="title">
<div class="icon icon-gears"></div>
## 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.
${_("Account Links")}
${_("Connected Accounts")}
</span>
<span class="data">
<span class="third-party-auth">
% for state in provider_user_states:
<div>
<div class="auth-provider">
% if state.has_account:
<span class="icon icon-link pull-left"></span>
% else:
<span class="icon icon-unlink pull-left"></span>
% endif
<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">
......@@ -252,17 +253,19 @@
method="post"
name="${state.get_unlink_form_name()}">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
</form>
<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>
<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
......
......@@ -190,19 +190,17 @@
% if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
<hr />
<p class="instructions">
## Developers: this is a sentence fragment, which is usually frowned upon. The design of the pags uses this fragment to provide an "else" clause underneath a number of choices. It's OK to leave it.
## Translators: this is the last choice of a number of choices of how to log in to the site.
${_('or, if you have connected one of these providers, log in below.')}
</p>
<span class="deco-divider">
## Developers: this is a sentence fragment, which is usually frowned upon. The design of the pags uses this fragment to provide an "else" clause underneath a number of choices. It's OK to leave it.
## Translators: this is the last choice of a number of choices of how to log in to the site.
<span class="copy">${_('or')}</span>
</span>
<div class="form-actions form-third-party-auth">
% for enabled in provider.Registry.enabled():
## Translators: provider_name is the name of an external, third-party user authentication provider (like Google or LinkedIn).
<button type="submit" class="button button-primary" onclick="thirdPartySignin(event, '${pipeline.get_login_url(enabled.NAME, pipeline.AUTH_ENTRY_LOGIN)}');"><span class="icon ${enabled.ICON_CLASS}"></span>${_('Sign in with {provider_name}').format(provider_name=enabled.NAME)}</button>
<button type="submit" class="button button-primary button-${enabled.NAME}" onclick="thirdPartySignin(event, '${pipeline.get_login_url(enabled.NAME, pipeline.AUTH_ENTRY_LOGIN)}');"><span class="icon ${enabled.ICON_CLASS}"></span>${_('Sign in with {provider_name}').format(provider_name=enabled.NAME)}</button>
% endfor
</div>
......
......@@ -12,11 +12,11 @@ from django.core.urlresolvers import reverse
% if has_extauth_info is UNDEFINED:
<div class="cta">
<h3>${_("Already registered?")}</h3>
<div class="cta cta-login">
<h3 class="title">${_("Already registered?")}</h3>
<p class="instructions">
<a href="${reverse('signin_user')}${login_query()}">
${_("Click here to log in.")}
<a class="cta-login-action" href="${reverse('signin_user')}${login_query()}">
${_("Log in")}
</a>
</p>
</div>
......
......@@ -120,23 +120,27 @@
% if not running_pipeline:
<p class="instructions">
${_("Register to start learning today!")}
</p>
<div class="form-actions form-third-party-auth">
% for enabled in provider.Registry.enabled():
## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn).
<button type="submit" class="button button-primary" onclick="thirdPartySignin(event, '${pipeline.get_login_url(enabled.NAME, pipeline.AUTH_ENTRY_REGISTER)}');"><span class="icon ${enabled.ICON_CLASS}"></span>${_('Sign in with {provider_name}').format(provider_name=enabled.NAME)}</button>
<button type="submit" class="button button-primary button-${enabled.NAME}" onclick="thirdPartySignin(event, '${pipeline.get_login_url(enabled.NAME, pipeline.AUTH_ENTRY_REGISTER)}');"><span class="icon ${enabled.ICON_CLASS}"></span>${_('Sign up with {provider_name}').format(provider_name=enabled.NAME)}</button>
% endfor
</div>
<span class="deco-divider">
## Developers: this is a sentence fragment, which is usually frowned upon. The design of the pags uses this fragment to provide an "else" clause underneath a number of choices. It's OK to leave it.
## Translators: this is the last choice of a number of choices of how to log in to the site.
<span class="copy">${_('or')}</span>
</span>
<p class="instructions">
${_('or create your own {platform_name} account by completing all <strong>required*</strong> fields below.').format(platform_name=platform_name)}
${_('Create your own {platform_name} account below').format(platform_name=platform_name)}
<span class="note">${_('Required fields are noted by <strong class="indicator">bold text and an asterisk (*)</strong>.')}</span>
</p>
% else:
<p class="instructions">
......@@ -235,7 +239,7 @@
</div>
<div class="group group-form group-form-secondary group-form-personalinformation">
<h2 class="sr">${_("Extra Personal Information")}</h2>
<h2 class="sr">${_("Additional Personal Information")}</h2>
<ol class="list-input">
% if settings.REGISTRATION_EXTRA_FIELDS['city'] != 'hidden':
......@@ -258,7 +262,7 @@
</li>
% endif
% if settings.REGISTRATION_EXTRA_FIELDS['level_of_education'] != 'hidden':
<li class="field-group">
<li class="field-group field-education-level">
<div class="field ${settings.REGISTRATION_EXTRA_FIELDS['level_of_education']} select" id="field-education-level">
<label for="education-level">${_("Highest Level of Education Completed")}</label>
<select id="education-level" name="level_of_education" ${'required aria-required="true"' if settings.REGISTRATION_EXTRA_FIELDS['level_of_education'] == 'required' else ''}>
......@@ -271,7 +275,7 @@
</li>
% endif
% if settings.REGISTRATION_EXTRA_FIELDS['gender'] != 'hidden':
<li class="field-group">
<li class="field-group field-gender">
<div class="field ${settings.REGISTRATION_EXTRA_FIELDS['gender']} select" id="field-gender">
<label for="gender">${_("Gender")}</label>
<select id="gender" name="gender" ${'required aria-required="true"' if settings.REGISTRATION_EXTRA_FIELDS['gender'] == 'required' else ''}>
......@@ -284,7 +288,7 @@
</li>
% endif
% if settings.REGISTRATION_EXTRA_FIELDS['year_of_birth'] != 'hidden':
<li class="field-group">
<li class="field-group field-yob">
<div class="field ${settings.REGISTRATION_EXTRA_FIELDS['year_of_birth']} select" id="field-yob">
<label for="yob">${_("Year of Birth")}</label>
<select id="yob" name="year_of_birth" ${'required aria-required="true"' if settings.REGISTRATION_EXTRA_FIELDS['year_of_birth'] == 'required' else ''}>
......@@ -300,8 +304,6 @@
</div>
<div class="group group-form group-form-personalinformation2">
<h2 class="sr">${_("Extra Personal Information")}</h2>
<ol class="list-input">
% if settings.REGISTRATION_EXTRA_FIELDS['mailing_address'] != 'hidden':
<li class="field ${settings.REGISTRATION_EXTRA_FIELDS['mailing_address']} text" id="field-address-mailing">
......
......@@ -12,18 +12,8 @@
// Access the query string, stripping the leading "?"
var queryString = window.location.search.substring(1);
if (queryString != "") {
// Convert the query string to a key/value object
var parameters = window.parseQueryString(queryString);
window.identifyUser("${user.id}", "${user.email}", "${user.username}");
if ("signin" in parameters) {
window.assessUserSignIn(parameters, "${user.id}", "${user.email}", "${user.username}");
} else {
window.identifyUser("${user.id}", "${user.email}", "${user.username}");
}
} else {
window.identifyUser("${user.id}", "${user.email}", "${user.username}");
}
% endif
// Get current page URL
......
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