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 @@
{% trans 'Change your password' %}
-{% else %} - -{% endif %} ++
+ + + 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