From 22fb786cff3e082857d949efb34eef899748336c Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 10:38:47 +0100 Subject: [PATCH 001/110] Fix issue #16 --- ivatar/ivataraccount/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index 0c2cfc6..9cbe02a 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -336,8 +336,8 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView): else: context['photos'].append({ 'service_url': libravatar_service_url, - 'thumbnail_url': libravatar_service_url + '?s=80', - 'image_url': libravatar_service_url + '?s=512', + 'thumbnail_url': libravatar_service_url + '&s=80', + 'image_url': libravatar_service_url + '&s=512', 'width': 80, 'height': 80, 'service_name': 'Libravatar', From c3ed5fb014bf268d9d8f9ca12e4adebd2faf1130 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 10:45:42 +0100 Subject: [PATCH 002/110] Fix issue #17 --- ivatar/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ivatar/views.py b/ivatar/views.py index 56223aa..b8ffe1d 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -54,7 +54,12 @@ class AvatarImageView(TemplateView): sizetemp = request.GET['size'] if sizetemp: if sizetemp != '' and sizetemp is not None and sizetemp != '0': - size = int(sizetemp) + try: + 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) From 0d404dc5592852d4a8b8f4a616781681a617396d Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 10:51:08 +0100 Subject: [PATCH 003/110] Return nobody instead of misteryman - as pointed out in issue #18 (fixed) --- ivatar/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ivatar/views.py b/ivatar/views.py index b8ffe1d..9b79880 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -127,10 +127,10 @@ class AvatarImageView(TemplateView): else: return HttpResponseRedirect(default) - static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png')) + static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png')) if not path.isfile(static_img): # We trust this exists!!! - static_img = path.join('static', 'img', 'mm', '512.png') + static_img = path.join('static', 'img', 'nobody', '512.png') # We trust static/ is mapped to /static/ return HttpResponseRedirect('/' + static_img) From da6c0691dca46b6d8d6f082853217d937351cfe2 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 11:03:50 +0100 Subject: [PATCH 004/110] Address issue #19, inconsistent padding size in relation to requested size + final image return is larger, since padding is added --- ivatar/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ivatar/views.py b/ivatar/views.py index 9b79880..32d55fb 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -110,7 +110,11 @@ class AvatarImageView(TemplateView): 'rgb(49,203,115)', 'rgb(141,69,170)'] background = 'rgb(224,224,224)' - padding = (10, 10, 10, 10) + padwidth = int(size/10) + padding = (padwidth, padwidth, padwidth, padwidth) + # Since padding is _added_ around the generated image, we + # need to reduce the image size by padding*2 (left/right, top/bottom) + size = size - 2*padwidth generator = IdenticonGenerator( 10, 10, digest=hashlib.sha1, foreground=foreground, background=background) From 3be9611cf812672c75848280f094a53250eee258 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 11:09:24 +0100 Subject: [PATCH 005/110] Regression from fixing issue #18 - adapt test accordingly --- ivatar/ivataraccount/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index 779c051..a06242f 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -1110,7 +1110,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/mm/80.png', + expected_url='/static/img/nobody/80.png', msg_prefix='Why does this not redirect to the default img?') # Eventually one should check if the data is the same From c6710052d2a4d9514e78f6b480108225f14224bb Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 12:08:11 +0100 Subject: [PATCH 006/110] Fix regression introduced by commit c3ed5fb0 (issue #18 - fixes issue #20 --- ivatar/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ivatar/views.py b/ivatar/views.py index 32d55fb..5c2b1c7 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -127,7 +127,12 @@ class AvatarImageView(TemplateView): if str(default) == 'mm' or str(default) == 'mp': # If mm is explicitly given, we need to catch that - pass + static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png')) + if not path.isfile(static_img): + # We trust this exists!!! + static_img = path.join('static', 'img', 'mm', '512.png') + # We trust static/ is mapped to /static/ + return HttpResponseRedirect('/' + static_img) else: return HttpResponseRedirect(default) From 9119e5f78c2589bdd1d67ed881242102d51d4250 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 12:18:21 +0100 Subject: [PATCH 007/110] Make sure we don't run into regressions again (issue #18 #20) --- ivatar/ivataraccount/test_views.py | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index a06242f..fe1a2ca 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -1114,6 +1114,43 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods 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(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' % (urlobj.path, urlobj.query) + response = self.client.get(url, follow=True) + self.assertRedirects( + response=response, + expected_url='/static/img/mm/80.png', + 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(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' % (urlobj.path, urlobj.query) + response = self.client.get(url, follow=True) + self.assertRedirects( + response=response, + expected_url='/static/img/nobody/80.png', + 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_default(self): # pylint: disable=invalid-name ''' Test fetching avatar for not existing mail with default specified From 6c886fdb1f1c856cd7d068c7d932aba4571e4ca1 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 13:21:26 +0100 Subject: [PATCH 008/110] Revert commit e22275f5 and e6d65637, since this anyway didn't fix the OpenID redirect problem --- templates/openid/login.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/templates/openid/login.html b/templates/openid/login.html index 3bcfbd7..b0a800e 100644 --- a/templates/openid/login.html +++ b/templates/openid/login.html @@ -29,10 +29,7 @@

