From 793bb0f1e9b01ec845f7d9b060261e0cecfea425 Mon Sep 17 00:00:00 2001 From: Omar Khan Date: Tue, 22 Mar 2016 16:19:26 +0700 Subject: [PATCH] Custom icons for third party auth login buttons - Icon images can be uploaded from the django admin - Test coverage improved --- cms/envs/bok_choy.env.json | 2 +- common/djangoapps/config_models/admin.py | 12 ++- common/djangoapps/third_party_auth/admin.py | 4 +- .../0002_schema__provider_icon_image.py | 44 +++++++++ common/djangoapps/third_party_auth/models.py | 33 ++++++- .../third_party_auth/tests/test_admin.py | 85 ++++++++++++++++++ .../third_party_auth/tests/testutil.py | 14 ++- common/static/sass/_mixins.scss | 15 ++++ common/test/acceptance/tests/lms/test_lms.py | 11 ++- common/test/db_fixtures/third_party_auth.json | 5 +- .../student_account/test/test_views.py | 26 +++++- lms/djangoapps/student_account/views.py | 3 +- lms/envs/bok_choy.env.json | 2 +- lms/envs/bok_choy.py | 1 - lms/static/sass/multicourse/_account.scss | 3 +- lms/static/sass/partials/base/_variables.scss | 2 + lms/static/sass/views/_login-register.scss | 33 +++---- lms/templates/login.html | 9 +- lms/templates/register-form.html | 9 +- .../student_account/hinted_login.underscore | 6 +- .../student_account/login.underscore | 6 +- .../student_account/register.underscore | 6 +- lms/urls.py | 1 + screenshots/baseline/hinted-login.png | Bin 0 -> 14626 bytes screenshots/baseline/login-providers.png | Bin 0 -> 5299 bytes screenshots/baseline/register-providers.png | Bin 0 -> 5889 bytes test_root/uploads/.gitignore | 2 + test_root/uploads/test-icon.png | Bin 0 -> 208 bytes .../lms/templates/register-form.html | 9 +- 29 files changed, 302 insertions(+), 41 deletions(-) create mode 100644 common/djangoapps/third_party_auth/migrations/0002_schema__provider_icon_image.py create mode 100644 common/djangoapps/third_party_auth/tests/test_admin.py create mode 100644 screenshots/baseline/hinted-login.png create mode 100644 screenshots/baseline/login-providers.png create mode 100644 screenshots/baseline/register-providers.png create mode 100644 test_root/uploads/test-icon.png 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 0000000000000000000000000000000000000000..fe581ce31af24b6e1805aabe99a18944166342bb GIT binary patch literal 14626 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 A*8l(j literal 0 HcmV?d00001 diff --git a/screenshots/baseline/login-providers.png b/screenshots/baseline/login-providers.png new file mode 100644 index 0000000000000000000000000000000000000000..70c19ca2262e8ccf0768480bccd243e1c174bc75 GIT binary patch literal 5299 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1c8e813fdc51eea71e479901f623b463a4fc875 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^DL{OJ14uAfhfB)>DW)WEcNd2L?fqx=19_YU9+AZi z4BWyX%*Zfnjs#GUy~NYkmHh#;IJ>skb${_('Sign up with {provider_name}').format(provider_name=enabled.name)} + % endfor