mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-19 06:28:03 +00:00
Merge branch 'master' into libravatar_export
This commit is contained in:
25
config.py
25
config.py
@@ -49,11 +49,11 @@ TEMPLATES[0]['OPTIONS']['context_processors'].append(
|
||||
OPENID_CREATE_USERS = True
|
||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||
|
||||
SITE_NAME = 'ivatar'
|
||||
SITE_NAME = os.environ.get('SITE_NAME', 'ivatar')
|
||||
IVATAR_VERSION = '0.1'
|
||||
|
||||
SECURE_BASE_URL = 'https://avatars.linux-kernel.at/avatar/'
|
||||
BASE_URL = 'http://avatars.linux-kernel.at/avatar/'
|
||||
SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/')
|
||||
BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/')
|
||||
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('profile')
|
||||
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
||||
@@ -96,12 +96,17 @@ BOOTSTRAP4 = {
|
||||
},
|
||||
}
|
||||
|
||||
if 'test' not in sys.argv and 'collectstatic' not in sys.argv:
|
||||
ANYMAIL = { # pragma: no cover
|
||||
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
|
||||
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
|
||||
}
|
||||
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
|
||||
if 'EMAIL_BACKEND' in os.environ:
|
||||
EMAIL_BACKEND = os.environ['EMAIL_BACKEND']
|
||||
else:
|
||||
if 'test' in sys.argv or 'collectstatic' in sys.argv:
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||
else:
|
||||
ANYMAIL = { # pragma: no cover
|
||||
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
|
||||
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
|
||||
}
|
||||
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
|
||||
DEFAULT_FROM_EMAIL = 'ivatar@mg.linux-kernel.at'
|
||||
|
||||
try:
|
||||
@@ -140,3 +145,5 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||
|
||||
USE_X_FORWARDED_HOST = True
|
||||
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',]
|
||||
|
||||
DEFAULT_AVATAR_SIZE = 80
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=invalid-name,missing-docstring
|
||||
# Generated by Django 2.0.6 on 2018-07-04 12:32
|
||||
|
||||
from django.conf import settings
|
||||
@@ -5,18 +6,18 @@ from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def add_preference_to_user(apps, schema_editor):
|
||||
def add_preference_to_user(apps, schema_editor): # pylint: disable=unused-argument
|
||||
'''
|
||||
Make sure all users have preferences set up
|
||||
'''
|
||||
from django.contrib.auth.models import User
|
||||
UserPreference = apps.get_model('ivataraccount', 'UserPreference')
|
||||
for u in User.objects.filter(userpreference=None):
|
||||
p = UserPreference.objects.create(user_id=u.pk)
|
||||
p.save()
|
||||
UserPreference = apps.get_model('ivataraccount', 'UserPreference') # pylint: disable=invalid-name
|
||||
for user in User.objects.filter(userpreference=None):
|
||||
pref = UserPreference.objects.create(user_id=user.pk) # pragma: no cover
|
||||
pref.save() # pragma: no cover
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
class Migration(migrations.Migration): # pylint: disable=missing-docstring
|
||||
|
||||
dependencies = [
|
||||
('auth', '0009_alter_user_last_name_max_length'),
|
||||
@@ -27,8 +28,16 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserPreference',
|
||||
fields=[
|
||||
('theme', models.CharField(choices=[('default', 'Default theme'), ('clime', 'Climes theme')], default='default', max_length=10)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
('theme', models.CharField(
|
||||
choices=[
|
||||
('default', 'Default theme'),
|
||||
('clime', 'Climes theme')],
|
||||
default='default', max_length=10)),
|
||||
('user', models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(add_preference_to_user),
|
||||
|
||||
18
ivatar/ivataraccount/migrations/0013_auto_20181203_1421.py
Normal file
18
ivatar/ivataraccount/migrations/0013_auto_20181203_1421.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,20 +7,6 @@
|
||||
{% block content %}
|
||||
<style>
|
||||
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 {
|
||||
font-family: FontAwesome;
|
||||
display: inline-block;
|
||||
@@ -36,7 +22,9 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}
|
||||
{% if emails %}
|
||||
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
{% if photos %}
|
||||
|
||||
@@ -8,11 +8,7 @@
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.jcrop-holder > div > div:nth-child(1) {
|
||||
outline-width:2px;
|
||||
outline-style:solid;
|
||||
outline-color:#36b7d7;
|
||||
}
|
||||
|
||||
</style>
|
||||
<h1>{% trans 'Crop photo' %}</h1>
|
||||
|
||||
|
||||
@@ -7,11 +7,23 @@
|
||||
{% 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">
|
||||
|
||||
{% 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> -->
|
||||
|
||||
|
||||
@@ -5,30 +5,6 @@
|
||||
{% block title %}{% trans 'Upload an export from libravatar' %} - ivatar{% endblock title %}
|
||||
|
||||
{% 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>
|
||||
|
||||
<form enctype="multipart/form-data" method="post">
|
||||
|
||||
@@ -8,30 +8,6 @@
|
||||
<link rel="prefetch" href="{% static '/js/jcrop.js' %}">{% endblock header %}
|
||||
|
||||
{% 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>
|
||||
|
||||
<form enctype="multipart/form-data" action="{% url 'upload_photo' %}" method="post">{% csrf_token %}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.contrib.auth.views import PasswordResetView, PasswordResetDoneView,\
|
||||
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from . views import ProfileView
|
||||
from . views import CreateView, PasswordSetView, AddEmailView
|
||||
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView
|
||||
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
|
||||
@@ -62,9 +63,7 @@ urlpatterns = [ # pylint: disable=invalid-name
|
||||
path('delete/', login_required(
|
||||
TemplateView.as_view(template_name='delete.html')
|
||||
), name='delete'),
|
||||
path('profile/', login_required(
|
||||
TemplateView.as_view(template_name='profile.html')
|
||||
), name='profile'),
|
||||
path('profile/', ProfileView.as_view(), name='profile'),
|
||||
path('add_email/', AddEmailView.as_view(), name='add_email'),
|
||||
path('add_openid/', AddOpenIDView.as_view(), name='add_openid'),
|
||||
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'),
|
||||
|
||||
@@ -9,6 +9,7 @@ import binascii
|
||||
from PIL import Image
|
||||
|
||||
from django.db.models import ProtectedError
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -32,7 +33,7 @@ from openid.consumer import consumer
|
||||
from ipware import get_client_ip
|
||||
|
||||
from libravatar import libravatar_url
|
||||
from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE, JPEG_QUALITY
|
||||
from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE, JPEG_QUALITY, AVATAR_MAX_SIZE
|
||||
from .gravatar import get_photo as get_gravatar_photo
|
||||
|
||||
from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm
|
||||
@@ -117,8 +118,8 @@ class AddEmailView(SuccessMessageMixin, FormView):
|
||||
def form_valid(self, form):
|
||||
if not form.save(self.request):
|
||||
return render(self.request, self.template_name, {'form': form})
|
||||
else:
|
||||
messages.success(self.request, _('Address added successfully'))
|
||||
|
||||
messages.success(self.request, _('Address added successfully'))
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
@@ -310,14 +311,13 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
|
||||
if 'email_id' in kwargs:
|
||||
try:
|
||||
addr = ConfirmedEmail.objects.get(pk=kwargs['email_id']).email
|
||||
except ConfirmedEmail.ObjectDoesNotExist:
|
||||
except ConfirmedEmail.ObjectDoesNotExist: # pylint: disable=no-member
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Address does not exist'))
|
||||
return context
|
||||
|
||||
if 'email_addr' in kwargs:
|
||||
addr = kwargs['email_addr']
|
||||
addr = kwargs.get('email_addr', None)
|
||||
|
||||
if addr:
|
||||
gravatar = get_gravatar_photo(addr)
|
||||
@@ -327,6 +327,7 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
|
||||
libravatar_service_url = libravatar_url(
|
||||
email=addr,
|
||||
default=404,
|
||||
size=AVATAR_MAX_SIZE,
|
||||
)
|
||||
if libravatar_service_url:
|
||||
try:
|
||||
@@ -350,18 +351,10 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
|
||||
Handle post to photo import
|
||||
'''
|
||||
|
||||
addr = None
|
||||
email_id = None
|
||||
imported = None
|
||||
|
||||
if 'email_id' in kwargs:
|
||||
email_id = kwargs['email_id']
|
||||
if 'email_id' in request.POST:
|
||||
email_id = request.POST['email_id']
|
||||
if 'email_addr' in kwargs:
|
||||
addr = kwargs['email_addr']
|
||||
if 'email_addr' in request.POST:
|
||||
addr = request.POST['email_addr']
|
||||
email_id = kwargs.get('email_id', request.POST.get('email_id', None))
|
||||
addr = kwargs.get('emali_addr', request.POST.get('email_addr', None))
|
||||
|
||||
if email_id:
|
||||
email = ConfirmedEmail.objects.filter(
|
||||
@@ -415,7 +408,7 @@ class RawImageView(DetailView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
photo = self.model.objects.get(pk=kwargs['pk']) # pylint: disable=no-member
|
||||
if not photo.user.id is request.user.id:
|
||||
if not photo.user.id == request.user.id:
|
||||
return HttpResponseRedirect(reverse_lazy('home'))
|
||||
return HttpResponse(
|
||||
BytesIO(photo.data), content_type='image/%s' % photo.format)
|
||||
@@ -436,7 +429,7 @@ class DeletePhotoView(SuccessMessageMixin, View):
|
||||
photo = self.model.objects.get( # pylint: disable=no-member
|
||||
pk=kwargs['pk'], user=request.user)
|
||||
photo.delete()
|
||||
except (self.model.DoesNotExist, ProtectedError):
|
||||
except (self.model.DoesNotExist, ProtectedError): # pylint: disable=no-member
|
||||
messages.error(
|
||||
request,
|
||||
_('No such image or no permission to delete it'))
|
||||
@@ -519,7 +512,7 @@ class RemoveUnconfirmedOpenIDView(View):
|
||||
user=request.user, id=kwargs['openid_id'])
|
||||
openid.delete()
|
||||
messages.success(request, _('ID removed'))
|
||||
except self.model.DoesNotExist: # pragma: no cover # pylint: disable=no-member
|
||||
except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member
|
||||
messages.error(request, _('ID does not exist'))
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
|
||||
@@ -543,9 +536,9 @@ class RemoveConfirmedOpenIDView(View):
|
||||
user_id=request.user.id,
|
||||
claimed_id=openid.openid)
|
||||
openidobj.delete()
|
||||
except:
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# Why it is not there?
|
||||
pass
|
||||
print('How did we get here: %s' % exc)
|
||||
openid.delete()
|
||||
messages.success(request, _('ID removed'))
|
||||
except self.model.DoesNotExist: # pylint: disable=no-member
|
||||
@@ -567,7 +560,7 @@ class RedirectOpenIDView(View):
|
||||
try:
|
||||
unconfirmed = self.model.objects.get( # pylint: disable=no-member
|
||||
user=request.user, id=kwargs['openid_id'])
|
||||
except self.model.DoesNotExist: # pragma: no cover # pylint: disable=no-member
|
||||
except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member
|
||||
messages.error(request, _('ID does not exist'))
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
|
||||
@@ -585,7 +578,7 @@ class RedirectOpenIDView(View):
|
||||
except UnicodeDecodeError as exc: # pragma: no cover
|
||||
msg = _('OpenID discovery failed (userid=%s) for %s: %s' %
|
||||
(request.user.id, user_url.encode('utf-8'), exc))
|
||||
print(msg)
|
||||
print("message: %s" % msg)
|
||||
messages.error(request, msg)
|
||||
|
||||
if auth_request is None: # pragma: no cover
|
||||
@@ -621,10 +614,12 @@ class ConfirmOpenIDView(View): # pragma: no cover
|
||||
self.request,
|
||||
_('Confirmation failed: "') + str(info.message) + '"')
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
elif info.status == consumer.CANCEL:
|
||||
|
||||
if info.status == consumer.CANCEL:
|
||||
messages.error(self.request, _('Cancelled by user'))
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
elif info.status != consumer.SUCCESS:
|
||||
|
||||
if info.status != consumer.SUCCESS:
|
||||
messages.error(self.request, _('Unknown verification error'))
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
|
||||
@@ -705,7 +700,7 @@ class CropPhotoView(TemplateView):
|
||||
if 'email' in request.POST:
|
||||
try:
|
||||
email = ConfirmedEmail.objects.get(email=request.POST['email'])
|
||||
except ConfirmedEmail.DoesNotExist:
|
||||
except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member
|
||||
pass # Ignore automatic assignment
|
||||
|
||||
if 'openid' in request.POST:
|
||||
@@ -728,8 +723,26 @@ class UserPreferenceView(FormView, UpdateView):
|
||||
form_class = UpdatePreferenceForm
|
||||
success_url = reverse_lazy('user_preference')
|
||||
|
||||
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
||||
userpref = None
|
||||
try:
|
||||
userpref = self.request.user.userpreference
|
||||
except ObjectDoesNotExist:
|
||||
userpref = UserPreference(user=self.request.user)
|
||||
userpref.theme = request.POST['theme']
|
||||
userpref.save()
|
||||
return HttpResponseRedirect(reverse_lazy('user_preference'))
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return render(self.request, self.template_name, {
|
||||
'THEMES': UserPreference.THEMES,
|
||||
})
|
||||
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return self.request.user.userpreference
|
||||
(obj, created) = UserPreference.objects.get_or_create(user=self.request.user) # pylint: disable=no-member,unused-variable
|
||||
return obj
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
@@ -852,3 +865,32 @@ class IvatarLoginView(LoginView):
|
||||
if request.user.is_authenticated:
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
return super().get(self, request, args, kwargs)
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class ProfileView(TemplateView):
|
||||
'''
|
||||
View class for profile
|
||||
'''
|
||||
|
||||
template_name = 'profile.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self._confirm_claimed_openid()
|
||||
return super().get(self, request, args, kwargs)
|
||||
|
||||
def _confirm_claimed_openid(self):
|
||||
openids = self.request.user.useropenid_set.all()
|
||||
# If there is only one OpenID, we eventually need to add it to the user account
|
||||
if openids.count() == 1:
|
||||
# Already confirmed, skip
|
||||
if ConfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member
|
||||
return
|
||||
# For whatever reason, this is in unconfirmed state, skip
|
||||
if UnconfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member
|
||||
return
|
||||
print('need to confirm: %s' % openids.first())
|
||||
confirmed = ConfirmedOpenId()
|
||||
confirmed.user = self.request.user
|
||||
confirmed.ip_address = get_client_ip(self.request)[0]
|
||||
confirmed.openid = openids.first().claimed_id
|
||||
confirmed.save()
|
||||
|
||||
1
ivatar/static/css/green.css
Normal file
1
ivatar/static/css/green.css
Normal file
File diff suppressed because one or more lines are too long
2
ivatar/static/css/green.less
Normal file
2
ivatar/static/css/green.less
Normal file
@@ -0,0 +1,2 @@
|
||||
@import 'tortin.less';
|
||||
@bg-hero:@lab-green;
|
||||
1
ivatar/static/css/red.css
Normal file
1
ivatar/static/css/red.css
Normal file
File diff suppressed because one or more lines are too long
2
ivatar/static/css/red.less
Normal file
2
ivatar/static/css/red.less
Normal file
@@ -0,0 +1,2 @@
|
||||
@import 'tortin.less';
|
||||
@bg-hero:@lab-red;
|
||||
File diff suppressed because one or more lines are too long
@@ -258,6 +258,46 @@ background-color:@bg-hero;
|
||||
.navbar-tortin .navbar-collapse, .navbar-tortin .navbar-form {
|
||||
border:0;
|
||||
}
|
||||
.dropdown-menu {
|
||||
background-color:@bg-hero;
|
||||
border:1px solid darken(@bg-hero, 10%);
|
||||
}
|
||||
.dropdown-menu>li>a {
|
||||
color:#FFFFFF;
|
||||
}
|
||||
.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover {
|
||||
background-color:darken(@bg-hero, 10%);
|
||||
color:#FFFFFF;
|
||||
}
|
||||
.checkbox input, .radio input {display:none}
|
||||
.checkbox input + label, .radio input + label {
|
||||
padding-left:0;
|
||||
}
|
||||
.checkbox input + label:before, .radio input + label:before {
|
||||
font-family: FontAwesome;
|
||||
display: inline-block;
|
||||
letter-spacing:5px;
|
||||
font-size:20px;
|
||||
color:@bg-hero;
|
||||
vertical-align:middle;
|
||||
}
|
||||
.checkbox input + label:before {content: "\f0c8"}
|
||||
.checkbox input:checked + label:before {content: "\f14a"}
|
||||
.radio input + label:before {content: "\f10c"}
|
||||
.radio input:checked + label:before {content: "\f192"}
|
||||
.uploadbtn:before {
|
||||
position:absolute;
|
||||
left:0;
|
||||
right:0;
|
||||
text-align:center;
|
||||
content:"Select file";
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
.jcrop-holder > div > div:nth-child(1) {
|
||||
outline-width:2px;
|
||||
outline-style:solid;
|
||||
outline-color:@bg-hero;
|
||||
}
|
||||
@media (max-width:767px) {
|
||||
.navbar-tortin .navbar-nav .open .dropdown-menu > li > a {
|
||||
color:#FFFFFF
|
||||
@@ -311,3 +351,9 @@ background: none;
|
||||
width:auto;
|
||||
height:36px;
|
||||
}
|
||||
.radio {
|
||||
color: @bg-hero;
|
||||
}
|
||||
input[type="radio"]:checked+label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.conf.urls import url
|
||||
from django.conf.urls.static import static
|
||||
from django.views.generic import TemplateView, RedirectView
|
||||
from ivatar import settings
|
||||
from . views import AvatarImageView
|
||||
from . views import AvatarImageView, GravatarProxyView
|
||||
|
||||
urlpatterns = [ # pylint: disable=invalid-name
|
||||
path('admin/', admin.site.urls),
|
||||
@@ -20,6 +20,16 @@ urlpatterns = [ # pylint: disable=invalid-name
|
||||
url(
|
||||
r'avatar/(?P<digest>\w{32})',
|
||||
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'),
|
||||
# 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'),
|
||||
|
||||
164
ivatar/views.py
164
ivatar/views.py
@@ -4,18 +4,50 @@ views under /
|
||||
from io import BytesIO
|
||||
from os import path
|
||||
import hashlib
|
||||
from PIL import Image
|
||||
from django.views.generic.base import TemplateView
|
||||
from urllib.request import urlopen
|
||||
from urllib.error import HTTPError, URLError
|
||||
from ssl import SSLError
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from monsterid.id import build_monster as BuildMonster
|
||||
from pydenticon import Generator as IdenticonGenerator
|
||||
from robohash import Robohash
|
||||
|
||||
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY
|
||||
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
|
||||
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
||||
from . ivataraccount.models import pil_format
|
||||
from . ivataraccount.models import pil_format, file_format
|
||||
|
||||
URL_TIMEOUT = 5 # in seconds
|
||||
|
||||
|
||||
def get_size(request, size=DEFAULT_AVATAR_SIZE):
|
||||
'''
|
||||
Get size from the URL arguments
|
||||
'''
|
||||
sizetemp = None
|
||||
if 's' in request.GET:
|
||||
sizetemp = request.GET['s']
|
||||
if 'size' in request.GET:
|
||||
sizetemp = request.GET['size']
|
||||
if sizetemp:
|
||||
if sizetemp != '' and sizetemp is not None and sizetemp != '0':
|
||||
try:
|
||||
if int(sizetemp) > 0:
|
||||
size = int(sizetemp)
|
||||
# Should we receive something we cannot convert to int, leave
|
||||
# the user with the default value of 80
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if size > int(AVATAR_MAX_SIZE):
|
||||
size = int(AVATAR_MAX_SIZE)
|
||||
return size
|
||||
|
||||
|
||||
class AvatarImageView(TemplateView):
|
||||
@@ -24,16 +56,18 @@ class AvatarImageView(TemplateView):
|
||||
'''
|
||||
# TODO: Do cache resize images!! Memcached?
|
||||
|
||||
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals
|
||||
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
|
||||
'''
|
||||
Override get from parent class
|
||||
'''
|
||||
model = ConfirmedEmail
|
||||
size = 80
|
||||
size = get_size(request)
|
||||
imgformat = 'png'
|
||||
obj = None
|
||||
default = None
|
||||
forcedefault = False
|
||||
gravatarredirect = False
|
||||
gravatarproxy = True
|
||||
|
||||
if 'd' in request.GET:
|
||||
default = request.GET['d']
|
||||
@@ -47,34 +81,13 @@ class AvatarImageView(TemplateView):
|
||||
if request.GET['forcedefault'] == 'y':
|
||||
forcedefault = True
|
||||
|
||||
sizetemp = None
|
||||
if 's' in request.GET:
|
||||
sizetemp = request.GET['s']
|
||||
if 'size' in request.GET:
|
||||
sizetemp = request.GET['size']
|
||||
if sizetemp:
|
||||
if sizetemp != '' and sizetemp is not None and sizetemp != '0':
|
||||
try:
|
||||
if int(sizetemp) > 0:
|
||||
size = int(sizetemp)
|
||||
# Should we receive something we cannot convert to int, leave
|
||||
# the user with the default value of 80
|
||||
except ValueError:
|
||||
pass
|
||||
if 'gravatarredirect' in request.GET:
|
||||
if request.GET['gravatarredirect'] == 'y':
|
||||
gravatarredirect = True
|
||||
|
||||
if size > int(AVATAR_MAX_SIZE):
|
||||
size = int(AVATAR_MAX_SIZE)
|
||||
if len(kwargs['digest']) == 32:
|
||||
# Fetch by digest from mail
|
||||
pass
|
||||
elif len(kwargs['digest']) == 64:
|
||||
if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
|
||||
digest=kwargs['digest']).count():
|
||||
# Fetch by digest from OpenID
|
||||
model = ConfirmedOpenId
|
||||
else: # pragma: no cover
|
||||
# We should actually never ever reach this code...
|
||||
raise Exception('Digest provided is wrong: %s' % kwargs['digest'])
|
||||
if 'gravatarproxy' in request.GET:
|
||||
if request.GET['gravatarproxy'] == 'n':
|
||||
gravatarproxy = False
|
||||
|
||||
try:
|
||||
obj = model.objects.get(digest=kwargs['digest'])
|
||||
@@ -82,10 +95,29 @@ class AvatarImageView(TemplateView):
|
||||
try:
|
||||
obj = model.objects.get(digest_sha256=kwargs['digest'])
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
model = ConfirmedOpenId
|
||||
try:
|
||||
obj = model.objects.get(digest=kwargs['digest'])
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# If that mail/openid doesn't exist, or has no photo linked to it
|
||||
if not obj or not obj.photo or forcedefault:
|
||||
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
||||
+ '?s=%i' % size
|
||||
|
||||
# If we have redirection to Gravatar enabled, this overrides all
|
||||
# default= settings, except forcedefault!
|
||||
if gravatarredirect and not forcedefault:
|
||||
return HttpResponseRedirect(gravatar_url)
|
||||
|
||||
# Request to proxy Gravatar image - only if not forcedefault
|
||||
if gravatarproxy and not forcedefault:
|
||||
url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
|
||||
+ '?s=%i' % size
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Return the default URL, as specified, or 404 Not Found, if default=404
|
||||
if default:
|
||||
if str(default) == str(404):
|
||||
@@ -100,6 +132,19 @@ class AvatarImageView(TemplateView):
|
||||
data,
|
||||
content_type='image/png')
|
||||
|
||||
if str(default) == 'robohash':
|
||||
roboset = 'any'
|
||||
if request.GET.get('robohash'):
|
||||
roboset = request.GET.get('robohash')
|
||||
robohash = Robohash(kwargs['digest'])
|
||||
robohash.assemble(roboset=roboset, sizex=size, sizey=size)
|
||||
data = BytesIO()
|
||||
robohash.img.save(data, format='png')
|
||||
data.seek(0)
|
||||
return HttpResponse(
|
||||
data,
|
||||
content_type='image/png')
|
||||
|
||||
if str(default) == 'identicon' or str(default) == 'retro':
|
||||
# Taken from example code
|
||||
foreground = [
|
||||
@@ -134,8 +179,7 @@ class AvatarImageView(TemplateView):
|
||||
static_img = path.join('static', 'img', 'mm', '512.png')
|
||||
# We trust static/ is mapped to /static/
|
||||
return HttpResponseRedirect('/' + static_img)
|
||||
else:
|
||||
return HttpResponseRedirect(default)
|
||||
return HttpResponseRedirect(default)
|
||||
|
||||
static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png'))
|
||||
if not path.isfile(static_img):
|
||||
@@ -158,3 +202,51 @@ class AvatarImageView(TemplateView):
|
||||
return HttpResponse(
|
||||
data,
|
||||
content_type='image/%s' % imgformat)
|
||||
|
||||
class GravatarProxyView(View):
|
||||
'''
|
||||
Proxy request to Gravatar and return the image from there
|
||||
'''
|
||||
# TODO: Do cache images!! Memcached?
|
||||
|
||||
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument
|
||||
'''
|
||||
Override get from parent class
|
||||
'''
|
||||
size = get_size(request)
|
||||
gravatarimagedata = None
|
||||
|
||||
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
||||
+ '?s=%i' % size
|
||||
|
||||
try:
|
||||
gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)
|
||||
except HTTPError as exc:
|
||||
if exc.code != 404 and exc.code != 503:
|
||||
print(
|
||||
'Gravatar fetch failed with an unexpected %s HTTP error' %
|
||||
exc.code)
|
||||
except URLError as exc:
|
||||
print(
|
||||
'Gravatar fetch failed with URL error: %s' %
|
||||
exc.reason)
|
||||
except SSLError as exc:
|
||||
print(
|
||||
'Gravatar fetch failed with SSL error: %s' %
|
||||
exc.reason)
|
||||
try:
|
||||
data = BytesIO(gravatarimagedata.read())
|
||||
img = Image.open(data)
|
||||
data.seek(0)
|
||||
return HttpResponse(
|
||||
data.read(),
|
||||
content_type='image/%s' % file_format(img.format))
|
||||
|
||||
except ValueError as exc:
|
||||
print('Value error: %s' % exc)
|
||||
|
||||
# TODO: In case anything strange happens, we need to redirect to the default
|
||||
url = reverse_lazy(
|
||||
'avatar_view',
|
||||
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -34,3 +34,4 @@ psycopg2
|
||||
notsetuptools
|
||||
git+https://github.com/ofalk/monsterid.git
|
||||
git+https://github.com/azaghal/pydenticon.git
|
||||
git+https://github.com/ofalk/Robohash.git@devel
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,67 +6,7 @@
|
||||
{% spaceless %}
|
||||
<div id="page">
|
||||
<div id="header">
|
||||
{% 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>
|
||||
</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 %}
|
||||
{% include 'navbar.html' %}
|
||||
</div>
|
||||
|
||||
{% autoescape off %}{% endautoescape %}
|
||||
@@ -89,4 +29,3 @@
|
||||
|
||||
<script src="{% static '/js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static '/js/ivatar.js' %}"></script>
|
||||
{{ settings }}
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
{% load i18n %}<!DOCTYPE HTML>
|
||||
{% include 'header.html' %}
|
||||
<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 %}
|
||||
<div id="page">
|
||||
|
||||
{% autoescape off %}{% endautoescape %}
|
||||
|
||||
{% block content %}{% endblock content %}
|
||||
|
||||
{% block content %}{% endblock content %}
|
||||
{% block footer %}{% include 'footer.html' %}{% endblock footer %}
|
||||
</div>
|
||||
{% endspaceless %}
|
||||
|
||||
<script src="{% static '/js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static '/js/ivatar.js' %}"></script>
|
||||
{{ settings }}
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
{% block content %}
|
||||
<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>
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
<script src="{% static '/js/jquery-3.3.1.slim.min.js' %}"></script>
|
||||
{% if user.is_authenticated %}
|
||||
{% 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 %}
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
<h1>Useful links</h1>
|
||||
<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="https://code.launchpad.net/libravatar">{% 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/">{% trans 'Source code' %}</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://wiki.libravatar.org/">{% trans 'Wiki' %}</a><br/>
|
||||
<a class="btn btn-default" href="http://blog.libravatar.org/">{% trans 'Blog' %}</a><br/>
|
||||
|
||||
60
templates/navbar.html
Normal file
60
templates/navbar.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user