diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 9965132302..2da6d459cb 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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 = { diff --git a/common/djangoapps/third_party_auth/provider.py b/common/djangoapps/third_party_auth/provider.py index 4067db832f..d398e1584c 100644 --- a/common/djangoapps/third_party_auth/provider.py +++ b/common/djangoapps/third_party_auth/provider.py @@ -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. diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index 4eb40fff3d..0ba253c27e 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -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): diff --git a/common/static/js/src/utility.js b/common/static/js/src/utility.js index ef540373fc..2ea5da08fc 100644 --- a/common/static/js/src/utility.js +++ b/common/static/js/src/utility.js @@ -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 diff --git a/lms/envs/dev.py b/lms/envs/dev.py index f0ced63d07..d57ab3bd5b 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -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', diff --git a/lms/startup.py b/lms/startup.py index 61926b0782..bb6b312a50 100644 --- a/lms/startup.py +++ b/lms/startup.py @@ -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) diff --git a/lms/static/sass/elements/_controls.scss b/lms/static/sass/elements/_controls.scss index 776c2e3ac2..f094f78df1 100644 --- a/lms/static/sass/elements/_controls.scss +++ b/lms/static/sass/elements/_controls.scss @@ -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 diff --git a/lms/static/sass/multicourse/_account.scss b/lms/static/sass/multicourse/_account.scss index 26cafdc432..00b19e636d 100644 --- a/lms/static/sass/multicourse/_account.scss +++ b/lms/static/sass/multicourse/_account.scss @@ -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; } } diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 6f5f4c1e71..a2a2f1a90b 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -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); - .icon { - margin-top: 4px; + .status { + width: flex-grid(1); + display: inline-block; + color: $gray-l2; + + .icon-link { + color: $base-font-color; + } + + .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; + } } } } diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 4f086d901c..8099775f95 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -198,7 +198,7 @@ % if duplicate_provider:
## 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='', link_start='' % logout_url, provider_name='%s' % duplicate_provider.NAME, platform_name=platform_name)} +

${_('The {provider_name} account you selected is already linked to another {platform_name} account.').format(provider_name='%s' % duplicate_provider.NAME, platform_name=platform_name)}

% endif @@ -226,22 +226,23 @@ % if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
  • -
    ## 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")}
    % for state in provider_user_states: -
    + % endfor diff --git a/lms/templates/login.html b/lms/templates/login.html index 293bd5d934..9ab2e0e2fe 100644 --- a/lms/templates/login.html +++ b/lms/templates/login.html @@ -190,19 +190,17 @@ % if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): -
    - -

    - ## 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.')} -

    + + ## 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')} +
    % for enabled in provider.Registry.enabled(): ## Translators: provider_name is the name of an external, third-party user authentication provider (like Google or LinkedIn). - + % endfor
    diff --git a/lms/templates/register-sidebar.html b/lms/templates/register-sidebar.html index 96f9d5d592..cd63b602c4 100644 --- a/lms/templates/register-sidebar.html +++ b/lms/templates/register-sidebar.html @@ -12,11 +12,11 @@ from django.core.urlresolvers import reverse % if has_extauth_info is UNDEFINED: -
    -

    ${_("Already registered?")}

    + diff --git a/lms/templates/register.html b/lms/templates/register.html index 63a8a8cfa2..aa587951f0 100644 --- a/lms/templates/register.html +++ b/lms/templates/register.html @@ -120,23 +120,27 @@ % if not running_pipeline: -

    - ${_("Register to start learning today!")} -

    -
    % for enabled in provider.Registry.enabled(): ## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn). - + % endfor
    + + ## 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')} + +

    - ${_('or create your own {platform_name} account by completing all required* fields below.').format(platform_name=platform_name)} + ${_('Create your own {platform_name} account below').format(platform_name=platform_name)} + ${_('Required fields are noted by bold text and an asterisk (*).')}

    + % else:

    @@ -235,7 +239,7 @@

    -

    ${_("Extra Personal Information")}

    +

    ${_("Additional Personal Information")}

      % if settings.REGISTRATION_EXTRA_FIELDS['city'] != 'hidden': @@ -258,7 +262,7 @@ % endif % if settings.REGISTRATION_EXTRA_FIELDS['level_of_education'] != 'hidden': -
    1. +
    2. @@ -284,7 +288,7 @@
    3. % endif % if settings.REGISTRATION_EXTRA_FIELDS['year_of_birth'] != 'hidden': -
    4. +