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 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/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/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/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 9cbe02a..afe8977 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 @@ -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) @@ -327,6 +326,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 +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( @@ -436,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')) @@ -519,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')) @@ -543,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 @@ -567,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')) @@ -621,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')) @@ -705,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: @@ -728,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/green.css b/ivatar/static/css/green.css new file mode 100644 index 0000000..1268223 --- /dev/null +++ b/ivatar/static/css/green.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/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; +} 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 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 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) 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..a1f785d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -54,7 +54,7 @@ @@ -89,4 +89,3 @@ -{{ settings }} diff --git a/templates/base_home.html b/templates/base_home.html index 4f39189..dab4f52 100644 --- a/templates/base_home.html +++ b/templates/base_home.html @@ -16,4 +16,3 @@ -{{ settings }}