New provider config options, New Institution Login Menu - PR 8603
This commit is contained in:
@@ -1498,6 +1498,13 @@ def create_account_with_params(request, params):
|
||||
|
||||
dog_stats_api.increment("common.student.account_created")
|
||||
|
||||
# If the user is registering via 3rd party auth, track which provider they use
|
||||
third_party_provider = None
|
||||
running_pipeline = None
|
||||
if third_party_auth.is_enabled() and pipeline.running(request):
|
||||
running_pipeline = pipeline.get(request)
|
||||
third_party_provider = provider.Registry.get_from_pipeline(running_pipeline)
|
||||
|
||||
# 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()
|
||||
@@ -1506,20 +1513,13 @@ def create_account_with_params(request, params):
|
||||
'username': user.username,
|
||||
})
|
||||
|
||||
# If the user is registering via 3rd party auth, track which provider they use
|
||||
provider_name = None
|
||||
if third_party_auth.is_enabled() and pipeline.running(request):
|
||||
running_pipeline = pipeline.get(request)
|
||||
current_provider = provider.Registry.get_from_pipeline(running_pipeline)
|
||||
provider_name = current_provider.name
|
||||
|
||||
analytics.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.registered",
|
||||
{
|
||||
'category': 'conversion',
|
||||
'label': params.get('course_id'),
|
||||
'provider': provider_name
|
||||
'provider': third_party_provider.name if third_party_provider else None
|
||||
},
|
||||
context={
|
||||
'Google Analytics': {
|
||||
@@ -1536,6 +1536,7 @@ def create_account_with_params(request, params):
|
||||
# 2. Random user generation for other forms of testing.
|
||||
# 3. External auth bypassing activation.
|
||||
# 4. Have the platform configured to not require e-mail activation.
|
||||
# 5. Registering a new user using a trusted third party provider (with skip_email_verification=True)
|
||||
#
|
||||
# Note that this feature is only tested as a flag set one way or
|
||||
# the other for *new* systems. we need to be careful about
|
||||
@@ -1544,7 +1545,11 @@ def create_account_with_params(request, params):
|
||||
send_email = (
|
||||
not settings.FEATURES.get('SKIP_EMAIL_VALIDATION', None) and
|
||||
not settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING') and
|
||||
not (do_external_auth and settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'))
|
||||
not (do_external_auth and settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH')) and
|
||||
not (
|
||||
third_party_provider and third_party_provider.skip_email_verification and
|
||||
user.email == running_pipeline['kwargs'].get('details', {}).get('email')
|
||||
)
|
||||
)
|
||||
if send_email:
|
||||
context = {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'SAMLProviderConfig.secondary'
|
||||
db.add_column('third_party_auth_samlproviderconfig', 'secondary',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'SAMLProviderConfig.skip_registration_form'
|
||||
db.add_column('third_party_auth_samlproviderconfig', 'skip_registration_form',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'SAMLProviderConfig.skip_email_verification'
|
||||
db.add_column('third_party_auth_samlproviderconfig', 'skip_email_verification',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'OAuth2ProviderConfig.secondary'
|
||||
db.add_column('third_party_auth_oauth2providerconfig', 'secondary',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'OAuth2ProviderConfig.skip_registration_form'
|
||||
db.add_column('third_party_auth_oauth2providerconfig', 'skip_registration_form',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'OAuth2ProviderConfig.skip_email_verification'
|
||||
db.add_column('third_party_auth_oauth2providerconfig', 'skip_email_verification',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'SAMLProviderConfig.secondary'
|
||||
db.delete_column('third_party_auth_samlproviderconfig', 'secondary')
|
||||
|
||||
# Deleting field 'SAMLProviderConfig.skip_registration_form'
|
||||
db.delete_column('third_party_auth_samlproviderconfig', 'skip_registration_form')
|
||||
|
||||
# Deleting field 'SAMLProviderConfig.skip_email_verification'
|
||||
db.delete_column('third_party_auth_samlproviderconfig', 'skip_email_verification')
|
||||
|
||||
# Deleting field 'OAuth2ProviderConfig.secondary'
|
||||
db.delete_column('third_party_auth_oauth2providerconfig', 'secondary')
|
||||
|
||||
# Deleting field 'OAuth2ProviderConfig.skip_registration_form'
|
||||
db.delete_column('third_party_auth_oauth2providerconfig', 'skip_registration_form')
|
||||
|
||||
# Deleting field 'OAuth2ProviderConfig.skip_email_verification'
|
||||
db.delete_column('third_party_auth_oauth2providerconfig', 'skip_email_verification')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'third_party_auth.oauth2providerconfig': {
|
||||
'Meta': {'object_name': 'OAuth2ProviderConfig'},
|
||||
'backend_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
|
||||
'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'secret': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
|
||||
},
|
||||
'third_party_auth.samlconfiguration': {
|
||||
'Meta': {'object_name': 'SAMLConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'entity_id': ('django.db.models.fields.CharField', [], {'default': "'http://saml.example.com'", 'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'org_info_str': ('django.db.models.fields.TextField', [], {'default': '\'{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}\''}),
|
||||
'other_config_str': ('django.db.models.fields.TextField', [], {'default': '\'{\\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\\n}\''}),
|
||||
'private_key': ('django.db.models.fields.TextField', [], {}),
|
||||
'public_key': ('django.db.models.fields.TextField', [], {})
|
||||
},
|
||||
'third_party_auth.samlproviderconfig': {
|
||||
'Meta': {'object_name': 'SAMLProviderConfig'},
|
||||
'attr_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'attr_first_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'attr_full_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'attr_last_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'attr_user_permanent_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'attr_username': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'backend_name': ('django.db.models.fields.CharField', [], {'default': "'tpa-saml'", 'max_length': '50'}),
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'idp_slug': ('django.db.models.fields.SlugField', [], {'max_length': '30'}),
|
||||
'metadata_source': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
|
||||
'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
|
||||
},
|
||||
'third_party_auth.samlproviderdata': {
|
||||
'Meta': {'ordering': "('-fetched_at',)", 'object_name': 'SAMLProviderData'},
|
||||
'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'expires_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'fetched_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'public_key': ('django.db.models.fields.TextField', [], {}),
|
||||
'sso_url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['third_party_auth']
|
||||
@@ -8,7 +8,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import json
|
||||
import logging
|
||||
from social.backends.base import BaseAuth
|
||||
@@ -54,7 +54,7 @@ class AuthNotConfigured(SocialAuthBaseException):
|
||||
self.provider_name = provider_name
|
||||
|
||||
def __str__(self):
|
||||
return _('Authentication with {} is currently unavailable.').format(
|
||||
return _('Authentication with {} is currently unavailable.').format( # pylint: disable=no-member
|
||||
self.provider_name
|
||||
)
|
||||
|
||||
@@ -68,10 +68,34 @@ class ProviderConfig(ConfigurationModel):
|
||||
help_text=(
|
||||
'The Font Awesome (or custom) icon class to use on the login button for this provider. '
|
||||
'Examples: fa-google-plus, fa-facebook, fa-linkedin, fa-sign-in, fa-university'
|
||||
))
|
||||
),
|
||||
)
|
||||
name = models.CharField(max_length=50, blank=False, help_text="Name of this provider (shown to users)")
|
||||
secondary = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
'Secondary providers are displayed less prominently, '
|
||||
'in a separate list of "Institution" login providers.'
|
||||
),
|
||||
)
|
||||
skip_registration_form = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"If this option is enabled, users will not be asked to confirm their details "
|
||||
"(name, email, etc.) during the registration process. Only select this option "
|
||||
"for trusted providers that are known to provide accurate user information."
|
||||
),
|
||||
)
|
||||
skip_email_verification = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"If this option is selected, users will not be required to confirm their "
|
||||
"email, and their account will be activated immediately upon registration."
|
||||
),
|
||||
)
|
||||
prefix = None # used for provider_id. Set to a string value in subclass
|
||||
backend_name = None # Set to a field or fixed value in subclass
|
||||
|
||||
# "enabled" field is inherited from ConfigurationModel
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
|
||||
@@ -503,12 +503,19 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
|
||||
"""Redirects to the registration page."""
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER])
|
||||
|
||||
def should_force_account_creation():
|
||||
""" For some third party providers, we auto-create user accounts """
|
||||
current_provider = provider.Registry.get_from_pipeline({'backend': backend.name, 'kwargs': kwargs})
|
||||
return current_provider and current_provider.skip_email_verification
|
||||
|
||||
if not user:
|
||||
if auth_entry in [AUTH_ENTRY_LOGIN_API, AUTH_ENTRY_REGISTER_API]:
|
||||
return HttpResponseBadRequest()
|
||||
elif auth_entry in [AUTH_ENTRY_LOGIN, AUTH_ENTRY_LOGIN_2]:
|
||||
# User has authenticated with the third party provider but we don't know which edX
|
||||
# account corresponds to them yet, if any.
|
||||
if should_force_account_creation():
|
||||
return dispatch_to_register()
|
||||
return dispatch_to_login()
|
||||
elif auth_entry in [AUTH_ENTRY_REGISTER, AUTH_ENTRY_REGISTER_2]:
|
||||
# User has authenticated with the third party provider and now wants to finish
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Third_party_auth integration tests using a mock version of the TestShib provider
|
||||
"""
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
import httpretty
|
||||
from mock import patch
|
||||
@@ -62,8 +63,6 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('Authentication with TestShib is currently unavailable.', response.content)
|
||||
|
||||
# Note: the following patch is only needed until https://github.com/edx/edx-platform/pull/8262 is merged
|
||||
@patch.dict("django.conf.settings.FEATURES", {"AUTOMATIC_AUTH_FOR_TESTING": True})
|
||||
def test_register(self):
|
||||
self._configure_testshib_provider()
|
||||
self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
|
||||
@@ -107,6 +106,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
|
||||
# Now check that we can login again:
|
||||
self.client.logout()
|
||||
self._verify_user_email('myself@testshib.org')
|
||||
self._test_return_login()
|
||||
|
||||
def test_login(self):
|
||||
@@ -222,3 +222,9 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
content_type='application/x-www-form-urlencoded',
|
||||
data=self._read_data_file('testshib_response.txt'),
|
||||
)
|
||||
|
||||
def _verify_user_email(self, email):
|
||||
""" Mark the user with the given email as verified """
|
||||
user = User.objects.get(email=email)
|
||||
user.is_active = True
|
||||
user.save()
|
||||
|
||||
@@ -359,6 +359,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
|
||||
json.dumps({
|
||||
"currentProvider": current_provider,
|
||||
"providers": providers,
|
||||
"secondaryProviders": [],
|
||||
"finishAuthUrl": finish_auth_url,
|
||||
"errorMessage": None,
|
||||
})
|
||||
|
||||
@@ -164,13 +164,14 @@ def _third_party_auth_context(request, redirect_to):
|
||||
context = {
|
||||
"currentProvider": None,
|
||||
"providers": [],
|
||||
"secondaryProviders": [],
|
||||
"finishAuthUrl": None,
|
||||
"errorMessage": None,
|
||||
}
|
||||
|
||||
if third_party_auth.is_enabled():
|
||||
context["providers"] = [
|
||||
{
|
||||
for enabled in third_party_auth.provider.Registry.enabled():
|
||||
info = {
|
||||
"id": enabled.provider_id,
|
||||
"name": enabled.name,
|
||||
"iconClass": enabled.icon_class,
|
||||
@@ -185,8 +186,7 @@ def _third_party_auth_context(request, redirect_to):
|
||||
redirect_url=redirect_to,
|
||||
),
|
||||
}
|
||||
for enabled in third_party_auth.provider.Registry.enabled()
|
||||
]
|
||||
context["providers" if not enabled.secondary else "secondaryProviders"].append(info)
|
||||
|
||||
running_pipeline = pipeline.get(request)
|
||||
if running_pipeline is not None:
|
||||
@@ -194,6 +194,10 @@ def _third_party_auth_context(request, redirect_to):
|
||||
context["currentProvider"] = current_provider.name
|
||||
context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name)
|
||||
|
||||
if current_provider.skip_registration_form:
|
||||
# As a reliable way of "skipping" the registration form, we just submit it automatically
|
||||
context["autoSubmitRegForm"] = True
|
||||
|
||||
# Check for any error messages we may want to display:
|
||||
for msg in messages.get_messages(request):
|
||||
if msg.extra_tags.split()[0] == "social-auth":
|
||||
|
||||
@@ -1274,6 +1274,7 @@ student_account_js = [
|
||||
'js/student_account/views/RegisterView.js',
|
||||
'js/student_account/views/PasswordResetView.js',
|
||||
'js/student_account/views/AccessView.js',
|
||||
'js/student_account/views/InstitutionLoginView.js',
|
||||
'js/student_account/accessApp.js',
|
||||
]
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
'js/student_account/views/FormView': 'js/student_account/views/FormView',
|
||||
'js/student_account/models/LoginModel': 'js/student_account/models/LoginModel',
|
||||
'js/student_account/views/LoginView': 'js/student_account/views/LoginView',
|
||||
'js/student_account/views/InstitutionLoginView': 'js/student_account/views/InstitutionLoginView',
|
||||
'js/student_account/models/PasswordResetModel': 'js/student_account/models/PasswordResetModel',
|
||||
'js/student_account/views/PasswordResetView': 'js/student_account/views/PasswordResetView',
|
||||
'js/student_account/models/RegisterModel': 'js/student_account/models/RegisterModel',
|
||||
@@ -410,6 +411,14 @@
|
||||
'js/student_account/views/FormView'
|
||||
]
|
||||
},
|
||||
'js/student_account/views/InstitutionLoginView': {
|
||||
exports: 'edx.student.account.InstitutionLoginView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone'
|
||||
]
|
||||
},
|
||||
'js/student_account/models/PasswordResetModel': {
|
||||
exports: 'edx.student.account.PasswordResetModel',
|
||||
deps: ['jquery', 'jquery.cookie', 'backbone']
|
||||
@@ -450,6 +459,7 @@
|
||||
'js/student_account/views/LoginView',
|
||||
'js/student_account/views/PasswordResetView',
|
||||
'js/student_account/views/RegisterView',
|
||||
'js/student_account/views/InstitutionLoginView',
|
||||
'js/student_account/models/LoginModel',
|
||||
'js/student_account/models/PasswordResetModel',
|
||||
'js/student_account/models/RegisterModel',
|
||||
@@ -613,6 +623,7 @@
|
||||
'lms/include/js/spec/student_account/access_spec.js',
|
||||
'lms/include/js/spec/student_account/finish_auth_spec.js',
|
||||
'lms/include/js/spec/student_account/login_spec.js',
|
||||
'lms/include/js/spec/student_account/institution_login_spec.js',
|
||||
'lms/include/js/spec/student_account/register_spec.js',
|
||||
'lms/include/js/spec/student_account/password_reset_spec.js',
|
||||
'lms/include/js/spec/student_account/enrollment_spec.js',
|
||||
|
||||
@@ -58,6 +58,7 @@ define([
|
||||
thirdPartyAuth: {
|
||||
currentProvider: null,
|
||||
providers: [],
|
||||
secondaryProviders: [{name: "provider"}],
|
||||
finishAuthUrl: finishAuthUrl
|
||||
},
|
||||
nextUrl: nextUrl, // undefined for default
|
||||
@@ -97,6 +98,8 @@ define([
|
||||
TemplateHelpers.installTemplate('templates/student_account/register');
|
||||
TemplateHelpers.installTemplate('templates/student_account/password_reset');
|
||||
TemplateHelpers.installTemplate('templates/student_account/form_field');
|
||||
TemplateHelpers.installTemplate('templates/student_account/institution_login');
|
||||
TemplateHelpers.installTemplate('templates/student_account/institution_register');
|
||||
|
||||
// Stub analytics tracking
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'pageview', 'trackLink']);
|
||||
@@ -135,6 +138,30 @@ define([
|
||||
assertForms('#login-form', '#register-form');
|
||||
});
|
||||
|
||||
it('toggles between the login and institution login view', function() {
|
||||
ajaxSpyAndInitialize(this, 'login');
|
||||
|
||||
// Simulate clicking on institution login button
|
||||
$('#login-form .button-secondary-login[data-type="institution_login"]').click();
|
||||
assertForms('#institution_login-form', '#login-form');
|
||||
|
||||
// Simulate selection of the login form
|
||||
selectForm('login');
|
||||
assertForms('#login-form', '#institution_login-form');
|
||||
});
|
||||
|
||||
it('toggles between the register and institution register view', function() {
|
||||
ajaxSpyAndInitialize(this, 'register');
|
||||
|
||||
// Simulate clicking on institution login button
|
||||
$('#register-form .button-secondary-login[data-type="institution_login"]').click();
|
||||
assertForms('#institution_login-form', '#register-form');
|
||||
|
||||
// Simulate selection of the login form
|
||||
selectForm('register');
|
||||
assertForms('#register-form', '#institution_login-form');
|
||||
});
|
||||
|
||||
it('displays the reset password form', function() {
|
||||
ajaxSpyAndInitialize(this, 'login');
|
||||
|
||||
|
||||
80
lms/static/js/spec/student_account/institution_login_spec.js
Normal file
80
lms/static/js/spec/student_account/institution_login_spec.js
Normal file
@@ -0,0 +1,80 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'common/js/spec_helpers/template_helpers',
|
||||
'js/student_account/views/InstitutionLoginView',
|
||||
], function($, _, TemplateHelpers, InstitutionLoginView) {
|
||||
'use strict';
|
||||
describe('edx.student.account.InstitutionLoginView', function() {
|
||||
|
||||
var view = null,
|
||||
PLATFORM_NAME = 'edX',
|
||||
THIRD_PARTY_AUTH = {
|
||||
currentProvider: null,
|
||||
providers: [],
|
||||
secondaryProviders: [
|
||||
{
|
||||
id: 'oa2-google-oauth2',
|
||||
name: 'Google',
|
||||
iconClass: 'fa-google-plus',
|
||||
loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login',
|
||||
registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register'
|
||||
},
|
||||
{
|
||||
id: 'oa2-facebook',
|
||||
name: 'Facebook',
|
||||
iconClass: 'fa-facebook',
|
||||
loginUrl: '/auth/login/facebook/?auth_entry=account_login',
|
||||
registerUrl: '/auth/login/facebook/?auth_entry=account_register'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var createInstLoginView = function(mode) {
|
||||
// Initialize the login view
|
||||
view = new InstitutionLoginView({
|
||||
mode: mode,
|
||||
thirdPartyAuth: THIRD_PARTY_AUTH,
|
||||
platformName: PLATFORM_NAME
|
||||
});
|
||||
view.render();
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures('<div id="institution_login-form"></div>');
|
||||
TemplateHelpers.installTemplate('templates/student_account/institution_login');
|
||||
TemplateHelpers.installTemplate('templates/student_account/institution_register');
|
||||
});
|
||||
|
||||
it('displays a list of providers', function() {
|
||||
createInstLoginView('login');
|
||||
expect($('#institution_login-form').html()).not.toBe("");
|
||||
var $google = $('li a:contains("Google")');
|
||||
expect($google).toBeVisible();
|
||||
expect($google).toHaveAttr(
|
||||
'href', '/auth/login/google-oauth2/?auth_entry=account_login'
|
||||
);
|
||||
var $facebook = $('li a:contains("Facebook")');
|
||||
expect($facebook).toBeVisible();
|
||||
expect($facebook).toHaveAttr(
|
||||
'href', '/auth/login/facebook/?auth_entry=account_login'
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a list of providers', function() {
|
||||
createInstLoginView('register');
|
||||
expect($('#institution_login-form').html()).not.toBe("");
|
||||
var $google = $('li a:contains("Google")');
|
||||
expect($google).toBeVisible();
|
||||
expect($google).toHaveAttr(
|
||||
'href', '/auth/login/google-oauth2/?auth_entry=account_register'
|
||||
);
|
||||
var $facebook = $('li a:contains("Facebook")');
|
||||
expect($facebook).toBeVisible();
|
||||
expect($facebook).toHaveAttr(
|
||||
'href', '/auth/login/facebook/?auth_entry=account_register'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,8 @@ var edx = edx || {};
|
||||
subview: {
|
||||
login: {},
|
||||
register: {},
|
||||
passwordHelp: {}
|
||||
passwordHelp: {},
|
||||
institutionLogin: {}
|
||||
},
|
||||
|
||||
nextUrl: '/dashboard',
|
||||
@@ -52,7 +53,8 @@ var edx = edx || {};
|
||||
this.formDescriptions = {
|
||||
login: obj.loginFormDesc,
|
||||
register: obj.registrationFormDesc,
|
||||
reset: obj.passwordResetFormDesc
|
||||
reset: obj.passwordResetFormDesc,
|
||||
institution_login: null
|
||||
};
|
||||
|
||||
this.platformName = obj.platformName;
|
||||
@@ -148,6 +150,16 @@ var edx = edx || {};
|
||||
|
||||
// Listen for 'auth-complete' event so we can enroll/redirect the user appropriately.
|
||||
this.listenTo( this.subview.register, 'auth-complete', this.authComplete );
|
||||
},
|
||||
|
||||
institution_login: function ( unused ) {
|
||||
this.subview.institutionLogin = new edx.student.account.InstitutionLoginView({
|
||||
thirdPartyAuth: this.thirdPartyAuth,
|
||||
platformName: this.platformName,
|
||||
mode: this.activeForm
|
||||
});
|
||||
|
||||
this.subview.institutionLogin.render();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -180,9 +192,11 @@ var edx = edx || {};
|
||||
category: 'user-engagement'
|
||||
});
|
||||
|
||||
if ( !this.form.isLoaded( $form ) ) {
|
||||
// Load the form. Institution login is always refreshed since it changes based on the previous form.
|
||||
if ( !this.form.isLoaded( $form ) || type == "institution_login") {
|
||||
this.loadForm( type );
|
||||
}
|
||||
this.activeForm = type;
|
||||
|
||||
this.element.hide( $(this.el).find('.submission-success') );
|
||||
this.element.hide( $(this.el).find('.form-wrapper') );
|
||||
@@ -190,11 +204,13 @@ var edx = edx || {};
|
||||
this.element.scrollTop( $anchor );
|
||||
|
||||
// Update url without reloading page
|
||||
History.pushState( null, document.title, '/' + type + queryStr );
|
||||
if (type != "institution_login") {
|
||||
History.pushState( null, document.title, '/' + type + queryStr );
|
||||
}
|
||||
analytics.page( 'login_and_registration', type );
|
||||
|
||||
// Focus on the form
|
||||
document.getElementById(type).focus();
|
||||
$("#" + type).focus();
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -215,7 +215,9 @@ var edx = edx || {};
|
||||
submitForm: function( event ) {
|
||||
var data = this.getFormData();
|
||||
|
||||
event.preventDefault();
|
||||
if (!_.isUndefined(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.toggleDisableButton(true);
|
||||
|
||||
|
||||
30
lms/static/js/student_account/views/InstitutionLoginView.js
Normal file
30
lms/static/js/student_account/views/InstitutionLoginView.js
Normal file
@@ -0,0 +1,30 @@
|
||||
var edx = edx || {};
|
||||
|
||||
(function($, _, Backbone) {
|
||||
'use strict';
|
||||
|
||||
edx.student = edx.student || {};
|
||||
edx.student.account = edx.student.account || {};
|
||||
|
||||
edx.student.account.InstitutionLoginView = Backbone.View.extend({
|
||||
el: '#institution_login-form',
|
||||
|
||||
initialize: function( data ) {
|
||||
var tpl = data.mode == "register" ? '#institution_register-tpl' : '#institution_login-tpl';
|
||||
this.tpl = $(tpl).html();
|
||||
this.providers = data.thirdPartyAuth.secondaryProviders || [];
|
||||
this.platformName = data.platformName;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
$(this.el).html( _.template( this.tpl, {
|
||||
// We pass the context object to the template so that
|
||||
// we can perform variable interpolation using sprintf
|
||||
providers: this.providers,
|
||||
platformName: this.platformName
|
||||
}));
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
})(jQuery, _, Backbone);
|
||||
@@ -25,6 +25,9 @@ var edx = edx || {};
|
||||
|
||||
preRender: function( data ) {
|
||||
this.providers = data.thirdPartyAuth.providers || [];
|
||||
this.hasSecondaryProviders = (
|
||||
data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length
|
||||
);
|
||||
this.currentProvider = data.thirdPartyAuth.currentProvider || '';
|
||||
this.errorMessage = data.thirdPartyAuth.errorMessage || '';
|
||||
this.platformName = data.platformName;
|
||||
@@ -45,6 +48,7 @@ var edx = edx || {};
|
||||
currentProvider: this.currentProvider,
|
||||
errorMessage: this.errorMessage,
|
||||
providers: this.providers,
|
||||
hasSecondaryProviders: this.hasSecondaryProviders,
|
||||
platformName: this.platformName
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -22,9 +22,13 @@ var edx = edx || {};
|
||||
|
||||
preRender: function( data ) {
|
||||
this.providers = data.thirdPartyAuth.providers || [];
|
||||
this.hasSecondaryProviders = (
|
||||
data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length
|
||||
);
|
||||
this.currentProvider = data.thirdPartyAuth.currentProvider || '';
|
||||
this.errorMessage = data.thirdPartyAuth.errorMessage || '';
|
||||
this.platformName = data.platformName;
|
||||
this.autoSubmit = data.thirdPartyAuth.autoSubmitRegForm;
|
||||
|
||||
this.listenTo( this.model, 'sync', this.saveSuccess );
|
||||
},
|
||||
@@ -41,12 +45,19 @@ var edx = edx || {};
|
||||
currentProvider: this.currentProvider,
|
||||
errorMessage: this.errorMessage,
|
||||
providers: this.providers,
|
||||
hasSecondaryProviders: this.hasSecondaryProviders,
|
||||
platformName: this.platformName
|
||||
}
|
||||
}));
|
||||
|
||||
this.postRender();
|
||||
|
||||
if (this.autoSubmit) {
|
||||
$(this.el).hide();
|
||||
$('#register-honor_code').prop('checked', true);
|
||||
this.submitForm();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -63,6 +74,7 @@ var edx = edx || {};
|
||||
},
|
||||
|
||||
saveError: function( error ) {
|
||||
$(this.el).show(); // Show in case the form was hidden for auto-submission
|
||||
this.errors = _.flatten(
|
||||
_.map(
|
||||
JSON.parse(error.responseText),
|
||||
@@ -76,6 +88,13 @@ var edx = edx || {};
|
||||
);
|
||||
this.setErrors();
|
||||
this.toggleDisableButton(false);
|
||||
}
|
||||
},
|
||||
|
||||
postFormSubmission: function() {
|
||||
if (_.compact(this.errors).length) {
|
||||
// The form did not get submitted due to validation errors.
|
||||
$(this.el).show(); // Show in case the form was hidden for auto-submission
|
||||
}
|
||||
},
|
||||
});
|
||||
})(jQuery, _, gettext);
|
||||
|
||||
@@ -14,6 +14,7 @@ $sm-btn-linkedin: #0077b5;
|
||||
background: $white;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
$third-party-button-height: ($baseline*1.75);
|
||||
|
||||
h2 {
|
||||
@extend %t-title5;
|
||||
@@ -22,6 +23,10 @@ $sm-btn-linkedin: #0077b5;
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
@extend %t-copy-base;
|
||||
}
|
||||
|
||||
/* Temp. fix until applied globally */
|
||||
> {
|
||||
@include box-sizing(border-box);
|
||||
@@ -67,10 +72,11 @@ $sm-btn-linkedin: #0077b5;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
form,
|
||||
.wrapper-other-login {
|
||||
border: 1px solid $gray-l4;
|
||||
border-radius: 5px;
|
||||
padding: 0px 25px 20px 25px;
|
||||
border-radius: ($baseline/4);
|
||||
padding: 0 ($baseline*1.25) $baseline ($baseline*1.25);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@@ -106,16 +112,20 @@ $sm-btn-linkedin: #0077b5;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
%nav-btn-base {
|
||||
@extend %btn-secondary-blue-outline;
|
||||
width: 100%;
|
||||
height: ($baseline*2);
|
||||
text-transform: none;
|
||||
text-shadow: none;
|
||||
font-weight: 600;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
@extend %nav-btn-base;
|
||||
@extend %t-strong;
|
||||
}
|
||||
|
||||
.form-type,
|
||||
.toggle-form {
|
||||
@include box-sizing(border-box);
|
||||
@@ -348,29 +358,31 @@ $sm-btn-linkedin: #0077b5;
|
||||
|
||||
.login-provider {
|
||||
@extend %btn-secondary-grey-outline;
|
||||
width: 130px;
|
||||
padding: 0 0 0 ($baseline*2);
|
||||
height: 34px;
|
||||
text-align: left;
|
||||
@extend %t-action4;
|
||||
|
||||
@include padding(0, 0, 0, $baseline*2);
|
||||
@include text-align(left);
|
||||
|
||||
position: relative;
|
||||
margin-right: ($baseline/4);
|
||||
margin-bottom: $baseline;
|
||||
border-color: $lightGrey1;
|
||||
width: $baseline*6.5;
|
||||
height: $third-party-button-height;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
position: relative;
|
||||
font-size: 0.8em;
|
||||
border-color: $lightGrey1;
|
||||
|
||||
&:nth-of-type(odd) {
|
||||
margin-right: 13px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: white;
|
||||
@include left(0);
|
||||
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
width: 30px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
bottom: -1px;
|
||||
background: $m-blue-d3;
|
||||
line-height: $third-party-button-height;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
@@ -378,16 +390,12 @@ $sm-btn-linkedin: #0077b5;
|
||||
background-image: none;
|
||||
|
||||
.icon {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
line-height: ($third-party-button-height - 2px);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
&.button-oa2-google-oauth2 {
|
||||
color: $sm-btn-google;
|
||||
|
||||
@@ -447,6 +455,19 @@ $sm-btn-linkedin: #0077b5;
|
||||
|
||||
}
|
||||
|
||||
.button-secondary-login {
|
||||
@extend %nav-btn-base;
|
||||
@extend %t-action4;
|
||||
@extend %t-regular;
|
||||
border-color: $lightGrey1;
|
||||
padding: 0;
|
||||
height: $third-party-button-height;
|
||||
|
||||
&:hover {
|
||||
border-color: $m-blue-d3;
|
||||
}
|
||||
}
|
||||
|
||||
/** Error Container - from _account.scss **/
|
||||
.status {
|
||||
@include box-sizing(border-box);
|
||||
@@ -503,6 +524,13 @@ $sm-btn-linkedin: #0077b5;
|
||||
}
|
||||
}
|
||||
|
||||
.institution-list {
|
||||
|
||||
.institution {
|
||||
@extend %t-copy-base;
|
||||
}
|
||||
}
|
||||
|
||||
@include media( max-width 330px) {
|
||||
.form-type {
|
||||
width: 98%;
|
||||
|
||||
@@ -9,3 +9,7 @@
|
||||
<section id="password-reset-anchor" class="form-type">
|
||||
<div id="password-reset-form" class="form-wrapper hidden" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<section id="institution_login-anchor" class="form-type">
|
||||
<div id="institution_login-form" class="form-wrapper hidden" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
31
lms/templates/student_account/institution_login.underscore
Normal file
31
lms/templates/student_account/institution_login.underscore
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="wrapper-other-login">
|
||||
<div class="section-title lines">
|
||||
<h2>
|
||||
<span class="text">
|
||||
<%- gettext("Sign in with Institution/Campus Credentials") %>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="instructions"><%- gettext("Choose your institution from the list below:") %></p>
|
||||
|
||||
<ul class="institution-list">
|
||||
<% _.each( _.sortBy(providers, "name"), function( provider ) {
|
||||
if ( provider.loginUrl ) { %>
|
||||
<li class="institution">
|
||||
<a class="institution-login-link" href="<%- provider.loginUrl %>"><%- provider.name %></a>
|
||||
</li>
|
||||
<% }
|
||||
}); %>
|
||||
</ul>
|
||||
|
||||
<div class="section-title lines">
|
||||
<h2>
|
||||
<span class="text"><%- gettext("or") %></span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="toggle-form">
|
||||
<button class="nav-btn form-toggle" data-type="login"><%- gettext("Back to sign in") %></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="wrapper-other-login">
|
||||
<div class="section-title lines">
|
||||
<h2>
|
||||
<span class="text">
|
||||
<%- gettext("Register with Institution/Campus Credentials") %>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="instructions"><%- gettext("Choose your institution from the list below:") %></p>
|
||||
|
||||
<ul class="institution-list">
|
||||
<% _.each( _.sortBy(providers, "name"), function( provider ) {
|
||||
if ( provider.registerUrl ) { %>
|
||||
<li class="institution">
|
||||
<a class="institution-login-link" href="<%- provider.registerUrl %>"><%- provider.name %></a>
|
||||
</li>
|
||||
<% }
|
||||
}); %>
|
||||
</ul>
|
||||
|
||||
<div class="section-title lines">
|
||||
<h2>
|
||||
<span class="text"><%- gettext("or") %></span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="toggle-form">
|
||||
<button class="nav-btn form-toggle" data-type="register"><%- gettext("Register through edX") %></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<button type="submit" class="action action-primary action-update js-login login-button"><%- gettext("Sign in") %></button>
|
||||
|
||||
<% if ( context.providers.length > 0 && !context.currentProvider ) { %>
|
||||
<% if ( context.providers.length > 0 && !context.currentProvider || context.hasSecondaryProviders ) { %>
|
||||
<div class="login-providers">
|
||||
<div class="section-title lines">
|
||||
<h2>
|
||||
@@ -55,6 +55,12 @@
|
||||
</button>
|
||||
<% }
|
||||
}); %>
|
||||
|
||||
<% if ( context.hasSecondaryProviders ) { %>
|
||||
<button type="button" class="button-secondary-login form-toggle" data-type="institution_login">
|
||||
<%- gettext("Use my institution/campus credentials") %>
|
||||
</button>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</form>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["account", "access", "form_field", "login", "register", "password_reset"]:
|
||||
% for template_name in ["account", "access", "form_field", "login", "register", "institution_login", "institution_register", "password_reset"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="student_account/${template_name}.underscore" />
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<%- _.sprintf( gettext("We just need a little more information before you start learning with %(platformName)s."), context ) %>
|
||||
</p>
|
||||
</div>
|
||||
<% } else if ( context.providers.length > 0 ) { %>
|
||||
<% } else if ( context.providers.length > 0 || context.hasSecondaryProviders ) { %>
|
||||
<div class="login-providers">
|
||||
<div class="section-title lines">
|
||||
<h2>
|
||||
@@ -35,6 +35,12 @@
|
||||
</button>
|
||||
<% }
|
||||
}); %>
|
||||
|
||||
<% if ( context.hasSecondaryProviders ) { %>
|
||||
<button type="button" class="button-secondary-login form-toggle" data-type="institution_login">
|
||||
<%- gettext("Use my institution/campus credentials") %>
|
||||
</button>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="section-title lines">
|
||||
<h2>
|
||||
|
||||
Reference in New Issue
Block a user