- - +   From 740c03e7316d44bdb1a3d05adbd9133fc9def11e Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 13:23:18 +0100 Subject: [PATCH 009/110] Set option to use proxy set value from X_FORWARDED_HOST --- config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.py b/config.py index 4866404..9bec80d 100644 --- a/config.py +++ b/config.py @@ -132,3 +132,5 @@ if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')): from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' + +USE_X_FORWARDED_HOST = True From 81b4417e4fd2be26b3c6d5c687881c828adcc42d Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 16 Nov 2018 13:33:39 +0100 Subject: [PATCH 010/110] Happy lint, happy coverage --- .../migrations/0008_userpreference.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/ivatar/ivataraccount/migrations/0008_userpreference.py b/ivatar/ivataraccount/migrations/0008_userpreference.py index 5f76f52..5838a97 100644 --- a/ivatar/ivataraccount/migrations/0008_userpreference.py +++ b/ivatar/ivataraccount/migrations/0008_userpreference.py @@ -1,3 +1,4 @@ +# pylint: disable=invalid-name,missing-docstring # Generated by Django 2.0.6 on 2018-07-04 12:32 from django.conf import settings @@ -5,18 +6,18 @@ from django.db import migrations, models import django.db.models.deletion -def add_preference_to_user(apps, schema_editor): +def add_preference_to_user(apps, schema_editor): # pylint: disable=unused-argument ''' Make sure all users have preferences set up ''' from django.contrib.auth.models import User - UserPreference = apps.get_model('ivataraccount', 'UserPreference') - for u in User.objects.filter(userpreference=None): - p = UserPreference.objects.create(user_id=u.pk) - p.save() + UserPreference = apps.get_model('ivataraccount', 'UserPreference') # pylint: disable=invalid-name + for user in User.objects.filter(userpreference=None): + pref = UserPreference.objects.create(user_id=user.pk) # pragma: no cover + pref.save() # pragma: no cover -class Migration(migrations.Migration): +class Migration(migrations.Migration): # pylint: disable=missing-docstring dependencies = [ ('auth', '0009_alter_user_last_name_max_length'), @@ -27,8 +28,16 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserPreference', fields=[ - ('theme', models.CharField(choices=[('default', 'Default theme'), ('clime', 'Climes theme')], default='default', max_length=10)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('theme', models.CharField( + choices=[ + ('default', 'Default theme'), + ('clime', 'Climes theme')], + default='default', max_length=10)), + ('user', models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL)), ], ), migrations.RunPython(add_preference_to_user), From 8f466b368220cdeace0bfbf8d6b776369f061404 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 19 Nov 2018 15:43:21 +0100 Subject: [PATCH 011/110] Add middleware to fix multiple proxies issue --- ivatar/middleware.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 ivatar/middleware.py diff --git a/ivatar/middleware.py b/ivatar/middleware.py new file mode 100644 index 0000000..1520fee --- /dev/null +++ b/ivatar/middleware.py @@ -0,0 +1,26 @@ +""" +Middleware classes +""" +from django.utils.deprecation import MiddlewareMixin + +class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods + """ + Middleware to rewrite proxy headers for deployments + multiple proxies + """ + FORWARDED_FOR_FIELDS = [ + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED_HOST', + 'HTTP_X_FORWARDED_SERVER', + ] + + def process_request(self, request): + """ + Rewrites the proxy headers so that only the most + recent proxy is used. + """ + for field in self.FORWARDED_FOR_FIELDS: + if field in request.META: + if ',' in request.META[field]: + parts = request.META[field].split(',') + request.META[field] = parts[-1].strip() From 2f79608a599d350a6446df7d6f9414b1b16e6955 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 19 Nov 2018 15:43:46 +0100 Subject: [PATCH 012/110] Add middleware and rearrange to make pylint happier --- config.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/config.py b/config.py index 9bec80d..93fdab6 100644 --- a/config.py +++ b/config.py @@ -4,14 +4,16 @@ Configuration overrides for settings.py import os import sys -from socket import gethostname, gethostbyname from django.urls import reverse_lazy from ivatar.settings import BASE_DIR -ADMIN_USERS = [] -ALLOWED_HOSTS = [ '*' ] +from ivatar.settings import MIDDLEWARE +from ivatar.settings import INSTALLED_APPS +from ivatar.settings import TEMPLATES + +ADMIN_USERS = [] +ALLOWED_HOSTS = ['*'] -from ivatar.settings import INSTALLED_APPS # noqa INSTALLED_APPS.extend([ 'django_extensions', 'django_openid_auth', @@ -22,10 +24,12 @@ INSTALLED_APPS.extend([ 'ivatar.tools', ]) -from ivatar.settings import MIDDLEWARE # noqa MIDDLEWARE.extend([ 'django.middleware.locale.LocaleMiddleware', ]) +MIDDLEWARE.insert( + 0, 'ivatar.middleware.MultipleProxyMiddleware', +) AUTHENTICATION_BACKENDS = ( # Enable this to allow LDAP authentication. @@ -35,7 +39,6 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', ) -from ivatar.settings import TEMPLATES # noqa TEMPLATES[0]['DIRS'].extend([ os.path.join(BASE_DIR, 'templates'), ]) @@ -76,7 +79,8 @@ BOOTSTRAP4 = { 'javascript_in_head': False, 'css_url': { 'href': '/static/css/bootstrap.min.css', - 'integrity': 'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB', # noqa + 'integrity': + 'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB', 'crossorigin': 'anonymous', }, 'javascript_url': { @@ -86,7 +90,8 @@ BOOTSTRAP4 = { }, 'popper_url': { 'url': '/static/js/popper.min.js', - 'integrity': 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49', # noqa + 'integrity': + 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49', 'crossorigin': 'anonymous', }, } @@ -134,3 +139,4 @@ if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')): SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' USE_X_FORWARDED_HOST = True +ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',] From 033a288b8d183d6777e22c324576ea19a9d76301 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 19 Nov 2018 16:03:41 +0100 Subject: [PATCH 013/110] Middleware for multiple proxies --- config.py | 22 ++++++++++++++-------- ivatar/middleware.py | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 ivatar/middleware.py diff --git a/config.py b/config.py index 9bec80d..93fdab6 100644 --- a/config.py +++ b/config.py @@ -4,14 +4,16 @@ Configuration overrides for settings.py import os import sys -from socket import gethostname, gethostbyname from django.urls import reverse_lazy from ivatar.settings import BASE_DIR -ADMIN_USERS = [] -ALLOWED_HOSTS = [ '*' ] +from ivatar.settings import MIDDLEWARE +from ivatar.settings import INSTALLED_APPS +from ivatar.settings import TEMPLATES + +ADMIN_USERS = [] +ALLOWED_HOSTS = ['*'] -from ivatar.settings import INSTALLED_APPS # noqa INSTALLED_APPS.extend([ 'django_extensions', 'django_openid_auth', @@ -22,10 +24,12 @@ INSTALLED_APPS.extend([ 'ivatar.tools', ]) -from ivatar.settings import MIDDLEWARE # noqa MIDDLEWARE.extend([ 'django.middleware.locale.LocaleMiddleware', ]) +MIDDLEWARE.insert( + 0, 'ivatar.middleware.MultipleProxyMiddleware', +) AUTHENTICATION_BACKENDS = ( # Enable this to allow LDAP authentication. @@ -35,7 +39,6 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', ) -from ivatar.settings import TEMPLATES # noqa TEMPLATES[0]['DIRS'].extend([ os.path.join(BASE_DIR, 'templates'), ]) @@ -76,7 +79,8 @@ BOOTSTRAP4 = { 'javascript_in_head': False, 'css_url': { 'href': '/static/css/bootstrap.min.css', - 'integrity': 'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB', # noqa + 'integrity': + 'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB', 'crossorigin': 'anonymous', }, 'javascript_url': { @@ -86,7 +90,8 @@ BOOTSTRAP4 = { }, 'popper_url': { 'url': '/static/js/popper.min.js', - 'integrity': 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49', # noqa + 'integrity': + 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49', 'crossorigin': 'anonymous', }, } @@ -134,3 +139,4 @@ if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')): SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' USE_X_FORWARDED_HOST = True +ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',] diff --git a/ivatar/middleware.py b/ivatar/middleware.py new file mode 100644 index 0000000..1520fee --- /dev/null +++ b/ivatar/middleware.py @@ -0,0 +1,26 @@ +""" +Middleware classes +""" +from django.utils.deprecation import MiddlewareMixin + +class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods + """ + Middleware to rewrite proxy headers for deployments + multiple proxies + """ + FORWARDED_FOR_FIELDS = [ + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED_HOST', + 'HTTP_X_FORWARDED_SERVER', + ] + + def process_request(self, request): + """ + Rewrites the proxy headers so that only the most + recent proxy is used. + """ + for field in self.FORWARDED_FOR_FIELDS: + if field in request.META: + if ',' in request.META[field]: + parts = request.META[field].split(',') + request.META[field] = parts[-1].strip() From c1d6a751da0c74130f8c8a842b8b1ca4561fe4a8 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 19 Nov 2018 16:20:25 +0100 Subject: [PATCH 014/110] Rewrite middleware to use FORWARDED_SERVER as FORWARDED_HOST, if available --- ivatar/middleware.py | 18 +++++------------- templates/openid/login.html | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/ivatar/middleware.py b/ivatar/middleware.py index 1520fee..6f6e066 100644 --- a/ivatar/middleware.py +++ b/ivatar/middleware.py @@ -6,21 +6,13 @@ from django.utils.deprecation import MiddlewareMixin class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods """ Middleware to rewrite proxy headers for deployments - multiple proxies + with multiple proxies """ - FORWARDED_FOR_FIELDS = [ - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_FORWARDED_HOST', - 'HTTP_X_FORWARDED_SERVER', - ] def process_request(self, request): """ - Rewrites the proxy headers so that only the most - recent proxy is used. + Rewrites the proxy headers so that forwarded server is + used if available. """ - for field in self.FORWARDED_FOR_FIELDS: - if field in request.META: - if ',' in request.META[field]: - parts = request.META[field].split(',') - request.META[field] = parts[-1].strip() + if 'HTTP_X_FORWARDED_SERVER' in request.META: + request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER'] diff --git a/templates/openid/login.html b/templates/openid/login.html index b0a800e..a1ae3bb 100644 --- a/templates/openid/login.html +++ b/templates/openid/login.html @@ -29,7 +29,7 @@

