Merge branch 'master' into libravatar_export

This commit is contained in:
Oliver Falk
2019-01-08 10:06:43 +01:00
30 changed files with 538 additions and 249 deletions

View File

@@ -49,11 +49,11 @@ TEMPLATES[0]['OPTIONS']['context_processors'].append(
OPENID_CREATE_USERS = True OPENID_CREATE_USERS = True
OPENID_UPDATE_DETAILS_FROM_SREG = True OPENID_UPDATE_DETAILS_FROM_SREG = True
SITE_NAME = 'ivatar' SITE_NAME = os.environ.get('SITE_NAME', 'ivatar')
IVATAR_VERSION = '0.1' IVATAR_VERSION = '0.1'
SECURE_BASE_URL = 'https://avatars.linux-kernel.at/avatar/' SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/')
BASE_URL = 'http://avatars.linux-kernel.at/avatar/' BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/')
LOGIN_REDIRECT_URL = reverse_lazy('profile') LOGIN_REDIRECT_URL = reverse_lazy('profile')
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
@@ -96,12 +96,17 @@ BOOTSTRAP4 = {
}, },
} }
if 'test' not in sys.argv and 'collectstatic' not in sys.argv: if 'EMAIL_BACKEND' in os.environ:
ANYMAIL = { # pragma: no cover EMAIL_BACKEND = os.environ['EMAIL_BACKEND']
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'], else:
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'], if 'test' in sys.argv or 'collectstatic' in sys.argv:
} EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover else:
ANYMAIL = { # pragma: no cover
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
}
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
DEFAULT_FROM_EMAIL = 'ivatar@mg.linux-kernel.at' DEFAULT_FROM_EMAIL = 'ivatar@mg.linux-kernel.at'
try: try:
@@ -140,3 +145,5 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
USE_X_FORWARDED_HOST = True USE_X_FORWARDED_HOST = True
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',] 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 from urllib.request import urlopen, HTTPError, URLError
import hashlib import hashlib
from .. settings import AVATAR_MAX_SIZE
URL_TIMEOUT = 5 # in seconds URL_TIMEOUT = 5 # in seconds
@@ -15,7 +17,7 @@ def get_photo(email):
hash_object = hashlib.new('md5') hash_object = hashlib.new('md5')
hash_object.update(email.lower().encode('utf-8')) hash_object.update(email.lower().encode('utf-8'))
thumbnail_url = 'https://secure.gravatar.com/avatar/' + \ 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( image_url = 'https://secure.gravatar.com/avatar/' + hash_object.hexdigest(
) + '?s=512&d=404' ) + '?s=512&d=404'
@@ -44,8 +46,8 @@ def get_photo(email):
return { return {
'thumbnail_url': thumbnail_url, 'thumbnail_url': thumbnail_url,
'image_url': image_url, 'image_url': image_url,
'width': 80, 'width': AVATAR_MAX_SIZE,
'height': 80, 'height': AVATAR_MAX_SIZE,
'service_url': service_url, 'service_url': service_url,
'service_name': 'Gravatar' 'service_name': 'Gravatar'
} }

View File

@@ -1,3 +1,4 @@
# pylint: disable=invalid-name,missing-docstring
# Generated by Django 2.0.6 on 2018-07-04 12:32 # Generated by Django 2.0.6 on 2018-07-04 12:32
from django.conf import settings from django.conf import settings
@@ -5,18 +6,18 @@ from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
def add_preference_to_user(apps, schema_editor): def add_preference_to_user(apps, schema_editor): # pylint: disable=unused-argument
''' '''
Make sure all users have preferences set up Make sure all users have preferences set up
''' '''
from django.contrib.auth.models import User from django.contrib.auth.models import User
UserPreference = apps.get_model('ivataraccount', 'UserPreference') UserPreference = apps.get_model('ivataraccount', 'UserPreference') # pylint: disable=invalid-name
for u in User.objects.filter(userpreference=None): for user in User.objects.filter(userpreference=None):
p = UserPreference.objects.create(user_id=u.pk) pref = UserPreference.objects.create(user_id=user.pk) # pragma: no cover
p.save() pref.save() # pragma: no cover
class Migration(migrations.Migration): class Migration(migrations.Migration): # pylint: disable=missing-docstring
dependencies = [ dependencies = [
('auth', '0009_alter_user_last_name_max_length'), ('auth', '0009_alter_user_last_name_max_length'),
@@ -27,8 +28,16 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='UserPreference', name='UserPreference',
fields=[ fields=[
('theme', models.CharField(choices=[('default', 'Default theme'), ('clime', 'Climes theme')], default='default', max_length=10)), ('theme', models.CharField(
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), choices=[
('default', 'Default theme'),
('clime', 'Climes theme')],
default='default', max_length=10)),
('user', models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL)),
], ],
), ),
migrations.RunPython(add_preference_to_user), migrations.RunPython(add_preference_to_user),

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 = ( THEMES = (
('default', 'Default theme'), ('default', 'Default theme'),
('clime', 'climes theme'), ('clime', 'climes theme'),
('falko', 'falkos theme'), ('green', 'green theme'),
('red', 'red theme'),
) )
theme = models.CharField( theme = models.CharField(
@@ -135,7 +136,7 @@ class Photo(BaseAccountModel):
image_url = gravatar['image_url'] image_url = gravatar['image_url']
if service_name == 'Libravatar': if service_name == 'Libravatar':
image_url = libravatar_url(email_address) image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE)
if not image_url: if not image_url:
return False # pragma: no cover return False # pragma: no cover

View File

@@ -25,7 +25,7 @@
</h3></div> </h3></div>
<div class="panel-body"> <div class="panel-body">
<center> <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> </center>
</div> </div>
</div> </div>

View File

@@ -7,20 +7,6 @@
{% block content %} {% block content %}
<style> <style>
input[type=checkbox] {display:none} input[type=checkbox] {display:none}
input[type=checkbox].text + label {
padding-left:0;
font-weight:normal;
}
input[type=checkbox].text + label:before {
font-family: FontAwesome;
display: inline-block;
letter-spacing:5px;
font-size:20px;
color:#36b7d7;
vertical-align:middle;
}
input[type=checkbox].text + label:before {content: "\f0c8"}
input[type=checkbox].text:checked + label:before {content: "\f14a"}
input[type=checkbox].image + label:before { input[type=checkbox].image + label:before {
font-family: FontAwesome; font-family: FontAwesome;
display: inline-block; display: inline-block;
@@ -36,7 +22,9 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}
{% if emails %} {% if emails %}
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4> <h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
{% for email in emails %} {% for email in emails %}
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email }}</label><br/> <div class="checkbox">
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email }}</label>
</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if photos %} {% if photos %}

