From 5146bd8b82eb17e46fec0256590457e29fd668ae Mon Sep 17 00:00:00 2001
From: Oliver Falk
Date: Fri, 30 Nov 2018 14:06:56 +0100
Subject: [PATCH 01/11] Allow max sized image to be imported, make sure we
resize it on the import view to not overflow the div
---
ivatar/ivataraccount/gravatar.py | 8 +++++---
ivatar/ivataraccount/templates/_import_photo_form.html | 2 +-
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/ivatar/ivataraccount/gravatar.py b/ivatar/ivataraccount/gravatar.py
index a1889be..df1cfe6 100644
--- a/ivatar/ivataraccount/gravatar.py
+++ b/ivatar/ivataraccount/gravatar.py
@@ -5,6 +5,8 @@ from ssl import SSLError
from urllib.request import urlopen, HTTPError, URLError
import hashlib
+from .. settings import AVATAR_MAX_SIZE
+
URL_TIMEOUT = 5 # in seconds
@@ -15,7 +17,7 @@ def get_photo(email):
hash_object = hashlib.new('md5')
hash_object.update(email.lower().encode('utf-8'))
thumbnail_url = 'https://secure.gravatar.com/avatar/' + \
- hash_object.hexdigest() + '?s=80&d=404'
+ hash_object.hexdigest() + '?s=%i&d=404' % AVATAR_MAX_SIZE
image_url = 'https://secure.gravatar.com/avatar/' + hash_object.hexdigest(
) + '?s=512&d=404'
@@ -44,8 +46,8 @@ def get_photo(email):
return {
'thumbnail_url': thumbnail_url,
'image_url': image_url,
- 'width': 80,
- 'height': 80,
+ 'width': AVATAR_MAX_SIZE,
+ 'height': AVATAR_MAX_SIZE,
'service_url': service_url,
'service_name': 'Gravatar'
}
diff --git a/ivatar/ivataraccount/templates/_import_photo_form.html b/ivatar/ivataraccount/templates/_import_photo_form.html
index 77070b2..ef950e4 100644
--- a/ivatar/ivataraccount/templates/_import_photo_form.html
+++ b/ivatar/ivataraccount/templates/_import_photo_form.html
@@ -25,7 +25,7 @@
-
+
From e783ea6601f16b7f6598b7bfa72d0e5086bab0ee Mon Sep 17 00:00:00 2001
From: Oliver Falk
Date: Fri, 30 Nov 2018 14:07:30 +0100
Subject: [PATCH 02/11] Add DEFAULT_AVATAR_SIZE to config, as we need it in
various places
---
config.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/config.py b/config.py
index 93fdab6..1b33994 100644
--- a/config.py
+++ b/config.py
@@ -140,3 +140,5 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
USE_X_FORWARDED_HOST = True
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',]
+
+DEFAULT_AVATAR_SIZE = 80
From c4a8d2e6404a2602539d4738285f85fe8944f151 Mon Sep 17 00:00:00 2001
From: Oliver Falk
Date: Fri, 30 Nov 2018 14:23:52 +0100
Subject: [PATCH 03/11] Use max avatar size for import
---
ivatar/ivataraccount/models.py | 2 +-
ivatar/ivataraccount/views.py | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/ivatar/ivataraccount/models.py b/ivatar/ivataraccount/models.py
index 304a858..d2c6193 100644
--- a/ivatar/ivataraccount/models.py
+++ b/ivatar/ivataraccount/models.py
@@ -135,7 +135,7 @@ class Photo(BaseAccountModel):
image_url = gravatar['image_url']
if service_name == 'Libravatar':
- image_url = libravatar_url(email_address)
+ image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE)
if not image_url:
return False # pragma: no cover
diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py
index 9cbe02a..77d67a9 100644
--- a/ivatar/ivataraccount/views.py
+++ b/ivatar/ivataraccount/views.py
@@ -32,7 +32,7 @@ from openid.consumer import consumer
from ipware import get_client_ip
from libravatar import libravatar_url
-from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE, JPEG_QUALITY
+from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE, JPEG_QUALITY, AVATAR_MAX_SIZE
from .gravatar import get_photo as get_gravatar_photo
from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm
@@ -327,6 +327,7 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
libravatar_service_url = libravatar_url(
email=addr,
default=404,
+ size=AVATAR_MAX_SIZE,
)
if libravatar_service_url:
try:
From 51c863c94f4ecc8abf466fa5c2d74c036de4b568 Mon Sep 17 00:00:00 2001
From: Oliver Falk
Date: Fri, 30 Nov 2018 14:26:20 +0100
Subject: [PATCH 04/11] Add special view to proxy Gravatar (which is the
default now if we do not have an image on our end). This proxying can be
replaced by the frontend web server (Apache, NGINX), but helps now for dev.
Alternatively we can redirect to Gravatar.
---
ivatar/urls.py | 7 ++-
ivatar/views.py | 135 +++++++++++++++++++++++++++++++++++++-----------
2 files changed, 109 insertions(+), 33 deletions(-)
diff --git a/ivatar/urls.py b/ivatar/urls.py
index d86fd04..720da9e 100644
--- a/ivatar/urls.py
+++ b/ivatar/urls.py
@@ -7,7 +7,7 @@ from django.conf.urls import url
from django.conf.urls.static import static
from django.views.generic import TemplateView, RedirectView
from ivatar import settings
-from . views import AvatarImageView
+from . views import AvatarImageView, GravatarProxyView
urlpatterns = [ # pylint: disable=invalid-name
path('admin/', admin.site.urls),
@@ -21,12 +21,15 @@ urlpatterns = [ # pylint: disable=invalid-name
r'avatar/(?P\w{32})',
AvatarImageView.as_view(), name='avatar_view'),
url(
- r'avatar/(?P\w)',
+ r'avatar/(?P\w*)',
TemplateView.as_view(
template_name='error.html',
extra_context={
'errormessage': 'Incorrect digest length',
})),
+ url(
+ r'gravatarproxy/(?P\w*)',
+ GravatarProxyView.as_view(), name='gravatarproxy'),
url('description/', TemplateView.as_view(template_name='description.html'), name='description'),
# The following two are TODO TODO TODO TODO TODO
url('run_your_own/', TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'),
diff --git a/ivatar/views.py b/ivatar/views.py
index 770f15c..a9e9348 100644
--- a/ivatar/views.py
+++ b/ivatar/views.py
@@ -5,17 +5,45 @@ from io import BytesIO
from os import path
import hashlib
from PIL import Image
-from django.views.generic.base import TemplateView
+from django.views.generic.base import TemplateView, View
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _
+from django.urls import reverse_lazy
+
+from urllib.request import urlopen
+from urllib.error import HTTPError, URLError
+from ssl import SSLError
from monsterid.id import build_monster as BuildMonster
from pydenticon import Generator as IdenticonGenerator
-from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY
+from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
-from . ivataraccount.models import pil_format
+from . ivataraccount.models import pil_format, file_format
+
+URL_TIMEOUT = 5 # in seconds
+
+
+def get_size(request, size=DEFAULT_AVATAR_SIZE):
+ sizetemp = None
+ if 's' in request.GET:
+ sizetemp = request.GET['s']
+ if 'size' in request.GET:
+ sizetemp = request.GET['size']
+ if sizetemp:
+ if sizetemp != '' and sizetemp is not None and sizetemp != '0':
+ try:
+ if int(sizetemp) > 0:
+ size = int(sizetemp)
+ # Should we receive something we cannot convert to int, leave
+ # the user with the default value of 80
+ except ValueError:
+ pass
+
+ if size > int(AVATAR_MAX_SIZE):
+ size = int(AVATAR_MAX_SIZE)
+ return size
class AvatarImageView(TemplateView):
@@ -29,11 +57,13 @@ class AvatarImageView(TemplateView):
Override get from parent class
'''
model = ConfirmedEmail
- size = 80
+ size = get_size(request)
imgformat = 'png'
obj = None
default = None
forcedefault = False
+ gravatarredirect = False
+ gravatarproxy = True
if 'd' in request.GET:
default = request.GET['d']
@@ -47,34 +77,13 @@ class AvatarImageView(TemplateView):
if request.GET['forcedefault'] == 'y':
forcedefault = True
- sizetemp = None
- if 's' in request.GET:
- sizetemp = request.GET['s']
- if 'size' in request.GET:
- sizetemp = request.GET['size']
- if sizetemp:
- if sizetemp != '' and sizetemp is not None and sizetemp != '0':
- try:
- if int(sizetemp) > 0:
- size = int(sizetemp)
- # Should we receive something we cannot convert to int, leave
- # the user with the default value of 80
- except ValueError:
- pass
+ if 'gravatarredirect' in request.GET:
+ if request.GET['gravatarredirect'] == 'y':
+ gravatarredirect = True
- if size > int(AVATAR_MAX_SIZE):
- size = int(AVATAR_MAX_SIZE)
- if len(kwargs['digest']) == 32:
- # Fetch by digest from mail
- pass
- elif len(kwargs['digest']) == 64:
- if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
- digest=kwargs['digest']).count():
- # Fetch by digest from OpenID
- model = ConfirmedOpenId
- else: # pragma: no cover
- # We should actually never ever reach this code...
- raise Exception('Digest provided is wrong: %s' % kwargs['digest'])
+ if 'gravatarproxy' in request.GET:
+ if request.GET['gravatarproxy'] == 'n':
+ gravatarproxy = False
try:
obj = model.objects.get(digest=kwargs['digest'])
@@ -86,6 +95,20 @@ class AvatarImageView(TemplateView):
# If that mail/openid doesn't exist, or has no photo linked to it
if not obj or not obj.photo or forcedefault:
+ gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ + '?s=%i' % size
+
+ # If we have redirection to Gravatar enabled, this overrides all
+ # default= settings, except forcedefault!
+ if gravatarredirect and not forcedefault:
+ return HttpResponseRedirect(gravatar_url)
+
+ # Request to proxy Gravatar image - only if not forcedefault
+ if gravatarproxy and not forcedefault:
+ url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
+ + '?s=%i' % size
+ return HttpResponseRedirect(url)
+
# Return the default URL, as specified, or 404 Not Found, if default=404
if default:
if str(default) == str(404):
@@ -158,3 +181,53 @@ class AvatarImageView(TemplateView):
return HttpResponse(
data,
content_type='image/%s' % imgformat)
+
+class GravatarProxyView(View):
+ '''
+ Proxy request to Gravatar and return the image from there
+ '''
+ # TODO: Do cache images!! Memcached?
+
+ def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals
+ '''
+ Override get from parent class
+ '''
+ size = get_size(request)
+ gravatarimagedata = None
+
+ gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ + '?s=%i' % size
+
+ try:
+ gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)
+ except HTTPError as exc:
+ if exc.code != 404 and exc.code != 503:
+ print(
+ 'Gravatar fetch failed with an unexpected %s HTTP error' %
+ exc.code)
+ pass
+ except URLError as exc:
+ print(
+ 'Gravatar fetch failed with URL error: %s' %
+ exc.reason)
+ pass
+ except SSLError as exc:
+ print(
+ 'Gravatar fetch failed with SSL error: %s' %
+ exc.reason)
+ pass
+ try:
+ data = BytesIO(gravatarimagedata.read())
+ img = Image.open(data)
+ data.seek(0)
+ return HttpResponse(
+ data.read(),
+ content_type='image/%s' % file_format(img.format))
+
+ except ValueError as exc:
+ print('Value error: %s' % exc)
+ pass
+
+ # TODO: In case anything strange happens, we need to redirect to the default
+ url = reverse_lazy('avatar_view', args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
+ return HttpResponseRedirect(url)
From 1376024aa3c712de94b14111b514607d3534eedd Mon Sep 17 00:00:00 2001
From: Oliver Falk
Date: Sat, 1 Dec 2018 14:56:48 +0100
Subject: [PATCH 05/11] Make lint happier and adapt/add some tests to reflect
gravatarproxy functionality
---
ivatar/ivataraccount/test_views.py | 106 ++++++++++++++++++++++++++++-
ivatar/ivataraccount/views.py | 39 +++++------
2 files changed, 121 insertions(+), 24 deletions(-)
diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py
index fe1a2ca..1b09a65 100644
--- a/ivatar/ivataraccount/test_views.py
+++ b/ivatar/ivataraccount/test_views.py
@@ -12,6 +12,7 @@ from django.test import Client
from django.urls import reverse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
+import hashlib
from libravatar import libravatar_url
@@ -1092,6 +1093,34 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
'Why is this not the correct size?')
def test_avatar_url_inexisting_mail_digest(self): # pylint: disable=invalid-name
+ '''
+ Test fetching avatar via inexisting mail digest
+ '''
+ self.test_upload_image()
+ self.test_confirm_email()
+ urlobj = urlsplit(
+ libravatar_url(
+ email=self.user.confirmedemail_set.first().email,
+ size=80,
+ )
+ )
+ # Simply delete it, then it's digest is 'correct', but
+ # the hash is no longer there
+ addr = self.user.confirmedemail_set.first().email
+ check_hash = hashlib.md5(
+ addr.strip().lower().encode('utf-8')
+ ).hexdigest()
+
+ self.user.confirmedemail_set.first().delete()
+ url = '%s?%s' % (urlobj.path, urlobj.query)
+ response = self.client.get(url, follow=True)
+ self.assertRedirects(
+ response=response,
+ expected_url='/gravatarproxy/%s?s=80' % check_hash,
+ msg_prefix='Why does this not redirect to Gravatar?')
+ # Eventually one should check if the data is the same
+
+ def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled(self): # pylint: disable=invalid-name
'''
Test fetching avatar via inexisting mail digest
'''
@@ -1106,7 +1135,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
# Simply delete it, then it digest is 'correct', but
# the hash is no longer there
self.user.confirmedemail_set.first().delete()
- url = '%s?%s' % (urlobj.path, urlobj.query)
+ url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
@@ -1127,6 +1156,25 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
+ self.assertRedirects(
+ response=response,
+ expected_url='/gravatarproxy/1b1d0b654430c012e47e350db07c83c5?s=80',
+ msg_prefix='Why does this not redirect to the default img?')
+ # Eventually one should check if the data is the same
+
+ def test_avatar_url_inexisting_mail_digest_w_default_mm_gravatarproxy_disabled(self): # pylint: disable=invalid-name
+ '''
+ Test fetching avatar via inexisting mail digest and default 'mm'
+ '''
+ urlobj = urlsplit(
+ libravatar_url(
+ email='asdf@company.local',
+ size=80,
+ default='mm',
+ )
+ )
+ url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
+ response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/mm/80.png',
@@ -1145,6 +1193,24 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
+ self.assertRedirects(
+ response=response,
+ expected_url='/gravatarproxy/1b1d0b654430c012e47e350db07c83c5?s=80',
+ msg_prefix='Why does this not redirect to the default img?')
+ # Eventually one should check if the data is the same
+
+ def test_avatar_url_inexisting_mail_digest_wo_default_gravatarproxy_disabled(self): # pylint: disable=invalid-name
+ '''
+ Test fetching avatar via inexisting mail digest and default 'mm'
+ '''
+ urlobj = urlsplit(
+ libravatar_url(
+ email='asdf@company.local',
+ size=80,
+ )
+ )
+ url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
+ response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/nobody/80.png',
@@ -1164,6 +1230,24 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
+ self.assertRedirects(
+ response=response,
+ expected_url='/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80',
+ msg_prefix='Why does this not redirect to the default img?')
+
+ def test_avatar_url_default_gravatarproxy_disabled(self): # pylint: disable=invalid-name
+ '''
+ Test fetching avatar for not existing mail with default specified
+ '''
+ urlobj = urlsplit(
+ libravatar_url(
+ 'xxx@xxx.xxx',
+ size=80,
+ default='/static/img/nobody.png',
+ )
+ )
+ url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
+ response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/nobody.png',
@@ -1183,6 +1267,26 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=False)
+ self.assertRedirects(
+ response=response,
+ expected_url='/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80',
+ fetch_redirect_response=False,
+ msg_prefix='Why does this not redirect to the default img?')
+
+ def test_avatar_url_default_external_gravatarproxy_disabled(self): # pylint: disable=invalid-name
+ '''
+ Test fetching avatar for not existing mail with external default specified
+ '''
+ default = 'http://host.tld/img.png'
+ urlobj = urlsplit(
+ libravatar_url(
+ 'xxx@xxx.xxx',
+ size=80,
+ default=default,
+ )
+ )
+ url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
+ response = self.client.get(url, follow=False)
self.assertRedirects(
response=response,
expected_url=default,
diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py
index 77d67a9..9452697 100644
--- a/ivatar/ivataraccount/views.py
+++ b/ivatar/ivataraccount/views.py
@@ -117,8 +117,8 @@ class AddEmailView(SuccessMessageMixin, FormView):
def form_valid(self, form):
if not form.save(self.request):
return render(self.request, self.template_name, {'form': form})
- else:
- messages.success(self.request, _('Address added successfully'))
+
+ messages.success(self.request, _('Address added successfully'))
return super().form_valid(form)
@@ -310,14 +310,13 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
if 'email_id' in kwargs:
try:
addr = ConfirmedEmail.objects.get(pk=kwargs['email_id']).email
- except ConfirmedEmail.ObjectDoesNotExist:
+ except ConfirmedEmail.ObjectDoesNotExist: # pylint: disable=no-member
messages.error(
self.request,
_('Address does not exist'))
return context
- if 'email_addr' in kwargs:
- addr = kwargs['email_addr']
+ addr = kwargs.get('email_addr', None)
if addr:
gravatar = get_gravatar_photo(addr)
@@ -351,18 +350,10 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
Handle post to photo import
'''
- addr = None
- email_id = None
imported = None
- if 'email_id' in kwargs:
- email_id = kwargs['email_id']
- if 'email_id' in request.POST:
- email_id = request.POST['email_id']
- if 'email_addr' in kwargs:
- addr = kwargs['email_addr']
- if 'email_addr' in request.POST:
- addr = request.POST['email_addr']
+ email_id = kwargs.get('email_id', request.POST.get('email_id', None))
+ addr = kwargs.get('emali_addr', request.POST.get('email_addr', None))
if email_id:
email = ConfirmedEmail.objects.filter(
@@ -437,7 +428,7 @@ class DeletePhotoView(SuccessMessageMixin, View):
photo = self.model.objects.get( # pylint: disable=no-member
pk=kwargs['pk'], user=request.user)
photo.delete()
- except (self.model.DoesNotExist, ProtectedError):
+ except (self.model.DoesNotExist, ProtectedError): # pylint: disable=no-member
messages.error(
request,
_('No such image or no permission to delete it'))
@@ -520,7 +511,7 @@ class RemoveUnconfirmedOpenIDView(View):
user=request.user, id=kwargs['openid_id'])
openid.delete()
messages.success(request, _('ID removed'))
- except self.model.DoesNotExist: # pragma: no cover # pylint: disable=no-member
+ except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member
messages.error(request, _('ID does not exist'))
return HttpResponseRedirect(reverse_lazy('profile'))
@@ -544,9 +535,9 @@ class RemoveConfirmedOpenIDView(View):
user_id=request.user.id,
claimed_id=openid.openid)
openidobj.delete()
- except:
+ except Exception as exc: # pylint: disable=broad-except
# Why it is not there?
- pass
+ print('How did we get here: %s' % exc)
openid.delete()
messages.success(request, _('ID removed'))
except self.model.DoesNotExist: # pylint: disable=no-member
@@ -568,7 +559,7 @@ class RedirectOpenIDView(View):
try:
unconfirmed = self.model.objects.get( # pylint: disable=no-member
user=request.user, id=kwargs['openid_id'])
- except self.model.DoesNotExist: # pragma: no cover # pylint: disable=no-member
+ except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member
messages.error(request, _('ID does not exist'))
return HttpResponseRedirect(reverse_lazy('profile'))
@@ -622,10 +613,12 @@ class ConfirmOpenIDView(View): # pragma: no cover
self.request,
_('Confirmation failed: "') + str(info.message) + '"')
return HttpResponseRedirect(reverse_lazy('profile'))
- elif info.status == consumer.CANCEL:
+
+ if info.status == consumer.CANCEL:
messages.error(self.request, _('Cancelled by user'))
return HttpResponseRedirect(reverse_lazy('profile'))
- elif info.status != consumer.SUCCESS:
+
+ if info.status != consumer.SUCCESS:
messages.error(self.request, _('Unknown verification error'))
return HttpResponseRedirect(reverse_lazy('profile'))
@@ -706,7 +699,7 @@ class CropPhotoView(TemplateView):
if 'email' in request.POST:
try:
email = ConfirmedEmail.objects.get(email=request.POST['email'])
- except ConfirmedEmail.DoesNotExist:
+ except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member
pass # Ignore automatic assignment
if 'openid' in request.POST:
From 03e9a59cd1ac46ea1fcd9f3c9c2f448a32f86787 Mon Sep 17 00:00:00 2001
From: Oliver Falk
Date: Sat, 1 Dec 2018 21:58:16 +0100
Subject: [PATCH 06/11] Allow user preferences again
---
.../ivataraccount/templates/preferences.html | 20 +-
ivatar/ivataraccount/views.py | 15 +-
ivatar/static/css/falko.css | 338 ++++++++++++++++++
templates/_account_bar.html | 2 -
templates/base.html | 1 -
templates/base_home.html | 1 -
6 files changed, 367 insertions(+), 10 deletions(-)
create mode 100644 ivatar/static/css/falko.css
diff --git a/ivatar/ivataraccount/templates/preferences.html b/ivatar/ivataraccount/templates/preferences.html
index e391abf..78d3be5 100644
--- a/ivatar/ivataraccount/templates/preferences.html
+++ b/ivatar/ivataraccount/templates/preferences.html
@@ -7,11 +7,21 @@
{% block content %}
{% trans 'Account settings' %}
-{% if has_password %}
-{% trans 'Change your password' %}
-{% else %}
-{% trans 'Set a password' %}
-{% endif %}
+
+
+
+
+
diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py
index 9452697..afe8977 100644
--- a/ivatar/ivataraccount/views.py
+++ b/ivatar/ivataraccount/views.py
@@ -722,8 +722,21 @@ class UserPreferenceView(FormView, UpdateView):
form_class = UpdatePreferenceForm
success_url = reverse_lazy('user_preference')
+ def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
+ self.request.user.userpreference.theme = request.POST['theme']
+ self.request.user.userpreference.save()
+ return HttpResponseRedirect(reverse_lazy('user_preference'))
+
+
+ def get(self, request, *args, **kwargs):
+ return render(self.request, self.template_name, {
+ 'THEMES': UserPreference.THEMES,
+ })
+
+
def get_object(self, queryset=None):
- return self.request.user.userpreference
+ (obj, created) = UserPreference.objects.get_or_create(user=self.request.user) # pylint: disable=no-member,unused-variable
+ return obj
@method_decorator(login_required, name='dispatch')
diff --git a/ivatar/static/css/falko.css b/ivatar/static/css/falko.css
new file mode 100644
index 0000000..1268223
--- /dev/null
+++ b/ivatar/static/css/falko.css
@@ -0,0 +1,338 @@
+/// Example theme using tortin with bg-hero:@lab-green;
+body {
+ font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
+ color: #525252;
+}
+.btn {
+ border-bottom-width: 3px;
+ box-sizing: border-box;
+ font-family: 'Montserrat', sans-serif;
+ text-transform: uppercase;
+ background: #3aa850;
+ overflow: hidden;
+ position: relative;
+ -webkit-transition: all 0.3s;
+ -moz-transition: all 0.3s;
+ -ms-transition: all 0.3s;
+ transition: all 0.3s;
+}
+.btn.btn-default {
+ color: #52c368;
+ border-color: #52c368;
+ background: none;
+}
+.btn.btn-primary {
+ border-color: #266f35;
+}
+.btn:hover,
+.btn:active,
+.btn:focus {
+ background: none;
+ border-color: #2d823e;
+ color: #2d823e;
+}
+.btn:hover:after,
+.btn:active:after,
+.btn:focus:after {
+ top: 50%;
+}
+.btn:after {
+ content: '';
+ position: absolute;
+ z-index: -1;
+ width: 150%;
+ height: 200%;
+ top: -190%;
+ left: 50%;
+ background: #78d089;
+ -webkit-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);
+ -moz-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);
+ -ms-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);
+ transform: translateX(-50%) translateY(-50%) skew(0, 5deg);
+ -webkit-transition: all 0.5s ease-out;
+ -moz-transition: all 0.5s ease-out;
+ -ms-transition: all 0.5s ease-out;
+ transition: all 0.5s ease-out;
+}
+.btn.btn-block:after {
+ height: 250%;
+ width: 200%;
+ -webkit-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);
+ -moz-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);
+ -ms-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);
+ transform: translateX(-50%) translateY(-50%) skew(0, 2deg);
+}
+.hero {
+ background-color: #3aa850;
+ color: #fff;
+ padding: 90px 0 40px;
+}
+.hero h1 {
+ font-weight: 600;
+ font-size: 6em;
+ color: rgba(255, 255, 255, 0.5);
+}
+.hero h2 {
+ font-weight: 200;
+ font-size: 30px;
+ margin-bottom: 30px;
+}
+.hero small {
+ color: rgba(0, 0, 0, 0.4);
+}
+.hero .btn {
+ display: inline-block;
+}
+.hero .btn.btn-default {
+ color: #7fd390;
+ border-color: #7fd390;
+ background: none;
+}
+.hero .btn.btn-primary {
+ border-color: #fff;
+}
+.hero .btn:hover,
+.hero .btn:active,
+.hero .btn:focus {
+ border-color: #fff;
+ color: #205c2c;
+}
+.hero .btn:after {
+ background: rgba(255, 255, 255, 0.5);
+}
+.hero .container {
+ position: relative;
+ z-index: 10;
+}
+.social {
+ background-color: #3aa850;
+ padding: 30px 0 140px;
+}
+.social ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+.social ul li {
+ float: left;
+ margin-right: 15px;
+ width: 100px;
+}
+.clipper,
+.clipper-footer {
+ background-color: #fff;
+ height: 110px;
+ width: 100%;
+ position: relative;
+ top: -40px;
+ -webkit-transform: skew(0, 2deg);
+ -moz-transform: skew(0, 2deg);
+ -ms-transform: skew(0, 2deg);
+ transform: skew(0, 2deg);
+ pointer-events: none;
+ z-index: 1;
+}
+.clipper-footer {
+ top: 0;
+}
+section.content {
+ position: relative;
+ top: -100px;
+ margin-bottom: -100px;
+ z-index: 10;
+}
+section.content h1,
+section.content h2,
+section.content h3,
+section.content h4,
+section.content h5,
+section.content h6 {
+ color: #2d823e;
+}
+section.content h2 {
+ font-weight: 200;
+ font-size: 40px;
+}
+section.content section {
+ margin-bottom: 20px;
+ margin-top: 20px;
+}
+section.content .container > hr {
+ -webkit-transform: skew(0, 2deg);
+ -moz-transform: skew(0, 2deg);
+ -ms-transform: skew(0, 2deg);
+ transform: skew(0, 2deg);
+ margin-top: 80px;
+ margin-bottom: 40px;
+}
+footer {
+ background-color: #dddddd;
+ color: #888888;
+ padding: 100px 0 40px;
+ margin-top: -40px;
+}
+footer .pull-left {
+ margin-right: 20px;
+}
+footer .logo {
+ float: left;
+ display: inline-block;
+ margin-right: 5px;
+ margin-top: -8px;
+}
+footer .logo .circle {
+ stroke: #888888;
+ stroke-width: 7;
+ fill: none;
+}
+footer .logo .polygon {
+ fill: #888888;
+}
+@media (max-width: 768px) {
+ .hero {
+ padding: 50px 0 30px;
+ }
+ .hero h1 {
+ font-size: 4em;
+ }
+ .social {
+ padding: 30px 0 100px;
+ }
+ .btn {
+ margin-bottom: 5px;
+ }
+ section.content section {
+ margin-bottom: 50px;
+ }
+}
+.color {
+ display: inline-block;
+ border-radius: 50%;
+ height: 20px;
+ width: 20px;
+}
+.color.blue {
+ background-color: #36b7d7;
+}
+.color.green {
+ background-color: #3aa850;
+}
+.color.red {
+ background-color: #f7645e;
+}
+.color.black {
+ background-color: #525252;
+}
+.navbar-tortin {
+ border: 0;
+ background-color: #3aa850;
+ color: #FFFFFF;
+ border-radius: 0;
+}
+.form-control {
+ border-bottom-width: 3px;
+ box-sizing: border-box;
+ font-family: 'Montserrat', sans-serif;
+ overflow: hidden;
+ position: relative;
+ -webkit-transition: all 0.3s;
+ -moz-transition: all 0.3s;
+ -ms-transition: all 0.3s;
+ transition: all 0.3s;
+ border-color: #52c368;
+ background: none;
+}
+.form-control:focus {
+ border-color: #2d823e;
+ box-shadow: none;
+}
+.navbar-tortin .navbar-brand,
+.navbar-tortin .navbar-text,
+.navbar-tortin .navbar-nav > li > a,
+.navbar-tortin .navbar-link,
+.navbar-tortin .btn-link {
+ color: #FFFFFF;
+}
+.navbar-tortin .navbar-nav > .active > a,
+.navbar-tortin .navbar-nav > .active > a:focus,
+.navbar-tortin .navbar-nav > .active > a:hover,
+.navbar-tortin .navbar-nav > li > a:focus,
+.navbar-tortin .navbar-nav > li > a:hover,
+.navbar-tortin .navbar-link:hover,
+.navbar-tortin .btn-link:focus,
+.navbar-tortin .btn-link:hover,
+.navbar-tortin .navbar-nav > .open > a,
+.navbar-tortin .navbar-nav > .open > a:focus,
+.navbar-tortin .navbar-nav > .open > a:hover {
+ background-color: #2d823e;
+}
+.navbar-tortin .navbar-toggle {
+ border-color: #FFFFFF;
+}
+.navbar-tortin .navbar-toggle:hover {
+ background-color: #FFFFFF;
+}
+.navbar-tortin .navbar-toggle .icon-bar {
+ background-color: #FFFFFF;
+}
+.navbar-tortin .navbar-toggle:hover .icon-bar {
+ background-color: #3aa850;
+}
+.navbar-tortin .navbar-collapse,
+.navbar-tortin .navbar-form {
+ border: 0;
+}
+@media (max-width: 767px) {
+ .navbar-tortin .navbar-nav .open .dropdown-menu > li > a {
+ color: #FFFFFF;
+ }
+ .navbar-tortin .navbar-nav .open .dropdown-menu > li > a:hover {
+ background-color: #2d823e;
+ }
+ .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a,
+ .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a:focus,
+ .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a:hover {
+ background-color: #2d823e;
+ }
+}
+.panel-tortin {
+ border-color: #3aa850;
+ border-bottom-width: 3px;
+}
+.panel-tortin > .panel-heading {
+ color: #fff;
+ background-color: #3aa850;
+ border-color: #3aa850;
+ font-family: 'Montserrat', sans-serif;
+}
+.panel-tortin > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #3aa850;
+}
+.panel-tortin > .panel-heading .badge {
+ color: #3aa850;
+ background-color: #fff;
+}
+.panel-tortin > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #3aa850;
+}
+.alert.alert-danger {
+ background-color: #FFFFFF;
+ color: #f7645e;
+ border-color: #f7645e;
+ border-bottom-width: 3px;
+ box-sizing: border-box;
+ font-family: 'Montserrat', sans-serif;
+ overflow: hidden;
+ position: relative;
+}
+.input-group-addon {
+ border-bottom-width: 3px;
+ box-sizing: border-box;
+ font-family: 'Montserrat', sans-serif;
+ overflow: hidden;
+ position: relative;
+ border-color: #52c368;
+ background: none;
+ width: auto;
+ height: 36px;
+}
diff --git a/templates/_account_bar.html b/templates/_account_bar.html
index b44b4ef..0e43024 100644
--- a/templates/_account_bar.html
+++ b/templates/_account_bar.html
@@ -12,9 +12,7 @@
From a3ffd18dc25a04e6ed146a60048b384f495386e9 Mon Sep 17 00:00:00 2001
From: Niklas Poslovski
Date: Sun, 2 Dec 2018 16:50:01 +0100
Subject: [PATCH 09/11] Improved navbar dropdowns in tortin.less
---
ivatar/static/css/tortin.less | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/ivatar/static/css/tortin.less b/ivatar/static/css/tortin.less
index 87f9a53..214b08d 100644
--- a/ivatar/static/css/tortin.less
+++ b/ivatar/static/css/tortin.less
@@ -258,6 +258,17 @@ background-color:@bg-hero;
.navbar-tortin .navbar-collapse, .navbar-tortin .navbar-form {
border:0;
}
+.dropdown-menu {
+background-color:@bg-hero;
+border:1px solid darken(@bg-hero, 10%);
+}
+.dropdown-menu>li>a {
+color:#FFFFFF;
+}
+.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover {
+background-color:darken(@bg-hero, 10%);
+color:#FFFFFF;
+}
@media (max-width:767px) {
.navbar-tortin .navbar-nav .open .dropdown-menu > li > a {
color:#FFFFFF
@@ -310,4 +321,4 @@ border-color: lighten(@bg-hero, 10%);
background: none;
width:auto;
height:36px;
-}
+}
\ No newline at end of file
From b56f8d6366c0ccac2ac268f38c0999c2727ff088 Mon Sep 17 00:00:00 2001
From: Niklas Poslovski
Date: Sun, 2 Dec 2018 16:51:03 +0100
Subject: [PATCH 10/11] Compile new tortin.css from updated tortin.less
---
ivatar/static/css/tortin.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ivatar/static/css/tortin.css b/ivatar/static/css/tortin.css
index 3032ded..1c55bdb 100644
--- a/ivatar/static/css/tortin.css
+++ b/ivatar/static/css/tortin.css
@@ -1 +1 @@
-body {font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;color: #525252;}.btn {border-bottom-width: 3px;box-sizing: border-box;font-family: 'Montserrat', sans-serif;text-transform: uppercase;background: #36b7d7;overflow: hidden;position: relative;-webkit-transition: all 0.3s;-moz-transition: all 0.3s;-ms-transition: all 0.3s;transition: all 0.3s;}.btn.btn-default {color: #61c6df;border-color: #61c6df;background: none;}.btn.btn-primary {border-color: #2087a1;}.btn:hover, .btn:active, .btn:focus {background: none;border-color: #2499b6;color: #2499b6;}.btn:hover:after, .btn:active:after, .btn:focus:after {top: 50%;}.btn:after {content: '';position: absolute;z-index: -1;width: 150%;height: 200%;top: -190%;left: 50%;background: #8bd5e8;-webkit-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);-moz-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);-ms-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);transform: translateX(-50%) translateY(-50%) skew(0, 5deg);-webkit-transition: all 0.5s ease-out;-moz-transition: all 0.5s ease-out;-ms-transition: all 0.5s ease-out;transition: all 0.5s ease-out;}.btn.btn-block:after {height: 250%;width: 200%;-webkit-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);-moz-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);-ms-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);transform: translateX(-50%) translateY(-50%) skew(0, 2deg);}.hero {background-color: #36b7d7;color: #fff;padding: 90px 0 40px;}.hero h1 {font-weight: 600;font-size: 6em;color: rgba(255, 255, 255, 0.5);}.hero h2 {font-weight: 200;font-size: 30px;margin-bottom: 30px;}.hero small {color: rgba(0, 0, 0, 0.4);}.hero .btn {display: inline-block;}.hero .btn.btn-default {color: #94d9ea;border-color: #94d9ea;background: none;}.hero .btn.btn-primary {border-color: #fff;}.hero .btn:hover, .hero .btn:active, .hero .btn:focus {border-color: #fff;color: #1c758b;}.hero .btn:after {background: rgba(255, 255, 255, 0.5);}.hero .container {position: relative;z-index: 10;}.social {background-color: #36b7d7;padding: 30px 0 140px;}.social ul {list-style: none;padding: 0;margin: 0;}.social ul li {float: left;margin-right: 15px;width: 100px;}.clipper, .clipper-footer {background-color: #fff;height: 110px;width: 100%;position: relative;top: -40px;-webkit-transform: skew(0, 2deg);-moz-transform: skew(0, 2deg);-ms-transform: skew(0, 2deg);transform: skew(0, 2deg);pointer-events: none;z-index: 1;}.clipper-footer {top: 0;}section.content {position: relative;top: -100px;margin-bottom: -100px;z-index: 10;}section.content h1, section.content h2, section.content h3, section.content h4, section.content h5, section.content h6 {color: #2499b6;}section.content h2 {font-weight: 200;font-size: 40px;}section.content section {margin-bottom: 20px;margin-top: 20px;}section.content .container > hr {-webkit-transform: skew(0, 2deg);-moz-transform: skew(0, 2deg);-ms-transform: skew(0, 2deg);transform: skew(0, 2deg);margin-top: 80px;margin-bottom: 40px;}footer {background-color: #dddddd;color: #888888;padding: 100px 0 40px;margin-top: -40px;}footer .pull-left {margin-right: 20px;}footer .logo {float: left;display: inline-block;margin-right: 5px;margin-top: -8px;}footer .logo .circle {stroke: #888888;stroke-width: 7;fill: none;}footer .logo .polygon {fill: #888888;}@media (max-width: 768px) {.hero {padding: 50px 0 30px;}.hero h1 {font-size: 4em;}.social {padding: 30px 0 100px;}.btn {margin-bottom: 5px;}section.content section {margin-bottom: 50px;}}.color {display: inline-block;border-radius: 50%;height: 20px;width: 20px;}.color.blue {background-color: #36b7d7;}.color.green {background-color: #3aa850;}.color.red {background-color: #f7645e;}.color.black {background-color: #525252;}.navbar-tortin {border: 0;background-color: #36b7d7;color: #FFFFFF;border-radius: 0;}.form-control {border-bottom-width: 3px;box-sizing: border-box;font-family: 'Montserrat', sans-serif;overflow: hidden;position: relative;-webkit-transition: all 0.3s;-moz-transition: all 0.3s;-ms-transition: all 0.3s;transition: all 0.3s;border-color: #61c6df;background: none;}.form-control:focus {border-color: #2499b6;box-shadow: none;}.navbar-tortin .navbar-brand, .navbar-tortin .navbar-text, .navbar-tortin .navbar-nav > li > a, .navbar-tortin .navbar-link, .navbar-tortin .btn-link {color: #FFFFFF;}.navbar-tortin .navbar-nav > .active > a, .navbar-tortin .navbar-nav > .active > a:focus, .navbar-tortin .navbar-nav > .active > a:hover, .navbar-tortin .navbar-nav > li > a:focus, .navbar-tortin .navbar-nav > li > a:hover, .navbar-tortin .navbar-link:hover, .navbar-tortin .btn-link:focus, .navbar-tortin .btn-link:hover, .navbar-tortin .navbar-nav > .open > a, .navbar-tortin .navbar-nav > .open > a:focus, .navbar-tortin .navbar-nav > .open > a:hover {background-color: #2499b6;}.navbar-tortin .navbar-toggle {border-color: #FFFFFF;}.navbar-tortin .navbar-toggle:hover {background-color: #FFFFFF;}.navbar-tortin .navbar-toggle .icon-bar {background-color: #FFFFFF;}.navbar-tortin .navbar-toggle:hover .icon-bar {background-color: #36b7d7;}.navbar-tortin .navbar-collapse, .navbar-tortin .navbar-form {border: 0;}@media (max-width: 767px) {.navbar-tortin .navbar-nav .open .dropdown-menu > li > a {color: #FFFFFF;}.navbar-tortin .navbar-nav .open .dropdown-menu > li > a:hover {background-color: #2499b6;}.navbar-tortin .navbar-nav .open .dropdown-menu > .active > a, .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a:focus, .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a:hover {background-color: #2499b6;}}.panel-tortin {border-color: #36b7d7;border-bottom-width: 3px;}.panel-tortin > .panel-heading {color: #fff;background-color: #36b7d7;border-color: #36b7d7;font-family: 'Montserrat', sans-serif;}.panel-tortin > .panel-heading + .panel-collapse > .panel-body {border-top-color: #36b7d7;}.panel-tortin > .panel-heading .badge {color: #36b7d7;background-color: #fff;}.panel-tortin > .panel-footer + .panel-collapse > .panel-body {border-bottom-color: #36b7d7;}.alert.alert-danger {background-color: #FFFFFF;color: #f7645e;border-color: #f7645e;border-bottom-width: 3px;box-sizing: border-box;font-family: 'Montserrat', sans-serif;overflow: hidden;position: relative;}.input-group-addon {border-bottom-width: 3px;box-sizing: border-box;font-family: 'Montserrat', sans-serif;overflow: hidden;position: relative;border-color: #61c6df;background: none;width: auto;height: 36px;}
+body {font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;color: #525252;}.btn {border-bottom-width: 3px;box-sizing: border-box;font-family: 'Montserrat', sans-serif;text-transform: uppercase;background: #36b7d7;overflow: hidden;position: relative;-webkit-transition: all 0.3s;-moz-transition: all 0.3s;-ms-transition: all 0.3s;transition: all 0.3s;}.btn.btn-default {color: #61c6df;border-color: #61c6df;background: none;}.btn.btn-primary {border-color: #2087a1;}.btn:hover, .btn:active, .btn:focus {background: none;border-color: #2499b6;color: #2499b6;}.btn:hover:after, .btn:active:after, .btn:focus:after {top: 50%;}.btn:after {content: '';position: absolute;z-index: -1;width: 150%;height: 200%;top: -190%;left: 50%;background: #8bd5e8;-webkit-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);-moz-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);-ms-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);transform: translateX(-50%) translateY(-50%) skew(0, 5deg);-webkit-transition: all 0.5s ease-out;-moz-transition: all 0.5s ease-out;-ms-transition: all 0.5s ease-out;transition: all 0.5s ease-out;}.btn.btn-block:after {height: 250%;width: 200%;-webkit-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);-moz-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);-ms-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);transform: translateX(-50%) translateY(-50%) skew(0, 2deg);}.hero {background-color: #36b7d7;color: #fff;padding: 90px 0 40px;}.hero h1 {font-weight: 600;font-size: 6em;color: rgba(255, 255, 255, 0.5);}.hero h2 {font-weight: 200;font-size: 30px;margin-bottom: 30px;}.hero small {color: rgba(0, 0, 0, 0.4);}.hero .btn {display: inline-block;}.hero .btn.btn-default {color: #94d9ea;border-color: #94d9ea;background: none;}.hero .btn.btn-primary {border-color: #fff;}.hero .btn:hover, .hero .btn:active, .hero .btn:focus {border-color: #fff;color: #1c758b;}.hero .btn:after {background: rgba(255, 255, 255, 0.5);}.hero .container {position: relative;z-index: 10;}.social {background-color: #36b7d7;padding: 30px 0 140px;}.social ul {list-style: none;padding: 0;margin: 0;}.social ul li {float: left;margin-right: 15px;width: 100px;}.clipper, .clipper-footer {background-color: #fff;height: 110px;width: 100%;position: relative;top: -40px;-webkit-transform: skew(0, 2deg);-moz-transform: skew(0, 2deg);-ms-transform: skew(0, 2deg);transform: skew(0, 2deg);pointer-events: none;z-index: 1;}.clipper-footer {top: 0;}section.content {position: relative;top: -100px;margin-bottom: -100px;z-index: 10;}section.content h1, section.content h2, section.content h3, section.content h4, section.content h5, section.content h6 {color: #2499b6;}section.content h2 {font-weight: 200;font-size: 40px;}section.content section {margin-bottom: 20px;margin-top: 20px;}section.content .container > hr {-webkit-transform: skew(0, 2deg);-moz-transform: skew(0, 2deg);-ms-transform: skew(0, 2deg);transform: skew(0, 2deg);margin-top: 80px;margin-bottom: 40px;}footer {background-color: #dddddd;color: #888888;padding: 100px 0 40px;margin-top: -40px;}footer .pull-left {margin-right: 20px;}footer .logo {float: left;display: inline-block;margin-right: 5px;margin-top: -8px;}footer .logo .circle {stroke: #888888;stroke-width: 7;fill: none;}footer .logo .polygon {fill: #888888;}@media (max-width: 768px) {.hero {padding: 50px 0 30px;}.hero h1 {font-size: 4em;}.social {padding: 30px 0 100px;}.btn {margin-bottom: 5px;}section.content section {margin-bottom: 50px;}}.color {display: inline-block;border-radius: 50%;height: 20px;width: 20px;}.color.blue {background-color: #36b7d7;}.color.green {background-color: #3aa850;}.color.red {background-color: #f7645e;}.color.black {background-color: #525252;}.navbar-tortin {border: 0;background-color: #36b7d7;color: #FFFFFF;border-radius: 0;}.form-control {border-bottom-width: 3px;box-sizing: border-box;font-family: 'Montserrat', sans-serif;overflow: hidden;position: relative;-webkit-transition: all 0.3s;-moz-transition: all 0.3s;-ms-transition: all 0.3s;transition: all 0.3s;border-color: #61c6df;background: none;}.form-control:focus {border-color: #2499b6;box-shadow: none;}.navbar-tortin .navbar-brand, .navbar-tortin .navbar-text, .navbar-tortin .navbar-nav > li > a, .navbar-tortin .navbar-link, .navbar-tortin .btn-link {color: #FFFFFF;}.navbar-tortin .navbar-nav > .active > a, .navbar-tortin .navbar-nav > .active > a:focus, .navbar-tortin .navbar-nav > .active > a:hover, .navbar-tortin .navbar-nav > li > a:focus, .navbar-tortin .navbar-nav > li > a:hover, .navbar-tortin .navbar-link:hover, .navbar-tortin .btn-link:focus, .navbar-tortin .btn-link:hover, .navbar-tortin .navbar-nav > .open > a, .navbar-tortin .navbar-nav > .open > a:focus, .navbar-tortin .navbar-nav > .open > a:hover {background-color: #2499b6;}.navbar-tortin .navbar-toggle {border-color: #FFFFFF;}.navbar-tortin .navbar-toggle:hover {background-color: #FFFFFF;}.navbar-tortin .navbar-toggle .icon-bar {background-color: #FFFFFF;}.navbar-tortin .navbar-toggle:hover .icon-bar {background-color: #36b7d7;}.navbar-tortin .navbar-collapse, .navbar-tortin .navbar-form {border: 0;}.dropdown-menu {background-color: #36b7d7;border: 1px solid #2499b6;}.dropdown-menu > li > a {color: #FFFFFF;}.dropdown-menu > li > a:focus, .dropdown-menu > li > a:hover {background-color: #2499b6;color: #FFFFFF;}@media (max-width: 767px) {.navbar-tortin .navbar-nav .open .dropdown-menu > li > a {color: #FFFFFF;}.navbar-tortin .navbar-nav .open .dropdown-menu > li > a:hover {background-color: #2499b6;}.navbar-tortin .navbar-nav .open .dropdown-menu > .active > a, .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a:focus, .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a:hover {background-color: #2499b6;}}.panel-tortin {border-color: #36b7d7;border-bottom-width: 3px;}.panel-tortin > .panel-heading {color: #fff;background-color: #36b7d7;border-color: #36b7d7;font-family: 'Montserrat', sans-serif;}.panel-tortin > .panel-heading + .panel-collapse > .panel-body {border-top-color: #36b7d7;}.panel-tortin > .panel-heading .badge {color: #36b7d7;background-color: #fff;}.panel-tortin > .panel-footer + .panel-collapse > .panel-body {border-bottom-color: #36b7d7;}.alert.alert-danger {background-color: #FFFFFF;color: #f7645e;border-color: #f7645e;border-bottom-width: 3px;box-sizing: border-box;font-family: 'Montserrat', sans-serif;overflow: hidden;position: relative;}.input-group-addon {border-bottom-width: 3px;box-sizing: border-box;font-family: 'Montserrat', sans-serif;overflow: hidden;position: relative;border-color: #61c6df;background: none;width: auto;height: 36px;}
\ No newline at end of file
From bc9730450cc647706f39b5989164b53ba0155e31 Mon Sep 17 00:00:00 2001
From: Oliver Falk
Date: Mon, 3 Dec 2018 15:24:16 +0100
Subject: [PATCH 11/11] Move falko to green and add red theme (because we can)
---
.../migrations/0013_auto_20181203_1421.py | 18 +
ivatar/ivataraccount/models.py | 3 +-
ivatar/static/css/{falko.css => green.css} | 0
ivatar/static/css/red.css | 337 ++++++++++++++++++
4 files changed, 357 insertions(+), 1 deletion(-)
create mode 100644 ivatar/ivataraccount/migrations/0013_auto_20181203_1421.py
rename ivatar/static/css/{falko.css => green.css} (100%)
create mode 100644 ivatar/static/css/red.css
diff --git a/ivatar/ivataraccount/migrations/0013_auto_20181203_1421.py b/ivatar/ivataraccount/migrations/0013_auto_20181203_1421.py
new file mode 100644
index 0000000..e857c27
--- /dev/null
+++ b/ivatar/ivataraccount/migrations/0013_auto_20181203_1421.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.3 on 2018-12-03 14:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ivataraccount', '0012_auto_20181107_1732'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='userpreference',
+ name='theme',
+ field=models.CharField(choices=[('default', 'Default theme'), ('clime', 'climes theme'), ('green', 'green theme'), ('red', 'red theme')], default='default', max_length=10),
+ ),
+ ]
diff --git a/ivatar/ivataraccount/models.py b/ivatar/ivataraccount/models.py
index d2c6193..fc4a611 100644
--- a/ivatar/ivataraccount/models.py
+++ b/ivatar/ivataraccount/models.py
@@ -70,7 +70,8 @@ class UserPreference(models.Model):
THEMES = (
('default', 'Default theme'),
('clime', 'climes theme'),
- ('falko', 'falkos theme'),
+ ('green', 'green theme'),
+ ('red', 'red theme'),
)
theme = models.CharField(
diff --git a/ivatar/static/css/falko.css b/ivatar/static/css/green.css
similarity index 100%
rename from ivatar/static/css/falko.css
rename to ivatar/static/css/green.css
diff --git a/ivatar/static/css/red.css b/ivatar/static/css/red.css
new file mode 100644
index 0000000..f912aa8
--- /dev/null
+++ b/ivatar/static/css/red.css
@@ -0,0 +1,337 @@
+body {
+ font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
+ color: #525252;
+}
+.btn {
+ border-bottom-width: 3px;
+ box-sizing: border-box;
+ font-family: 'Montserrat', sans-serif;
+ text-transform: uppercase;
+ background: #f7645e;
+ overflow: hidden;
+ position: relative;
+ -webkit-transition: all 0.3s;
+ -moz-transition: all 0.3s;
+ -ms-transition: all 0.3s;
+ transition: all 0.3s;
+}
+.btn.btn-default {
+ color: #f9938f;
+ border-color: #f9938f;
+ background: none;
+}
+.btn.btn-primary {
+ border-color: #f31e15;
+}
+.btn:hover,
+.btn:active,
+.btn:focus {
+ background: none;
+ border-color: #f5352d;
+ color: #f5352d;
+}
+.btn:hover:after,
+.btn:active:after,
+.btn:focus:after {
+ top: 50%;
+}
+.btn:after {
+ content: '';
+ position: absolute;
+ z-index: -1;
+ width: 150%;
+ height: 200%;
+ top: -190%;
+ left: 50%;
+ background: #fcc2bf;
+ -webkit-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);
+ -moz-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);
+ -ms-transform: translateX(-50%) translateY(-50%) skew(0, 5deg);
+ transform: translateX(-50%) translateY(-50%) skew(0, 5deg);
+ -webkit-transition: all 0.5s ease-out;
+ -moz-transition: all 0.5s ease-out;
+ -ms-transition: all 0.5s ease-out;
+ transition: all 0.5s ease-out;
+}
+.btn.btn-block:after {
+ height: 250%;
+ width: 200%;
+ -webkit-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);
+ -moz-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);
+ -ms-transform: translateX(-50%) translateY(-50%) skew(0, 2deg);
+ transform: translateX(-50%) translateY(-50%) skew(0, 2deg);
+}
+.hero {
+ background-color: #f7645e;
+ color: #fff;
+ padding: 90px 0 40px;
+}
+.hero h1 {
+ font-weight: 600;
+ font-size: 6em;
+ color: rgba(255, 255, 255, 0.5);
+}
+.hero h2 {
+ font-weight: 200;
+ font-size: 30px;
+ margin-bottom: 30px;
+}
+.hero small {
+ color: rgba(0, 0, 0, 0.4);
+}
+.hero .btn {
+ display: inline-block;
+}
+.hero .btn.btn-default {
+ color: #fccbc9;
+ border-color: #fccbc9;
+ background: none;
+}
+.hero .btn.btn-primary {
+ border-color: #fff;
+}
+.hero .btn:hover,
+.hero .btn:active,
+.hero .btn:focus {
+ border-color: #fff;
+ color: #e4140b;
+}
+.hero .btn:after {
+ background: rgba(255, 255, 255, 0.5);
+}
+.hero .container {
+ position: relative;
+ z-index: 10;
+}
+.social {
+ background-color: #f7645e;
+ padding: 30px 0 140px;
+}
+.social ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+.social ul li {
+ float: left;
+ margin-right: 15px;
+ width: 100px;
+}
+.clipper,
+.clipper-footer {
+ background-color: #fff;
+ height: 110px;
+ width: 100%;
+ position: relative;
+ top: -40px;
+ -webkit-transform: skew(0, 2deg);
+ -moz-transform: skew(0, 2deg);
+ -ms-transform: skew(0, 2deg);
+ transform: skew(0, 2deg);
+ pointer-events: none;
+ z-index: 1;
+}
+.clipper-footer {
+ top: 0;
+}
+section.content {
+ position: relative;
+ top: -100px;
+ margin-bottom: -100px;
+ z-index: 10;
+}
+section.content h1,
+section.content h2,
+section.content h3,
+section.content h4,
+section.content h5,
+section.content h6 {
+ color: #f5352d;
+}
+section.content h2 {
+ font-weight: 200;
+ font-size: 40px;
+}
+section.content section {
+ margin-bottom: 20px;
+ margin-top: 20px;
+}
+section.content .container > hr {
+ -webkit-transform: skew(0, 2deg);
+ -moz-transform: skew(0, 2deg);
+ -ms-transform: skew(0, 2deg);
+ transform: skew(0, 2deg);
+ margin-top: 80px;
+ margin-bottom: 40px;
+}
+footer {
+ background-color: #dddddd;
+ color: #888888;
+ padding: 100px 0 40px;
+ margin-top: -40px;
+}
+footer .pull-left {
+ margin-right: 20px;
+}
+footer .logo {
+ float: left;
+ display: inline-block;
+ margin-right: 5px;
+ margin-top: -8px;
+}
+footer .logo .circle {
+ stroke: #888888;
+ stroke-width: 7;
+ fill: none;
+}
+footer .logo .polygon {
+ fill: #888888;
+}
+@media (max-width: 768px) {
+ .hero {
+ padding: 50px 0 30px;
+ }
+ .hero h1 {
+ font-size: 4em;
+ }
+ .social {
+ padding: 30px 0 100px;
+ }
+ .btn {
+ margin-bottom: 5px;
+ }
+ section.content section {
+ margin-bottom: 50px;
+ }
+}
+.color {
+ display: inline-block;
+ border-radius: 50%;
+ height: 20px;
+ width: 20px;
+}
+.color.blue {
+ background-color: #36b7d7;
+}
+.color.green {
+ background-color: #3aa850;
+}
+.color.red {
+ background-color: #f7645e;
+}
+.color.black {
+ background-color: #525252;
+}
+.navbar-tortin {
+ border: 0;
+ background-color: #f7645e;
+ color: #FFFFFF;
+ border-radius: 0;
+}
+.form-control {
+ border-bottom-width: 3px;
+ box-sizing: border-box;
+ font-family: 'Montserrat', sans-serif;
+ overflow: hidden;
+ position: relative;
+ -webkit-transition: all 0.3s;
+ -moz-transition: all 0.3s;
+ -ms-transition: all 0.3s;
+ transition: all 0.3s;
+ border-color: #f9938f;
+ background: none;
+}
+.form-control:focus {
+ border-color: #f5352d;
+ box-shadow: none;
+}
+.navbar-tortin .navbar-brand,
+.navbar-tortin .navbar-text,
+.navbar-tortin .navbar-nav > li > a,
+.navbar-tortin .navbar-link,
+.navbar-tortin .btn-link {
+ color: #FFFFFF;
+}
+.navbar-tortin .navbar-nav > .active > a,
+.navbar-tortin .navbar-nav > .active > a:focus,
+.navbar-tortin .navbar-nav > .active > a:hover,
+.navbar-tortin .navbar-nav > li > a:focus,
+.navbar-tortin .navbar-nav > li > a:hover,
+.navbar-tortin .navbar-link:hover,
+.navbar-tortin .btn-link:focus,
+.navbar-tortin .btn-link:hover,
+.navbar-tortin .navbar-nav > .open > a,
+.navbar-tortin .navbar-nav > .open > a:focus,
+.navbar-tortin .navbar-nav > .open > a:hover {
+ background-color: #f5352d;
+}
+.navbar-tortin .navbar-toggle {
+ border-color: #FFFFFF;
+}
+.navbar-tortin .navbar-toggle:hover {
+ background-color: #FFFFFF;
+}
+.navbar-tortin .navbar-toggle .icon-bar {
+ background-color: #FFFFFF;
+}
+.navbar-tortin .navbar-toggle:hover .icon-bar {
+ background-color: #f7645e;
+}
+.navbar-tortin .navbar-collapse,
+.navbar-tortin .navbar-form {
+ border: 0;
+}
+@media (max-width: 767px) {
+ .navbar-tortin .navbar-nav .open .dropdown-menu > li > a {
+ color: #FFFFFF;
+ }
+ .navbar-tortin .navbar-nav .open .dropdown-menu > li > a:hover {
+ background-color: #f5352d;
+ }
+ .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a,
+ .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a:focus,
+ .navbar-tortin .navbar-nav .open .dropdown-menu > .active > a:hover {
+ background-color: #f5352d;
+ }
+}
+.panel-tortin {
+ border-color: #f7645e;
+ border-bottom-width: 3px;
+}
+.panel-tortin > .panel-heading {
+ color: #fff;
+ background-color: #f7645e;
+ border-color: #f7645e;
+ font-family: 'Montserrat', sans-serif;
+}
+.panel-tortin > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #f7645e;
+}
+.panel-tortin > .panel-heading .badge {
+ color: #f7645e;
+ background-color: #fff;
+}
+.panel-tortin > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #f7645e;
+}
+.alert.alert-danger {
+ background-color: #FFFFFF;
+ color: #f7645e;
+ border-color: #f7645e;
+ border-bottom-width: 3px;
+ box-sizing: border-box;
+ font-family: 'Montserrat', sans-serif;
+ overflow: hidden;
+ position: relative;
+}
+.input-group-addon {
+ border-bottom-width: 3px;
+ box-sizing: border-box;
+ font-family: 'Montserrat', sans-serif;
+ overflow: hidden;
+ position: relative;
+ border-color: #f9938f;
+ background: none;
+ width: auto;
+ height: 36px;
+}