- +   From 755143ea794a8bd4bd6da41061a9709ed486e10b Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 23 Nov 2018 11:33:47 +0100 Subject: [PATCH 015/110] Also check the username the user has in his export file, will be used for the mass-import of libravatar data --- ivatar/ivataraccount/read_libravatar_export.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ivatar/ivataraccount/read_libravatar_export.py b/ivatar/ivataraccount/read_libravatar_export.py index db09992..b248399 100644 --- a/ivatar/ivataraccount/read_libravatar_export.py +++ b/ivatar/ivataraccount/read_libravatar_export.py @@ -20,6 +20,7 @@ def read_gzdata(gzdata=None): emails = [] # pylint: disable=invalid-name openids = [] # pylint: disable=invalid-name photos = [] # pylint: disable=invalid-name + username = None # pylint: disable=invalid-name if not gzdata: return False @@ -32,6 +33,11 @@ def read_gzdata(gzdata=None): print('Unknown export format: %s' % root.tag) exit(-1) + # Username + for item in root.findall('{%s}account' % SCHEMAROOT)[0].items(): + if item[0] == 'username': + username = item[1] + # Emails for email in root.findall('{%s}emails' % SCHEMAROOT)[0]: if email.tag == '{%s}email' % SCHEMAROOT: @@ -69,4 +75,5 @@ def read_gzdata(gzdata=None): 'emails': emails, 'openids': openids, 'photos': photos, + 'username': username, } From cf4974f24d76c03ea2ce6cabe26bb8f17c2fd637 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 23 Nov 2018 12:46:10 +0100 Subject: [PATCH 016/110] Slash at the end of OpenId is not a must, remove it --- ivatar/ivataraccount/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ivatar/ivataraccount/models.py b/ivatar/ivataraccount/models.py index ca1c823..304a858 100644 --- a/ivatar/ivataraccount/models.py +++ b/ivatar/ivataraccount/models.py @@ -448,8 +448,8 @@ class ConfirmedOpenId(BaseAccountModel): lowercase_url = urlunsplit( (url.scheme.lower(), netloc, url.path, url.query, url.fragment) ) - if lowercase_url[-1] != '/': - lowercase_url += '/' + #if lowercase_url[-1] != '/': + # lowercase_url += '/' self.openid = lowercase_url self.digest = hashlib.sha256(lowercase_url.encode('utf-8')).hexdigest() return super().save(force_insert, force_update, using, update_fields) From 2643e11890e7575fc8487ec78711455528dab566 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 23 Nov 2018 12:54:12 +0100 Subject: [PATCH 017/110] Only use the value if >0, Fixes issue #21 --- ivatar/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ivatar/views.py b/ivatar/views.py index 5c2b1c7..770f15c 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -55,7 +55,8 @@ class AvatarImageView(TemplateView): if sizetemp: if sizetemp != '' and sizetemp is not None and sizetemp != '0': try: - size = int(sizetemp) + 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: From 93e0e624b518c901ffb3d9f1d34582e8bd583a7d Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 23 Nov 2018 13:05:12 +0100 Subject: [PATCH 018/110] Add script to import a full libravatar export --- import_libravatar.py | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 import_libravatar.py diff --git a/import_libravatar.py b/import_libravatar.py new file mode 100644 index 0000000..8240301 --- /dev/null +++ b/import_libravatar.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +''' +Import the whole libravatar export +''' + +import os +from os.path import isfile, isdir, join +import sys +import base64 +from io import BytesIO +import django +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ivatar.settings") # pylint: disable=wrong-import-position +django.setup() # pylint: disable=wrong-import-position +from django.contrib.auth.models import User +from PIL import Image +from ivatar.settings import JPEG_QUALITY +from ivatar.ivataraccount.read_libravatar_export import read_gzdata as libravatar_read_gzdata +from ivatar.ivataraccount.models import ConfirmedEmail +from ivatar.ivataraccount.models import ConfirmedOpenId +from ivatar.ivataraccount.models import Photo +from ivatar.ivataraccount.models import file_format + +if len(sys.argv) < 2: + print("First argument to '%s' must be the path to the exports" % sys.argv[0]) + exit(-255) + +if not isdir(sys.argv[1]): + print("First argument to '%s' must be a directory containing the exports" % sys.argv[0]) + exit(-255) + +PATH = sys.argv[1] +for file in os.listdir(PATH): + if not file.endswith('.xml.gz'): + continue + if isfile(join(PATH, file)): + fh = open(join(PATH, file), 'rb') + items = libravatar_read_gzdata(fh.read()) + print('Adding user "%s"' % items['username']) + (user, created) = User.objects.get_or_create(username=items['username']) + for email in items['emails']: + try: + ConfirmedEmail.objects.get_or_create(email=email, user=user) + except django.db.utils.IntegrityError: + print('%s not unique?' % email) + for openid in items['openids']: + try: + ConfirmedOpenId.objects.get_or_create(openid=openid, user=user) # pylint: disable=no-member + except django.db.utils.IntegrityError: + print('%s not unique?' % openid) + for photo in items['photos']: + data = base64.decodebytes(bytes(photo['data'], 'utf-8')) + pilobj = Image.open(BytesIO(data)) + out = BytesIO() + pilobj.save(out, pilobj.format, quality=JPEG_QUALITY) + out.seek(0) + photo = Photo() + photo.user = user + photo.ip_address = '0.0.0.0' + photo.format = file_format(pilobj.format) + photo.data = out.read() + photo.save() + + fh.close() From e02e064998ffc986a5b6970f16c94d4abd3088a3 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 23 Nov 2018 13:14:33 +0100 Subject: [PATCH 019/110] Add script, able run export all data from libravatar --- exportaccounts.py | 129 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100755 exportaccounts.py diff --git a/exportaccounts.py b/exportaccounts.py new file mode 100755 index 0000000..3a2ac6f --- /dev/null +++ b/exportaccounts.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2018 Oliver Falk +# +# This file is part of Libravatar +# +# Libravatar is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Libravatar is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Libravatar. If not, see . + +import base64 +import gzip +import json +import os +import sys +from xml.sax import saxutils +import hashlib + +# pylint: disable=relative-import +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libravatar.settings") +import libravatar.settings as settings +from libravatar.utils import create_logger, is_hex +import django +django.setup() +from django.contrib.auth.models import User + +os.umask(022) +LOGGER = create_logger('exportaccount') + +SCHEMA_ROOT = 'https://www.libravatar.org/schemas/export/0.2' +SCHEMA_XSD = '%s/export.xsd' % SCHEMA_ROOT + + +def xml_header(): + return ''' +\n''' % (SCHEMA_ROOT, SCHEMA_XSD, SCHEMA_ROOT) + + +def xml_footer(): + return '\n' + + +def xml_account(username): + escaped_username = saxutils.quoteattr(username) + escaped_site_url = saxutils.quoteattr(settings.SITE_URL) + return ' \n' % (escaped_username, escaped_site_url) + + +def xml_list(list_type, list_elements): + s = ' <%ss>\n' % list_type + for element in list_elements: + element = element.encode('utf-8') + s += ' <%s>%s\n' % (list_type, saxutils.escape(element), list_type) + s += ' \n' % list_type + return s + + +def xml_photos(photos): + s = ' \n' + for photo in photos: + (photo_filename, photo_format) = photo + encoded_photo = encode_photo(photo_filename, photo_format) + if encoded_photo: + s += ''' +%s + \n''' % (saxutils.quoteattr(photo_format), encoded_photo) + s += ' \n' + return s + + +def encode_photo(photo_filename, photo_format): + filename = settings.USER_FILES_ROOT + photo_filename + '.' + photo_format + if not os.path.isfile(filename): + LOGGER.warning('Photo not found: %s', filename) + return None + + photo_content = None + with open(filename) as photo: + photo_content = photo.read() + + if not photo_content: + LOGGER.warning('Could not read photo: %s', filename) + return None + + return base64.b64encode(photo_content) + + +def main(argv=None): + if argv is None: + argv = sys.argv + + for user in User.objects.all(): + hash_object = hashlib.new('sha256') + hash_object.update(user.username + user.password) + file_hash = hash_object.hexdigest() + emails = list(user.confirmed_emails.values_list('email', flat=True)) + openids = list(user.confirmed_openids.values_list('openid', flat=True)) + photos = [] + for photo in user.photos.all(): + photo_details = (photo.filename, photo.format) + photos.append(photo_details) + username = user.username + + dest_filename = settings.EXPORT_FILES_ROOT + file_hash + '.xml.gz' + destination = gzip.open(dest_filename, 'w') + destination.write(xml_header()) + destination.write(xml_account(username)) + destination.write(xml_list('email', emails)) + destination.write(xml_list('openid', openids)) + destination.write(xml_photos(photos)) + destination.write(xml_footer()) + destination.close() + print(dest_filename) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From e96e17bd013378dd81c72d59059a6dcf71bf3a7f Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 23 Nov 2018 15:52:41 +0100 Subject: [PATCH 020/110] Add error view if digest is to long/short --- ivatar/urls.py | 7 +++++++ templates/error.html | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ivatar/urls.py b/ivatar/urls.py index f875a5c..d86fd04 100644 --- a/ivatar/urls.py +++ b/ivatar/urls.py @@ -20,6 +20,13 @@ urlpatterns = [ # pylint: disable=invalid-name url( r'avatar/(?P\w{32})', AvatarImageView.as_view(), name='avatar_view'), + url( + r'avatar/(?P\w)', + TemplateView.as_view( + template_name='error.html', + extra_context={ + 'errormessage': 'Incorrect digest length', + })), 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/templates/error.html b/templates/error.html index 420b9b0..292fff8 100644 --- a/templates/error.html +++ b/templates/error.html @@ -8,7 +8,14 @@ {% block content %}