View File

@@ -8,11 +8,7 @@
{% block content %} {% block content %}
<style> <style>
.jcrop-holder > div > div:nth-child(1) {
outline-width:2px;
outline-style:solid;
outline-color:#36b7d7;
}
</style> </style>
<h1>{% trans 'Crop photo' %}</h1> <h1>{% trans 'Crop photo' %}</h1>

View File

@@ -7,11 +7,23 @@
{% block content %} {% block content %}
<h1>{% trans 'Account settings' %}</h1> <h1>{% trans 'Account settings' %}</h1>
{% if has_password %} <p>
<p><a href="{% url 'password_change' %}" class="btn btn-default">{% trans 'Change your password' %}</a></p> <form method="post" action="{% url 'user_preference' %}">{% csrf_token %}
{% else %} <div class="form-group">
<p><a href="{% url 'password_set' %}" class="btn btn-default">{% trans 'Set a password' %}</a></p>
{% endif %} {% for theme in THEMES %}
<div class="radio">
<input type="radio" name="theme" value="{{ theme.0 }}" id="theme-{{ theme.0 }}" {% if user.userpreference.theme == theme.0 %}checked{% endif %}>
<label for="theme-{{ theme.0 }}">{{ theme.1 }}</label>
</div>
{% endfor %}
<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> --> <!-- <p><a href="{% url 'export' %}" class="btn btn-default">{% trans 'Export your data' %}</a></p> -->

View File

@@ -5,30 +5,6 @@
{% block title %}{% trans 'Upload an export from libravatar' %} - ivatar{% endblock title %} {% block title %}{% trans 'Upload an export from libravatar' %} - ivatar{% endblock title %}
{% block content %} {% block content %}
<style>
input[type=checkbox] {display:none}
input[type=checkbox] + label {
padding-left:0;
}
input[type=checkbox] + label:before {
font-family: FontAwesome;
display: inline-block;
letter-spacing:5px;
font-size:20px;
color:#36b7d7;
vertical-align:middle;
}
input[type=checkbox] + label:before {content: "\f0c8"}
input[type=checkbox]:checked + label:before {content: "\f14a"}
.uploadbtn:before {
position:absolute;
left:0;
right:0;
text-align:center;
content:"Select file";
font-family: 'Montserrat', sans-serif;
}
</style>
<h1>{% trans 'Upload an export from libravatar' %}</h1> <h1>{% trans 'Upload an export from libravatar' %}</h1>
<form enctype="multipart/form-data" method="post"> <form enctype="multipart/form-data" method="post">

View File

@@ -8,30 +8,6 @@
<link rel="prefetch" href="{% static '/js/jcrop.js' %}">{% endblock header %} <link rel="prefetch" href="{% static '/js/jcrop.js' %}">{% endblock header %}
{% block content %} {% block content %}
<style>
input[type=checkbox] {display:none}
input[type=checkbox] + label {
padding-left:0;
}
input[type=checkbox] + label:before {
font-family: FontAwesome;
display: inline-block;
letter-spacing:5px;
font-size:20px;
color:#36b7d7;
vertical-align:middle;
}
input[type=checkbox] + label:before {content: "\f0c8"}
input[type=checkbox]:checked + label:before {content: "\f14a"}
.uploadbtn:before {
position:absolute;
left:0;
right:0;
text-align:center;
content:"Select file";
font-family: 'Montserrat', sans-serif;
}
</style>
<h1>{% trans 'Upload a new photo' %}</h1> <h1>{% trans 'Upload a new photo' %}</h1>
<form enctype="multipart/form-data" action="{% url 'upload_photo' %}" method="post">{% csrf_token %} <form enctype="multipart/form-data" action="{% url 'upload_photo' %}" method="post">{% csrf_token %}

