Custom icons for third party auth login buttons
- Icon images can be uploaded from the django admin - Test coverage improved
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
85
common/djangoapps/third_party_auth/tests/test_admin.py
Normal file
85
common/djangoapps/third_party_auth/tests/test_admin.py
Normal file
@@ -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', '<svg><rect width="50" height="100"/></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'])
|
||||
@@ -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()
|
||||
@@ -124,7 +136,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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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', '<svg><rect width="50" height="100"/></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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -524,8 +524,9 @@
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
.icon {
|
||||
color: inherit;
|
||||
@extend %sso-icon;
|
||||
@include margin-right($baseline/2);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
<button type="submit" class="button button-primary button-${enabled.provider_id} login-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_url[enabled.provider_id]}');"><span class="icon fa ${enabled.icon_class}"></span>${_('Sign in with {provider_name}').format(provider_name=enabled.name)}</button>
|
||||
<button type="submit" class="button button-primary button-${enabled.provider_id} login-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_url[enabled.provider_id]}');">
|
||||
% if enabled.icon_class:
|
||||
<span class="icon fa ${enabled.icon_class}" aria-hidden="true"></span>
|
||||
% else:
|
||||
<span class="icon" aria-hidden="true"><img class="icon-image" src="${enabled.icon_image.url}" alt="${enabled.name} icon" /></span>
|
||||
% endif
|
||||
${_('Sign in with {provider_name}').format(provider_name=enabled.name)}
|
||||
</button>
|
||||
% endfor
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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).
|
||||
<button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');"><span class="icon fa ${enabled.icon_class}"></span>${_('Sign up with {provider_name}').format(provider_name=enabled.name)}</button>
|
||||
<button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');">
|
||||
% if enabled.icon_class:
|
||||
<span class="icon fa ${enabled.icon_class}" aria-hidden="true"></span>
|
||||
% else:
|
||||
<span class="icon" aria-hidden="true"><img class="icon-image" src="${enabled.icon_image.url}" alt="${enabled.name} icon" /></span>
|
||||
% endif
|
||||
${_('Sign up with {provider_name}').format(provider_name=enabled.name)}
|
||||
</button>
|
||||
% endfor
|
||||
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
<p class="instructions"><%- _.sprintf( gettext("Would you like to sign in using your %(providerName)s credentials?"), { providerName: hintedProvider.name } ) %></p>
|
||||
|
||||
<button class="action action-primary action-update proceed-button button-<%- hintedProvider.id %> hinted-login-<%- hintedProvider.id %>">
|
||||
<div class="icon fa <%- hintedProvider.iconClass %>" aria-hidden="true"></div>
|
||||
<span class="icon <% if ( hintedProvider.iconClass ) { %>fa <%- hintedProvider.iconClass %><% } %>" aria-hidden="true">
|
||||
<% if ( hintedProvider.iconImage ) { %>
|
||||
<img class="icon-image" src="<%- hintedProvider.iconImage %>" alt="<%- hintedProvider.name %> icon" />
|
||||
<% } %>
|
||||
</span>
|
||||
<%- _.sprintf( gettext("Sign in using %(providerName)s"), { providerName: hintedProvider.name } ) %>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -61,7 +61,11 @@
|
||||
<% _.each( context.providers, function( provider ) {
|
||||
if ( provider.loginUrl ) { %>
|
||||
<button type="button" class="button button-primary button-<%- provider.id %> login-provider login-<%- provider.id %>" data-provider-url="<%- provider.loginUrl %>">
|
||||
<div class="icon fa <%- provider.iconClass %>" aria-hidden="true"></div>
|
||||
<div class="icon <% if ( provider.iconClass ) { %>fa <%- provider.iconClass %><% } %>" aria-hidden="true">
|
||||
<% if ( provider.iconImage ) { %>
|
||||
<img class="icon-image" src="<%- provider.iconImage %>" alt="<%- provider.name %> icon" />
|
||||
<% } %>
|
||||
</div>
|
||||
<span aria-hidden="true"><%- provider.name %></span>
|
||||
<span class="sr"><%- _.sprintf( gettext("Sign in with %(providerName)s"), {providerName: provider.name} ) %></span>
|
||||
</button>
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
_.each( context.providers, function( provider) {
|
||||
if ( provider.registerUrl ) { %>
|
||||
<button type="button" class="button button-primary button-<%- provider.id %> login-provider register-<%- provider.id %>" data-provider-url="<%- provider.registerUrl %>">
|
||||
<span class="icon fa <%- provider.iconClass %>" aria-hidden="true"></span>
|
||||
<div class="icon <% if ( provider.iconClass ) { %>fa <%- provider.iconClass %><% } %>" aria-hidden="true">
|
||||
<% if ( provider.iconImage ) { %>
|
||||
<img class="icon-image" src="<%- provider.iconImage %>" alt="<%- provider.name %> icon" />
|
||||
<% } %>
|
||||
</div>
|
||||
<span aria-hidden="true"><%- provider.name %></span>
|
||||
<span class="sr"><%- _.sprintf( gettext("Create account using %(providerName)s."), {providerName: provider.name} ) %></span>
|
||||
</button>
|
||||
|
||||
@@ -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']
|
||||
|
||||
BIN
screenshots/baseline/hinted-login.png
Normal file
BIN
screenshots/baseline/hinted-login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
screenshots/baseline/login-providers.png
Normal file
BIN
screenshots/baseline/login-providers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
screenshots/baseline/register-providers.png
Normal file
BIN
screenshots/baseline/register-providers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
2
test_root/uploads/.gitignore
vendored
2
test_root/uploads/.gitignore
vendored
@@ -2,3 +2,5 @@
|
||||
*.jpg
|
||||
*.png
|
||||
*.txt
|
||||
*.svg
|
||||
!test_icon.png
|
||||
|
||||
BIN
test_root/uploads/test-icon.png
Normal file
BIN
test_root/uploads/test-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 B |
@@ -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).
|
||||
<button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');"><span class="icon fa ${enabled.icon_class}"></span>${_('Sign up with {provider_name}').format(provider_name=enabled.name)}</button>
|
||||
<button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');">
|
||||
% if enabled.icon_class:
|
||||
<span class="icon fa ${enabled.icon_class}" aria-hidden="true"></span>
|
||||
% else:
|
||||
<span class="icon" aria-hidden="true"><img class="icon-image" src="${enabled.icon_image.url}" alt="${enabled.name} icon" /></span>
|
||||
% endif
|
||||
${_('Sign up with {provider_name}').format(provider_name=enabled.name)}
|
||||
</button>
|
||||
% endfor
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user