{% trans 'Error!' %}

-

{% block errormessage %}{% trans 'Libravatar has encountered an error.' %}{% endblock errormessage %}

+

{% block errormessage %} +{% trans 'Libravatar has encountered an error.' %} +{% if errormessage %} +
+
+{% blocktrans %}{{ errormessage }}{% endblocktrans %} +{% endif %} +{% endblock errormessage %}

From e8e2e3af71b58d96aef4dd5bd2576d2a799987de Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 23 Nov 2018 16:10:57 +0100 Subject: [PATCH 021/110] Test --- x | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 x diff --git a/x b/x new file mode 100644 index 0000000..e69de29 From f501c9403283b9c988f02e86b7f7f1149a3877cd Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 23 Nov 2018 16:17:04 +0100 Subject: [PATCH 022/110] Remove x --- x | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 x diff --git a/x b/x deleted file mode 100644 index e69de29..0000000 From 5146bd8b82eb17e46fec0256590457e29fd668ae Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 30 Nov 2018 14:06:56 +0100 Subject: [PATCH 023/110] 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 @@
- {{ photo.service_name }} image + {{ photo.service_name }} image
From e783ea6601f16b7f6598b7bfa72d0e5086bab0ee Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 30 Nov 2018 14:07:30 +0100 Subject: [PATCH 024/110] 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 025/110] 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 026/110] 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 027/110] 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 028/110] 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 %} +