View File

@@ -12,6 +12,7 @@ from django.test import Client
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
import hashlib
from libravatar import libravatar_url 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?') 'Why is this not the correct size?')
def test_avatar_url_inexisting_mail_digest(self): # pylint: disable=invalid-name 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 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 # Simply delete it, then it digest is 'correct', but
# the hash is no longer there # the hash is no longer there
self.user.confirmedemail_set.first().delete() 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) response = self.client.get(url, follow=True)
self.assertRedirects( self.assertRedirects(
response=response, response=response,
@@ -1127,6 +1156,25 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
) )
url = '%s?%s' % (urlobj.path, urlobj.query) url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) 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( self.assertRedirects(
response=response, response=response,
expected_url='/static/img/mm/80.png', 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) url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) 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( self.assertRedirects(
response=response, response=response,
expected_url='/static/img/nobody/80.png', 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) url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) 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( self.assertRedirects(
response=response, response=response,
expected_url='/static/img/nobody.png', 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) url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=False) 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( self.assertRedirects(
response=response, response=response,
expected_url=default, expected_url=default,

View File

@@ -11,6 +11,7 @@ from django.contrib.auth.views import PasswordResetView, PasswordResetDoneView,\
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from . views import ProfileView
from . views import CreateView, PasswordSetView, AddEmailView from . views import CreateView, PasswordSetView, AddEmailView
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView from . views import RemoveUnconfirmedEmailView, ConfirmEmailView
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
@@ -62,9 +63,7 @@ urlpatterns = [ # pylint: disable=invalid-name
path('delete/', login_required( path('delete/', login_required(
TemplateView.as_view(template_name='delete.html') TemplateView.as_view(template_name='delete.html')
), name='delete'), ), name='delete'),
path('profile/', login_required( path('profile/', ProfileView.as_view(), name='profile'),
TemplateView.as_view(template_name='profile.html')
), name='profile'),
path('add_email/', AddEmailView.as_view(), name='add_email'), path('add_email/', AddEmailView.as_view(), name='add_email'),
path('add_openid/', AddOpenIDView.as_view(), name='add_openid'), path('add_openid/', AddOpenIDView.as_view(), name='add_openid'),
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'), path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'),

View File

@@ -9,6 +9,7 @@ import binascii
from PIL import Image from PIL import Image
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@@ -32,7 +33,7 @@ from openid.consumer import consumer
from ipware import get_client_ip from ipware import get_client_ip
from libravatar import libravatar_url 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 .gravatar import get_photo as get_gravatar_photo
from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm
@@ -117,8 +118,8 @@ class AddEmailView(SuccessMessageMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
if not form.save(self.request): if not form.save(self.request):
return render(self.request, self.template_name, {'form': form}) 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) return super().form_valid(form)
@@ -310,14 +311,13 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
if 'email_id' in kwargs: if 'email_id' in kwargs:
try: try:
addr = ConfirmedEmail.objects.get(pk=kwargs['email_id']).email addr = ConfirmedEmail.objects.get(pk=kwargs['email_id']).email
except ConfirmedEmail.ObjectDoesNotExist: except ConfirmedEmail.ObjectDoesNotExist: # pylint: disable=no-member
messages.error( messages.error(
self.request, self.request,
_('Address does not exist')) _('Address does not exist'))
return context return context
if 'email_addr' in kwargs: addr = kwargs.get('email_addr', None)
addr = kwargs['email_addr']
if addr: if addr:
gravatar = get_gravatar_photo(addr) gravatar = get_gravatar_photo(addr)
@@ -327,6 +327,7 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
libravatar_service_url = libravatar_url( libravatar_service_url = libravatar_url(
email=addr, email=addr,
default=404, default=404,
size=AVATAR_MAX_SIZE,
) )
if libravatar_service_url: if libravatar_service_url:
try: try:
@@ -350,18 +351,10 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
Handle post to photo import Handle post to photo import
''' '''
addr = None
email_id = None
imported = None imported = None
if 'email_id' in kwargs: email_id = kwargs.get('email_id', request.POST.get('email_id', None))
email_id = kwargs['email_id'] addr = kwargs.get('emali_addr', request.POST.get('email_addr', None))
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']
if email_id: if email_id:
email = ConfirmedEmail.objects.filter( email = ConfirmedEmail.objects.filter(
@@ -415,7 +408,7 @@ class RawImageView(DetailView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
photo = self.model.objects.get(pk=kwargs['pk']) # pylint: disable=no-member photo = self.model.objects.get(pk=kwargs['pk']) # pylint: disable=no-member
if not photo.user.id is request.user.id: if not photo.user.id == request.user.id:
return HttpResponseRedirect(reverse_lazy('home')) return HttpResponseRedirect(reverse_lazy('home'))
return HttpResponse( return HttpResponse(
BytesIO(photo.data), content_type='image/%s' % photo.format) BytesIO(photo.data), content_type='image/%s' % photo.format)
@@ -436,7 +429,7 @@ class DeletePhotoView(SuccessMessageMixin, View):
photo = self.model.objects.get( # pylint: disable=no-member photo = self.model.objects.get( # pylint: disable=no-member
pk=kwargs['pk'], user=request.user) pk=kwargs['pk'], user=request.user)
photo.delete() photo.delete()
except (self.model.DoesNotExist, ProtectedError): except (self.model.DoesNotExist, ProtectedError): # pylint: disable=no-member
messages.error( messages.error(
request, request,
_('No such image or no permission to delete it')) _('No such image or no permission to delete it'))
@@ -519,7 +512,7 @@ class RemoveUnconfirmedOpenIDView(View):
user=request.user, id=kwargs['openid_id']) user=request.user, id=kwargs['openid_id'])
openid.delete() openid.delete()
messages.success(request, _('ID removed')) 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')) messages.error(request, _('ID does not exist'))
return HttpResponseRedirect(reverse_lazy('profile')) return HttpResponseRedirect(reverse_lazy('profile'))
@@ -543,9 +536,9 @@ class RemoveConfirmedOpenIDView(View):
user_id=request.user.id, user_id=request.user.id,
claimed_id=openid.openid) claimed_id=openid.openid)
openidobj.delete() openidobj.delete()
except: except Exception as exc: # pylint: disable=broad-except
# Why it is not there? # Why it is not there?
pass print('How did we get here: %s' % exc)
openid.delete() openid.delete()
messages.success(request, _('ID removed')) messages.success(request, _('ID removed'))
except self.model.DoesNotExist: # pylint: disable=no-member except self.model.DoesNotExist: # pylint: disable=no-member
@@ -567,7 +560,7 @@ class RedirectOpenIDView(View):
try: try:
unconfirmed = self.model.objects.get( # pylint: disable=no-member unconfirmed = self.model.objects.get( # pylint: disable=no-member
user=request.user, id=kwargs['openid_id']) 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')) messages.error(request, _('ID does not exist'))
return HttpResponseRedirect(reverse_lazy('profile')) return HttpResponseRedirect(reverse_lazy('profile'))
@@ -585,7 +578,7 @@ class RedirectOpenIDView(View):
except UnicodeDecodeError as exc: # pragma: no cover except UnicodeDecodeError as exc: # pragma: no cover
msg = _('OpenID discovery failed (userid=%s) for %s: %s' % msg = _('OpenID discovery failed (userid=%s) for %s: %s' %
(request.user.id, user_url.encode('utf-8'), exc)) (request.user.id, user_url.encode('utf-8'), exc))
print(msg) print("message: %s" % msg)
messages.error(request, msg) messages.error(request, msg)
if auth_request is None: # pragma: no cover if auth_request is None: # pragma: no cover
@@ -621,10 +614,12 @@ class ConfirmOpenIDView(View): # pragma: no cover
self.request, self.request,
_('Confirmation failed: "') + str(info.message) + '"') _('Confirmation failed: "') + str(info.message) + '"')
return HttpResponseRedirect(reverse_lazy('profile')) return HttpResponseRedirect(reverse_lazy('profile'))
elif info.status == consumer.CANCEL:
if info.status == consumer.CANCEL:
messages.error(self.request, _('Cancelled by user')) messages.error(self.request, _('Cancelled by user'))
return HttpResponseRedirect(reverse_lazy('profile')) return HttpResponseRedirect(reverse_lazy('profile'))
elif info.status != consumer.SUCCESS:
if info.status != consumer.SUCCESS:
messages.error(self.request, _('Unknown verification error')) messages.error(self.request, _('Unknown verification error'))
return HttpResponseRedirect(reverse_lazy('profile')) return HttpResponseRedirect(reverse_lazy('profile'))
@@ -705,7 +700,7 @@ class CropPhotoView(TemplateView):
if 'email' in request.POST: if 'email' in request.POST:
try: try:
email = ConfirmedEmail.objects.get(email=request.POST['email']) email = ConfirmedEmail.objects.get(email=request.POST['email'])
except ConfirmedEmail.DoesNotExist: except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member
pass # Ignore automatic assignment pass # Ignore automatic assignment
if 'openid' in request.POST: if 'openid' in request.POST:
@@ -728,8 +723,26 @@ class UserPreferenceView(FormView, UpdateView):
form_class = UpdatePreferenceForm form_class = UpdatePreferenceForm
success_url = reverse_lazy('user_preference') success_url = reverse_lazy('user_preference')
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
userpref = None
try:
userpref = self.request.user.userpreference
except ObjectDoesNotExist:
userpref = UserPreference(user=self.request.user)
userpref.theme = request.POST['theme']
userpref.save()
return HttpResponseRedirect(reverse_lazy('user_preference'))
def get(self, request, *args, **kwargs):
return render(self.request, self.template_name, {
'THEMES': UserPreference.THEMES,
})
def get_object(self, queryset=None): 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') @method_decorator(login_required, name='dispatch')
@@ -852,3 +865,32 @@ class IvatarLoginView(LoginView):
if request.user.is_authenticated: if request.user.is_authenticated:
return HttpResponseRedirect(reverse_lazy('profile')) return HttpResponseRedirect(reverse_lazy('profile'))
return super().get(self, request, args, kwargs) return super().get(self, request, args, kwargs)
@method_decorator(login_required, name='dispatch')
class ProfileView(TemplateView):
'''
View class for profile
'''
template_name = 'profile.html'
def get(self, request, *args, **kwargs):
self._confirm_claimed_openid()
return super().get(self, request, args, kwargs)
def _confirm_claimed_openid(self):
openids = self.request.user.useropenid_set.all()
# If there is only one OpenID, we eventually need to add it to the user account
if openids.count() == 1:
# Already confirmed, skip
if ConfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member
return
# For whatever reason, this is in unconfirmed state, skip
if UnconfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member
return
print('need to confirm: %s' % openids.first())
confirmed = ConfirmedOpenId()
confirmed.user = self.request.user
confirmed.ip_address = get_client_ip(self.request)[0]
confirmed.openid = openids.first().claimed_id
confirmed.save()

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
@import 'tortin.less';
@bg-hero:@lab-green;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
@import 'tortin.less';
@bg-hero:@lab-red;

File diff suppressed because one or more lines are too long

View File

@@ -258,6 +258,46 @@ background-color:@bg-hero;
.navbar-tortin .navbar-collapse, .navbar-tortin .navbar-form { .navbar-tortin .navbar-collapse, .navbar-tortin .navbar-form {
border:0; border:0;
} }
.dropdown-menu {
background-color:@bg-hero;
border:1px solid darken(@bg-hero, 10%);
}
.dropdown-menu>li>a {
color:#FFFFFF;
}
.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover {
background-color:darken(@bg-hero, 10%);
color:#FFFFFF;
}
.checkbox input, .radio input {display:none}
.checkbox input + label, .radio input + label {
padding-left:0;
}
.checkbox input + label:before, .radio input + label:before {
font-family: FontAwesome;
display: inline-block;
letter-spacing:5px;
font-size:20px;
color:@bg-hero;
vertical-align:middle;
}
.checkbox input + label:before {content: "\f0c8"}
.checkbox input:checked + label:before {content: "\f14a"}
.radio input + label:before {content: "\f10c"}
.radio input:checked + label:before {content: "\f192"}
.uploadbtn:before {
position:absolute;
left:0;
right:0;
text-align:center;
content:"Select file";
font-family: 'Montserrat', sans-serif;
}
.jcrop-holder > div > div:nth-child(1) {
outline-width:2px;
outline-style:solid;
outline-color:@bg-hero;
}
@media (max-width:767px) { @media (max-width:767px) {
.navbar-tortin .navbar-nav .open .dropdown-menu > li > a { .navbar-tortin .navbar-nav .open .dropdown-menu > li > a {
color:#FFFFFF color:#FFFFFF
@@ -311,3 +351,9 @@ background: none;
width:auto; width:auto;
height:36px; height:36px;
} }
.radio {
color: @bg-hero;
}
input[type="radio"]:checked+label {
font-weight: bold;
}

View File

@@ -7,7 +7,7 @@ from django.conf.urls import url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.views.generic import TemplateView, RedirectView from django.views.generic import TemplateView, RedirectView
from ivatar import settings from ivatar import settings
from . views import AvatarImageView from . views import AvatarImageView, GravatarProxyView
urlpatterns = [ # pylint: disable=invalid-name urlpatterns = [ # pylint: disable=invalid-name
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@@ -20,6 +20,16 @@ urlpatterns = [ # pylint: disable=invalid-name
url( url(
r'avatar/(?P<digest>\w{32})', r'avatar/(?P<digest>\w{32})',
AvatarImageView.as_view(), name='avatar_view'), AvatarImageView.as_view(), name='avatar_view'),
url(
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'), url('description/', TemplateView.as_view(template_name='description.html'), name='description'),
# The following two are TODO TODO TODO TODO TODO # 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'), url('run_your_own/', TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'),

View File

@@ -4,18 +4,50 @@ views under /
from io import BytesIO from io import BytesIO
from os import path from os import path
import hashlib import hashlib
from PIL import Image from urllib.request import urlopen
from django.views.generic.base import TemplateView from urllib.error import HTTPError, URLError
from ssl import SSLError
from django.views.generic.base import TemplateView, View
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy
from PIL import Image
from monsterid.id import build_monster as BuildMonster from monsterid.id import build_monster as BuildMonster
from pydenticon import Generator as IdenticonGenerator from pydenticon import Generator as IdenticonGenerator
from robohash import Robohash
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
from . ivataraccount.models import pil_format from . ivataraccount.models import pil_format, file_format
URL_TIMEOUT = 5 # in seconds
def get_size(request, size=DEFAULT_AVATAR_SIZE):
'''
Get size from the URL arguments
'''
sizetemp = None
if 's' in request.GET:
sizetemp = request.GET['s']
if 'size' in request.GET:
sizetemp = request.GET['size']
if sizetemp:
if sizetemp != '' and sizetemp is not None and sizetemp != '0':
try:
if int(sizetemp) > 0:
size = int(sizetemp)
# Should we receive something we cannot convert to int, leave
# the user with the default value of 80
except ValueError:
pass
if size > int(AVATAR_MAX_SIZE):
size = int(AVATAR_MAX_SIZE)
return size
class AvatarImageView(TemplateView): class AvatarImageView(TemplateView):
@@ -24,16 +56,18 @@ class AvatarImageView(TemplateView):
''' '''
# TODO: Do cache resize images!! Memcached? # TODO: Do cache resize images!! Memcached?
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
''' '''
Override get from parent class Override get from parent class
''' '''
model = ConfirmedEmail model = ConfirmedEmail
size = 80 size = get_size(request)
imgformat = 'png' imgformat = 'png'
obj = None obj = None
default = None default = None
forcedefault = False forcedefault = False
gravatarredirect = False
gravatarproxy = True
if 'd' in request.GET: if 'd' in request.GET:
default = request.GET['d'] default = request.GET['d']
@@ -47,34 +81,13 @@ class AvatarImageView(TemplateView):
if request.GET['forcedefault'] == 'y': if request.GET['forcedefault'] == 'y':
forcedefault = True forcedefault = True
sizetemp = None if 'gravatarredirect' in request.GET:
if 's' in request.GET: if request.GET['gravatarredirect'] == 'y':
sizetemp = request.GET['s'] gravatarredirect = True
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): if 'gravatarproxy' in request.GET:
size = int(AVATAR_MAX_SIZE) if request.GET['gravatarproxy'] == 'n':
if len(kwargs['digest']) == 32: gravatarproxy = False
# 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'])
try: try:
obj = model.objects.get(digest=kwargs['digest']) obj = model.objects.get(digest=kwargs['digest'])
@@ -82,10 +95,29 @@ class AvatarImageView(TemplateView):
try: try:
obj = model.objects.get(digest_sha256=kwargs['digest']) obj = model.objects.get(digest_sha256=kwargs['digest'])
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass model = ConfirmedOpenId
try:
obj = model.objects.get(digest=kwargs['digest'])
except:
pass
# If that mail/openid doesn't exist, or has no photo linked to it # If that mail/openid doesn't exist, or has no photo linked to it
if not obj or not obj.photo or forcedefault: 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 # Return the default URL, as specified, or 404 Not Found, if default=404
if default: if default:
if str(default) == str(404): if str(default) == str(404):
@@ -100,6 +132,19 @@ class AvatarImageView(TemplateView):
data, data,
content_type='image/png') content_type='image/png')
if str(default) == 'robohash':
roboset = 'any'
if request.GET.get('robohash'):
roboset = request.GET.get('robohash')
robohash = Robohash(kwargs['digest'])
robohash.assemble(roboset=roboset, sizex=size, sizey=size)
data = BytesIO()
robohash.img.save(data, format='png')
data.seek(0)
return HttpResponse(
data,
content_type='image/png')
if str(default) == 'identicon' or str(default) == 'retro': if str(default) == 'identicon' or str(default) == 'retro':
# Taken from example code # Taken from example code
foreground = [ foreground = [
@@ -134,8 +179,7 @@ class AvatarImageView(TemplateView):
static_img = path.join('static', 'img', 'mm', '512.png') static_img = path.join('static', 'img', 'mm', '512.png')
# We trust static/ is mapped to /static/ # We trust static/ is mapped to /static/
return HttpResponseRedirect('/' + static_img) return HttpResponseRedirect('/' + static_img)
else: return HttpResponseRedirect(default)
return HttpResponseRedirect(default)
static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png')) static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png'))
if not path.isfile(static_img): if not path.isfile(static_img):
@@ -158,3 +202,51 @@ class AvatarImageView(TemplateView):
return HttpResponse( return HttpResponse(
data, data,
content_type='image/%s' % imgformat) content_type='image/%s' % imgformat)
class GravatarProxyView(View):
'''
Proxy request to Gravatar and return the image from there
'''
# TODO: Do cache images!! Memcached?
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument
'''
Override get from parent class
'''
size = get_size(request)
gravatarimagedata = None
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % size
try:
gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)
except HTTPError as exc:
if exc.code != 404 and exc.code != 503:
print(
'Gravatar fetch failed with an unexpected %s HTTP error' %
exc.code)
except URLError as exc:
print(
'Gravatar fetch failed with URL error: %s' %
exc.reason)
except SSLError as exc:
print(
'Gravatar fetch failed with SSL error: %s' %
exc.reason)
try:
data = BytesIO(gravatarimagedata.read())
img = Image.open(data)
data.seek(0)
return HttpResponse(
data.read(),
content_type='image/%s' % file_format(img.format))
except ValueError as exc:
print('Value error: %s' % exc)
# TODO: In case anything strange happens, we need to redirect to the default
url = reverse_lazy(
'avatar_view',
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
return HttpResponseRedirect(url)

View File

@@ -34,3 +34,4 @@ psycopg2
notsetuptools notsetuptools
git+https://github.com/ofalk/monsterid.git git+https://github.com/ofalk/monsterid.git
git+https://github.com/azaghal/pydenticon.git git+https://github.com/azaghal/pydenticon.git
git+https://github.com/ofalk/Robohash.git@devel

View File

@@ -11,24 +11,22 @@
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
{% if request.user.is_authenticated %} {% 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 '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 '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-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 'import_photo' %}"><i class="fa fa-envelope-square" aria-hidden="true"></i> {% trans 'Import photo via mail address' %}</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 '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_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</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 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</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>
{% else %} {% else %}
<li><a href="{% url 'login' %}"><i class="fa fa-sign-in" aria-hidden="true"></i> {% trans 'Local' %}</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-user-plus" aria-hidden="true"></i> {% trans 'Create account' %}</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 %} {% endif %}
</ul> </ul>
</li> </li>
{% if user.is_staff %} {% if user.is_staff %}
<li> <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> </li>
{% endif %} {% endif %}
</ul> </ul>

View File

@@ -6,67 +6,7 @@
{% spaceless %} {% spaceless %}
<div id="page"> <div id="page">
<div id="header"> <div id="header">
{% block topbar_base %} {% include 'navbar.html' %}
<nav class="navbar navbar-tortin">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% block topbar %}
{% block site_brand %}
{% if user.is_anonymous %}
<a class="navbar-brand" href="/">
{% else %}
<a class="navbar-brand" href="{% url 'profile' %}">
{% endif %}
ivatar
</a>
</div>
{% endblock %}
{% block nav %}
<div class="collapse navbar-collapse" id="navbar">
<ul class="nav navbar-nav">
{% if not user.is_anonymous %}
<li>
<a href="/"><i class="fa fa-home" aria-hidden="true"></i>
{% trans 'Home' %}
</a>
</li>
{% endif %}
<li>
<a class="nav-link" href="{% url 'contact' %}"><i class="fa fa-envelope" aria-hidden="true"></i>
{% trans 'Contact' %}
</a>
</li>
<li>
<a href="{% url 'security' %}"><i class="fa fa-user-secret" aria-hidden="true"></i>
{% trans 'Security' %}
</a>
</li>
<li class="dropdown" id="tab_tools">
<a class="dropdown-toggle" href="#" id="tools_dropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans 'Tools' %}">
<i class="fa fa-wrench" aria-hidden="true"></i>
{% trans 'Tools' %}
</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' %}
</a></li>
</ul>
</li>
</ul>
{% block account_bar %}{% include "_account_bar.html" %}{% endblock %}
</div>
</div>
</nav>
{% endblock %}
{% endblock %}
</nav>
{% endblock %}
</div> </div>
{% autoescape off %}{% endautoescape %} {% autoescape off %}{% endautoescape %}
@@ -89,4 +29,3 @@
<script src="{% static '/js/bootstrap.min.js' %}"></script> <script src="{% static '/js/bootstrap.min.js' %}"></script>
<script src="{% static '/js/ivatar.js' %}"></script> <script src="{% static '/js/ivatar.js' %}"></script>
{{ settings }}

View File

@@ -2,18 +2,16 @@
{% load i18n %}<!DOCTYPE HTML> {% load i18n %}<!DOCTYPE HTML>
{% include 'header.html' %} {% include 'header.html' %}
<title>iVatar :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title> <title>iVatar :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
{% if not user.is_anonymous %}
{% include 'navbar.html' %}
{% endif %}
{% spaceless %} {% spaceless %}
<div id="page"> <div id="page">
{% block content %}{% endblock content %}
{% autoescape off %}{% endautoescape %}
{% block content %}{% endblock content %}
{% block footer %}{% include 'footer.html' %}{% endblock footer %} {% block footer %}{% include 'footer.html' %}{% endblock footer %}
</div> </div>
{% endspaceless %} {% endspaceless %}
<script src="{% static '/js/bootstrap.min.js' %}"></script> <script src="{% static '/js/bootstrap.min.js' %}"></script>
<script src="{% static '/js/ivatar.js' %}"></script> <script src="{% static '/js/ivatar.js' %}"></script>
{{ settings }}

View File

@@ -8,7 +8,14 @@
{% block content %} {% block content %}
<h1 class="error">{% trans 'Error!' %}</h1> <h1 class="error">{% trans 'Error!' %}</h1>
<p>{% block errormessage %}{% trans 'Libravatar has encountered an error.' %}{% endblock errormessage %}</p> <p>{% block errormessage %}
{% trans 'Libravatar has encountered an error.' %}
{% if errormessage %}
<br/>
<br/>
{% blocktrans %}{{ errormessage }}{% endblocktrans %}
{% endif %}
{% endblock errormessage %}</p>
<div style="height:40px"></div> <div style="height:40px"></div>

View File

@@ -25,7 +25,9 @@
<script src="{% static '/js/jquery-3.3.1.slim.min.js' %}"></script> <script src="{% static '/js/jquery-3.3.1.slim.min.js' %}"></script>
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if user.userpreference and user.userpreference.theme != 'default' %} {% if user.userpreference and user.userpreference.theme != 'default' %}
<link rel="stylesheet" href="{% static 'css/' %}{{ user.userpreference.theme }}.css" type="text/css"> {% with 'css/'|add:user.userpreference.theme|add:'.css' as theme_css %}
<link rel="stylesheet" href="{% static theme_css %}" type="text/css">
{% endwith %}
{% endif %} {% endif %}
{% endif %} {% endif %}
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">

