Commit 793bb0f1 by Omar Khan

Custom icons for third party auth login buttons

- Icon images can be uploaded from the django admin
- Test coverage improved
parent d426931e
......@@ -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',
)
......
# -*- 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(
......
"""
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()
......@@ -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)
......
......@@ -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):
......@@ -314,9 +322,18 @@ 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
......
......@@ -524,8 +524,9 @@
margin-right: ($baseline/2);
.icon {
color: inherit;
@extend %sso-icon;
@include margin-right($baseline/2);
color: inherit;
}
&:last-child {
......
......@@ -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;
......
......@@ -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>
......
......@@ -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']
......
......@@ -2,3 +2,5 @@
*.jpg
*.png
*.txt
*.svg
!test_icon.png
......@@ -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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment