diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json
index 44cd55ef73..943a80bf1e 100644
--- a/cms/envs/bok_choy.env.json
+++ b/cms/envs/bok_choy.env.json
@@ -90,7 +90,7 @@
"LOCAL_LOGLEVEL": "INFO",
"LOGGING_ENV": "sandbox",
"LOG_DIR": "** OVERRIDDEN **",
- "MEDIA_URL": "",
+ "MEDIA_URL": "/media/",
"MKTG_URL_LINK_MAP": {},
"PLATFORM_NAME": "edX",
"SERVER_EMAIL": "devops@example.com",
diff --git a/common/djangoapps/config_models/admin.py b/common/djangoapps/config_models/admin.py
index a0753ff33c..3718ad3131 100644
--- a/common/djangoapps/config_models/admin.py
+++ b/common/djangoapps/config_models/admin.py
@@ -6,6 +6,7 @@ from django.forms import models
from django.contrib import admin
from django.contrib.admin import ListFilter
from django.core.cache import caches, InvalidCacheBackendError
+from django.core.files.base import File
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
@@ -178,7 +179,16 @@ class KeyedConfigurationModelAdmin(ConfigurationModelAdmin):
get = request.GET.copy()
source_id = int(get.pop('source')[0])
source = get_object_or_404(self.model, pk=source_id)
- get.update(models.model_to_dict(source))
+ source_dict = models.model_to_dict(source)
+ for field_name, field_value in source_dict.items():
+ # read files into request.FILES, if:
+ # * user hasn't ticked the "clear" checkbox
+ # * user hasn't uploaded a new file
+ if field_value and isinstance(field_value, File):
+ clear_checkbox_name = '{0}-clear'.format(field_name)
+ if request.POST.get(clear_checkbox_name) != 'on':
+ request.FILES.setdefault(field_name, field_value)
+ get[field_name] = field_value
request.GET = get
# Call our grandparent's add_view, skipping the parent code
# because the parent code has a different way to prepopulate new configuration entries
diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py
index 1d1deb8787..a7409bad23 100644
--- a/common/djangoapps/course_modes/models.py
+++ b/common/djangoapps/course_modes/models.py
@@ -398,7 +398,7 @@ class CourseMode(models.Model):
@classmethod
def has_verified_mode(cls, course_mode_dict):
- """Check whether the modes for a course allow a student to pursue a verfied certificate.
+ """Check whether the modes for a course allow a student to pursue a verified certificate.
Args:
course_mode_dict (dictionary mapping course mode slugs to Modes)
diff --git a/common/djangoapps/third_party_auth/admin.py b/common/djangoapps/third_party_auth/admin.py
index 02cc865325..3491be6eee 100644
--- a/common/djangoapps/third_party_auth/admin.py
+++ b/common/djangoapps/third_party_auth/admin.py
@@ -37,7 +37,8 @@ class SAMLProviderConfigAdmin(KeyedConfigurationModelAdmin):
""" Don't show every single field in the admin change list """
return (
'name', 'enabled', 'backend_name', 'entity_id', 'metadata_source',
- 'has_data', 'icon_class', 'change_date', 'changed_by', 'edit_link'
+ 'has_data', 'icon_class', 'icon_image', 'change_date',
+ 'changed_by', 'edit_link'
)
def has_data(self, inst):
@@ -104,6 +105,7 @@ class LTIProviderConfigAdmin(KeyedConfigurationModelAdmin):
exclude = (
'icon_class',
+ 'icon_image',
'secondary',
)
diff --git a/common/djangoapps/third_party_auth/migrations/0002_schema__provider_icon_image.py b/common/djangoapps/third_party_auth/migrations/0002_schema__provider_icon_image.py
new file mode 100644
index 0000000000..073cddf69c
--- /dev/null
+++ b/common/djangoapps/third_party_auth/migrations/0002_schema__provider_icon_image.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('third_party_auth', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='ltiproviderconfig',
+ name='icon_image',
+ field=models.FileField(help_text=b'If there is no Font Awesome icon available for this provider, upload a custom image. SVG images are recommended as they can scale to any size.', upload_to=b'', blank=True),
+ ),
+ migrations.AddField(
+ model_name='oauth2providerconfig',
+ name='icon_image',
+ field=models.FileField(help_text=b'If there is no Font Awesome icon available for this provider, upload a custom image. SVG images are recommended as they can scale to any size.', upload_to=b'', blank=True),
+ ),
+ migrations.AddField(
+ model_name='samlproviderconfig',
+ name='icon_image',
+ field=models.FileField(help_text=b'If there is no Font Awesome icon available for this provider, upload a custom image. SVG images are recommended as they can scale to any size.', upload_to=b'', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='ltiproviderconfig',
+ name='icon_class',
+ field=models.CharField(default=b'fa-sign-in', help_text=b'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', max_length=50, blank=True),
+ ),
+ migrations.AlterField(
+ model_name='oauth2providerconfig',
+ name='icon_class',
+ field=models.CharField(default=b'fa-sign-in', help_text=b'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', max_length=50, blank=True),
+ ),
+ migrations.AlterField(
+ model_name='samlproviderconfig',
+ name='icon_class',
+ field=models.CharField(default=b'fa-sign-in', help_text=b'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', max_length=50, blank=True),
+ ),
+ ]
diff --git a/common/djangoapps/third_party_auth/models.py b/common/djangoapps/third_party_auth/models.py
index b699cbe25e..5200a92b71 100644
--- a/common/djangoapps/third_party_auth/models.py
+++ b/common/djangoapps/third_party_auth/models.py
@@ -70,12 +70,25 @@ class ProviderConfig(ConfigurationModel):
Abstract Base Class for configuring a third_party_auth provider
"""
icon_class = models.CharField(
- max_length=50, default='fa-sign-in',
+ max_length=50,
+ blank=True,
+ default='fa-sign-in',
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'
),
)
+ # We use a FileField instead of an ImageField here because ImageField
+ # doesn't support SVG. This means we don't get any image validation, but
+ # that should be fine because only trusted users should be uploading these
+ # anyway.
+ icon_image = models.FileField(
+ blank=True,
+ help_text=(
+ 'If there is no Font Awesome icon available for this provider, upload a custom image. '
+ 'SVG images are recommended as they can scale to any size.'
+ ),
+ )
name = models.CharField(max_length=50, blank=False, help_text="Name of this provider (shown to users)")
secondary = models.BooleanField(
default=False,
@@ -109,6 +122,12 @@ class ProviderConfig(ConfigurationModel):
app_label = "third_party_auth"
abstract = True
+ def clean(self):
+ """ Ensure that either `icon_class` or `icon_image` is set """
+ super(ProviderConfig, self).clean()
+ if bool(self.icon_class) == bool(self.icon_image):
+ raise ValidationError('Either an icon class or an icon image must be given (but not both)')
+
@property
def provider_id(self):
""" Unique string key identifying this provider. Must be URL and css class friendly. """
@@ -500,9 +519,15 @@ class LTIProviderConfig(ProviderConfig):
"""
prefix = 'lti'
backend_name = 'lti'
- icon_class = None # This provider is not visible to users
- secondary = False # This provider is not visible to users
- accepts_logins = False # LTI login cannot be initiated by the tool provider
+
+ # This provider is not visible to users
+ icon_class = None
+ icon_image = None
+ secondary = False
+
+ # LTI login cannot be initiated by the tool provider
+ accepts_logins = False
+
KEY_FIELDS = ('lti_consumer_key', )
lti_consumer_key = models.CharField(
diff --git a/common/djangoapps/third_party_auth/tests/specs/test_azuread.py b/common/djangoapps/third_party_auth/tests/specs/test_azuread.py
new file mode 100644
index 0000000000..680983250a
--- /dev/null
+++ b/common/djangoapps/third_party_auth/tests/specs/test_azuread.py
@@ -0,0 +1,46 @@
+"""Integration tests for Azure Active Directory / Microsoft Account provider."""
+
+from third_party_auth.tests.specs import base
+
+
+# pylint: disable=test-inherits-tests
+class AzureADOauth2IntegrationTest(base.Oauth2IntegrationTest):
+ """Integration tests for Azure Active Directory / Microsoft Account provider."""
+
+ def setUp(self):
+ super(AzureADOauth2IntegrationTest, self).setUp()
+ self.provider = self.configure_azure_ad_provider(
+ enabled=True,
+ key='azure_ad_oauth2_key',
+ secret='azure_ad_oauth2_secret',
+ )
+
+ TOKEN_RESPONSE_DATA = {
+ 'exp': 1234590302,
+ 'nbf': 1234586402,
+ 'iat': 1234586402,
+ 'expires_on': '1234590302',
+ 'ver': '1.0',
+ 'access_token': 'access_token_value',
+ 'expires_in': '3599',
+ 'id_token': 'id_token_value',
+ 'token_type': 'Bearer',
+ 'refresh_token': 'REFRESH1234567890',
+ 'iss': 'https://sts.windows.net/abcdefgh-1234-5678-900a-0aa0a00aa0aa/',
+ 'ipaddr': '123.123.123.123',
+ }
+ USER_RESPONSE_DATA = {
+ 'oid': 'abcdefgh-1234-5678-900a-0aa0a00aa0aa',
+ 'aud': 'abcdefgh-1234-5678-900a-0aa0a00aa0aa',
+ 'tid': 'abcdefgh-1234-5678-900a-0aa0a00aa0aa',
+ 'amr': ['pwd'],
+ 'unique_name': 'email_value@example.com',
+ 'upn': 'email_value@example.com',
+ 'family_name': 'family_name_value',
+ 'name': 'name_value',
+ 'given_name': 'given_name_value',
+ 'sub': 'aBC_ab12345678h94CSgP1lTYJCHATGQDAcfg8jSOck',
+ }
+
+ def get_username(self):
+ return self.get_response_data().get('name')
diff --git a/common/djangoapps/third_party_auth/tests/test_admin.py b/common/djangoapps/third_party_auth/tests/test_admin.py
new file mode 100644
index 0000000000..9ae56b5a86
--- /dev/null
+++ b/common/djangoapps/third_party_auth/tests/test_admin.py
@@ -0,0 +1,85 @@
+"""
+Tests third_party_auth admin views
+"""
+import unittest
+
+from django.conf import settings
+from django.contrib.admin.sites import AdminSite
+from django.core.urlresolvers import reverse
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.forms import models
+
+from student.tests.factories import UserFactory
+from third_party_auth.admin import OAuth2ProviderConfigAdmin
+from third_party_auth.models import OAuth2ProviderConfig
+from third_party_auth.tests import testutil
+
+
+# This is necessary because cms does not implement third party auth
+@unittest.skipUnless(settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'), 'third party auth not enabled')
+class Oauth2ProviderConfigAdminTest(testutil.TestCase):
+ """
+ Tests for oauth2 provider config admin
+ """
+ def test_oauth2_provider_edit_icon_image(self):
+ """
+ Test that we can update an OAuth provider's icon image from the admin
+ form.
+
+ OAuth providers are updated using KeyedConfigurationModelAdmin, which
+ updates models by adding a new instance that replaces the old one,
+ instead of editing the old instance directly.
+
+ Updating the icon image is tricky here because
+ KeyedConfigurationModelAdmin copies data over from the previous
+ version by injecting its attributes into request.GET, but the icon
+ ends up in request.FILES. We need to ensure that the value is
+ prepopulated correctly, and that we can clear and update the image.
+ """
+ # Login as a super user
+ user = UserFactory.create(is_staff=True, is_superuser=True)
+ user.save()
+ self.client.login(username=user.username, password='test')
+
+ # Get baseline provider count
+ providers = OAuth2ProviderConfig.objects.all()
+ pcount = len(providers)
+
+ # Create a provider
+ provider1 = self.configure_dummy_provider(
+ enabled=True,
+ icon_class='',
+ icon_image=SimpleUploadedFile('icon.svg', ''),
+ )
+
+ # Get the provider instance with active flag
+ providers = OAuth2ProviderConfig.objects.all()
+ self.assertEquals(len(providers), 1)
+ self.assertEquals(providers[pcount].id, provider1.id)
+
+ # Edit the provider via the admin edit link
+ admin = OAuth2ProviderConfigAdmin(provider1, AdminSite())
+ # pylint: disable=protected-access
+ update_url = reverse('admin:{}_{}_add'.format(admin.model._meta.app_label, admin.model._meta.model_name))
+ update_url += "?source={}".format(provider1.pk)
+
+ # Remove the icon_image from the POST data, to simulate unchanged icon_image
+ post_data = models.model_to_dict(provider1)
+ del post_data['icon_image']
+
+ # Change the name, to verify POST
+ post_data['name'] = 'Another name'
+
+ # Post the edit form: expecting redirect
+ response = self.client.post(update_url, post_data)
+ self.assertEquals(response.status_code, 302)
+
+ # Editing the existing provider creates a new provider instance
+ providers = OAuth2ProviderConfig.objects.all()
+ self.assertEquals(len(providers), pcount + 2)
+ self.assertEquals(providers[pcount].id, provider1.id)
+ provider2 = providers[pcount + 1]
+
+ # Ensure the icon_image was preserved on the new provider instance
+ self.assertEquals(provider2.icon_image, provider1.icon_image)
+ self.assertEquals(provider2.name, post_data['name'])
diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py
index 36ac1c698c..fa9ef60bf1 100644
--- a/common/djangoapps/third_party_auth/tests/testutil.py
+++ b/common/djangoapps/third_party_auth/tests/testutil.py
@@ -13,6 +13,7 @@ import django.test
from mako.template import Template
import mock
import os.path
+from storages.backends.overwrite import OverwriteStorage
from third_party_auth.models import (
OAuth2ProviderConfig,
@@ -52,6 +53,17 @@ class FakeDjangoSettings(object):
class ThirdPartyAuthTestMixin(object):
""" Helper methods useful for testing third party auth functionality """
+ def setUp(self, *args, **kwargs):
+ # Django's FileSystemStorage will rename files if they already exist.
+ # This storage backend overwrites files instead, which makes it easier
+ # to make assertions about filenames.
+ icon_image_field = OAuth2ProviderConfig._meta.get_field('icon_image') # pylint: disable=protected-access
+ patch = mock.patch.object(icon_image_field, 'storage', OverwriteStorage())
+ patch.start()
+ self.addCleanup(patch.stop)
+
+ super(ThirdPartyAuthTestMixin, self).setUp(*args, **kwargs)
+
def tearDown(self):
config_cache.clear()
super(ThirdPartyAuthTestMixin, self).tearDown()
@@ -112,6 +124,16 @@ class ThirdPartyAuthTestMixin(object):
kwargs.setdefault("secret", "test")
return cls.configure_oauth_provider(**kwargs)
+ @classmethod
+ def configure_azure_ad_provider(cls, **kwargs):
+ """ Update the settings for the Azure AD third party auth provider/backend """
+ kwargs.setdefault("name", "Azure AD")
+ kwargs.setdefault("backend_name", "azuread-oauth2")
+ kwargs.setdefault("icon_class", "fa-azuread")
+ kwargs.setdefault("key", "test")
+ kwargs.setdefault("secret", "test")
+ return cls.configure_oauth_provider(**kwargs)
+
@classmethod
def configure_twitter_provider(cls, **kwargs):
""" Update the settings for the Twitter third party auth provider/backend """
@@ -124,7 +146,7 @@ class ThirdPartyAuthTestMixin(object):
@classmethod
def configure_dummy_provider(cls, **kwargs):
- """ Update the settings for the Twitter third party auth provider/backend """
+ """ Update the settings for the Dummy third party auth provider/backend """
kwargs.setdefault("name", "Dummy")
kwargs.setdefault("backend_name", "dummy")
return cls.configure_oauth_provider(**kwargs)
diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss
index 09f2e1b982..cf781c3b50 100644
--- a/common/static/sass/_mixins.scss
+++ b/common/static/sass/_mixins.scss
@@ -24,6 +24,7 @@
// * +Content - Text Wrap - Extend
// * +Content - Text Truncate - Extend
// * +Icon - Font-Awesome - Extend
+// * +Icon - SSO icon images
// +Font Sizing - Mixin
// ====================
@@ -448,3 +449,17 @@
padding: 0;
margin: 0;
}
+
+
+// * +Icon - SSO icon images
+// ====================
+
+%sso-icon {
+ .icon-image {
+ width: auto;
+ height: auto;
+ max-height: 1.4em;
+ max-width: 1.4em;
+ margin-top: -2px;
+ }
+}
diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py
index 1999c385ae..2ab32a4640 100644
--- a/common/test/acceptance/tests/lms/test_lms.py
+++ b/common/test/acceptance/tests/lms/test_lms.py
@@ -166,8 +166,11 @@ class LoginFromCombinedPageTest(UniqueCourseTest):
# Create a user account
email, password = self._create_unique_user()
- # Navigate to the login page and try to log in using "Dummy" provider
+ # Navigate to the login page
self.login_page.visit()
+ self.assertScreenshot('#login .login-providers', 'login-providers')
+
+ # Try to log in using "Dummy" provider
self.login_page.click_third_party_dummy_provider()
# The user will be redirected somewhere and then back to the login page:
@@ -206,6 +209,7 @@ class LoginFromCombinedPageTest(UniqueCourseTest):
# We should now be redirected to the login page
self.login_page.wait_for_page()
self.assertIn("Would you like to sign in using your Dummy credentials?", self.login_page.hinted_login_prompt)
+ self.assertScreenshot('#hinted-login-form', 'hinted-login')
self.login_page.click_third_party_dummy_provider()
# We should now be redirected to the course page
@@ -329,8 +333,11 @@ class RegisterFromCombinedPageTest(UniqueCourseTest):
Test that we can register using third party credentials, and that the
third party account gets linked to the edX account.
"""
- # Navigate to the register page and try to authenticate using the "Dummy" provider
+ # Navigate to the register page
self.register_page.visit()
+ self.assertScreenshot('#register .login-providers', 'register-providers')
+
+ # Try to authenticate using the "Dummy" provider
self.register_page.click_third_party_dummy_provider()
# The user will be redirected somewhere and then back to the register page:
diff --git a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
index dd79a53310..df4e0a2455 100644
--- a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
+++ b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
@@ -3,9 +3,8 @@
End-to-end tests for the LMS Instructor Dashboard.
"""
-import time
+import ddt
-from flaky import flaky
from nose.plugins.attrib import attr
from bok_choy.promise import EmptyPromise
@@ -652,6 +651,7 @@ class DataDownloadsTest(BaseInstructorDashboardTest):
@attr('shard_7')
+@ddt.ddt
class CertificatesTest(BaseInstructorDashboardTest):
"""
Tests for Certificates functionality on instructor dashboard.
@@ -907,6 +907,36 @@ class CertificatesTest(BaseInstructorDashboardTest):
self.certificates_section.message.text
)
+ @ddt.data(
+ ('Test \nNotes', 'Test Notes'),
+ ('
<%- _.sprintf( gettext("Would you like to sign in using your %(providerName)s credentials?"), { providerName: hintedProvider.name } ) %>
diff --git a/lms/templates/student_account/login.underscore b/lms/templates/student_account/login.underscore index 5ee369923a..c28ead2c27 100644 --- a/lms/templates/student_account/login.underscore +++ b/lms/templates/student_account/login.underscore @@ -61,7 +61,11 @@ <% _.each( context.providers, function( provider ) { if ( provider.loginUrl ) { %> diff --git a/lms/templates/student_account/register.underscore b/lms/templates/student_account/register.underscore index ddd409dbda..bbcb7f12cd 100644 --- a/lms/templates/student_account/register.underscore +++ b/lms/templates/student_account/register.underscore @@ -30,7 +30,11 @@ _.each( context.providers, function( provider) { if ( provider.registerUrl ) { %> diff --git a/lms/urls.py b/lms/urls.py index 18c5cb80c4..c8b96c99c9 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -943,6 +943,7 @@ urlpatterns = patterns(*urlpatterns) if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static( settings.PROFILE_IMAGE_BACKEND['options']['base_url'], document_root=settings.PROFILE_IMAGE_BACKEND['options']['location'] diff --git a/pavelib/bok_choy.py b/pavelib/bok_choy.py index 3c4bb319fa..af76e7512a 100644 --- a/pavelib/bok_choy.py +++ b/pavelib/bok_choy.py @@ -30,7 +30,8 @@ BOKCHOY_OPTS = [ make_option("-q", "--quiet", action="store_const", const=0, dest="verbosity"), make_option("-v", "--verbosity", action="count", dest="verbosity"), make_option("--pdb", action="store_true", help="Drop into debugger on failures or errors"), - make_option("--skip_firefox_version_validation", action='store_false', dest="validate_firefox_version") + make_option("--skip_firefox_version_validation", action='store_false', dest="validate_firefox_version"), + make_option("--save_screenshots", action='store_true', dest="save_screenshots"), ] @@ -52,6 +53,7 @@ def parse_bokchoy_opts(options): 'extra_args': getattr(options, 'extra_args', ''), 'pdb': getattr(options, 'pdb', False), 'test_dir': getattr(options, 'test_dir', 'tests'), + 'save_screenshots': getattr(options, 'save_screenshots', False), } diff --git a/pavelib/utils/test/suites/bokchoy_suite.py b/pavelib/utils/test/suites/bokchoy_suite.py index bdf8566f8c..38461a311a 100644 --- a/pavelib/utils/test/suites/bokchoy_suite.py +++ b/pavelib/utils/test/suites/bokchoy_suite.py @@ -62,6 +62,7 @@ class BokChoyTestSuite(TestSuite): self.a11y_file = Env.BOK_CHOY_A11Y_CUSTOM_RULES_FILE self.imports_dir = kwargs.get('imports_dir', None) self.coveragerc = kwargs.get('coveragerc', None) + self.save_screenshots = kwargs.get('save_screenshots', False) def __enter__(self): super(BokChoyTestSuite, self).__enter__() @@ -234,6 +235,8 @@ class BokChoyTestSuite(TestSuite): ] if self.pdb: cmd.append("--pdb") + if self.save_screenshots: + cmd.append("--with-save-baseline") cmd.append(self.extra_args) cmd = (" ").join(cmd) diff --git a/screenshots/baseline/hinted-login.png b/screenshots/baseline/hinted-login.png new file mode 100644 index 0000000000..fe581ce31a Binary files /dev/null and b/screenshots/baseline/hinted-login.png differ diff --git a/screenshots/baseline/login-providers.png b/screenshots/baseline/login-providers.png new file mode 100644 index 0000000000..70c19ca226 Binary files /dev/null and b/screenshots/baseline/login-providers.png differ diff --git a/screenshots/baseline/register-providers.png b/screenshots/baseline/register-providers.png new file mode 100644 index 0000000000..43eabefd87 Binary files /dev/null and b/screenshots/baseline/register-providers.png differ diff --git a/test_root/uploads/.gitignore b/test_root/uploads/.gitignore index 409deae7b2..8e2c34b24a 100644 --- a/test_root/uploads/.gitignore +++ b/test_root/uploads/.gitignore @@ -2,3 +2,5 @@ *.jpg *.png *.txt +*.svg +!test_icon.png diff --git a/test_root/uploads/test-icon.png b/test_root/uploads/test-icon.png new file mode 100644 index 0000000000..c1c8e813fd Binary files /dev/null and b/test_root/uploads/test-icon.png differ diff --git a/themes/stanford-style/lms/templates/register-form.html b/themes/stanford-style/lms/templates/register-form.html index b498ddf130..cf5b017968 100644 --- a/themes/stanford-style/lms/templates/register-form.html +++ b/themes/stanford-style/lms/templates/register-form.html @@ -26,7 +26,14 @@ from student.models import UserProfile % for enabled in provider.Registry.accepting_logins(): ## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn). - + % endfor