View File

@@ -43,8 +43,8 @@
<h1>Useful links</h1> <h1>Useful links</h1>
<a class="btn btn-default" href="{% url 'contact' %}">{% trans 'Contact us' %}</a><br/> <a class="btn btn-default" href="{% url 'contact' %}">{% trans 'Contact us' %}</a><br/>
<a class="btn btn-default" href="{% url 'security' %}">{% trans 'Security' %}</a><br/> <a class="btn btn-default" href="{% url 'security' %}">{% trans 'Security' %}</a><br/>
<a class="btn btn-default" href="https://code.launchpad.net/libravatar">{% trans 'Source code' %}</a><br/> <a class="btn btn-default" href="https://git.linux-kernel.at/oliver/ivatar/">{% trans 'Source code' %}</a><br/>
<a class="btn btn-default" href="https://bugs.launchpad.net/libravatar">{% trans 'Report bugs' %}</a><br/> <a class="btn btn-default" href="https://git.linux-kernel.at/oliver/ivatar/issues">{% trans 'Report bugs' %}</a><br/>
<a class="btn btn-default" href="https://answers.launchpad.net/libravatar">{% trans 'Questions' %}</a><br/> <a class="btn btn-default" href="https://answers.launchpad.net/libravatar">{% trans 'Questions' %}</a><br/>
<a class="btn btn-default" href="https://wiki.libravatar.org/">{% trans 'Wiki' %}</a><br/> <a class="btn btn-default" href="https://wiki.libravatar.org/">{% trans 'Wiki' %}</a><br/>
<a class="btn btn-default" href="http://blog.libravatar.org/">{% trans 'Blog' %}</a><br/> <a class="btn btn-default" href="http://blog.libravatar.org/">{% trans 'Blog' %}</a><br/>

