mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-14 20:18:02 +00:00
Merge branch 'devel' into trust
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
[run]
|
||||
plugins =
|
||||
django_coverage_plugin
|
||||
omit =
|
||||
node_modules/*
|
||||
.virtualenv/*
|
||||
import_libravatar.py
|
||||
|
||||
[html]
|
||||
extra_css = coverage_extra_style.css
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,4 +10,6 @@ htmlcov/
|
||||
**.pyc
|
||||
.ropeproject/
|
||||
db.sqlite3.SAVE
|
||||
|
||||
node_modules/
|
||||
config_local.py
|
||||
locale/*/LC_MESSAGES/django.mo
|
||||
|
||||
@@ -16,6 +16,9 @@ before_script:
|
||||
test_and_coverage:
|
||||
stage: test
|
||||
script:
|
||||
- echo 'from ivatar.settings import TEMPLATES' > config_local.py
|
||||
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
|
||||
- echo "DEBUG = True" >> config_local.py
|
||||
- python manage.py collectstatic --noinput
|
||||
- coverage run --source . manage.py test -v3
|
||||
- coverage report --fail-under=70
|
||||
|
||||
94
config.py
94
config.py
@@ -4,14 +4,18 @@ Configuration overrides for settings.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
from socket import gethostname, gethostbyname
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.messages import constants as message_constants
|
||||
from ivatar.settings import BASE_DIR
|
||||
|
||||
ADMIN_USERS = []
|
||||
ALLOWED_HOSTS = [ '*' ]
|
||||
from ivatar.settings import MIDDLEWARE
|
||||
from ivatar.settings import INSTALLED_APPS
|
||||
from ivatar.settings import TEMPLATES
|
||||
|
||||
ADMIN_USERS = []
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
from ivatar.settings import INSTALLED_APPS # noqa
|
||||
INSTALLED_APPS.extend([
|
||||
'django_extensions',
|
||||
'django_openid_auth',
|
||||
@@ -22,10 +26,12 @@ INSTALLED_APPS.extend([
|
||||
'ivatar.tools',
|
||||
])
|
||||
|
||||
from ivatar.settings import MIDDLEWARE # noqa
|
||||
MIDDLEWARE.extend([
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
])
|
||||
MIDDLEWARE.insert(
|
||||
0, 'ivatar.middleware.MultipleProxyMiddleware',
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
# Enable this to allow LDAP authentication.
|
||||
@@ -35,7 +41,6 @@ AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
from ivatar.settings import TEMPLATES # noqa
|
||||
TEMPLATES[0]['DIRS'].extend([
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
])
|
||||
@@ -46,16 +51,14 @@ TEMPLATES[0]['OPTIONS']['context_processors'].append(
|
||||
OPENID_CREATE_USERS = True
|
||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||
|
||||
SITE_NAME = 'ivatar'
|
||||
IVATAR_VERSION = '0.1'
|
||||
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
|
||||
IVATAR_VERSION = '1.0'
|
||||
|
||||
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
|
||||
SERVER_EMAIL = 'accounts@mg.linux-kernel.at'
|
||||
DEFAULT_FROM_EMAIL = SERVER_EMAIL
|
||||
|
||||
MAX_NUM_PHOTOS = 5
|
||||
MAX_NUM_UNCONFIRMED_EMAILS = 5
|
||||
@@ -76,7 +79,8 @@ BOOTSTRAP4 = {
|
||||
'javascript_in_head': False,
|
||||
'css_url': {
|
||||
'href': '/static/css/bootstrap.min.css',
|
||||
'integrity': 'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB', # noqa
|
||||
'integrity':
|
||||
'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB',
|
||||
'crossorigin': 'anonymous',
|
||||
},
|
||||
'javascript_url': {
|
||||
@@ -86,18 +90,26 @@ BOOTSTRAP4 = {
|
||||
},
|
||||
'popper_url': {
|
||||
'url': '/static/js/popper.min.js',
|
||||
'integrity': 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49', # noqa
|
||||
'integrity':
|
||||
'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49',
|
||||
'crossorigin': 'anonymous',
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
DEFAULT_FROM_EMAIL = 'ivatar@mg.linux-kernel.at'
|
||||
if 'EMAIL_BACKEND' in os.environ:
|
||||
EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover
|
||||
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
|
||||
|
||||
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'ivatar@mg.linux-kernel.at')
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'ivatar@mg.linux-kernel.at')
|
||||
|
||||
try:
|
||||
from ivatar.settings import DATABASES
|
||||
@@ -132,3 +144,43 @@ if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')):
|
||||
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
|
||||
|
||||
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
|
||||
|
||||
LANGUAGES = (
|
||||
('de', _('Deutsch')),
|
||||
('en', _('English')),
|
||||
('ca', _('Català')),
|
||||
('cs', _('Česky')),
|
||||
('es', _('Español')),
|
||||
('eu', _('Basque')),
|
||||
('fr', _('Français')),
|
||||
('it', _('Italiano')),
|
||||
('ja', _('日本語')),
|
||||
('nl', _('Nederlands')),
|
||||
('pt', _('Português')),
|
||||
('ru', _('Русский')),
|
||||
('sq', _('Shqip')),
|
||||
('tr', _('Türkçe')),
|
||||
('uk', _('Українська')),
|
||||
)
|
||||
|
||||
MESSAGE_TAGS = {
|
||||
message_constants.DEBUG: 'debug',
|
||||
message_constants.INFO: 'info',
|
||||
message_constants.SUCCESS: 'success',
|
||||
message_constants.WARNING: 'warning',
|
||||
message_constants.ERROR: 'danger',
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': [
|
||||
'127.0.0.1:11211',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
'''
|
||||
Local config
|
||||
'''
|
||||
|
||||
from ivatar.settings import TEMPLATES # noqa
|
||||
|
||||
SESSION_COOKIE_SECURE = False
|
||||
DEBUG = True
|
||||
TEMPLATES[0]['OPTIONS']['debug'] = True
|
||||
139
exportaccounts.py
Executable file
139
exportaccounts.py
Executable file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2018 Oliver Falk <oliver@linux-kernel.at>
|
||||
#
|
||||
# This file is part of Libravatar
|
||||
#
|
||||
# Libravatar is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Libravatar is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Libravatar. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from xml.sax import saxutils
|
||||
import hashlib
|
||||
|
||||
# pylint: disable=relative-import
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libravatar.settings")
|
||||
import libravatar.settings as settings
|
||||
from libravatar.utils import create_logger, is_hex
|
||||
import django
|
||||
django.setup()
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
os.umask(022)
|
||||
LOGGER = create_logger('exportaccount')
|
||||
|
||||
SCHEMA_ROOT = 'https://www.libravatar.org/schemas/export/0.2'
|
||||
SCHEMA_XSD = '%s/export.xsd' % SCHEMA_ROOT
|
||||
|
||||
|
||||
def xml_header():
|
||||
return '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<user xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="%s %s"
|
||||
xmlns="%s">\n''' % (SCHEMA_ROOT, SCHEMA_XSD, SCHEMA_ROOT)
|
||||
|
||||
|
||||
def xml_footer():
|
||||
return '</user>\n'
|
||||
|
||||
|
||||
def xml_account(username, password):
|
||||
escaped_username = saxutils.quoteattr(username)
|
||||
escaped_site_url = saxutils.quoteattr(settings.SITE_URL)
|
||||
escaped_password = saxutils.quoteattr(password)
|
||||
return ' <account username=%s password=%s site=%s/>\n' % (escaped_username, escaped_password, escaped_site_url)
|
||||
|
||||
def xml_email(emails):
|
||||
returnstring = " <emails>\n"
|
||||
for email in emails:
|
||||
returnstring += ' <email photo_id="' + str(email.photo_id) + '">' + email.email.encode('utf-8') + '</email>' + "\n"
|
||||
returnstring += " </emails>\n"
|
||||
return returnstring
|
||||
|
||||
def xml_openid(openids):
|
||||
returnstring = " <openids>\n"
|
||||
for openid in openids:
|
||||
returnstring += ' <openid photo_id="' + str(openid.photo_id) + '">' + openid.openid.encode('utf-8') + '</openid>' + "\n"
|
||||
returnstring += " </openids>\n"
|
||||
return returnstring
|
||||
|
||||
|
||||
def xml_photos(photos):
|
||||
s = ' <photos>\n'
|
||||
for photo in photos:
|
||||
(photo_filename, photo_format, id) = photo
|
||||
encoded_photo = encode_photo(photo_filename, photo_format)
|
||||
if encoded_photo:
|
||||
s += ''' <photo id="%s" encoding="base64" format=%s>
|
||||
%s
|
||||
</photo>\n''' % (id, saxutils.quoteattr(photo_format), encoded_photo)
|
||||
s += ' </photos>\n'
|
||||
return s
|
||||
|
||||
|
||||
def encode_photo(photo_filename, photo_format):
|
||||
filename = settings.USER_FILES_ROOT + photo_filename + '.' + photo_format
|
||||
if not os.path.isfile(filename):
|
||||
LOGGER.warning('Photo not found: %s', filename)
|
||||
return None
|
||||
|
||||
photo_content = None
|
||||
with open(filename) as photo:
|
||||
photo_content = photo.read()
|
||||
|
||||
if not photo_content:
|
||||
LOGGER.warning('Could not read photo: %s', filename)
|
||||
return None
|
||||
|
||||
return base64.b64encode(photo_content)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
if(len(sys.argv) > 1):
|
||||
userobjs = User.objects.filter(username=sys.argv[1])
|
||||
else:
|
||||
userobjs = User.objects.all()
|
||||
|
||||
for user in userobjs:
|
||||
hash_object = hashlib.new('sha256')
|
||||
hash_object.update(user.username + user.password)
|
||||
file_hash = hash_object.hexdigest()
|
||||
photos = []
|
||||
for photo in user.photos.all():
|
||||
photo_details = (photo.filename, photo.format, photo.id)
|
||||
photos.append(photo_details)
|
||||
username = user.username
|
||||
|
||||
dest_filename = settings.EXPORT_FILES_ROOT + file_hash + '.xml.gz'
|
||||
|
||||
destination = gzip.open(dest_filename, 'w')
|
||||
destination.write(xml_header())
|
||||
destination.write(xml_account(username, user.password))
|
||||
destination.write(xml_email(user.confirmed_emails.all()))
|
||||
destination.write(xml_openid(user.confirmed_openids.all()))
|
||||
destination.write(xml_photos(photos))
|
||||
destination.write(xml_footer())
|
||||
destination.close()
|
||||
print(dest_filename)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
79
import_libravatar.py
Normal file
79
import_libravatar.py
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
'''
|
||||
Import the whole libravatar export
|
||||
'''
|
||||
|
||||
import os
|
||||
from os.path import isfile, isdir, join
|
||||
import sys
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import django
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ivatar.settings") # pylint: disable=wrong-import-position
|
||||
django.setup() # pylint: disable=wrong-import-position
|
||||
from django.contrib.auth.models import User
|
||||
from PIL import Image
|
||||
from django_openid_auth.models import UserOpenID
|
||||
from ivatar.settings import JPEG_QUALITY
|
||||
from ivatar.ivataraccount.read_libravatar_export import read_gzdata as libravatar_read_gzdata
|
||||
from ivatar.ivataraccount.models import ConfirmedEmail
|
||||
from ivatar.ivataraccount.models import ConfirmedOpenId
|
||||
from ivatar.ivataraccount.models import Photo
|
||||
from ivatar.ivataraccount.models import file_format
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("First argument to '%s' must be the path to the exports" % sys.argv[0])
|
||||
exit(-255)
|
||||
|
||||
if not isdir(sys.argv[1]):
|
||||
print("First argument to '%s' must be a directory containing the exports" % sys.argv[0])
|
||||
exit(-255)
|
||||
|
||||
PATH = sys.argv[1]
|
||||
for file in os.listdir(PATH):
|
||||
if not file.endswith('.xml.gz'):
|
||||
continue
|
||||
if isfile(join(PATH, file)):
|
||||
fh = open(join(PATH, file), 'rb')
|
||||
items = libravatar_read_gzdata(fh.read())
|
||||
print('Adding user "%s"' % items['username'])
|
||||
(user, created) = User.objects.get_or_create(username=items['username'])
|
||||
user.password = items['password']
|
||||
user.save()
|
||||
|
||||
saved_photos = {}
|
||||
for photo in items['photos']:
|
||||
photo_id = photo['id']
|
||||
data = base64.decodebytes(bytes(photo['data'], 'utf-8'))
|
||||
pilobj = Image.open(BytesIO(data))
|
||||
out = BytesIO()
|
||||
pilobj.save(out, pilobj.format, quality=JPEG_QUALITY)
|
||||
out.seek(0)
|
||||
photo = Photo()
|
||||
photo.user = user
|
||||
photo.ip_address = '0.0.0.0'
|
||||
photo.format = file_format(pilobj.format)
|
||||
photo.data = out.read()
|
||||
photo.save()
|
||||
saved_photos[photo_id] = photo
|
||||
|
||||
for email in items['emails']:
|
||||
try:
|
||||
ConfirmedEmail.objects.get_or_create(email=email['email'], user=user,
|
||||
photo=saved_photos.get(email['photo_id']))
|
||||
except django.db.utils.IntegrityError:
|
||||
print('%s not unique?' % email['email'])
|
||||
|
||||
for openid in items['openids']:
|
||||
try:
|
||||
ConfirmedOpenId.objects.get_or_create(openid=openid['openid'], user=user,
|
||||
photo=saved_photos.get(openid['photo_id'])) # pylint: disable=no-member
|
||||
UserOpenID.objects.get_or_create(
|
||||
user_id=user.id,
|
||||
claimed_id=openid['openid'],
|
||||
display_id=openid['openid'],
|
||||
)
|
||||
except django.db.utils.IntegrityError:
|
||||
print('%s not unique?' % openid['openid'])
|
||||
|
||||
fh.close()
|
||||
@@ -90,16 +90,14 @@ class UploadPhotoForm(forms.Form):
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('We only host "G-rated" images and so this field must\
|
||||
be checked.')
|
||||
_('We only host "G-rated" images and so this field must be checked.')
|
||||
})
|
||||
can_distribute = forms.BooleanField(
|
||||
label=_('can be freely copied'),
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('This field must be checked since we need to be able to\
|
||||
distribute photos to third parties.')
|
||||
_('This field must be checked since we need to be able to distribute photos to third parties.')
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
@@ -191,8 +189,7 @@ class UploadLibravatarExportForm(forms.Form):
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('We only host "G-rated" images and so this field must\
|
||||
be checked.')
|
||||
_('We only host "G-rated" images and so this field must be checked.')
|
||||
})
|
||||
can_distribute = forms.BooleanField(
|
||||
label=_('can be freely copied'),
|
||||
@@ -202,3 +199,6 @@ class UploadLibravatarExportForm(forms.Form):
|
||||
_('This field must be checked since we need to be able to\
|
||||
distribute photos to third parties.')
|
||||
})
|
||||
|
||||
class DeleteAccountForm(forms.Form):
|
||||
password = forms.CharField(label=_('Password'), required=False, widget=forms.PasswordInput())
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
17
ivatar/ivataraccount/migrations/0014_auto_20190218_1602.py
Normal file
17
ivatar/ivataraccount/migrations/0014_auto_20190218_1602.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.1.5 on 2019-02-18 16:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ivataraccount', '0013_auto_20181203_1421'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='unconfirmedemail',
|
||||
options={'verbose_name': 'unconfirmed email', 'verbose_name_plural': 'unconfirmed emails'},
|
||||
),
|
||||
]
|
||||
@@ -31,7 +31,7 @@ from libravatar import libravatar_url
|
||||
from ivatar.settings import MAX_LENGTH_EMAIL, logger
|
||||
from ivatar.settings import MAX_PIXELS, AVATAR_MAX_SIZE, JPEG_QUALITY
|
||||
from ivatar.settings import MAX_LENGTH_URL
|
||||
from ivatar.settings import SECURE_BASE_URL, SITE_NAME, SERVER_EMAIL
|
||||
from ivatar.settings import SECURE_BASE_URL, SITE_NAME, DEFAULT_FROM_EMAIL
|
||||
from .gravatar import get_photo as get_gravatar_photo
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -220,8 +221,9 @@ class Photo(BaseAccountModel):
|
||||
if dimensions['a'] > MAX_PIXELS or dimensions['b'] > MAX_PIXELS:
|
||||
messages.error(
|
||||
request,
|
||||
_('Image dimensions are too big(max: %s x %s' %
|
||||
(MAX_PIXELS, MAX_PIXELS)))
|
||||
_('Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s' % {
|
||||
max_pixels: MAX_PIXELS,
|
||||
}))
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
|
||||
if dimensions['w'] == 0 and dimensions['h'] == 0:
|
||||
@@ -349,8 +351,8 @@ class UnconfirmedEmail(BaseAccountModel):
|
||||
'''
|
||||
Class attributes
|
||||
'''
|
||||
verbose_name = _('unconfirmed_email')
|
||||
verbose_name_plural = _('unconfirmed_emails')
|
||||
verbose_name = _('unconfirmed email')
|
||||
verbose_name_plural = _('unconfirmed emails')
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
update_fields=None):
|
||||
@@ -382,7 +384,7 @@ class UnconfirmedEmail(BaseAccountModel):
|
||||
# if settings.DEBUG:
|
||||
# print('DEBUG: %s' % link)
|
||||
send_mail(
|
||||
email_subject, email_body, SERVER_EMAIL,
|
||||
email_subject, email_body, DEFAULT_FROM_EMAIL,
|
||||
[self.email])
|
||||
return True
|
||||
|
||||
@@ -448,8 +450,8 @@ class ConfirmedOpenId(BaseAccountModel):
|
||||
lowercase_url = urlunsplit(
|
||||
(url.scheme.lower(), netloc, url.path, url.query, url.fragment)
|
||||
)
|
||||
if lowercase_url[-1] != '/':
|
||||
lowercase_url += '/'
|
||||
#if lowercase_url[-1] != '/':
|
||||
# lowercase_url += '/'
|
||||
self.openid = lowercase_url
|
||||
self.digest = hashlib.sha256(lowercase_url.encode('utf-8')).hexdigest()
|
||||
return super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
@@ -20,6 +20,8 @@ def read_gzdata(gzdata=None):
|
||||
emails = [] # pylint: disable=invalid-name
|
||||
openids = [] # pylint: disable=invalid-name
|
||||
photos = [] # pylint: disable=invalid-name
|
||||
username = None # pylint: disable=invalid-name
|
||||
password = None # pylint: disable=invalid-name
|
||||
|
||||
if not gzdata:
|
||||
return False
|
||||
@@ -32,15 +34,22 @@ def read_gzdata(gzdata=None):
|
||||
print('Unknown export format: %s' % root.tag)
|
||||
exit(-1)
|
||||
|
||||
# Username
|
||||
for item in root.findall('{%s}account' % SCHEMAROOT)[0].items():
|
||||
if item[0] == 'username':
|
||||
username = item[1]
|
||||
if item[0] == 'password':
|
||||
password = item[1]
|
||||
|
||||
# Emails
|
||||
for email in root.findall('{%s}emails' % SCHEMAROOT)[0]:
|
||||
if email.tag == '{%s}email' % SCHEMAROOT:
|
||||
emails.append(email.text)
|
||||
emails.append({'email': email.text, 'photo_id': email.attrib['photo_id']})
|
||||
|
||||
# OpenIDs
|
||||
for openid in root.findall('{%s}openids' % SCHEMAROOT)[0]:
|
||||
if openid.tag == '{%s}openid' % SCHEMAROOT:
|
||||
openids.append(openid.text)
|
||||
openids.append({'openid': openid.text, 'photo_id': openid.attrib['photo_id']})
|
||||
|
||||
# Photos
|
||||
for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]:
|
||||
@@ -48,14 +57,14 @@ def read_gzdata(gzdata=None):
|
||||
try:
|
||||
data = base64.decodebytes(bytes(photo.text, 'utf-8'))
|
||||
except binascii.Error as exc:
|
||||
print('Cannot decode photo; Encoding: %s, Format: %s: %s' % (
|
||||
photo.attrib['encoding'], photo.attrib['format'], exc))
|
||||
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
|
||||
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
|
||||
continue
|
||||
try:
|
||||
Image.open(BytesIO(data))
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
print('Cannot decode photo; Encoding: %s, Format: %s: %s' % (
|
||||
photo.attrib['encoding'], photo.attrib['format'], exc))
|
||||
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
|
||||
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
|
||||
continue
|
||||
else:
|
||||
# If it is a working image, we can use it
|
||||
@@ -63,10 +72,13 @@ def read_gzdata(gzdata=None):
|
||||
photos.append({
|
||||
'data': photo.text,
|
||||
'format': photo.attrib['format'],
|
||||
'id': photo.attrib['id'],
|
||||
})
|
||||
|
||||
return {
|
||||
'emails': emails,
|
||||
'openids': openids,
|
||||
'photos': photos,
|
||||
'username': username,
|
||||
'password': password,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
35
ivatar/ivataraccount/templates/delete.html
Normal file
35
ivatar/ivataraccount/templates/delete.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans 'Delete your Libravatar account' %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{% trans 'Delete your account' %}</h1>
|
||||
|
||||
<p><strong>{% trans 'There is no way to undo this operation.' %}</strong></p>
|
||||
|
||||
<form method="post" name="deleteaccount" id="form-deleteaccount">{% csrf_token %}
|
||||
|
||||
{% if user.password %}
|
||||
<p>{% trans 'Please confirm your identity by entering your current password.' %}</p>
|
||||
|
||||
{{ form.password.errors }}
|
||||
<div class="form-group" style='max-width:300px;'>
|
||||
<label for="id_password">{% trans 'Password' %}:</label>
|
||||
<input type="password" name="password" autofocus required class="form-control" id="id_password">
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<p>{% trans 'Are you sure you want to <strong>permanently delete</strong> your Libravatar account?' %}</p>
|
||||
|
||||
<button type="submit" class="btn btn-danger">{% trans 'Yes, delete all of my stuff' %}</button>
|
||||
|
||||
<button type="cancel" class="btn btn-default" href="{% url 'profile' %}">{% trans 'Cancel' %}</button>
|
||||
|
||||
</form>
|
||||
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
@@ -7,11 +7,48 @@
|
||||
{% 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 %}
|
||||
<h2>{% trans 'Theme' %}</h2>
|
||||
<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 %}
|
||||
{% if THEMES|first == theme %}{% if not user.userpreference.theme %}checked{% endif %}{% 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>
|
||||
|
||||
<!-- TODO: Language stuff not yet fully implemented; Esp. translations are only half-way there
|
||||
|
||||
<h2>{% trans 'Language' %}</h2>
|
||||
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<div class="radio">
|
||||
<input type="radio" name="language" value="{{ language.code }}" id="language-{{ language.code }}" {% if language.code == LANGUAGE_CODE %}checked{% endif %}>
|
||||
<label for="language-{{ language.code }}">{{ language.name_local }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<br/>
|
||||
<button type="submit" class="btn btn-default">{% trans 'Save' %}</button>
|
||||
</form>
|
||||
|
||||
-->
|
||||
|
||||
<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
|
||||
|
||||
@@ -39,6 +40,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
email = '%s@%s.%s' % (username, random_string(), random_string(2))
|
||||
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
|
||||
openid = 'http://%s.%s.%s/' % (username, random_string(), 'org')
|
||||
first_name = random_string()
|
||||
last_name = random_string()
|
||||
|
||||
def login(self):
|
||||
'''
|
||||
@@ -54,6 +57,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
)
|
||||
|
||||
def test_new_user(self):
|
||||
@@ -581,11 +586,12 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
|
||||
def test_upload_gif_image(self):
|
||||
'''
|
||||
Test if gif is correctly detected
|
||||
Test if gif is correctly detected and can be viewed
|
||||
'''
|
||||
self.login()
|
||||
url = reverse('upload_photo')
|
||||
# rb => Read binary
|
||||
# Broken is _not_ broken - it's just an 'x' :-)
|
||||
with open(os.path.join(settings.STATIC_ROOT, 'img', 'broken.gif'),
|
||||
'rb') as photo:
|
||||
response = self.client.post(url, {
|
||||
@@ -596,10 +602,59 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]['messages'])[0]),
|
||||
'Successfully uploaded',
|
||||
'Invalid image data should return error message!')
|
||||
'GIF upload failed?!')
|
||||
self.assertEqual(
|
||||
self.user.photo_set.first().format, 'gif',
|
||||
'Format must be gif, since we uploaded a GIF!')
|
||||
self.test_confirm_email()
|
||||
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
email=self.user.confirmedemail_set.first().email,
|
||||
)
|
||||
)
|
||||
url = '%s?%s' % (urlobj.path, urlobj.query)
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
200,
|
||||
'unable to fetch avatar?')
|
||||
|
||||
def test_upload_jpg_image(self):
|
||||
'''
|
||||
Test if jpg is correctly detected and can be viewed
|
||||
'''
|
||||
self.login()
|
||||
url = reverse('upload_photo')
|
||||
# rb => Read binary
|
||||
# Broken is _not_ broken - it's just an 'x' :-)
|
||||
with open(os.path.join(settings.STATIC_ROOT, 'img', 'broken.jpg'),
|
||||
'rb') as photo:
|
||||
response = self.client.post(url, {
|
||||
'photo': photo,
|
||||
'not_porn': True,
|
||||
'can_distribute': True,
|
||||
}, follow=True)
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]['messages'])[0]),
|
||||
'Successfully uploaded',
|
||||
'JPG upload failed?!')
|
||||
self.assertEqual(
|
||||
self.user.photo_set.first().format, 'jpg',
|
||||
'Format must be jpg, since we uploaded a jpg!')
|
||||
self.test_confirm_email()
|
||||
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
email=self.user.confirmedemail_set.first().email,
|
||||
)
|
||||
)
|
||||
url = '%s?%s' % (urlobj.path, urlobj.query)
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
200,
|
||||
'unable to fetch avatar?')
|
||||
|
||||
def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name
|
||||
'''
|
||||
@@ -653,6 +708,22 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
self.user.confirmedemail_set.first().photo,
|
||||
self.user.photo_set.first())
|
||||
|
||||
def test_no_photo_to_email(self):
|
||||
'''
|
||||
Test assigning photo to mail address
|
||||
'''
|
||||
self.test_confirm_email()
|
||||
url = reverse(
|
||||
'assign_photo_email',
|
||||
args=[self.user.confirmedemail_set.first().id])
|
||||
response = self.client.post(url, {
|
||||
'photoNone': True,
|
||||
}, follow=True)
|
||||
self.assertEqual(response.status_code, 200, 'cannot un-assign photo?')
|
||||
self.assertEqual(
|
||||
self.user.confirmedemail_set.first().photo,
|
||||
None)
|
||||
|
||||
def test_assign_photo_to_email_wo_photo_for_testing_template(self): # pylint: disable=invalid-name
|
||||
'''
|
||||
Test assign photo template
|
||||
@@ -1103,17 +1174,119 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
size=80,
|
||||
)
|
||||
)
|
||||
# Simply delete it, then it digest is 'correct', but
|
||||
# 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='/static/img/nobody/80.png',
|
||||
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
|
||||
'''
|
||||
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 digest is 'correct', but
|
||||
# the hash is no longer there
|
||||
self.user.confirmedemail_set.first().delete()
|
||||
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',
|
||||
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(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' % (urlobj.path, urlobj.query)
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertRedirects(
|
||||
response=response,
|
||||
expected_url='/static/img/mm/80.png',
|
||||
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',
|
||||
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(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' % (urlobj.path, urlobj.query)
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertRedirects(
|
||||
response=response,
|
||||
expected_url='/static/img/nobody/80.png',
|
||||
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',
|
||||
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_default(self): # pylint: disable=invalid-name
|
||||
'''
|
||||
Test fetching avatar for not existing mail with default specified
|
||||
@@ -1127,6 +1300,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='/static/img/nobody.png',
|
||||
msg_prefix='Why does this not redirect to nobody 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',
|
||||
@@ -1146,6 +1337,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&default=http://host.tld/img.png',
|
||||
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,
|
||||
@@ -1172,3 +1383,109 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
self.test_avatar_url_mail(do_upload_and_confirm=False, size=(20, 20))
|
||||
img = Image.open(BytesIO(self.user.photo_set.first().data))
|
||||
self.assertEqual(img.size, (20, 20), 'cropped to 20x20, but resulting image isn\'t 20x20!?')
|
||||
|
||||
def test_password_change_view(self):
|
||||
'''
|
||||
Test password change view
|
||||
'''
|
||||
self.login()
|
||||
url = reverse('password_change')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
200,
|
||||
'unable to view password change view?')
|
||||
|
||||
def test_password_change_view_post_wrong_old_pw(self):
|
||||
'''
|
||||
Test password change view post
|
||||
'''
|
||||
self.login()
|
||||
response = self.client.post(
|
||||
reverse('password_change'), {
|
||||
'old_password': 'xxx',
|
||||
'new_password1': self.password,
|
||||
'new_password2': self.password,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
'Your old password was entered incorrectly. Please enter it again.',
|
||||
1,
|
||||
200,
|
||||
'Old password as entered incorrectly, site should raise an error'
|
||||
)
|
||||
|
||||
def test_password_change_view_post_wrong_new_password1(self):
|
||||
'''
|
||||
Test password change view post
|
||||
'''
|
||||
self.login()
|
||||
response = self.client.post(
|
||||
reverse('password_change'), {
|
||||
'old_password': self.password,
|
||||
'new_password1': self.password + '.',
|
||||
'new_password2': self.password,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
'The two password fields didn't match.',
|
||||
1,
|
||||
200,
|
||||
'Old password as entered incorrectly, site should raise an error'
|
||||
)
|
||||
|
||||
def test_password_change_view_post_wrong_new_password2(self):
|
||||
'''
|
||||
Test password change view post
|
||||
'''
|
||||
self.login()
|
||||
response = self.client.post(
|
||||
reverse('password_change'), {
|
||||
'old_password': self.password,
|
||||
'new_password1': self.password,
|
||||
'new_password2': self.password + '.',
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
'The two password fields didn't match.',
|
||||
1,
|
||||
200,
|
||||
'Old password as entered incorrectly, site should raise an error'
|
||||
)
|
||||
|
||||
def test_profile_must_list_first_and_lastname(self):
|
||||
'''
|
||||
Test if profile view correctly lists first -/last name
|
||||
'''
|
||||
self.login()
|
||||
response = self.client.get(reverse('profile'))
|
||||
self.assertContains(
|
||||
response,
|
||||
self.first_name,
|
||||
1,
|
||||
200,
|
||||
'First name not listed in profile page',
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
self.last_name,
|
||||
1,
|
||||
200,
|
||||
'Last name not listed in profile page',
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
self.first_name + ' ' + self.last_name,
|
||||
1,
|
||||
200,
|
||||
'First and last name not correctly listed in profile page',
|
||||
)
|
||||
|
||||
@@ -6,11 +6,12 @@ from django.conf.urls import url
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.contrib.auth.views import PasswordResetView, PasswordResetDoneView,\
|
||||
from django.contrib.auth.views import PasswordResetDoneView,\
|
||||
PasswordResetConfirmView, PasswordResetCompleteView
|
||||
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from . views import ProfileView, PasswordResetView
|
||||
from . views import CreateView, PasswordSetView, AddEmailView
|
||||
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView
|
||||
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
|
||||
@@ -22,6 +23,7 @@ from . views import CropPhotoView
|
||||
from . views import UserPreferenceView, UploadLibravatarExportView
|
||||
from . views import ResendConfirmationMailView
|
||||
from . views import IvatarLoginView
|
||||
from . views import DeleteAccountView
|
||||
|
||||
# Define URL patterns, self documenting
|
||||
# To see the fancy, colorful evaluation of these use:
|
||||
@@ -59,12 +61,8 @@ urlpatterns = [ # pylint: disable=invalid-name
|
||||
path('export/', login_required(
|
||||
TemplateView.as_view(template_name='export.html')
|
||||
), name='export'),
|
||||
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('delete/', DeleteAccountView.as_view(), name='delete'),
|
||||
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
|
||||
@@ -20,6 +21,7 @@ from django.views.generic.detail import DetailView
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.forms import UserCreationForm, SetPasswordForm
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.contrib.auth.views import PasswordResetView as PasswordResetViewOriginal
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.urls import reverse_lazy, reverse
|
||||
@@ -32,11 +34,12 @@ 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
|
||||
from .forms import UpdatePreferenceForm, UploadLibravatarExportForm
|
||||
from .forms import DeleteAccountForm
|
||||
from .models import UnconfirmedEmail, ConfirmedEmail, Photo
|
||||
from .models import UnconfirmedOpenId, ConfirmedOpenId, DjangoOpenIDStore
|
||||
from .models import UserPreference
|
||||
@@ -117,8 +120,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)
|
||||
|
||||
|
||||
@@ -143,7 +146,6 @@ class RemoveUnconfirmedEmailView(SuccessMessageMixin, View):
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class ConfirmEmailView(SuccessMessageMixin, TemplateView):
|
||||
'''
|
||||
View class for confirming an unconfirmed email address
|
||||
@@ -310,14 +312,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 +328,7 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
|
||||
libravatar_service_url = libravatar_url(
|
||||
email=addr,
|
||||
default=404,
|
||||
size=AVATAR_MAX_SIZE,
|
||||
)
|
||||
if libravatar_service_url:
|
||||
try:
|
||||
@@ -336,8 +338,8 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
|
||||
else:
|
||||
context['photos'].append({
|
||||
'service_url': libravatar_service_url,
|
||||
'thumbnail_url': libravatar_service_url + '?s=80',
|
||||
'image_url': libravatar_service_url + '?s=512',
|
||||
'thumbnail_url': libravatar_service_url + '&s=80',
|
||||
'image_url': libravatar_service_url + '&s=512',
|
||||
'width': 80,
|
||||
'height': 80,
|
||||
'service_name': 'Libravatar',
|
||||
@@ -350,18 +352,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 +409,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 +430,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 +513,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 +537,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 +561,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'))
|
||||
|
||||
@@ -583,9 +577,12 @@ class RedirectOpenIDView(View):
|
||||
messages.error(request, _('OpenID discovery failed: %s' % exc))
|
||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||
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)
|
||||
msg = _('OpenID discovery failed (userid=%(userid)s) for %(userurl)s: %(message)s' % {
|
||||
userid: request.user.id,
|
||||
userurl: user_url.encode('utf-8'),
|
||||
message: exc,
|
||||
})
|
||||
print("message: %s" % msg)
|
||||
messages.error(request, msg)
|
||||
|
||||
if auth_request is None: # pragma: no cover
|
||||
@@ -621,10 +618,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 +704,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 +727,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 +869,83 @@ 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()
|
||||
|
||||
class PasswordResetView(PasswordResetViewOriginal):
|
||||
'''
|
||||
View class for password reset
|
||||
'''
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
'''
|
||||
Since we have the mail addresses in ConfirmedEmail model,
|
||||
we need to set the email on the user object in order for the
|
||||
PasswordResetView class to pick up the correct user
|
||||
'''
|
||||
if 'email' in request.POST:
|
||||
try:
|
||||
confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email'])
|
||||
confirmed_email.user.email = confirmed_email.email
|
||||
confirmed_email.user.save()
|
||||
except Exception as exc:
|
||||
pass
|
||||
return super().post(self, request, args, kwargs)
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class DeleteAccountView(SuccessMessageMixin, FormView):
|
||||
'''
|
||||
View class for account deletion
|
||||
'''
|
||||
|
||||
template_name = 'delete.html'
|
||||
form_class = DeleteAccountForm
|
||||
success_url = reverse_lazy('home')
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return super().get(self, request, args, kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
'''
|
||||
Handle account deletion
|
||||
'''
|
||||
if request.user.password:
|
||||
if 'password' in request.POST:
|
||||
if not request.user.check_password(request.POST['password']):
|
||||
messages.error(request, _('Incorrect password'))
|
||||
return HttpResponseRedirect(reverse_lazy('delete'))
|
||||
else:
|
||||
messages.error(request, _('No password given'))
|
||||
return HttpResponseRedirect(reverse_lazy('delete'))
|
||||
|
||||
raise(_('No password given'))
|
||||
request.user.delete() # should delete all confirmed/unconfirmed/photo objects
|
||||
return super().post(self, request, args, kwargs)
|
||||
|
||||
18
ivatar/middleware.py
Normal file
18
ivatar/middleware.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Middleware classes
|
||||
"""
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
Middleware to rewrite proxy headers for deployments
|
||||
with multiple proxies
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
"""
|
||||
Rewrites the proxy headers so that forwarded server is
|
||||
used if available.
|
||||
"""
|
||||
if 'HTTP_X_FORWARDED_SERVER' in request.META:
|
||||
request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER']
|
||||
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;
|
||||
}
|
||||
|
||||
80
ivatar/test_static_pages.py
Normal file
80
ivatar/test_static_pages.py
Normal file
@@ -0,0 +1,80 @@
|
||||
'''
|
||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||
'''
|
||||
# pylint: disable=too-many-lines
|
||||
from urllib.parse import urlsplit
|
||||
from io import BytesIO
|
||||
import io
|
||||
import os
|
||||
import django
|
||||
from django.test import TestCase
|
||||
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
|
||||
|
||||
from PIL import Image
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar import settings
|
||||
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
||||
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId
|
||||
from ivatar.utils import random_string
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
|
||||
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
'''
|
||||
Main test class
|
||||
'''
|
||||
client = Client()
|
||||
user = None
|
||||
username = random_string()
|
||||
password = random_string()
|
||||
email = '%s@%s.%s' % (username, random_string(), random_string(2))
|
||||
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
|
||||
openid = 'http://%s.%s.%s/' % (username, random_string(), 'org')
|
||||
|
||||
def login(self):
|
||||
'''
|
||||
Login as user
|
||||
'''
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def setUp(self):
|
||||
'''
|
||||
Prepare for tests.
|
||||
- Create user
|
||||
'''
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
)
|
||||
|
||||
def test_contact_page(self):
|
||||
"""
|
||||
Test contact page
|
||||
"""
|
||||
response = self.client.get(reverse('contact'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
|
||||
def test_description_page(self):
|
||||
"""
|
||||
Test description page
|
||||
"""
|
||||
response = self.client.get(reverse('description'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
|
||||
def test_security_page(self):
|
||||
"""
|
||||
Test security page
|
||||
"""
|
||||
response = self.client.get(reverse('security'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
|
||||
65
ivatar/test_views.py
Normal file
65
ivatar/test_views.py
Normal file
@@ -0,0 +1,65 @@
|
||||
'''
|
||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||
'''
|
||||
# pylint: disable=too-many-lines
|
||||
from urllib.parse import urlsplit
|
||||
from io import BytesIO
|
||||
import io
|
||||
import os
|
||||
import django
|
||||
from django.test import TestCase
|
||||
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
|
||||
|
||||
from PIL import Image
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar import settings
|
||||
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
||||
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId
|
||||
from ivatar.utils import random_string
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
|
||||
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
'''
|
||||
Main test class
|
||||
'''
|
||||
client = Client()
|
||||
user = None
|
||||
username = random_string()
|
||||
password = random_string()
|
||||
email = '%s@%s.%s' % (username, random_string(), random_string(2))
|
||||
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
|
||||
openid = 'http://%s.%s.%s/' % (username, random_string(), 'org')
|
||||
|
||||
def login(self):
|
||||
'''
|
||||
Login as user
|
||||
'''
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def setUp(self):
|
||||
'''
|
||||
Prepare for tests.
|
||||
- Create user
|
||||
'''
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
)
|
||||
|
||||
def test_incorrect_digest(self):
|
||||
"""
|
||||
Test incorrect digest
|
||||
"""
|
||||
response = self.client.get('/avatar/%s' % 'x'*65)
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
@@ -14,14 +14,12 @@ class CheckDomainForm(forms.Form):
|
||||
'''
|
||||
Form handling domain check
|
||||
'''
|
||||
can_distribute = forms.TextInput(
|
||||
attrs={
|
||||
'label': _('Domain'),
|
||||
'required': True,
|
||||
'error_messages': {
|
||||
'required':
|
||||
_('Cannot check without a domain name.')
|
||||
}
|
||||
domain = forms.CharField(
|
||||
label=_('Domain'),
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('Cannot check without a domain name.')
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
81
ivatar/tools/templates/check_domain.html
Normal file
81
ivatar/tools/templates/check_domain.html
Normal file
@@ -0,0 +1,81 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans 'Check domain' %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>{% trans 'Check domain' %}</h1>
|
||||
|
||||
{% if form.errors %}
|
||||
<p class="error">{% trans "Please correct errors below:" %}<br>
|
||||
{% if form.openid_identifier.errors %}
|
||||
{{ form.openid_identifier.errors|join:', ' }}
|
||||
{% endif %}
|
||||
{% if form.next.errors %}
|
||||
{{ form.next.errors|join:', ' }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div style="max-width:640px">
|
||||
<form method="post" name="lookup">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group"><label for="id_domain">{% trans 'Domain' %}</label>
|
||||
<input type="text" name="domain" maxlength="254" minlength="6" class="form-control" placeholder="{% trans 'Domain' %}" {% if form.domain.value %} value="{{ form.domain.value }}" {% endif %} id="id_domain">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-default">{% trans 'Check' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- TODO TODO TODO: I need better styling -->
|
||||
{% if result %}
|
||||
<hr/>
|
||||
<table>
|
||||
<tr>
|
||||
<td valign="top" style="padding: 0px 10px 0px 10px;">{% trans 'HTTP avatar server:' %}</td>
|
||||
<td>
|
||||
{% if result.avatar_server_http %}
|
||||
<tt>{{result.avatar_server_http}}</tt>
|
||||
{% if result.avatar_server_http_ipv4 %}
|
||||
<br>{{ result.avatar_server_http_ipv4 }}
|
||||
{% else %}
|
||||
<br><strong>{% trans 'Warning: no A record for this hostname' %}</strong>
|
||||
{% endif %}
|
||||
{% if result.avatar_server_http_ipv6 %}
|
||||
<br>{{ result.avatar_server_http_ipv6 }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<i>{% trans 'use <tt>http://cdn.libravatar.org</tt>' %}</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top" style="padding: 0px 10px 0px 10px;">{% trans 'HTTPS avatar server:' %}</td>
|
||||
<td>
|
||||
{% if result.avatar_server_https %}
|
||||
<tt>{{result.avatar_server_https}}</tt>
|
||||
{% if result.avatar_server_https_ipv4 %}
|
||||
<br>{{ result.avatar_server_https_ipv4 }}
|
||||
{% else %}
|
||||
<br><strong>{% trans 'Warning: no A record for this hostname' %}</strong>
|
||||
{% endif %}
|
||||
{% if result.avatar_server_https_ipv6 %}
|
||||
<br>{{ result.avatar_server_https_ipv6 }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<i>{% trans 'use <tt>https://seccdn.libravatar.org</tt>' %}</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
72
ivatar/tools/test_views.py
Normal file
72
ivatar/tools/test_views.py
Normal file
@@ -0,0 +1,72 @@
|
||||
'''
|
||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||
'''
|
||||
# pylint: disable=too-many-lines
|
||||
from urllib.parse import urlsplit
|
||||
from io import BytesIO
|
||||
import io
|
||||
import os
|
||||
import django
|
||||
from django.test import TestCase
|
||||
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
|
||||
|
||||
from PIL import Image
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar import settings
|
||||
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
||||
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId
|
||||
from ivatar.utils import random_string
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
|
||||
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
'''
|
||||
Main test class
|
||||
'''
|
||||
client = Client()
|
||||
user = None
|
||||
username = random_string()
|
||||
password = random_string()
|
||||
email = '%s@%s.%s' % (username, random_string(), random_string(2))
|
||||
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
|
||||
openid = 'http://%s.%s.%s/' % (username, random_string(), 'org')
|
||||
|
||||
def login(self):
|
||||
'''
|
||||
Login as user
|
||||
'''
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def setUp(self):
|
||||
'''
|
||||
Prepare for tests.
|
||||
- Create user
|
||||
'''
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
)
|
||||
|
||||
def test_check(self):
|
||||
"""
|
||||
Test check page
|
||||
"""
|
||||
response = self.client.get(reverse('tools_check'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
|
||||
def test_check_domain(self):
|
||||
"""
|
||||
Test check domain page
|
||||
"""
|
||||
response = self.client.get(reverse('tools_check_domain'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
@@ -5,6 +5,8 @@ from django.views.generic.edit import FormView
|
||||
from django.urls import reverse_lazy as reverse
|
||||
from django.shortcuts import render
|
||||
|
||||
import DNS
|
||||
|
||||
from libravatar import libravatar_url, parse_user_identity
|
||||
from libravatar import SECURE_BASE_URL as LIBRAVATAR_SECURE_BASE_URL
|
||||
from libravatar import BASE_URL as LIBRAVATAR_BASE_URL
|
||||
@@ -20,7 +22,24 @@ class CheckDomainView(FormView):
|
||||
'''
|
||||
template_name = 'check_domain.html'
|
||||
form_class = CheckDomainForm
|
||||
success_url = reverse('tools_check_domain')
|
||||
|
||||
def form_valid(self, form):
|
||||
result = {}
|
||||
super().form_valid(form)
|
||||
domain = form.cleaned_data['domain']
|
||||
result['avatar_server_http'] = lookup_avatar_server(domain, False)
|
||||
if result['avatar_server_http']:
|
||||
result['avatar_server_http_ipv4'] = lookup_ip_address(result['avatar_server_http'], False)
|
||||
result['avatar_server_http_ipv6'] = lookup_ip_address(result['avatar_server_http'], True)
|
||||
result['avatar_server_https'] = lookup_avatar_server(domain, True)
|
||||
if result['avatar_server_https']:
|
||||
result['avatar_server_https_ipv4'] = lookup_ip_address(result['avatar_server_https'], False)
|
||||
result['avatar_server_https_ipv6'] = lookup_ip_address(result['avatar_server_https'], True)
|
||||
return render(self.request, self.template_name, {
|
||||
'form': form,
|
||||
'result': result,
|
||||
})
|
||||
|
||||
class CheckView(FormView):
|
||||
'''
|
||||
@@ -69,8 +88,8 @@ class CheckView(FormView):
|
||||
mail_hash256 = hash_obj.hexdigest()
|
||||
size = form.cleaned_data['size']
|
||||
if form.cleaned_data['openid']:
|
||||
if form.cleaned_data['openid'][-1] != '/':
|
||||
form.cleaned_data['openid'] += '/'
|
||||
if not form.cleaned_data['openid'].startswith('http://') and not form.cleaned_data['openid'].startswith('https://'):
|
||||
form.cleaned_data['openid'] = 'http://%s' % form.cleaned_data['openid']
|
||||
openidurl = libravatar_url(
|
||||
openid=form.cleaned_data['openid'],
|
||||
size=form.cleaned_data['size'],
|
||||
@@ -100,3 +119,140 @@ class CheckView(FormView):
|
||||
'openid_hash': openid_hash,
|
||||
'size': size,
|
||||
})
|
||||
|
||||
|
||||
def lookup_avatar_server(domain, https):
|
||||
'''
|
||||
Extract the avatar server from an SRV record in the DNS zone
|
||||
|
||||
The SRV records should look like this:
|
||||
|
||||
_avatars._tcp.example.com. IN SRV 0 0 80 avatars.example.com
|
||||
_avatars-sec._tcp.example.com. IN SRV 0 0 443 avatars.example.com
|
||||
'''
|
||||
|
||||
if domain and len(domain) > 60:
|
||||
domain = domain[:60]
|
||||
|
||||
service_name = None
|
||||
if https:
|
||||
service_name = "_avatars-sec._tcp.%s" % domain
|
||||
else:
|
||||
service_name = "_avatars._tcp.%s" % domain
|
||||
|
||||
DNS.DiscoverNameServers()
|
||||
try:
|
||||
dns_request = DNS.Request(name=service_name, qtype='SRV').req()
|
||||
except DNS.DNSError as message:
|
||||
print("DNS Error: %s (%s)" % (message, domain))
|
||||
return None
|
||||
|
||||
if dns_request.header['status'] == 'NXDOMAIN':
|
||||
# Not an error, but no point in going any further
|
||||
return None
|
||||
|
||||
if dns_request.header['status'] != 'NOERROR':
|
||||
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], domain))
|
||||
return None
|
||||
|
||||
records = []
|
||||
for answer in dns_request.answers:
|
||||
if ('data' not in answer) or (not answer['data']) or (not answer['typename']) or (answer['typename'] != 'SRV'):
|
||||
continue
|
||||
|
||||
record = {'priority': int(answer['data'][0]), 'weight': int(answer['data'][1]),
|
||||
'port': int(answer['data'][2]), 'target': answer['data'][3]}
|
||||
|
||||
records.append(record)
|
||||
|
||||
target, port = srv_hostname(records)
|
||||
|
||||
if target and ((https and port != 443) or (not https and port != 80)):
|
||||
return "%s:%s" % (target, port)
|
||||
|
||||
return target
|
||||
|
||||
|
||||
def srv_hostname(records):
|
||||
'''
|
||||
Return the right (target, port) pair from a list of SRV records.
|
||||
'''
|
||||
|
||||
if len(records) < 1:
|
||||
return (None, None)
|
||||
|
||||
if len(records) == 1:
|
||||
ret = records[0]
|
||||
return (ret['target'], ret['port'])
|
||||
|
||||
# Keep only the servers in the top priority
|
||||
priority_records = []
|
||||
total_weight = 0
|
||||
top_priority = records[0]['priority'] # highest priority = lowest number
|
||||
|
||||
for ret in records:
|
||||
if ret['priority'] > top_priority:
|
||||
# ignore the record (ret has lower priority)
|
||||
continue
|
||||
elif ret['priority'] < top_priority:
|
||||
# reset the aretay (ret has higher priority)
|
||||
top_priority = ret['priority']
|
||||
total_weight = 0
|
||||
priority_records = []
|
||||
|
||||
total_weight += ret['weight']
|
||||
|
||||
if ret['weight'] > 0:
|
||||
priority_records.append((total_weight, ret))
|
||||
else:
|
||||
# zero-weigth elements must come first
|
||||
priority_records.insert(0, (0, ret))
|
||||
|
||||
if len(priority_records) == 1:
|
||||
unused, ret = priority_records[0]
|
||||
return (ret['target'], ret['port'])
|
||||
|
||||
# Select first record according to RFC2782 weight ordering algorithm (page 3)
|
||||
random_number = random.randint(0, total_weight)
|
||||
|
||||
for record in priority_records:
|
||||
weighted_index, ret = record
|
||||
|
||||
if weighted_index >= random_number:
|
||||
return (ret['target'], ret['port'])
|
||||
|
||||
print('There is something wrong with our SRV weight ordering algorithm')
|
||||
return (None, None)
|
||||
|
||||
|
||||
def lookup_ip_address(hostname, ipv6):
|
||||
"""
|
||||
Try to get IPv4 or IPv6 addresses for the given hostname
|
||||
"""
|
||||
|
||||
DNS.DiscoverNameServers()
|
||||
try:
|
||||
if ipv6:
|
||||
dns_request = DNS.Request(name=hostname, qtype=DNS.Type.AAAA).req()
|
||||
else:
|
||||
dns_request = DNS.Request(name=hostname, qtype=DNS.Type.A).req()
|
||||
except DNS.DNSError as message:
|
||||
print("DNS Error: %s (%s)" % (message, hostname))
|
||||
return None
|
||||
|
||||
if dns_request.header['status'] != 'NOERROR':
|
||||
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], hostname))
|
||||
return None
|
||||
|
||||
for answer in dns_request.answers:
|
||||
if ('data' not in answer) or (not answer['data']):
|
||||
continue
|
||||
if (ipv6 and answer['typename'] != 'AAAA') or (not ipv6 and answer['typename'] != 'A'):
|
||||
continue # skip CNAME records
|
||||
|
||||
if ipv6:
|
||||
return inet_ntop(AF_INET6, answer['data'])
|
||||
else:
|
||||
return answer['data']
|
||||
|
||||
return None
|
||||
|
||||
@@ -7,10 +7,11 @@ 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),
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
url('openid/', include('django_openid_auth.urls')),
|
||||
url('accounts/', include('ivatar.ivataraccount.urls')),
|
||||
url('tools/', include('ivatar.tools.urls')),
|
||||
@@ -20,6 +21,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'),
|
||||
|
||||
208
ivatar/views.py
208
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,28 +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':
|
||||
size = int(sizetemp)
|
||||
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'])
|
||||
@@ -76,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 + '&default=%s' % default
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Return the default URL, as specified, or 404 Not Found, if default=404
|
||||
if default:
|
||||
if str(default) == str(404):
|
||||
@@ -94,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 = [
|
||||
@@ -105,7 +156,15 @@ class AvatarImageView(TemplateView):
|
||||
'rgb(49,203,115)',
|
||||
'rgb(141,69,170)']
|
||||
background = 'rgb(224,224,224)'
|
||||
padding = (10, 10, 10, 10)
|
||||
padwidth = int(size/10)
|
||||
if padwidth < 10:
|
||||
padwidth = 10
|
||||
if size < 60:
|
||||
padwidth = 0
|
||||
padding = (padwidth, padwidth, padwidth, padwidth)
|
||||
# Since padding is _added_ around the generated image, we
|
||||
# need to reduce the image size by padding*2 (left/right, top/bottom)
|
||||
size = size - 2*padwidth
|
||||
generator = IdenticonGenerator(
|
||||
10, 10, digest=hashlib.sha1,
|
||||
foreground=foreground, background=background)
|
||||
@@ -118,14 +177,18 @@ class AvatarImageView(TemplateView):
|
||||
|
||||
if str(default) == 'mm' or str(default) == 'mp':
|
||||
# If mm is explicitly given, we need to catch that
|
||||
pass
|
||||
else:
|
||||
return HttpResponseRedirect(default)
|
||||
static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png'))
|
||||
if not path.isfile(static_img):
|
||||
# We trust this exists!!!
|
||||
static_img = path.join('static', 'img', 'mm', '512.png')
|
||||
# We trust static/ is mapped to /static/
|
||||
return HttpResponseRedirect('/' + static_img)
|
||||
return HttpResponseRedirect(default)
|
||||
|
||||
static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png'))
|
||||
static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png'))
|
||||
if not path.isfile(static_img):
|
||||
# We trust this exists!!!
|
||||
static_img = path.join('static', 'img', 'mm', '512.png')
|
||||
static_img = path.join('static', 'img', 'nobody', '512.png')
|
||||
# We trust static/ is mapped to /static/
|
||||
return HttpResponseRedirect('/' + static_img)
|
||||
|
||||
@@ -143,3 +206,80 @@ 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
|
||||
'''
|
||||
def redir_default(default=None):
|
||||
url = reverse_lazy(
|
||||
'avatar_view',
|
||||
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
|
||||
if default != None:
|
||||
url += '&default=%s' % default
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
size = get_size(request)
|
||||
gravatarimagedata = None
|
||||
default = None
|
||||
|
||||
try:
|
||||
if str(request.GET['default']) != 'None':
|
||||
default = request.GET['default']
|
||||
except:
|
||||
pass
|
||||
|
||||
# This part is special/hackish
|
||||
# Check if the image returned by Gravatar is their default image, if so,
|
||||
# redirect to our default instead.
|
||||
gravatar_test_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
||||
+ '?s=%i' % 50
|
||||
try:
|
||||
testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
|
||||
data = BytesIO(testdata.read())
|
||||
if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a':
|
||||
return redir_default(default)
|
||||
except Exception as exc:
|
||||
print('Gravatar test url fetch failed: %s' % exc)
|
||||
|
||||
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)
|
||||
return redir_default(default)
|
||||
except URLError as exc:
|
||||
print(
|
||||
'Gravatar fetch failed with URL error: %s' %
|
||||
exc.reason)
|
||||
return redir_default(default)
|
||||
except SSLError as exc:
|
||||
print(
|
||||
'Gravatar fetch failed with SSL error: %s' %
|
||||
exc.reason)
|
||||
return redir_default(default)
|
||||
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)
|
||||
return redir_default(default)
|
||||
|
||||
# We shouldn't reach this point... But make sure we do something
|
||||
return redir_default(default)
|
||||
|
||||
1546
locale/ca/LC_MESSAGES/django.po
Normal file
1546
locale/ca/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1133
locale/cs/LC_MESSAGES/django.po
Normal file
1133
locale/cs/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1566
locale/de/LC_MESSAGES/django.po
Normal file
1566
locale/de/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1133
locale/en_GB/LC_MESSAGES/django.po
Normal file
1133
locale/en_GB/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1023
locale/es/LC_MESSAGES/django.po
Normal file
1023
locale/es/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1197
locale/eu/LC_MESSAGES/django.po
Normal file
1197
locale/eu/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1158
locale/fr/LC_MESSAGES/django.po
Normal file
1158
locale/fr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1156
locale/it/LC_MESSAGES/django.po
Normal file
1156
locale/it/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1086
locale/ja/LC_MESSAGES/django.po
Normal file
1086
locale/ja/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1113
locale/nl/LC_MESSAGES/django.po
Normal file
1113
locale/nl/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1138
locale/pt_BR/LC_MESSAGES/django.po
Normal file
1138
locale/pt_BR/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1133
locale/ru/LC_MESSAGES/django.po
Normal file
1133
locale/ru/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1140
locale/sq/LC_MESSAGES/django.po
Normal file
1140
locale/sq/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1016
locale/tr/LC_MESSAGES/django.po
Normal file
1016
locale/tr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
1176
locale/uk/LC_MESSAGES/django.po
Normal file
1176
locale/uk/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,3 +34,5 @@ psycopg2
|
||||
notsetuptools
|
||||
git+https://github.com/ofalk/monsterid.git
|
||||
git+https://github.com/azaghal/pydenticon.git
|
||||
git+https://github.com/ofalk/Robohash.git@devel
|
||||
python-memcached
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,72 +1,12 @@
|
||||
{% load static %}
|
||||
{% load i18n %}<!DOCTYPE HTML>
|
||||
{% include 'header.html' %}
|
||||
<title>iVatar :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
|
||||
<title>{{ site_name }} :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
|
||||
|
||||
{% 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 }}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
{% load static %}
|
||||
{% load i18n %}<!DOCTYPE HTML>
|
||||
{% include 'header.html' %}
|
||||
<title>iVatar :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
|
||||
<title>{{ site_name }} :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
|
||||
|
||||
{% 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,31 +8,31 @@
|
||||
{% block content %}
|
||||
|
||||
There are a few ways to get in touch with the ivatar/libravatar developers:
|
||||
<h2>IRC</h2>
|
||||
<h4>IRC</h4>
|
||||
|
||||
If you have an IRC client already, you can join #libravatar on chat.freenode.net.
|
||||
<br/>
|
||||
Otherwise, you can use this <a href="http://webchat.freenode.net/?channels=libravatar">simple web interface</a>.
|
||||
Otherwise, you can use this <a href="http://webchat.freenode.net/?channels=libravatar" title="http://webchat.freenode.net/?channels=libravatar">simple web interface</a>.
|
||||
<br/>
|
||||
Please keep in mind that you may live in a different timezone than most of the developers. So if you do not get a response, it's not because we're ignoring you, it's probably because we're sleeping :)
|
||||
|
||||
<h2>Bug tracker</h2>
|
||||
<h4>Bug tracker</h4>
|
||||
|
||||
If you have a question about a particular bug, or some feedback on it, the best thing to do is to comment on that bug itself.
|
||||
<br/>
|
||||
TODO TODO TODO
|
||||
We use the bugtracker included in our <a href="https://git.linux-kernel.at/oliver/ivatar/issues/" title="https://git.linux-kernel.at/oliver/ivatar/issues/">GitLab instance</a>.
|
||||
|
||||
<h2>Mailing list</h2>
|
||||
<h4>Mailing list</h4>
|
||||
|
||||
If you've got a proposal to discuss or prefer to write to us, you can join our developers mailing list.
|
||||
If you've got a proposal to discuss or prefer to write to us, you can join our <a href="https://launchpad.net/~libravatar-fans#mailing-lists" title="https://launchpad.net/~libravatar-fans#mailing-lists">developers mailing list</a>.
|
||||
|
||||
<h2>Identica / Twitter</h2>
|
||||
<h4>Identica / Twitter</h4>
|
||||
|
||||
You can also put short notices to our attention on Identica or Twitter.
|
||||
You can also put short notices to our attention on <a href="http://identi.ca/libravatar" title="http://identi.ca/libravatar">Identica<a/> or <a href="http://twitter.com/libravatar" title="http://twitter.com/libravatar">Twitter</a>.
|
||||
|
||||
<h2>Email</h2>
|
||||
<h4>Email</h4>
|
||||
|
||||
Finally, if you need to email us: <a href="mailto:dev@libravatar.org">dev@libravatar.org</a>
|
||||
Finally, if you need to email us: <a href="mailto:dev@libravatar.org" title="mailto:dev@libravatar.org">dev@libravatar.org</a>
|
||||
|
||||
<div style="height:40px"></div>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="pull-left">
|
||||
<p><b>{% blocktrans %}{{ site_name }}</b> is an service running the <a href="https://launchpad.net/libravatar">ivatar</a> software, version {{ ivatar_version }} under the <a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPLv3.0</a> license.{% endblocktrans %}
|
||||
<p><b>{% blocktrans %}{{ site_name }}</b> is a service running the <a href="https://git.linux-kernel.at/oliver/ivatar/">ivatar</a> software, version {{ ivatar_version }} under the <a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPLv3.0</a> license.{% endblocktrans %}
|
||||
{% block footer %}{% endblock footer %}</p>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -11,10 +11,32 @@
|
||||
<div class="hero">
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>{% trans 'ivatar' %}</h1>
|
||||
<h1>{{ site_name }}</h1>
|
||||
<h2>{% trans 'freeing the web one face at a time' %}</h2>
|
||||
{% if user.is_anonymous %}
|
||||
<a href="/accounts/login/" class="btn btn-lg btn-primary">{% trans 'Login' %}</a>
|
||||
<a href="/accounts/new/" class="btn btn-lg btn-primary">{% trans 'Sign up' %}</a>
|
||||
{% else %}
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-lg btn-primary dropdown-toggle" href="#" id="account_dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-user" aria-hidden="true"></i> {{ request.user }}
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||
<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>
|
||||
{% if user.is_staff %}
|
||||
<li>
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="/tools/check/" class="btn btn-lg btn-primary">{% trans 'Check email' %}</a>
|
||||
</header>
|
||||
</div>
|
||||
@@ -25,14 +47,14 @@
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<h2>{% trans 'The open avatar service' %}</h2>
|
||||
<p class="lead">{% blocktrans %}ivatar is a service which delivers your avatar (profile picture) to other websites. If you create an account with us, your photo could start popping up next to forum posts or blog comments on any site where you left your email address.{% endblocktrans %}<br>
|
||||
<p class="lead">{% blocktrans %}{{ site_name }} is a service which delivers your avatar (profile picture) to other websites. If you create an account with us, your photo could start popping up next to forum posts or blog comments on any site where you left your email address.{% endblocktrans %}<br>
|
||||
<a href="https://wiki.libravatar.org/description/">{% trans 'Read more...' %}</a></p>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<section class="col-md-8">
|
||||
<h1>{% trans 'Federated Open Source Service' %}</h1>
|
||||
<p class="lead">{% trans 'This service is powered by <a href="https://www.gnu.org/licenses/agpl.html">Free and Open Source software</a> and allows owners of a domain name to <a href="https://wiki.libravatar.org/running_your_own/">run their own instance</a> of ivatar and serve avatars themselves.' %}</p>
|
||||
<p class="lead">{% trans 'This service is powered by <a href="https://www.gnu.org/licenses/agpl.html">Free and Open Source software</a> called <a href="https://git.linux-kernel.at/oliver/ivatar">ivatar</a>. With this software you can also <a href="https://wiki.libravatar.org/running_your_own/">run your own instance</a> and serve avatars yourself.' %}</p>
|
||||
<hr>
|
||||
<h1>{% trans 'Simple API for Developers' %}</h1>
|
||||
<p class="lead">{% trans 'Application developers can easily add support for this service using our <a href="https://wiki.libravatar.org/api/">simple API</a> or one of the <a href="https://wiki.libravatar.org/libraries/">libraries and plugins</a> available for a number of platforms and languages.' %}</p>
|
||||
@@ -43,8 +65,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/>
|
||||
|
||||
58
templates/navbar.html
Normal file
58
templates/navbar.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% 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 %}
|
||||
{{ site_name }}</a>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% block nav %}
|
||||
<div class="collapse navbar-collapse" id="navbar">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a href="/"><i class="fa fa-home" aria-hidden="true"></i>
|
||||
{% trans 'Home' %}
|
||||
</a>
|
||||
</li>
|
||||
<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 %}
|
||||
@@ -29,10 +29,7 @@
|
||||
|
||||
<p>
|
||||
<button type="submit" class="btn btn-default">{% trans 'Login' %}</button>
|
||||
<input type="hidden" name="next" id="next"/>
|
||||
<script>
|
||||
document.getElementById("next").value = window.location.protocol + '//' + window.location.hostname + "{% url 'profile' %}";
|
||||
</script>
|
||||
<input type="hidden" name="next" value="{{ request.build_absolute_uri }}{% url 'profile' %}" />
|
||||
|
||||
<button type="reset" class="btn btn-default" onclick="window.history.back();">{% trans 'Cancel' %}</button>
|
||||
|
||||
|
||||
58
templates/security.html
Normal file
58
templates/security.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block title %}{% trans 'federated avatar hosting service' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h4>Reporting security bugs</h4>
|
||||
|
||||
If you discover a security issue in ivatar, please report it to us privately so
|
||||
that we can push a fix to the main service before disclosing the problem
|
||||
publicly. We will credit you publicly (unless you don't want to) with this
|
||||
discovery.
|
||||
<p/><p/>
|
||||
The best way to do that is to file a security bug on our
|
||||
<a href="https://git.linux-kernel.at/oliver/ivatar/issues/new"
|
||||
title="https://git.linux-kernel.at/oliver/ivatar/issues/new">
|
||||
bug tracker
|
||||
</a>. Make sure you change the bug visibility (see "This issue is confidential
|
||||
and should only be visible to team members with at least Reporter access") and
|
||||
set the 'Security' label.
|
||||
<p/><p/>
|
||||
Alternatively, you can talk to us at
|
||||
<a href="mailto:security@libravatar.org"
|
||||
title="mailto:security@libravatar.org">
|
||||
security@libravatar.org
|
||||
</a>.
|
||||
<br/>
|
||||
We will do our best to respond to you within 24-48 hours.
|
||||
<br/>
|
||||
Also, please let us know if you are under any kind of publication deadline.
|
||||
<p/><p/>
|
||||
|
||||
<h4>Security Hall of fame</h4>
|
||||
|
||||
We would like to thank the following people who have helped make
|
||||
ivatar/Libravatar more secure by reporting security issues to us.
|
||||
|
||||
<ul>
|
||||
<li>Ahmed Adel Abdelfattah (
|
||||
<a href="https://twitter.com/00SystemError00"
|
||||
title="https://twitter.com/00SystemError00">@00SystemError00</a>):
|
||||
improvement to mail configuration on <code>libravatar.org</code> and
|
||||
<code>libravatar.com</code></li>
|
||||
<li>
|
||||
<a href="https://www.facebook.com/BugHunterID"
|
||||
title="https://www.facebook.com/BugHunterID">
|
||||
Putra Adhari</a>:
|
||||
<a href="https://bugs.launchpad.net/libravatar/+bug/1808720"
|
||||
title="https://bugs.launchpad.net/libravatar/+bug/1808720">
|
||||
server-side request forgery</a> in OpenID support</li>
|
||||
</ul>
|
||||
|
||||
<div style="height:40px"></div>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user