+

{% csrf_token %} +
+ +
+ +
+
+

+ +
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 031/110] 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 032/110] 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 033/110] 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; +} From 64f804b876d94f8456f187e6036916e6082239ed Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 3 Dec 2018 16:01:20 +0100 Subject: [PATCH 034/110] Fix some lint warnings, add Robohash (First shot, Issue #13) and make OpenId work again --- ivatar/views.py | 44 ++++++++++++++++++++++++++++++-------------- requirements.txt | 1 + 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/ivatar/views.py b/ivatar/views.py index a9e9348..9d8de33 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -4,19 +4,20 @@ views under / from io import BytesIO from os import path import hashlib -from PIL import Image +from urllib.request import urlopen +from urllib.error import HTTPError, URLError +from ssl import SSLError 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 PIL import Image from monsterid.id import build_monster as BuildMonster from pydenticon import Generator as IdenticonGenerator +from robohash import Robohash from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId @@ -26,6 +27,9 @@ URL_TIMEOUT = 5 # in seconds def get_size(request, size=DEFAULT_AVATAR_SIZE): + ''' + Get size from the URL arguments + ''' sizetemp = None if 's' in request.GET: sizetemp = request.GET['s'] @@ -52,7 +56,7 @@ class AvatarImageView(TemplateView): ''' # TODO: Do cache resize images!! Memcached? - def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals + def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements ''' Override get from parent class ''' @@ -91,7 +95,12 @@ class AvatarImageView(TemplateView): try: obj = model.objects.get(digest_sha256=kwargs['digest']) except ObjectDoesNotExist: - pass + model = ConfirmedOpenId + try: + obj = model.objects.get(digest=kwargs['digest']) + except: + pass + # If that mail/openid doesn't exist, or has no photo linked to it if not obj or not obj.photo or forcedefault: @@ -123,6 +132,16 @@ class AvatarImageView(TemplateView): data, content_type='image/png') + if str(default) == 'robohash': + robohash = Robohash(kwargs['digest']) + robohash.assemble(roboset='any', sizex=size, sizey=size) + data = BytesIO() + robohash.img.save(data, format='png') + data.seek(0) + return HttpResponse( + data, + content_type='image/png') + if str(default) == 'identicon' or str(default) == 'retro': # Taken from example code foreground = [ @@ -157,8 +176,7 @@ class AvatarImageView(TemplateView): static_img = path.join('static', 'img', 'mm', '512.png') # We trust static/ is mapped to /static/ return HttpResponseRedirect('/' + static_img) - else: - return HttpResponseRedirect(default) + return HttpResponseRedirect(default) static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png')) if not path.isfile(static_img): @@ -188,7 +206,7 @@ class GravatarProxyView(View): ''' # TODO: Do cache images!! Memcached? - def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals + def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument ''' Override get from parent class ''' @@ -205,17 +223,14 @@ class GravatarProxyView(View): 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) @@ -226,8 +241,9 @@ class GravatarProxyView(View): 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' + url = reverse_lazy( + 'avatar_view', + args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y' return HttpResponseRedirect(url) diff --git a/requirements.txt b/requirements.txt index cfd7b9e..aec5e22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,3 +34,4 @@ psycopg2 notsetuptools git+https://github.com/ofalk/monsterid.git git+https://github.com/azaghal/pydenticon.git +robohash From 7c1b8218201584dccc2506d06aa5dab0bf57daf3 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 3 Dec 2018 16:17:33 +0100 Subject: [PATCH 035/110] Use latest master tree on GitHub for robohash and allow to choose the set with robohash= (set1-3) --- ivatar/views.py | 5 ++++- requirements.txt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ivatar/views.py b/ivatar/views.py index 9d8de33..6629e93 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -133,8 +133,11 @@ class AvatarImageView(TemplateView): content_type='image/png') if str(default) == 'robohash': + roboset = 'any' + if request.GET.get('robohash'): + roboset = request.GET.get('robohash') robohash = Robohash(kwargs['digest']) - robohash.assemble(roboset='any', sizex=size, sizey=size) + robohash.assemble(roboset=roboset, sizex=size, sizey=size) data = BytesIO() robohash.img.save(data, format='png') data.seek(0) diff --git a/requirements.txt b/requirements.txt index aec5e22..f23c634 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,4 +34,4 @@ psycopg2 notsetuptools git+https://github.com/ofalk/monsterid.git git+https://github.com/azaghal/pydenticon.git -robohash +git+https://github.com/e1ven/Robohash.git From 3f04e183d4ea6217e55882e607040ac795e3ac05 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 3 Dec 2018 16:27:10 +0100 Subject: [PATCH 036/110] Regression from 7c1b8218201584dccc2506d06aa5dab0bf57daf3, switch back to latest official version, since build breaks with UnicodeDecodeError --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f23c634..aec5e22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,4 +34,4 @@ psycopg2 notsetuptools git+https://github.com/ofalk/monsterid.git git+https://github.com/azaghal/pydenticon.git -git+https://github.com/e1ven/Robohash.git +robohash From a3213de61f007a51bd8d7f2eadfeb1f1b53ff3cb Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 3 Dec 2018 18:49:49 +0100 Subject: [PATCH 037/110] Use my (fixed) version of robohash until upstream is fixed --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aec5e22..8483f73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,4 +34,4 @@ psycopg2 notsetuptools git+https://github.com/ofalk/monsterid.git git+https://github.com/azaghal/pydenticon.git -robohash +git+https://github.com/ofalk/Robohash.git@devel From 9bdc124333c495d8387e03ae7ddc9f419e37a8fc Mon Sep 17 00:00:00 2001 From: Niklas Poslovski Date: Tue, 4 Dec 2018 17:58:02 +0100 Subject: [PATCH 038/110] Improved theme select in preferences.html --- .../ivataraccount/templates/preferences.html | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ivatar/ivataraccount/templates/preferences.html b/ivatar/ivataraccount/templates/preferences.html index 78d3be5..6ae32f0 100644 --- a/ivatar/ivataraccount/templates/preferences.html +++ b/ivatar/ivataraccount/templates/preferences.html @@ -5,16 +5,34 @@ {% block title %}{% trans 'Your Preferences' %}{% endblock title %} {% block content %} +

{% trans 'Account settings' %}

{% csrf_token %}
- + +
{% endfor %} -
From 34e59c800057935f44f24860b1f999aa4748e746 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 5 Dec 2018 09:40:56 +0100 Subject: [PATCH 039/110] Update green/red theme --- ivatar/static/css/green.css | 13 ++++++++++++- ivatar/static/css/red.css | 12 ++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/ivatar/static/css/green.css b/ivatar/static/css/green.css index 1268223..c643f9c 100644 --- a/ivatar/static/css/green.css +++ b/ivatar/static/css/green.css @@ -1,4 +1,3 @@ -/// Example theme using tortin with bg-hero:@lab-green; body { font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; color: #525252; @@ -282,6 +281,18 @@ footer .logo .polygon { .navbar-tortin .navbar-form { border: 0; } +.dropdown-menu { + background-color: #3aa850; + border: 1px solid #2d823e; +} +.dropdown-menu > li > a { + color: #FFFFFF; +} +.dropdown-menu > li > a:focus, +.dropdown-menu > li > a:hover { + background-color: #2d823e; + color: #FFFFFF; +} @media (max-width: 767px) { .navbar-tortin .navbar-nav .open .dropdown-menu > li > a { color: #FFFFFF; diff --git a/ivatar/static/css/red.css b/ivatar/static/css/red.css index f912aa8..ddeed1e 100644 --- a/ivatar/static/css/red.css +++ b/ivatar/static/css/red.css @@ -281,6 +281,18 @@ footer .logo .polygon { .navbar-tortin .navbar-form { border: 0; } +.dropdown-menu { + background-color: #f7645e; + border: 1px solid #f5352d; +} +.dropdown-menu > li > a { + color: #FFFFFF; +} +.dropdown-menu > li > a:focus, +.dropdown-menu > li > a:hover { + background-color: #f5352d; + color: #FFFFFF; +} @media (max-width: 767px) { .navbar-tortin .navbar-nav .open .dropdown-menu > li > a { color: #FFFFFF; From 8521004841582d75f8932594481589c5c0219e99 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 5 Dec 2018 16:01:26 +0100 Subject: [PATCH 040/110] Add our own Profile view, so we can catch users logging in via OpenId and create the confirmed OpenId object accordingly; Fixes: Issue #9 --- ivatar/ivataraccount/views.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index afe8977..2390826 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -407,7 +407,7 @@ class RawImageView(DetailView): def get(self, request, *args, **kwargs): photo = self.model.objects.get(pk=kwargs['pk']) # pylint: disable=no-member - if not photo.user.id is request.user.id: + if not photo.user.id == request.user.id: return HttpResponseRedirect(reverse_lazy('home')) return HttpResponse( BytesIO(photo.data), content_type='image/%s' % photo.format) @@ -577,7 +577,7 @@ class RedirectOpenIDView(View): except UnicodeDecodeError as exc: # pragma: no cover msg = _('OpenID discovery failed (userid=%s) for %s: %s' % (request.user.id, user_url.encode('utf-8'), exc)) - print(msg) + print("message: %s" % msg) messages.error(request, msg) if auth_request is None: # pragma: no cover @@ -859,3 +859,32 @@ class IvatarLoginView(LoginView): if request.user.is_authenticated: return HttpResponseRedirect(reverse_lazy('profile')) return super().get(self, request, args, kwargs) + +@method_decorator(login_required, name='dispatch') +class ProfileView(TemplateView): + ''' + View class for profile + ''' + + template_name = 'profile.html' + + def get(self, request, *args, **kwargs): + self._confirm_claimed_openid() + return super().get(self, request, args, kwargs) + + def _confirm_claimed_openid(self): + openids = self.request.user.useropenid_set.all() + # If there is only one OpenID, we eventually need to add it to the user account + if openids.count() == 1: + # Already confirmed, skip + if ConfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member + return + # For whatever reason, this is in unconfirmed state, skip + if UnconfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member + return + print('need to confirm: %s' % openids.first()) + confirmed = ConfirmedOpenId() + confirmed.user = self.request.user + confirmed.ip_address = get_client_ip(self.request)[0] + confirmed.openid = openids.first().claimed_id + confirmed.save() From 2010549b5240b49410718ab469be296c6646e975 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 5 Dec 2018 16:54:04 +0100 Subject: [PATCH 041/110] Need to actually use profileview... --- ivatar/ivataraccount/urls.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ivatar/ivataraccount/urls.py b/ivatar/ivataraccount/urls.py index 0370466..3e45013 100644 --- a/ivatar/ivataraccount/urls.py +++ b/ivatar/ivataraccount/urls.py @@ -11,6 +11,7 @@ from django.contrib.auth.views import PasswordResetView, PasswordResetDoneView,\ from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView from django.contrib.auth.decorators import login_required +from . views import ProfileView from . views import CreateView, PasswordSetView, AddEmailView from . views import RemoveUnconfirmedEmailView, ConfirmEmailView from . views import RemoveConfirmedEmailView, AssignPhotoEmailView @@ -62,9 +63,7 @@ urlpatterns = [ # pylint: disable=invalid-name path('delete/', login_required( TemplateView.as_view(template_name='delete.html') ), name='delete'), - path('profile/', login_required( - TemplateView.as_view(template_name='profile.html') - ), name='profile'), + path('profile/', ProfileView.as_view(), name='profile'), path('add_email/', AddEmailView.as_view(), name='add_email'), path('add_openid/', AddOpenIDView.as_view(), name='add_openid'), path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'), From 57a66a3009e11994ed8eb11eae7f1aac42c856ba Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 6 Dec 2018 16:47:40 +0100 Subject: [PATCH 042/110] Fix color (use theme color) for radio buttons (Issue #8) --- .../ivataraccount/templates/preferences.html | 1 - ivatar/static/css/green.css | 6 + ivatar/static/css/green.less | 2 + ivatar/static/css/red.css | 6 + ivatar/static/css/red.less | 2 + ivatar/static/css/tortin.css | 356 +++++++++++++++++- ivatar/static/css/tortin.less | 8 +- 7 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 ivatar/static/css/green.less create mode 100644 ivatar/static/css/red.less diff --git a/ivatar/ivataraccount/templates/preferences.html b/ivatar/ivataraccount/templates/preferences.html index 6ae32f0..40c8387 100644 --- a/ivatar/ivataraccount/templates/preferences.html +++ b/ivatar/ivataraccount/templates/preferences.html @@ -15,7 +15,6 @@ font-family: FontAwesome; display: inline-block; letter-spacing:5px; font-size:20px; -color:#36b7d7; vertical-align:middle; } input[type=radio] + label:before {content: "\f10c"} diff --git a/ivatar/static/css/green.css b/ivatar/static/css/green.css index c643f9c..4807cec 100644 --- a/ivatar/static/css/green.css +++ b/ivatar/static/css/green.css @@ -347,3 +347,9 @@ footer .logo .polygon { width: auto; height: 36px; } +.radio { + color: #3aa850; +} +input[type="radio"]:checked + label { + font-weight: bold; +} diff --git a/ivatar/static/css/green.less b/ivatar/static/css/green.less new file mode 100644 index 0000000..da1ee44 --- /dev/null +++ b/ivatar/static/css/green.less @@ -0,0 +1,2 @@ +@import 'tortin.less'; +@bg-hero:@lab-green; diff --git a/ivatar/static/css/red.css b/ivatar/static/css/red.css index ddeed1e..8e2b8a5 100644 --- a/ivatar/static/css/red.css +++ b/ivatar/static/css/red.css @@ -347,3 +347,9 @@ footer .logo .polygon { width: auto; height: 36px; } +.radio { + color: #f7645e; +} +input[type="radio"]:checked + label { + font-weight: bold; +} diff --git a/ivatar/static/css/red.less b/ivatar/static/css/red.less new file mode 100644 index 0000000..ab580e4 --- /dev/null +++ b/ivatar/static/css/red.less @@ -0,0 +1,2 @@ +@import 'tortin.less'; +@bg-hero:@lab-red; diff --git a/ivatar/static/css/tortin.css b/ivatar/static/css/tortin.css index 1c55bdb..9aa0504 100644 --- a/ivatar/static/css/tortin.css +++ b/ivatar/static/css/tortin.css @@ -1 +1,355 @@ -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 +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; +} +.radio { + color: #36b7d7; +} +input[type="radio"]:checked + label { + font-weight: bold; +} diff --git a/ivatar/static/css/tortin.less b/ivatar/static/css/tortin.less index 214b08d..8f3eaa1 100644 --- a/ivatar/static/css/tortin.less +++ b/ivatar/static/css/tortin.less @@ -321,4 +321,10 @@ border-color: lighten(@bg-hero, 10%); background: none; width:auto; height:36px; -} \ No newline at end of file +} +.radio { +color: @bg-hero; +} +input[type="radio"]:checked+label { +font-weight: bold; +} From e0e33251a6a73648d10a36ba8a264b05e3d3aacd Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 6 Dec 2018 16:48:07 +0100 Subject: [PATCH 043/110] Make sure user has preferences --- ivatar/ivataraccount/views.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index 2390826..6cff83c 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -9,6 +9,7 @@ import binascii from PIL import Image from django.db.models import ProtectedError +from django.core.exceptions import ObjectDoesNotExist from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.utils.decorators import method_decorator @@ -723,8 +724,13 @@ class UserPreferenceView(FormView, UpdateView): 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() + userpref = None + try: + userpref = self.request.user.userpreference + except ObjectDoesNotExist: + userpref = UserPreference(user=self.request.user) + userpref.theme = request.POST['theme'] + userpref.save() return HttpResponseRedirect(reverse_lazy('user_preference')) From 18cde64e19a6bba15b78e71ff91324770aaee3cf Mon Sep 17 00:00:00 2001 From: Niklas Poslovski Date: Fri, 7 Dec 2018 17:34:02 +0100 Subject: [PATCH 044/110] Removed

{% trans 'Upload a new photo' %}

{% csrf_token %} From 6abdd8eefea3719fc80df845365fdfa42514ac86 Mon Sep 17 00:00:00 2001 From: Niklas Poslovski Date: Fri, 7 Dec 2018 17:35:58 +0100 Subject: [PATCH 045/110] Removed

{% trans 'Upload an export from libravatar' %}

From e91c35bab96b9f2b6e925eef3a941155b96aae83 Mon Sep 17 00:00:00 2001 From: Niklas Poslovski Date: Fri, 7 Dec 2018 17:39:47 +0100 Subject: [PATCH 046/110] Removed

{% trans 'Account settings' %}

From e0cfed1c5cd1df3f6a3cebb692f014aead4a963d Mon Sep 17 00:00:00 2001 From: Niklas Poslovski Date: Fri, 7 Dec 2018 17:46:06 +0100 Subject: [PATCH 047/110] Removed

{% trans 'Crop photo' %}

From aabeda903e83153de15b1af48d05538c1f9cd1e1 Mon Sep 17 00:00:00 2001 From: Niklas Poslovski Date: Fri, 7 Dec 2018 18:02:35 +0100 Subject: [PATCH 048/110] Removed