60
templates/navbar.html Normal file
View File

@@ -0,0 +1,60 @@
{% load i18n %}
{% block topbar_base %}
<nav class="navbar navbar-tortin">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% block topbar %}
{% block site_brand %}
{% if user.is_anonymous %}
<a class="navbar-brand" href="/">
{% else %}
<a class="navbar-brand" href="{% url 'profile' %}">
{% endif %}
ivatar</a>
{% endblock %}
{% endblock %}
</div>
{% block nav %}
<div class="collapse navbar-collapse" id="navbar">
<ul class="nav navbar-nav">
{% if not user.is_anonymous %}
<li>
<a href="/"><i class="fa fa-home" aria-hidden="true"></i>
{% trans 'Home' %}
</a>
</li>
{% endif %}
<li>
<a class="nav-link" href="{% url 'contact' %}"><i class="fa fa-envelope" aria-hidden="true"></i>
{% trans 'Contact' %}
</a>
</li>
<li>
<a href="{% url 'security' %}"><i class="fa fa-user-secret" aria-hidden="true"></i>
{% trans 'Security' %}
</a>
</li>
<li class="dropdown" id="tab_tools">
<a class="dropdown-toggle" href="#" id="tools_dropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans 'Tools' %}">
<i class="fa fa-wrench" aria-hidden="true"></i>
{% trans 'Tools' %}
</a>
<ul class="dropdown-menu" aria-labelledby="tools_dropdown">
<li><a id="tools-check" href="{% url 'tools_check' %}">
<i class="fa fa-fw fa-check-square" aria-hidden="true"></i> {% trans 'Check' %}
</a></li>
</ul>
</li>
</ul>
{% block account_bar %}{% include "_account_bar.html" %}{% endblock %}
</div>
{% endblock %}
</div>
</nav>
{% endblock %}