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/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/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 97b45ea0a9..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() @@ -134,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/db_fixtures/third_party_auth.json b/common/test/db_fixtures/third_party_auth.json index 3042ebbb66..9b4fad7bff 100644 --- a/common/test/db_fixtures/third_party_auth.json +++ b/common/test/db_fixtures/third_party_auth.json @@ -8,6 +8,7 @@ "changed_by": null, "name": "Google", "icon_class": "fa-google-plus", + "icon_image": null, "backend_name": "google-oauth2", "key": "test", "secret": "test", @@ -23,6 +24,7 @@ "changed_by": null, "name": "Facebook", "icon_class": "fa-facebook", + "icon_image": null, "backend_name": "facebook", "key": "test", "secret": "test", @@ -37,7 +39,8 @@ "change_date": "2001-02-03T04:05:06Z", "changed_by": null, "name": "Dummy", - "icon_class": "fa-sign-in", + "icon_class": "", + "icon_image": "test-icon.png", "backend_name": "dummy", "key": "", "secret": "", diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index b9376b75c5..38ce17f797 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -4,13 +4,13 @@ import re from unittest import skipUnless from urllib import urlencode -import json import mock import ddt from django.conf import settings -from django.core.urlresolvers import reverse from django.core import mail +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.urlresolvers import reverse from django.contrib import messages from django.contrib.messages.middleware import MessageMiddleware from django.test import TestCase @@ -214,9 +214,15 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi @mock.patch.dict(settings.FEATURES, {'EMBARGO': True}) def setUp(self): super(StudentAccountLoginAndRegistrationTest, self).setUp('embargo') - # For these tests, two third party auth providers are enabled by default: + + # For these tests, three third party auth providers are enabled by default: self.configure_google_provider(enabled=True) self.configure_facebook_provider(enabled=True) + self.configure_dummy_provider( + enabled=True, + icon_class='', + icon_image=SimpleUploadedFile('icon.svg', ''), + ) @ddt.data( ("signin_user", "login"), @@ -290,6 +296,8 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi ("register_user", "google-oauth2", "Google"), ("signin_user", "facebook", "Facebook"), ("register_user", "facebook", "Facebook"), + ("signin_user", "dummy", "Dummy"), + ("register_user", "dummy", "Dummy"), ) @ddt.unpack def test_third_party_auth(self, url_name, current_backend, current_provider): @@ -313,10 +321,19 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi # This relies on the THIRD_PARTY_AUTH configuration in the test settings expected_providers = [ + { + "id": "oa2-dummy", + "name": "Dummy", + "iconClass": None, + "iconImage": settings.MEDIA_URL + "icon.svg", + "loginUrl": self._third_party_login_url("dummy", "login", params), + "registerUrl": self._third_party_login_url("dummy", "register", params) + }, { "id": "oa2-facebook", "name": "Facebook", "iconClass": "fa-facebook", + "iconImage": None, "loginUrl": self._third_party_login_url("facebook", "login", params), "registerUrl": self._third_party_login_url("facebook", "register", params) }, @@ -324,9 +341,10 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi "id": "oa2-google-oauth2", "name": "Google", "iconClass": "fa-google-plus", + "iconImage": None, "loginUrl": self._third_party_login_url("google-oauth2", "login", params), "registerUrl": self._third_party_login_url("google-oauth2", "register", params) - } + }, ] self._assert_third_party_auth_data(response, current_backend, current_provider, expected_providers) diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 3db78369e2..427c717d40 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -198,7 +198,8 @@ def _third_party_auth_context(request, redirect_to): info = { "id": enabled.provider_id, "name": enabled.name, - "iconClass": enabled.icon_class, + "iconClass": enabled.icon_class or None, + "iconImage": enabled.icon_image.url if enabled.icon_image else None, "loginUrl": pipeline.get_login_url( enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, diff --git a/lms/envs/bok_choy.env.json b/lms/envs/bok_choy.env.json index a745c4c615..b73ed4f7c8 100644 --- a/lms/envs/bok_choy.env.json +++ b/lms/envs/bok_choy.env.json @@ -96,7 +96,7 @@ "LOCAL_LOGLEVEL": "INFO", "LOGGING_ENV": "sandbox", "LOG_DIR": "** OVERRIDDEN **", - "MEDIA_URL": "", + "MEDIA_URL": "/media/", "MKTG_URL_LINK_MAP": { "ABOUT": "about", "PRIVACY": "privacy", diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py index d4d8ff2be4..e72cdf034d 100644 --- a/lms/envs/bok_choy.py +++ b/lms/envs/bok_choy.py @@ -67,7 +67,6 @@ STATICFILES_DIRS = [ DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' MEDIA_ROOT = TEST_ROOT / "uploads" -MEDIA_URL = "/static/uploads/" # Don't use compression during tests PIPELINE_JS_COMPRESSOR = None diff --git a/lms/static/sass/multicourse/_account.scss b/lms/static/sass/multicourse/_account.scss index 1eacb688e8..600006c1e4 100644 --- a/lms/static/sass/multicourse/_account.scss +++ b/lms/static/sass/multicourse/_account.scss @@ -524,8 +524,9 @@ margin-right: ($baseline/2); .icon { - color: inherit; + @extend %sso-icon; @include margin-right($baseline/2); + color: inherit; } &:last-child { diff --git a/lms/static/sass/partials/base/_variables.scss b/lms/static/sass/partials/base/_variables.scss index 6e68f74e90..f68c6b1e36 100644 --- a/lms/static/sass/partials/base/_variables.scss +++ b/lms/static/sass/partials/base/_variables.scss @@ -188,6 +188,8 @@ $ui-notification-height: ($baseline*10); $twitter-blue: #55ACEE; $facebook-blue: #3B5998; $linkedin-blue: #0077B5; +$google-red: #DD4B39; +$microsoft-blue: #00BCF2; // shadows $shadow: rgba(0,0,0,0.2) !default; diff --git a/lms/static/sass/views/_login-register.scss b/lms/static/sass/views/_login-register.scss index e9b5cde41d..a7d95993e6 100644 --- a/lms/static/sass/views/_login-register.scss +++ b/lms/static/sass/views/_login-register.scss @@ -3,10 +3,6 @@ @import '../base/grid-settings'; @import "neat/neat"; // lib - Neat -$sm-btn-google: #dd4b39; -$sm-btn-facebook: #3b5998; -$sm-btn-linkedin: #0077b5; - .login-register { @include box-sizing(border-box); @include outer-container; @@ -356,6 +352,10 @@ $sm-btn-linkedin: #0077b5; text-transform: none; font-weight: 600; letter-spacing: normal; + + .icon { + @extend %sso-icon; + } } .login-provider { @@ -375,6 +375,7 @@ $sm-btn-linkedin: #0077b5; text-transform: none; .icon { + @extend %sso-icon; @include left(0); position: absolute; @@ -403,17 +404,17 @@ $sm-btn-linkedin: #0077b5; } &.button-oa2-google-oauth2 { - color: $sm-btn-google; + color: $google-red; .icon { - background: $sm-btn-google; + background: $google-red; } &:hover, &:focus { - background-color: $sm-btn-google; + background-color: $google-red; border: 1px solid #A5382B; - color: white; + color: $white; } &:hover { @@ -422,17 +423,17 @@ $sm-btn-linkedin: #0077b5; } &.button-oa2-facebook { - color: $sm-btn-facebook; + color: $facebook-blue; .icon { - background: $sm-btn-facebook; + background: $facebook-blue; } &:hover, &:focus { - background-color: $sm-btn-facebook; + background-color: $facebook-blue; border: 1px solid #263A62; - color: white; + color: $white; } &:hover { @@ -441,17 +442,17 @@ $sm-btn-linkedin: #0077b5; } &.button-oa2-linkedin-oauth2 { - color: $sm-btn-linkedin; + color: $linkedin-blue; .icon { - background: $sm-btn-linkedin; + background: $linkedin-blue; } &:hover, &:focus { - background-color: $sm-btn-linkedin; + background-color: $linkedin-blue; border: 1px solid #06527D; - color: white; + color: $white; } &:hover { diff --git a/lms/templates/login.html b/lms/templates/login.html index 439cfab8da..26338f5571 100644 --- a/lms/templates/login.html +++ b/lms/templates/login.html @@ -220,7 +220,14 @@ from third_party_auth import provider, pipeline % for enabled in provider.Registry.accepting_logins(): ## 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-form.html b/lms/templates/register-form.html index c8688cbc98..c9470bf4c3 100644 --- a/lms/templates/register-form.html +++ b/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 diff --git a/lms/templates/student_account/hinted_login.underscore b/lms/templates/student_account/hinted_login.underscore index d1cb0d8379..6b7dda7605 100644 --- a/lms/templates/student_account/hinted_login.underscore +++ b/lms/templates/student_account/hinted_login.underscore @@ -8,7 +8,11 @@
<%- _.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 a765df02f9..662031dd28 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -946,6 +946,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/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