Merge branch 'master' of git.linux-kernel.at:oliver/ivatar

This commit is contained in:
Oliver Falk
2018-12-03 16:21:34 +01:00
17 changed files with 989 additions and 87 deletions

View File

@@ -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

View File

@@ -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'
}

View File

@@ -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),
),
]

View File

@@ -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

View File

@@ -25,7 +25,7 @@
</h3></div>
<div class="panel-body">
<center>
<img src="{{ photo.thumbnail_url }}" alt="{{ photo.service_name }} image">
<img src="{{ photo.thumbnail_url }}" style="max-width: 80px; max-height: 80px;" alt="{{ photo.service_name }} image">
</center>
</div>
</div>

View File

@@ -7,11 +7,21 @@
{% block content %}
<h1>{% trans 'Account settings' %}</h1>
{% if has_password %}
<p><a href="{% url 'password_change' %}" class="btn btn-default">{% trans 'Change your password' %}</a></p>
{% else %}
<p><a href="{% url 'password_set' %}" class="btn btn-default">{% trans 'Set a password' %}</a></p>
{% endif %}
<p>
<form method="post" action="{% url 'user_preference' %}">{% csrf_token %}
<div class="form-group">
<select name='theme' class="form-control" style="width:200px;">
{% for theme in THEMES %}
<option {% if user.userpreference.theme == theme.0 %}selected{% endif %} value="{{ theme.0 }}">{{ theme.1 }}</option>
{% endfor %}
</select>
<br/>
<button type="submit" class="btn btn-default">{% trans 'Save' %}</button>
</div>
</form>
</p>
<div style="height:40px"></div>
<!-- <p><a href="{% url 'export' %}" class="btn btn-default">{% trans 'Export your data' %}</a></p> -->

View File

@@ -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,

View File

@@ -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')

338
ivatar/static/css/green.css Normal file
View File

@@ -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;
}

337
ivatar/static/css/red.css Normal file
View File

@@ -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;
}

File diff suppressed because one or more lines are too long

View File

@@ -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;
}
}

View File

@@ -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<digest>\w{32})',
AvatarImageView.as_view(), name='avatar_view'),
url(
r'avatar/(?P<digest>\w)',
r'avatar/(?P<digest>\w*)',
TemplateView.as_view(
template_name='error.html',
extra_context={
'errormessage': 'Incorrect digest length',
})),
url(
r'gravatarproxy/(?P<digest>\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'),

View File

@@ -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)

View File

@@ -11,24 +11,22 @@
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
{% if request.user.is_authenticated %}
<li><a href="{% url 'profile' %}"><i class="fa fa-image" aria-hidden="true"></i> {% trans 'Profile' %}</a></li>
<!--
<li><a href="{% url 'user_preference' %}"><i class="fa fa-cog" aria-hidden="true"></i> {% trans 'Preferences' %}</a></li>
-->
<li><a href="{% url 'import_photo' %}"><i class="fa fa-envelope-square" aria-hidden="true"></i> {% trans 'Import photo via mail address' %}</a></li>
<li><a href="{% url 'upload_export' %}"><i class="fa fa-file-archive-o" aria-hidden="true"></i> {% trans 'Import libravatar XML export' %}</a></li>
<li><a href="{% url 'password_change' %}"><i class="fa fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
<li><a href="{% url 'password_reset' %}"><i class="fa fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>
<li><a href="{% url 'profile' %}"><i class="fa fa-fw fa-image" aria-hidden="true"></i> {% trans 'Profile' %}</a></li>
<li><a href="{% url 'user_preference' %}"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> {% trans 'Preferences' %}</a></li>
<li><a href="{% url 'import_photo' %}"><i class="fa fa-fw fa-envelope-square" aria-hidden="true"></i> {% trans 'Import photo via mail address' %}</a></li>
<li><a href="{% url 'upload_export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Import libravatar XML export' %}</a></li>
<li><a href="{% url 'password_change' %}"><i class="fa fa-fw fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
<li><a href="{% url 'password_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>
{% else %}
<li><a href="{% url 'login' %}"><i class="fa fa-sign-in" aria-hidden="true"></i> {% trans 'Local' %}</a></li>
<li><a href="{% url 'new_account' %}"><i class="fa fa-user-plus" aria-hidden="true"></i> {% trans 'Create account' %}</a></li>
<li><a href="{% url 'login' %}"><i class="fa fa-fw fa-sign-in" aria-hidden="true"></i> {% trans 'Local' %}</a></li>
<li><a href="{% url 'new_account' %}"><i class="fa fa-fw fa-user-plus" aria-hidden="true"></i> {% trans 'Create account' %}</a></li>
{% endif %}
</ul>
</li>
{% if user.is_staff %}
<li>
<a href="{% url 'admin:index' %}" target="_new"><i class="fa fa-user-secret" aria-hidden="true"></i> {% trans 'Admin' %}</a>
<a href="{% url 'admin:index' %}" target="_new"><i class="fa fa-fw fa-user-secret" aria-hidden="true"></i> {% trans 'Admin' %}</a>
</li>
{% endif %}
</ul>

View File

@@ -54,7 +54,7 @@
</a>
<ul class="dropdown-menu" aria-labelledby="tools_dropdown">
<li><a id="tools-check" href="{% url 'tools_check' %}">
<i class="fa fa-check-square" aria-hidden="true"></i> {% trans 'Check' %}
<i class="fa fa-fw fa-check-square" aria-hidden="true"></i> {% trans 'Check' %}
</a></li>
</ul>
</li>
@@ -89,4 +89,3 @@
<script src="{% static '/js/bootstrap.min.js' %}"></script>
<script src="{% static '/js/ivatar.js' %}"></script>
{{ settings }}

View File

@@ -16,4 +16,3 @@
<script src="{% static '/js/bootstrap.min.js' %}"></script>
<script src="{% static '/js/ivatar.js' %}"></script>
{{ settings }}