diff --git a/config.py b/config.py index 93fdab6..e35ebb4 100644 --- a/config.py +++ b/config.py @@ -49,11 +49,11 @@ TEMPLATES[0]['OPTIONS']['context_processors'].append( OPENID_CREATE_USERS = True OPENID_UPDATE_DETAILS_FROM_SREG = True -SITE_NAME = 'ivatar' +SITE_NAME = os.environ.get('SITE_NAME', 'ivatar') IVATAR_VERSION = '0.1' -SECURE_BASE_URL = 'https://avatars.linux-kernel.at/avatar/' -BASE_URL = 'http://avatars.linux-kernel.at/avatar/' +SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/') +BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/') LOGIN_REDIRECT_URL = reverse_lazy('profile') MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 @@ -96,12 +96,17 @@ BOOTSTRAP4 = { }, } -if 'test' not in sys.argv and 'collectstatic' not in sys.argv: - ANYMAIL = { # pragma: no cover - 'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'], - 'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'], - } - EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover +if 'EMAIL_BACKEND' in os.environ: + EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] +else: + if 'test' in sys.argv or 'collectstatic' in sys.argv: + EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + else: + ANYMAIL = { # pragma: no cover + 'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'], + 'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'], + } + EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover DEFAULT_FROM_EMAIL = 'ivatar@mg.linux-kernel.at' try: @@ -140,3 +145,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 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/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), 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 304a858..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( @@ -135,7 +136,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/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
diff --git a/ivatar/ivataraccount/templates/choose_libravatar_export.html b/ivatar/ivataraccount/templates/choose_libravatar_export.html index 9767377..3d1b7e1 100644 --- a/ivatar/ivataraccount/templates/choose_libravatar_export.html +++ b/ivatar/ivataraccount/templates/choose_libravatar_export.html @@ -7,20 +7,6 @@ {% block content %}

{% trans 'Crop photo' %}

diff --git a/ivatar/ivataraccount/templates/preferences.html b/ivatar/ivataraccount/templates/preferences.html index e391abf..43469f0 100644 --- a/ivatar/ivataraccount/templates/preferences.html +++ b/ivatar/ivataraccount/templates/preferences.html @@ -7,11 +7,23 @@ {% block content %}

{% trans 'Account settings' %}

-{% if has_password %} -

{% trans 'Change your password' %}

-{% else %} -

{% trans 'Set a password' %}

-{% endif %} +

+

{% csrf_token %} +
+ + {% for theme in THEMES %} +
+ + +
+ {% endfor %} +
+ +
+
+

+ +
diff --git a/ivatar/ivataraccount/templates/upload_libravatar_export.html b/ivatar/ivataraccount/templates/upload_libravatar_export.html index c75a1ab..1886a5c 100644 --- a/ivatar/ivataraccount/templates/upload_libravatar_export.html +++ b/ivatar/ivataraccount/templates/upload_libravatar_export.html @@ -5,30 +5,6 @@ {% block title %}{% trans 'Upload an export from libravatar' %} - ivatar{% endblock title %} {% block content %} -

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

diff --git a/ivatar/ivataraccount/templates/upload_photo.html b/ivatar/ivataraccount/templates/upload_photo.html index c4a6e8b..4eb3e7b 100644 --- a/ivatar/ivataraccount/templates/upload_photo.html +++ b/ivatar/ivataraccount/templates/upload_photo.html @@ -8,30 +8,6 @@ {% endblock header %} {% block content %} -

{% trans 'Upload a new photo' %}

{% csrf_token %} 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/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'), diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index 9cbe02a..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 @@ -32,7 +33,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 @@ -117,8 +118,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 +311,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) @@ -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: @@ -350,18 +351,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( @@ -415,7 +408,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) @@ -436,7 +429,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')) @@ -519,7 +512,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')) @@ -543,9 +536,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 @@ -567,7 +560,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')) @@ -585,7 +578,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 @@ -621,10 +614,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')) @@ -705,7 +700,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: @@ -728,8 +723,26 @@ class UserPreferenceView(FormView, UpdateView): form_class = UpdatePreferenceForm success_url = reverse_lazy('user_preference') + def post(self, request, *args, **kwargs): # pylint: disable=unused-argument + 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')) + + + 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') @@ -852,3 +865,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() diff --git a/ivatar/static/css/green.css b/ivatar/static/css/green.css new file mode 100644 index 0000000..b826a98 --- /dev/null +++ b/ivatar/static/css/green.css @@ -0,0 +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: #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;}.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;}.checkbox input, .radio input {display: none;}.checkbox input + label, .radio input + label {padding-left: 0;}.checkbox input + label:before, .radio input + label:before {font-family: FontAwesome;display: inline-block;letter-spacing: 5px;font-size: 20px;color: #3aa850;vertical-align: middle;}.checkbox input + label:before {content: "\f0c8";}.checkbox input:checked + label:before {content: "\f14a";}.radio input + label:before {content: "\f10c";}.radio input:checked + label:before {content: "\f192";}.uploadbtn:before {position: absolute;left: 0;right: 0;text-align: center;content: "Select file";font-family: 'Montserrat', sans-serif;}.jcrop-holder > div > div:nth-child(1) {outline-width: 2px;outline-style: solid;outline-color: #3aa850;}@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;} \ No newline at end of file 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 new file mode 100644 index 0000000..0bb7e22 --- /dev/null +++ b/ivatar/static/css/red.css @@ -0,0 +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: #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;}.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;}.checkbox input, .radio input {display: none;}.checkbox input + label, .radio input + label {padding-left: 0;}.checkbox input + label:before, .radio input + label:before {font-family: FontAwesome;display: inline-block;letter-spacing: 5px;font-size: 20px;color: #f7645e;vertical-align: middle;}.checkbox input + label:before {content: "\f0c8";}.checkbox input:checked + label:before {content: "\f14a";}.radio input + label:before {content: "\f10c";}.radio input:checked + label:before {content: "\f192";}.uploadbtn:before {position: absolute;left: 0;right: 0;text-align: center;content: "Select file";font-family: 'Montserrat', sans-serif;}.jcrop-holder > div > div:nth-child(1) {outline-width: 2px;outline-style: solid;outline-color: #f7645e;}@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;} \ No newline at end of file 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 3032ded..d5d35ab 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;}.checkbox input, .radio input {display: none;}.checkbox input + label, .radio input + label {padding-left: 0;}.checkbox input + label:before, .radio input + label:before {font-family: FontAwesome;display: inline-block;letter-spacing: 5px;font-size: 20px;color: #36b7d7;vertical-align: middle;}.checkbox input + label:before {content: "\f0c8";}.checkbox input:checked + label:before {content: "\f14a";}.radio input + label:before {content: "\f10c";}.radio input:checked + label:before {content: "\f192";}.uploadbtn:before {position: absolute;left: 0;right: 0;text-align: center;content: "Select file";font-family: 'Montserrat', sans-serif;}.jcrop-holder > div > div:nth-child(1) {outline-width: 2px;outline-style: solid;outline-color: #36b7d7;}@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 diff --git a/ivatar/static/css/tortin.less b/ivatar/static/css/tortin.less index 87f9a53..e215c63 100644 --- a/ivatar/static/css/tortin.less +++ b/ivatar/static/css/tortin.less @@ -258,6 +258,46 @@ 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; +} +.checkbox input, .radio input {display:none} +.checkbox input + label, .radio input + label { +padding-left:0; +} +.checkbox input + label:before, .radio input + label:before { +font-family: FontAwesome; +display: inline-block; +letter-spacing:5px; +font-size:20px; +color:@bg-hero; +vertical-align:middle; +} +.checkbox input + label:before {content: "\f0c8"} +.checkbox input:checked + label:before {content: "\f14a"} +.radio input + label:before {content: "\f10c"} +.radio input:checked + label:before {content: "\f192"} +.uploadbtn:before { +position:absolute; +left:0; +right:0; +text-align:center; +content:"Select file"; +font-family: 'Montserrat', sans-serif; +} +.jcrop-holder > div > div:nth-child(1) { +outline-width:2px; +outline-style:solid; +outline-color:@bg-hero; +} @media (max-width:767px) { .navbar-tortin .navbar-nav .open .dropdown-menu > li > a { color:#FFFFFF @@ -311,3 +351,9 @@ background: none; width:auto; height:36px; } +.radio { +color: @bg-hero; +} +input[type="radio"]:checked+label { +font-weight: bold; +} diff --git a/ivatar/urls.py b/ivatar/urls.py index f875a5c..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), @@ -20,6 +20,16 @@ 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( + 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..6629e93 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -4,18 +4,50 @@ views under / from io import BytesIO from os import path import hashlib -from PIL import Image -from django.views.generic.base import TemplateView +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 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 +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): + ''' + Get size from the URL arguments + ''' + 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): @@ -24,16 +56,18 @@ 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 ''' 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 +81,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']) @@ -82,10 +95,29 @@ 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: + 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): @@ -100,6 +132,19 @@ class AvatarImageView(TemplateView): data, 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=roboset, 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 = [ @@ -134,8 +179,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): @@ -158,3 +202,51 @@ 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,no-self-use,unused-argument + ''' + 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) + except URLError as exc: + print( + 'Gravatar fetch failed with URL error: %s' % + exc.reason) + except SSLError as exc: + print( + 'Gravatar fetch failed with SSL error: %s' % + exc.reason) + 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) + + # 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) diff --git a/requirements.txt b/requirements.txt index cfd7b9e..8483f73 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 +git+https://github.com/ofalk/Robohash.git@devel diff --git a/templates/_account_bar.html b/templates/_account_bar.html index b44b4ef..c58edf8 100644 --- a/templates/_account_bar.html +++ b/templates/_account_bar.html @@ -11,24 +11,22 @@ {% if user.is_staff %}
  • - {% trans 'Admin' %} + {% trans 'Admin' %}
  • {% endif %} diff --git a/templates/base.html b/templates/base.html index 3eb96a7..f31128f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,67 +6,7 @@ {% spaceless %}
    {% autoescape off %}{% endautoescape %} @@ -89,4 +29,3 @@ -{{ settings }} diff --git a/templates/base_home.html b/templates/base_home.html index 4f39189..66b1220 100644 --- a/templates/base_home.html +++ b/templates/base_home.html @@ -2,18 +2,16 @@ {% load i18n %} {% include 'header.html' %} iVatar :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %} +{% if not user.is_anonymous %} +{% include 'navbar.html' %} +{% endif %} {% spaceless %}
    - - {% autoescape off %}{% endautoescape %} - - {% block content %}{% endblock content %} - + {% block content %}{% endblock content %} {% block footer %}{% include 'footer.html' %}{% endblock footer %}
    {% endspaceless %} -{{ settings }} 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 %}

    diff --git a/templates/header.html b/templates/header.html index 23ce10e..5015f04 100644 --- a/templates/header.html +++ b/templates/header.html @@ -25,7 +25,9 @@ {% if user.is_authenticated %} {% if user.userpreference and user.userpreference.theme != 'default' %} - + {% with 'css/'|add:user.userpreference.theme|add:'.css' as theme_css %} + + {% endwith %} {% endif %} {% endif %} diff --git a/templates/home.html b/templates/home.html index 6d8a419..c3acfee 100644 --- a/templates/home.html +++ b/templates/home.html @@ -43,8 +43,8 @@

    Useful links

    {% trans 'Contact us' %}
    {% trans 'Security' %}
    -{% trans 'Source code' %}
    -{% trans 'Report bugs' %}
    +{% trans 'Source code' %}
    +{% trans 'Report bugs' %}
    {% trans 'Questions' %}
    {% trans 'Wiki' %}
    {% trans 'Blog' %}
    diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 0000000..f3dc4ef --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,60 @@ +{% load i18n %} +{% block topbar_base %} + +{% endblock %}