Merge branch 'devel' into redesign

This commit is contained in:
Oliver Falk
2021-09-16 13:36:14 +02:00
17 changed files with 2582 additions and 1908 deletions

View File

@@ -7,8 +7,12 @@ omit =
import_libravatar.py
requirements.txt
static/admin/*
static/humans.txt
static/img/robots.txt
**/static/humans.txt
**/static/img/robots.txt
ivatar/ivataraccount/read_libravatar_export.py
templates/maintenance.html
encryption_test.py
libravatarproxy.py
[html]

6
.flake8 Normal file
View File

@@ -0,0 +1,6 @@
[flake8]
ignore = E501, W503, E402, C901
max-line-length = 79
max-complexity = 18
select = B,C,E,F,W,T4,B9
exclude = .git,__pycache__,.virtualenv

74
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,74 @@
fail_fast: true
repos:
- repo: meta
hooks:
- id: check-useless-excludes
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.4.0
hooks:
- id: prettier
files: \.(css|js|md|markdown|json)
- repo: https://github.com/python/black
rev: 21.9b0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-added-large-files
- id: check-ast
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-vcs-permalinks
- id: check-xml
- id: check-yaml
args:
- --unsafe
- id: end-of-file-fixer
- id: fix-encoding-pragma
- id: forbid-new-submodules
- id: no-commit-to-branch
args:
- --branch
- gh-pages
- id: requirements-txt-fixer
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
- repo: local
hooks:
- id: shfmt
name: shfmt
minimum_pre_commit_version: 2.4.0
language: golang
additional_dependencies:
- mvdan.cc/sh/v3/cmd/shfmt@v3.1.1
entry: shfmt
args:
- -w
- -i
- '4'
types:
- shell
- repo: https://github.com/asottile/blacken-docs
rev: v1.11.0
hooks:
- id: blacken-docs
- repo: https://github.com/hcodes/yaspeller.git
rev: v7.0.0
hooks:
- id: yaspeller
types:
- markdown
- repo: https://github.com/kadrach/pre-commit-gitlabci-lint
rev: 22d0495c9894e8b27cc37c2ed5d9a6b46385a44c
hooks:
- id: gitlabci-lint
args: ["https://git.linux-kernel.at"]

229
config.py
View File

@@ -1,6 +1,7 @@
''' yes
# -*- coding: utf-8 -*-
"""
Configuration overrides for settings.py
'''
"""
import os
import sys
@@ -14,50 +15,61 @@ from ivatar.settings import INSTALLED_APPS
from ivatar.settings import TEMPLATES
ADMIN_USERS = []
ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS.extend([
'django_extensions',
'django_openid_auth',
'bootstrap4',
'anymail',
'ivatar',
'ivatar.ivataraccount',
'ivatar.tools',
])
INSTALLED_APPS.extend(
[
"django_extensions",
"django_openid_auth",
"bootstrap4",
"anymail",
"ivatar",
"ivatar.ivataraccount",
"ivatar.tools",
]
)
MIDDLEWARE.extend([
'django.middleware.locale.LocaleMiddleware',
])
MIDDLEWARE.extend(
[
"django.middleware.locale.LocaleMiddleware",
]
)
MIDDLEWARE.insert(
0, 'ivatar.middleware.MultipleProxyMiddleware',
0,
"ivatar.middleware.MultipleProxyMiddleware",
)
AUTHENTICATION_BACKENDS = (
# Enable this to allow LDAP authentication.
# See INSTALL for more information.
# 'django_auth_ldap.backend.LDAPBackend',
'django_openid_auth.auth.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
"django_openid_auth.auth.OpenIDBackend",
"django.contrib.auth.backends.ModelBackend",
)
TEMPLATES[0]['DIRS'].extend([
os.path.join(BASE_DIR, 'templates'),
])
TEMPLATES[0]['OPTIONS']['context_processors'].append(
'ivatar.context_processors.basepage',
TEMPLATES[0]["DIRS"].extend(
[
os.path.join(BASE_DIR, "templates"),
]
)
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
"ivatar.context_processors.basepage",
)
OPENID_CREATE_USERS = True
OPENID_UPDATE_DETAILS_FROM_SREG = True
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
IVATAR_VERSION = '1.4'
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
IVATAR_VERSION = "1.5"
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/')
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
LOGIN_REDIRECT_URL = reverse_lazy('profile')
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
MAX_NUM_PHOTOS = 5
@@ -75,119 +87,120 @@ MIN_LENGTH_EMAIL = 6 # eg. x@x.xx
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
BOOTSTRAP4 = {
'include_jquery': False,
'javascript_in_head': False,
'css_url': {
'href': '/static/css/bootstrap.min.css',
'integrity':
'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB',
'crossorigin': 'anonymous',
"include_jquery": False,
"javascript_in_head": False,
"css_url": {
"href": "/static/css/bootstrap.min.css",
"integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB",
"crossorigin": "anonymous",
},
'javascript_url': {
'url': '/static/js/bootstrap.min.js',
'integrity': '',
'crossorigin': 'anonymous',
"javascript_url": {
"url": "/static/js/bootstrap.min.js",
"integrity": "",
"crossorigin": "anonymous",
},
'popper_url': {
'url': '/static/js/popper.min.js',
'integrity':
'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49',
'crossorigin': 'anonymous',
"popper_url": {
"url": "/static/js/popper.min.js",
"integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49",
"crossorigin": "anonymous",
},
}
if 'EMAIL_BACKEND' in os.environ:
EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover
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'
if "test" in sys.argv or "collectstatic" in sys.argv:
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
else:
try:
ANYMAIL = { # pragma: no cover
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
"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
except Exception as exc: # pragma: nocover
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover
except Exception: # pragma: nocover # pylint: disable=broad-except
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
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')
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
except ImportError: # pragma: no cover
DATABASES = [] # pragma: no cover
if 'default' not in DATABASES:
DATABASES['default'] = { # pragma: no cover
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
if "default" not in DATABASES:
DATABASES["default"] = { # pragma: no cover
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
if 'MYSQL_DATABASE' in os.environ:
DATABASES['default'] = { # pragma: no cover
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ['MYSQL_DATABASE'],
'USER': os.environ['MYSQL_USER'],
'PASSWORD': os.environ['MYSQL_PASSWORD'],
'HOST': 'mysql',
if "MYSQL_DATABASE" in os.environ:
DATABASES["default"] = { # pragma: no cover
"ENGINE": "django.db.backends.mysql",
"NAME": os.environ["MYSQL_DATABASE"],
"USER": os.environ["MYSQL_USER"],
"PASSWORD": os.environ["MYSQL_PASSWORD"],
"HOST": "mysql",
}
if 'POSTGRESQL_DATABASE' in os.environ:
DATABASES['default'] = { # pragma: no cover
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['POSTGRESQL_DATABASE'],
'USER': os.environ['POSTGRESQL_USER'],
'PASSWORD': os.environ['POSTGRESQL_PASSWORD'],
'HOST': 'postgresql',
if "POSTGRESQL_DATABASE" in os.environ:
DATABASES["default"] = { # pragma: no cover
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRESQL_DATABASE"],
"USER": os.environ["POSTGRESQL_USER"],
"PASSWORD": os.environ["POSTGRESQL_PASSWORD"],
"HOST": "postgresql",
}
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer"
USE_X_FORWARDED_HOST = True
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',]
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [
"avatars.linux-kernel.at",
"localhost",
]
DEFAULT_AVATAR_SIZE = 80
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', _('Українська')),
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',
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',
],
},
'filesystem': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/ivatar_cache',
'TIMEOUT': 900, # 15 minutes
}
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": [
"127.0.0.1:11211",
],
},
"filesystem": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/ivatar_cache",
"TIMEOUT": 900, # 15 minutes
},
}
# This is 5 minutes caching for generated/resized images,
@@ -197,5 +210,5 @@ CACHE_IMAGES_MAX_AGE = 5 * 60
CACHE_RESPONSE = True
# This MUST BE THE LAST!
if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')):
if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")):
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover

View File

@@ -1,4 +1,6 @@
'''
# -*- coding: utf-8 -*-
"""
Module init
'''
"""
app_label = __name__ # pylint: disable=invalid-name

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Default: useful variables for the base page templates.
'''
"""
from ipware import get_client_ip
from ivatar.settings import IVATAR_VERSION, SITE_NAME, MAX_PHOTO_SIZE
@@ -9,27 +10,28 @@ from ivatar.settings import MAX_NUM_UNCONFIRMED_EMAILS
def basepage(request):
'''
"""
Our contextprocessor adds additional context variables
in order to be used in the templates
'''
"""
context = {}
if 'openid_identifier' in request.GET:
context['openid_identifier'] = \
request.GET['openid_identifier'] # pragma: no cover
if "openid_identifier" in request.GET:
context["openid_identifier"] = request.GET[
"openid_identifier"
] # pragma: no cover
client_ip = get_client_ip(request)[0]
context['client_ip'] = client_ip
context['ivatar_version'] = IVATAR_VERSION
context['site_name'] = SITE_NAME
context['site_url'] = request.build_absolute_uri('/')[:-1]
context['max_file_size'] = MAX_PHOTO_SIZE
context['BASE_URL'] = BASE_URL
context['SECURE_BASE_URL'] = SECURE_BASE_URL
context['max_emails'] = False
context["client_ip"] = client_ip
context["ivatar_version"] = IVATAR_VERSION
context["site_name"] = SITE_NAME
context["site_url"] = request.build_absolute_uri("/")[:-1]
context["max_file_size"] = MAX_PHOTO_SIZE
context["BASE_URL"] = BASE_URL
context["SECURE_BASE_URL"] = SECURE_BASE_URL
context["max_emails"] = False
if request.user:
if not request.user.is_anonymous:
unconfirmed = request.user.unconfirmedemail_set.count()
if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS:
context['max_emails'] = True
context["max_emails"] = True
return context

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Our models for ivatar.ivataraccount
'''
"""
import base64
import hashlib
@@ -37,48 +38,49 @@ from .gravatar import get_photo as get_gravatar_photo
def file_format(image_type):
'''
"""
Helper method returning a short image type
'''
if image_type == 'JPEG':
return 'jpg'
elif image_type == 'PNG':
return 'png'
elif image_type == 'GIF':
return 'gif'
"""
if image_type == "JPEG":
return "jpg"
elif image_type == "PNG":
return "png"
elif image_type == "GIF":
return "gif"
return None
def pil_format(image_type):
'''
"""
Helper method returning the 'encoder name' for PIL
'''
if image_type == 'jpg' or image_type == 'jpeg':
return 'JPEG'
elif image_type == 'png':
return 'PNG'
elif image_type == 'gif':
return 'GIF'
"""
if image_type == "jpg" or image_type == "jpeg":
return "JPEG"
elif image_type == "png":
return "PNG"
elif image_type == "gif":
return "GIF"
logger.info('Unsupported file format: %s', image_type)
logger.info("Unsupported file format: %s", image_type)
return None
class UserPreference(models.Model):
'''
"""
Holds the user users preferences
'''
"""
THEMES = (
('default', 'Default theme'),
('clime', 'climes theme'),
('green', 'green theme'),
('red', 'red theme'),
("default", "Default theme"),
("clime", "climes theme"),
("green", "green theme"),
("red", "red theme"),
)
theme = models.CharField(
max_length=10,
choices=THEMES,
default='default',
default="default",
)
user = models.OneToOneField(
@@ -88,13 +90,14 @@ class UserPreference(models.Model):
)
def __str__(self):
return 'Preference (%i) for %s' % (self.pk, self.user)
return "Preference (%i) for %s" % (self.pk, self.user)
class BaseAccountModel(models.Model):
'''
"""
Base, abstract model, holding fields we use in all cases
'''
"""
user = models.ForeignKey(
User,
on_delete=models.deletion.CASCADE,
@@ -103,40 +106,43 @@ class BaseAccountModel(models.Model):
add_date = models.DateTimeField(default=timezone.now)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Class attributes
'''
"""
abstract = True
class Photo(BaseAccountModel):
'''
"""
Model holding the photos and information about them
'''
"""
ip_address = models.GenericIPAddressField(unpack_ipv4=True)
data = models.BinaryField()
format = models.CharField(max_length=3)
access_count = models.BigIntegerField(default=0, editable=False)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Class attributes
'''
verbose_name = _('photo')
verbose_name_plural = _('photos')
"""
verbose_name = _("photo")
verbose_name_plural = _("photos")
def import_image(self, service_name, email_address):
'''
"""
Allow to import image from other (eg. Gravatar) service
'''
"""
image_url = False
if service_name == 'Gravatar':
if service_name == "Gravatar":
gravatar = get_gravatar_photo(email_address)
if gravatar:
image_url = gravatar['image_url']
image_url = gravatar["image_url"]
if service_name == 'Libravatar':
if service_name == "Libravatar":
image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE)
if not image_url:
@@ -146,13 +152,12 @@ class Photo(BaseAccountModel):
# No idea how to test this
# pragma: no cover
except HTTPError as exc:
print('%s import failed with an HTTP error: %s' %
(service_name, exc.code))
print("%s import failed with an HTTP error: %s" % (service_name, exc.code))
return False
# No idea how to test this
# pragma: no cover
except URLError as exc:
print('%s import failed: %s' % (service_name, exc.reason))
print("%s import failed: %s" % (service_name, exc.reason))
return False
data = image.read()
@@ -164,35 +169,36 @@ class Photo(BaseAccountModel):
self.format = file_format(img.format)
if not self.format:
print('Unable to determine format: %s' % img) # pragma: no cover
print("Unable to determine format: %s" % img) # pragma: no cover
return False # pragma: no cover
self.data = data
super().save()
return True
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
'''
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
"""
Override save from parent, taking care about the image
'''
"""
# Use PIL to read the file format
try:
img = Image.open(BytesIO(self.data))
# Testing? Ideas anyone?
except Exception as exc: # pylint: disable=broad-except
# For debugging only
print('Exception caught in Photo.save(): %s' % exc)
print("Exception caught in Photo.save(): %s" % exc)
return False
self.format = file_format(img.format)
if not self.format:
print('Format not recognized')
print("Format not recognized")
return False
return super().save(force_insert, force_update, using, update_fields)
def perform_crop(self, request, dimensions, email, openid):
'''
"""
Helper to crop the image
'''
"""
if request.user.photo_set.count() == 1:
# This is the first photo, assign to all confirmed addresses
for addr in request.user.confirmedemail_set.all():
@@ -217,34 +223,40 @@ class Photo(BaseAccountModel):
img = Image.open(BytesIO(self.data))
# This should be anyway checked during save...
dimensions['a'], \
dimensions['b'] = img.size # pylint: disable=invalid-name
if dimensions['a'] > MAX_PIXELS or dimensions['b'] > MAX_PIXELS:
dimensions["a"], dimensions["b"] = img.size # pylint: disable=invalid-name
if dimensions["a"] > MAX_PIXELS or dimensions["b"] > MAX_PIXELS:
messages.error(
request,
_('Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s' % {
max_pixels: MAX_PIXELS,
}))
return HttpResponseRedirect(reverse_lazy('profile'))
_(
"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:
dimensions['w'], dimensions['h'] = dimensions['a'], dimensions['b']
min_from_w_h = min(dimensions['w'], dimensions['h'])
dimensions['w'], dimensions['h'] = min_from_w_h, min_from_w_h
elif ((dimensions['w'] < 0)
or ((dimensions['x'] + dimensions['w']) > dimensions['a'])
or (dimensions['h'] < 0)
or ((dimensions['y'] + dimensions['h']) > dimensions['b'])):
messages.error(
request,
_('Crop outside of original image bounding box'))
return HttpResponseRedirect(reverse_lazy('profile'))
if dimensions["w"] == 0 and dimensions["h"] == 0:
dimensions["w"], dimensions["h"] = dimensions["a"], dimensions["b"]
min_from_w_h = min(dimensions["w"], dimensions["h"])
dimensions["w"], dimensions["h"] = min_from_w_h, min_from_w_h
elif (
(dimensions["w"] < 0)
or ((dimensions["x"] + dimensions["w"]) > dimensions["a"])
or (dimensions["h"] < 0)
or ((dimensions["y"] + dimensions["h"]) > dimensions["b"])
):
messages.error(request, _("Crop outside of original image bounding box"))
return HttpResponseRedirect(reverse_lazy("profile"))
cropped = img.crop((
dimensions['x'],
dimensions['y'],
dimensions['x'] + dimensions['w'],
dimensions['y'] + dimensions['h']))
cropped = img.crop(
(
dimensions["x"],
dimensions["y"],
dimensions["x"] + dimensions["w"],
dimensions["y"] + dimensions["h"],
)
)
# cropped.load()
# Resize the image only if it's larger than the specified max width.
cropped_w, cropped_h = cropped.size
@@ -260,26 +272,26 @@ class Photo(BaseAccountModel):
self.data = data.read()
self.save()
return HttpResponseRedirect(reverse_lazy('profile'))
return HttpResponseRedirect(reverse_lazy("profile"))
def __str__(self):
return '%s (%i) from %s' % (self.format, self.pk or 0, self.user)
return "%s (%i) from %s" % (self.format, self.pk or 0, self.user)
# pylint: disable=too-few-public-methods
class ConfirmedEmailManager(models.Manager):
'''
"""
Manager for our confirmed email addresses model
'''
"""
@staticmethod
def create_confirmed_email(user, email_address, is_logged_in):
'''
"""
Helper method to create confirmed email address
'''
"""
confirmed = ConfirmedEmail()
confirmed.user = user
confirmed.ip_address = '0.0.0.0'
confirmed.ip_address = "0.0.0.0"
confirmed.email = email_address
confirmed.save()
@@ -293,14 +305,15 @@ class ConfirmedEmailManager(models.Manager):
class ConfirmedEmail(BaseAccountModel):
'''
"""
Model holding our confirmed email addresses, as well as the relation
to the assigned photo
'''
"""
email = models.EmailField(unique=True, max_length=MAX_LENGTH_EMAIL)
photo = models.ForeignKey(
Photo,
related_name='emails',
related_name="emails",
blank=True,
null=True,
on_delete=models.deletion.SET_NULL,
@@ -311,123 +324,129 @@ class ConfirmedEmail(BaseAccountModel):
access_count = models.BigIntegerField(default=0, editable=False)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Class attributes
'''
verbose_name = _('confirmed email')
verbose_name_plural = _('confirmed emails')
"""
verbose_name = _("confirmed email")
verbose_name_plural = _("confirmed emails")
def set_photo(self, photo):
'''
"""
Helper method to set photo
'''
"""
self.photo = photo
self.save()
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
'''
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
"""
Override save from parent, add digest
'''
"""
self.digest = hashlib.md5(
self.email.strip().lower().encode('utf-8')
self.email.strip().lower().encode("utf-8")
).hexdigest()
self.digest_sha256 = hashlib.sha256(
self.email.strip().lower().encode('utf-8')
self.email.strip().lower().encode("utf-8")
).hexdigest()
return super().save(force_insert, force_update, using, update_fields)
def __str__(self):
return '%s (%i) from %s' % (self.email, self.pk, self.user)
return "%s (%i) from %s" % (self.email, self.pk, self.user)
class UnconfirmedEmail(BaseAccountModel):
'''
"""
Model holding unconfirmed email addresses as well as the verification key
'''
"""
email = models.EmailField(max_length=MAX_LENGTH_EMAIL)
verification_key = models.CharField(max_length=64)
last_send_date = models.DateTimeField(null=True, blank=True)
last_status = models.TextField(max_length=2047, null=True, blank=True)
last_status = models.TextField(max_length=2047, null=True, blank=True)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Class attributes
'''
verbose_name = _('unconfirmed email')
verbose_name_plural = _('unconfirmed emails')
"""
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
verbose_name = _("unconfirmed email")
verbose_name_plural = _("unconfirmed emails")
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
if not self.verification_key:
hash_object = hashlib.new('sha256')
hash_object = hashlib.new("sha256")
hash_object.update(
urandom(1024) + self.user.username.encode('utf-8') # pylint: disable=no-member
urandom(1024)
+ self.user.username.encode("utf-8") # pylint: disable=no-member
) # pylint: disable=no-member
self.verification_key = hash_object.hexdigest()
super(UnconfirmedEmail, self).save(
force_insert,
force_update,
using,
update_fields)
force_insert, force_update, using, update_fields
)
def send_confirmation_mail(self, url=SECURE_BASE_URL):
'''
"""
Send confirmation mail to that mail address
'''
link = url + \
reverse(
'confirm_email',
kwargs={'verification_key': self.verification_key})
email_subject = _('Confirm your email address on %s') % \
SITE_NAME
email_body = render_to_string('email_confirmation.txt', {
'verification_link': link,
'site_name': SITE_NAME,
})
"""
link = url + reverse(
"confirm_email", kwargs={"verification_key": self.verification_key}
)
email_subject = _("Confirm your email address on %s") % SITE_NAME
email_body = render_to_string(
"email_confirmation.txt",
{
"verification_link": link,
"site_name": SITE_NAME,
},
)
self.last_send_date = timezone.now()
self.last_status = 'OK'
self.last_status = "OK"
# if settings.DEBUG:
# print('DEBUG: %s' % link)
try:
send_mail(
email_subject, email_body, DEFAULT_FROM_EMAIL,
[self.email])
send_mail(email_subject, email_body, DEFAULT_FROM_EMAIL, [self.email])
except Exception as e:
self.last_status = "%s" % e
self.save()
return True
def __str__(self):
return '%s (%i) from %s' % (self.email, self.pk, self.user)
return "%s (%i) from %s" % (self.email, self.pk, self.user)
class UnconfirmedOpenId(BaseAccountModel):
'''
"""
Model holding unconfirmed OpenIDs
'''
"""
openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Meta class
'''
verbose_name = _('unconfirmed OpenID')
verbose_name_plural = ('unconfirmed_OpenIDs')
"""
verbose_name = _("unconfirmed OpenID")
verbose_name_plural = "unconfirmed_OpenIDs"
def __str__(self):
return '%s (%i) from %s' % (self.openid, self.pk, self.user)
return "%s (%i) from %s" % (self.openid, self.pk, self.user)
class ConfirmedOpenId(BaseAccountModel):
'''
"""
Model holding confirmed OpenIDs, as well as the relation to
the assigned photo
'''
"""
openid = models.URLField(unique=True, max_length=MAX_LENGTH_URL)
photo = models.ForeignKey(
Photo,
related_name='openids',
related_name="openids",
blank=True,
null=True,
on_delete=models.deletion.SET_NULL,
@@ -444,25 +463,27 @@ class ConfirmedOpenId(BaseAccountModel):
access_count = models.BigIntegerField(default=0, editable=False)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Meta class
'''
verbose_name = _('confirmed OpenID')
verbose_name_plural = _('confirmed OpenIDs')
"""
verbose_name = _("confirmed OpenID")
verbose_name_plural = _("confirmed OpenIDs")
def set_photo(self, photo):
'''
"""
Helper method to save photo
'''
"""
self.photo = photo
self.save()
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
url = urlsplit(self.openid)
if url.username: # pragma: no cover
password = url.password or ''
netloc = url.username + ':' + password + '@' + url.hostname
password = url.password or ""
netloc = url.username + ":" + password + "@" + url.hostname
else:
netloc = url.hostname
lowercase_url = urlunsplit(
@@ -470,37 +491,44 @@ class ConfirmedOpenId(BaseAccountModel):
)
self.openid = lowercase_url
self.digest = hashlib.sha256(openid_variations(lowercase_url)[0].encode('utf-8')).hexdigest()
self.alt_digest1 = hashlib.sha256(openid_variations(lowercase_url)[1].encode('utf-8')).hexdigest()
self.alt_digest2 = hashlib.sha256(openid_variations(lowercase_url)[2].encode('utf-8')).hexdigest()
self.alt_digest3 = hashlib.sha256(openid_variations(lowercase_url)[3].encode('utf-8')).hexdigest()
self.digest = hashlib.sha256(
openid_variations(lowercase_url)[0].encode("utf-8")
).hexdigest()
self.alt_digest1 = hashlib.sha256(
openid_variations(lowercase_url)[1].encode("utf-8")
).hexdigest()
self.alt_digest2 = hashlib.sha256(
openid_variations(lowercase_url)[2].encode("utf-8")
).hexdigest()
self.alt_digest3 = hashlib.sha256(
openid_variations(lowercase_url)[3].encode("utf-8")
).hexdigest()
return super().save(force_insert, force_update, using, update_fields)
def __str__(self):
return '%s (%i) (%s)' % (self.openid, self.pk, self.user)
return "%s (%i) (%s)" % (self.openid, self.pk, self.user)
class OpenIDNonce(models.Model):
'''
"""
Model holding OpenID Nonces
See also: https://github.com/edx/django-openid-auth/
'''
"""
server_url = models.CharField(max_length=255)
timestamp = models.IntegerField()
salt = models.CharField(max_length=128)
def __str__(self):
return '%s (%i) (timestamp: %i)' % (
self.server_url,
self.pk,
self.timestamp)
return "%s (%i) (timestamp: %i)" % (self.server_url, self.pk, self.timestamp)
class OpenIDAssociation(models.Model):
'''
"""
Model holding the relation/association about OpenIDs
'''
"""
server_url = models.TextField(max_length=2047)
handle = models.CharField(max_length=255)
secret = models.TextField(max_length=255) # stored base64 encoded
@@ -509,56 +537,62 @@ class OpenIDAssociation(models.Model):
assoc_type = models.TextField(max_length=64)
def __str__(self):
return '%s (%i) (%s, lifetime: %i)' % (
return "%s (%i) (%s, lifetime: %i)" % (
self.server_url,
self.pk,
self.assoc_type,
self.lifetime)
self.lifetime,
)
class DjangoOpenIDStore(OpenIDStore):
'''
"""
The Python openid library needs an OpenIDStore subclass to persist data
related to OpenID authentications. This one uses our Django models.
'''
"""
@staticmethod
def storeAssociation(server_url, association): # pragma: no cover
'''
"""
Helper method to store associations
'''
"""
assoc = OpenIDAssociation(
server_url=server_url,
handle=association.handle,
secret=base64.encodebytes(association.secret),
issued=association.issued,
lifetime=association.issued,
assoc_type=association.assoc_type)
assoc_type=association.assoc_type,
)
assoc.save()
def getAssociation(self, server_url, handle=None): # pragma: no cover
'''
"""
Helper method to get associations
'''
"""
assocs = []
if handle is not None:
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
server_url=server_url,
handle=handle)
server_url=server_url, handle=handle
)
else:
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
server_url=server_url)
server_url=server_url
)
if not assocs:
return None
associations = []
for assoc in assocs:
if isinstance(assoc.secret, str):
assoc.secret = assoc.secret.split("b'")[1].split("'")[0]
assoc.secret = bytes(assoc.secret, 'utf-8')
association = OIDAssociation(assoc.handle,
base64.decodebytes(assoc.secret),
assoc.issued, assoc.lifetime,
assoc.assoc_type)
assoc.secret = bytes(assoc.secret, "utf-8")
association = OIDAssociation(
assoc.handle,
base64.decodebytes(assoc.secret),
assoc.issued,
assoc.lifetime,
assoc.assoc_type,
)
expires = 0
try:
# pylint: disable=no-member
@@ -575,12 +609,14 @@ class DjangoOpenIDStore(OpenIDStore):
@staticmethod
def removeAssociation(server_url, handle): # pragma: no cover
'''
"""
Helper method to remove associations
'''
"""
assocs = list(
OpenIDAssociation.objects.filter( # pylint: disable=no-member
server_url=server_url, handle=handle))
server_url=server_url, handle=handle
)
)
assocs_exist = len(assocs) > 0
for assoc in assocs:
assoc.delete()
@@ -588,9 +624,9 @@ class DjangoOpenIDStore(OpenIDStore):
@staticmethod
def useNonce(server_url, timestamp, salt): # pragma: no cover
'''
"""
Helper method to 'use' nonces
'''
"""
# Has nonce expired?
if abs(timestamp - time.time()) > oidnonce.SKEW:
return False
@@ -598,27 +634,30 @@ class DjangoOpenIDStore(OpenIDStore):
nonce = OpenIDNonce.objects.get( # pylint: disable=no-member
server_url__exact=server_url,
timestamp__exact=timestamp,
salt__exact=salt)
salt__exact=salt,
)
except ObjectDoesNotExist:
nonce = OpenIDNonce.objects.create( # pylint: disable=no-member
server_url=server_url, timestamp=timestamp, salt=salt)
server_url=server_url, timestamp=timestamp, salt=salt
)
return True
nonce.delete()
return False
@staticmethod
def cleanupNonces(): # pragma: no cover
'''
"""
Helper method to cleanup nonces
'''
"""
timestamp = int(time.time()) - oidnonce.SKEW
# pylint: disable=no-member
OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete()
@staticmethod
def cleanupAssociations(): # pragma: no cover
'''
"""
Helper method to cleanup associations
'''
"""
OpenIDAssociation.objects.extra( # pylint: disable=no-member
where=['issued + lifetimeint < (%s)' % time.time()]).delete()
where=["issued + lifetimeint < (%s)" % time.time()]
).delete()

View File

@@ -1,84 +1,122 @@
'''
# -*- coding: utf-8 -*-
"""
Reading libravatar export
'''
"""
import binascii
import os
from io import BytesIO
import gzip
import xml.etree.ElementTree
import base64
from PIL import Image
import django
import sys
sys.path.append(
os.path.join(
os.path.dirname(__file__),
"..",
"..",
)
)
SCHEMAROOT = 'https://www.libravatar.org/schemas/export/0.2'
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup()
# pylint: disable=wrong-import-position
from ivatar.settings import SCHEMAROOT
def read_gzdata(gzdata=None):
'''
"""
Read gzipped data file
'''
"""
emails = [] # pylint: disable=invalid-name
openids = [] # pylint: disable=invalid-name
photos = [] # 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
fh = gzip.open(BytesIO(gzdata), 'rb') # pylint: disable=invalid-name
fh = gzip.open(BytesIO(gzdata), "rb") # pylint: disable=invalid-name
content = fh.read()
fh.close()
root = xml.etree.ElementTree.fromstring(content)
if not root.tag == '{%s}user' % SCHEMAROOT:
print('Unknown export format: %s' % root.tag)
if not root.tag == "{%s}user" % SCHEMAROOT:
print("Unknown export format: %s" % root.tag)
exit(-1)
# Username
for item in root.findall('{%s}account' % SCHEMAROOT)[0].items():
if item[0] == 'username':
for item in root.findall("{%s}account" % SCHEMAROOT)[0].items():
if item[0] == "username":
username = item[1]
if item[0] == 'password':
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': email.text, 'photo_id': email.attrib['photo_id']})
for email in root.findall("{%s}emails" % SCHEMAROOT)[0]:
if email.tag == "{%s}email" % SCHEMAROOT:
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': openid.text, 'photo_id': openid.attrib['photo_id']})
for openid in root.findall("{%s}openids" % SCHEMAROOT)[0]:
if openid.tag == "{%s}openid" % SCHEMAROOT:
openids.append(
{"openid": openid.text, "photo_id": openid.attrib["photo_id"]}
)
# Photos
for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]:
if photo.tag == '{%s}photo' % SCHEMAROOT:
for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]:
if photo.tag == "{%s}photo" % SCHEMAROOT:
try:
data = base64.decodebytes(bytes(photo.text, 'utf-8'))
# Safty measures to make sure we do not try to parse
# a binary encoded string
photo.text = photo.text.strip("'")
photo.text = photo.text.strip("\\n")
photo.text = photo.text.lstrip("b'")
data = base64.decodebytes(bytes(photo.text, "utf-8"))
except binascii.Error as exc:
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], 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, Id: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], 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
photo.text.replace('\n', '')
photos.append({
'data': photo.text,
'format': photo.attrib['format'],
'id': photo.attrib['id'],
})
photo.text.replace("\n", "")
photos.append(
{
"data": photo.text,
"format": photo.attrib["format"],
"id": photo.attrib["id"],
}
)
return {
'emails': emails,
'openids': openids,
'photos': photos,
'username': username,
'password': password,
"emails": emails,
"openids": openids,
"photos": photos,
"username": username,
"password": password,
}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
@@ -23,7 +23,7 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
{% for email in emails %}
<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>
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email.email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email.email }}</label>
</div>
{% endfor %}
{% endif %}

View File

@@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Export your data' %}{% endblock title %}
{% block content %}
<h1>{% trans 'Export your data' %}</h1>
<p>{% trans 'Libravatar will now export all of your personal data to a compressed XML file.' %}</p>
<form action="{% url 'export' %}" method="post" name="export">{% csrf_token %}
<p><button type="submit" class="button">{% trans 'Export' %}</button>&nbsp;
<a href="{% url 'profile' %}" class="button">{% trans 'Cancel' %}</a>
</p>
</form>
{% endblock content %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,119 +1,155 @@
'''
# -*- coding: utf-8 -*-
"""
URLs for ivatar.ivataraccount
'''
"""
from django.urls import path
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 PasswordResetDoneView,\
PasswordResetConfirmView, PasswordResetCompleteView
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
from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
from . views import ImportPhotoView, RawImageView, DeletePhotoView
from . views import UploadPhotoView, AssignPhotoOpenIDView
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
from . views import CropPhotoView
from . views import UserPreferenceView, UploadLibravatarExportView
from . views import ResendConfirmationMailView
from . views import IvatarLoginView
from . views import DeleteAccountView
from .views import ProfileView, PasswordResetView
from .views import CreateView, PasswordSetView, AddEmailView
from .views import RemoveUnconfirmedEmailView, ConfirmEmailView
from .views import RemoveConfirmedEmailView, AssignPhotoEmailView
from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
from .views import ImportPhotoView, RawImageView, DeletePhotoView
from .views import UploadPhotoView, AssignPhotoOpenIDView
from .views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
from .views import CropPhotoView
from .views import UserPreferenceView, UploadLibravatarExportView
from .views import ResendConfirmationMailView
from .views import IvatarLoginView
from .views import DeleteAccountView
from .views import ExportView
# Define URL patterns, self documenting
# To see the fancy, colorful evaluation of these use:
# ./manager show_urls
urlpatterns = [ # pylint: disable=invalid-name
path('new/', CreateView.as_view(), name='new_account'),
path('login/', IvatarLoginView.as_view(), name='login'),
path("new/", CreateView.as_view(), name="new_account"),
path("login/", IvatarLoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(next_page="/"), name="logout"),
path(
'logout/', LogoutView.as_view(next_page='/'),
name='logout'),
path('password_change/',
PasswordChangeView.as_view(template_name='password_change.html'),
name='password_change'),
path('password_change/done/',
PasswordChangeDoneView.as_view(template_name='password_change_done.html'),
name='password_change_done'),
path('password_reset/',
PasswordResetView.as_view(template_name='password_reset.html'),
name='password_reset'),
path('password_reset/done/',
PasswordResetDoneView.as_view(
template_name='password_reset_submitted.html'),
name='password_reset_done'),
path('reset/<uidb64>/<token>/',
PasswordResetConfirmView.as_view(
template_name='password_change.html'),
name='password_reset_confirm'),
path('reset/done/',
PasswordResetCompleteView.as_view(
template_name='password_change_done.html'),
name='password_reset_complete'),
path('export/', login_required(
TemplateView.as_view(template_name='export.html')
), name='export'),
path('delete/', DeleteAccountView.as_view(), name='delete'),
path('profile/', ProfileView.as_view(), name='profile'),
url('profile/(?P<profile_username>.+)', ProfileView.as_view(), name='profile_with_profile_username'),
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'),
path('password_set/', PasswordSetView.as_view(), name='password_set'),
"password_change/",
PasswordChangeView.as_view(template_name="password_change.html"),
name="password_change",
),
path(
"password_change/done/",
PasswordChangeDoneView.as_view(template_name="password_change_done.html"),
name="password_change_done",
),
path(
"password_reset/",
PasswordResetView.as_view(template_name="password_reset.html"),
name="password_reset",
),
path(
"password_reset/done/",
PasswordResetDoneView.as_view(template_name="password_reset_submitted.html"),
name="password_reset_done",
),
path(
"reset/<uidb64>/<token>/",
PasswordResetConfirmView.as_view(template_name="password_change.html"),
name="password_reset_confirm",
),
path(
"reset/done/",
PasswordResetCompleteView.as_view(template_name="password_change_done.html"),
name="password_reset_complete",
),
path(
"export/",
ExportView.as_view(),
name="export",
),
path("delete/", DeleteAccountView.as_view(), name="delete"),
path("profile/", ProfileView.as_view(), name="profile"),
url(
r'remove_unconfirmed_openid/(?P<openid_id>\d+)',
"profile/(?P<profile_username>.+)",
ProfileView.as_view(),
name="profile_with_profile_username",
),
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"),
path("password_set/", PasswordSetView.as_view(), name="password_set"),
url(
r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
RemoveUnconfirmedOpenIDView.as_view(),
name='remove_unconfirmed_openid'),
name="remove_unconfirmed_openid",
),
url(
r'remove_confirmed_openid/(?P<openid_id>\d+)',
RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'),
r"remove_confirmed_openid/(?P<openid_id>\d+)",
RemoveConfirmedOpenIDView.as_view(),
name="remove_confirmed_openid",
),
url(
r'openid_redirection/(?P<openid_id>\d+)',
RedirectOpenIDView.as_view(), name='openid_redirection'),
r"openid_redirection/(?P<openid_id>\d+)",
RedirectOpenIDView.as_view(),
name="openid_redirection",
),
url(
r'confirm_openid/(?P<openid_id>\w+)',
ConfirmOpenIDView.as_view(), name='confirm_openid'),
r"confirm_openid/(?P<openid_id>\w+)",
ConfirmOpenIDView.as_view(),
name="confirm_openid",
),
url(
r'confirm_email/(?P<verification_key>\w+)',
ConfirmEmailView.as_view(), name='confirm_email'),
r"confirm_email/(?P<verification_key>\w+)",
ConfirmEmailView.as_view(),
name="confirm_email",
),
url(
r'remove_unconfirmed_email/(?P<email_id>\d+)',
RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'),
r"remove_unconfirmed_email/(?P<email_id>\d+)",
RemoveUnconfirmedEmailView.as_view(),
name="remove_unconfirmed_email",
),
url(
r'remove_confirmed_email/(?P<email_id>\d+)',
RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'),
r"remove_confirmed_email/(?P<email_id>\d+)",
RemoveConfirmedEmailView.as_view(),
name="remove_confirmed_email",
),
url(
r'assign_photo_email/(?P<email_id>\d+)',
AssignPhotoEmailView.as_view(), name='assign_photo_email'),
r"assign_photo_email/(?P<email_id>\d+)",
AssignPhotoEmailView.as_view(),
name="assign_photo_email",
),
url(
r'assign_photo_openid/(?P<openid_id>\d+)',
AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'),
r"assign_photo_openid/(?P<openid_id>\d+)",
AssignPhotoOpenIDView.as_view(),
name="assign_photo_openid",
),
url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
url(
r'import_photo/$',
ImportPhotoView.as_view(), name='import_photo'),
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
ImportPhotoView.as_view(),
name="import_photo",
),
url(
r'import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)',
ImportPhotoView.as_view(), name='import_photo'),
r"import_photo/(?P<email_id>\d+)",
ImportPhotoView.as_view(),
name="import_photo",
),
url(r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"),
url(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
url(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
url(r"pref/$", UserPreferenceView.as_view(), name="user_preference"),
url(r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"),
url(
r'import_photo/(?P<email_id>\d+)',
ImportPhotoView.as_view(), name='import_photo'),
r"upload_export/(?P<save>save)$",
UploadLibravatarExportView.as_view(),
name="upload_export",
),
url(
r'delete_photo/(?P<pk>\d+)',
DeletePhotoView.as_view(), name='delete_photo'),
url(r'raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
url(r'crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'),
url(r'pref/$', UserPreferenceView.as_view(), name='user_preference'),
url(r'upload_export/$', UploadLibravatarExportView.as_view(), name='upload_export'),
url(r'upload_export/(?P<save>save)$',
UploadLibravatarExportView.as_view(), name='upload_export'),
url(r'resend_confirmation_mail/(?P<email_id>\d+)',
ResendConfirmationMailView.as_view(), name='resend_confirmation_mail'),
r"resend_confirmation_mail/(?P<email_id>\d+)",
ResendConfirmationMailView.as_view(),
name="resend_confirmation_mail",
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,14 @@
# -*- coding: utf-8 -*-
"""
Middleware classes
"""
from django.utils.deprecation import MiddlewareMixin
class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods
class MultipleProxyMiddleware(
MiddlewareMixin
): # pylint: disable=too-few-public-methods
"""
Middleware to rewrite proxy headers for deployments
with multiple proxies
@@ -14,5 +19,7 @@ class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-publi
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']
if "HTTP_X_FORWARDED_SERVER" in request.META:
request.META["HTTP_X_FORWARDED_HOST"] = request.META[
"HTTP_X_FORWARDED_SERVER"
]

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
views under /
'''
"""
from io import BytesIO
from os import path
import hashlib
@@ -28,24 +29,24 @@ from robohash import Robohash
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
from ivatar.settings import CACHE_RESPONSE
from ivatar.settings import CACHE_IMAGES_MAX_AGE
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
from . ivataraccount.models import pil_format, file_format
from . utils import mm_ng
from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
from .ivataraccount.models import pil_format, file_format
from .utils import mm_ng
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 "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':
if sizetemp != "" and sizetemp is not None and sizetemp != "0":
try:
if int(sizetemp) > 0:
size = int(sizetemp)
@@ -60,39 +61,54 @@ def get_size(request, size=DEFAULT_AVATAR_SIZE):
class CachingHttpResponse(HttpResponse):
'''
"""
Handle caching of response
'''
def __init__(self, uri, content=b'', content_type=None, status=200, # pylint: disable=too-many-arguments
reason=None, charset=None):
"""
def __init__(
self,
uri,
content=b"",
content_type=None,
status=200, # pylint: disable=too-many-arguments
reason=None,
charset=None,
):
if CACHE_RESPONSE:
caches['filesystem'].set(uri, {
'content': content,
'content_type': content_type,
'status': status,
'reason': reason,
'charset': charset
})
caches["filesystem"].set(
uri,
{
"content": content,
"content_type": content_type,
"status": status,
"reason": reason,
"charset": charset,
},
)
super().__init__(content, content_type, status, reason, charset)
class AvatarImageView(TemplateView):
'''
"""
View to return (binary) image, based on OpenID/Email (both by digest)
'''
"""
# TODO: Do cache resize images!! Memcached?
def options(self, request, *args, **kwargs):
response = HttpResponse("", content_type='text/plain')
response['Allow'] = "404 mm mp retro pagan wavatar monsterid robohash identicon"
response = HttpResponse("", content_type="text/plain")
response["Allow"] = "404 mm mp retro pagan wavatar monsterid robohash identicon"
return response
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
'''
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 = get_size(request)
imgformat = 'png'
imgformat = "png"
obj = None
default = None
forcedefault = False
@@ -102,65 +118,69 @@ class AvatarImageView(TemplateView):
# Check the cache first
if CACHE_RESPONSE:
centry = caches['filesystem'].get(uri)
centry = caches["filesystem"].get(uri)
if centry:
# For DEBUG purpose only print('Cached entry for %s' % uri)
return HttpResponse(
centry['content'],
content_type=centry['content_type'],
status=centry['status'],
reason=centry['reason'],
charset=centry['charset'])
centry["content"],
content_type=centry["content_type"],
status=centry["status"],
reason=centry["reason"],
charset=centry["charset"],
)
# In case no digest at all is provided, return to home page
if 'digest' not in kwargs:
return HttpResponseRedirect(reverse_lazy('home'))
if "digest" not in kwargs:
return HttpResponseRedirect(reverse_lazy("home"))
if 'd' in request.GET:
default = request.GET['d']
if 'default' in request.GET:
default = request.GET['default']
if "d" in request.GET:
default = request.GET["d"]
if "default" in request.GET:
default = request.GET["default"]
if 'f' in request.GET:
if request.GET['f'] == 'y':
if "f" in request.GET:
if request.GET["f"] == "y":
forcedefault = True
if 'forcedefault' in request.GET:
if request.GET['forcedefault'] == 'y':
if "forcedefault" in request.GET:
if request.GET["forcedefault"] == "y":
forcedefault = True
if 'gravatarredirect' in request.GET:
if request.GET['gravatarredirect'] == 'y':
if "gravatarredirect" in request.GET:
if request.GET["gravatarredirect"] == "y":
gravatarredirect = True
if 'gravatarproxy' in request.GET:
if request.GET['gravatarproxy'] == 'n':
if "gravatarproxy" in request.GET:
if request.GET["gravatarproxy"] == "n":
gravatarproxy = False
try:
obj = model.objects.get(digest=kwargs['digest'])
obj = model.objects.get(digest=kwargs["digest"])
except ObjectDoesNotExist:
try:
obj = model.objects.get(digest_sha256=kwargs['digest'])
obj = model.objects.get(digest_sha256=kwargs["digest"])
except ObjectDoesNotExist:
model = ConfirmedOpenId
try:
d = kwargs['digest'] # pylint: disable=invalid-name
d = kwargs["digest"] # pylint: disable=invalid-name
# OpenID is tricky. http vs. https, versus trailing slash or not
# However, some users eventually have added their variations already
# and therfore we need to use filter() and first()
obj = model.objects.filter(
Q(digest=d) |
Q(alt_digest1=d) |
Q(alt_digest2=d) |
Q(alt_digest3=d)).first()
except: # pylint: disable=bare-except
Q(digest=d)
| Q(alt_digest1=d)
| Q(alt_digest2=d)
| Q(alt_digest3=d)
).first()
except Exception: # pylint: disable=bare-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
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!
@@ -169,119 +189,115 @@ class AvatarImageView(TemplateView):
# 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
url = (
reverse_lazy("gravatarproxy", args=[kwargs["digest"]])
+ "?s=%i" % size
)
# Ensure we do not convert None to string 'None'
if default:
url += "&default=%s" % default
return HttpResponseRedirect(url)
# Return the default URL, as specified, or 404 Not Found, if default=404
if default:
# Proxy to gravatar to generate wavatar - lazy me
if str(default) == 'wavatar':
url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
+ '?s=%i' % size + '&default=%s&f=y' % default
if str(default) == "wavatar":
url = (
reverse_lazy("gravatarproxy", args=[kwargs["digest"]])
+ "?s=%i" % size
+ "&default=%s&f=y" % default
)
return HttpResponseRedirect(url)
if str(default) == str(404):
return HttpResponseNotFound(_('<h1>Image not found</h1>'))
return HttpResponseNotFound(_("<h1>Image not found</h1>"))
if str(default) == 'monsterid':
monsterdata = BuildMonster(seed=kwargs['digest'], size=(size, size))
if str(default) == "monsterid":
monsterdata = BuildMonster(seed=kwargs["digest"], size=(size, size))
data = BytesIO()
monsterdata.save(data, 'PNG', quality=JPEG_QUALITY)
monsterdata.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'robohash':
roboset = 'any'
if request.GET.get('robohash'):
roboset = request.GET.get('robohash')
robohash = Robohash(kwargs['digest'])
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')
robohash.img.save(data, format="png")
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'retro':
identicon = Identicon.render(kwargs['digest'])
if str(default) == "retro":
identicon = Identicon.render(kwargs["digest"])
data = BytesIO()
img = Image.open(BytesIO(identicon))
img = img.resize((size, size), Image.ANTIALIAS)
img.save(data, 'PNG', quality=JPEG_QUALITY)
img.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'pagan':
paganobj = pagan.Avatar(kwargs['digest'])
if str(default) == "pagan":
paganobj = pagan.Avatar(kwargs["digest"])
data = BytesIO()
img = paganobj.img.resize((size, size), Image.ANTIALIAS)
img.save(data, 'PNG', quality=JPEG_QUALITY)
img.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'identicon':
if str(default) == "identicon":
p = Pydenticon5() # pylint: disable=invalid-name
# In order to make use of the whole 32 bytes digest, we need to redigest them.
newdigest = hashlib.md5(bytes(kwargs['digest'], 'utf-8')).hexdigest()
newdigest = hashlib.md5(
bytes(kwargs["digest"], "utf-8")
).hexdigest()
img = p.draw(newdigest, size, 0)
data = BytesIO()
img.save(data, 'PNG', quality=JPEG_QUALITY)
img.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'mmng':
mmngimg = mm_ng(idhash=kwargs['digest'], size=size)
if str(default) == "mmng":
mmngimg = mm_ng(idhash=kwargs["digest"], size=size)
data = BytesIO()
mmngimg.save(data, 'PNG', quality=JPEG_QUALITY)
mmngimg.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'mm' or str(default) == 'mp':
if str(default) == "mm" or str(default) == "mp":
# If mm is explicitly given, we need to catch that
static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png'))
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')
static_img = path.join("static", "img", "mm", "512.png")
# We trust static/ is mapped to /static/
return HttpResponseRedirect('/' + static_img)
return HttpResponseRedirect("/" + static_img)
return HttpResponseRedirect(default)
static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png'))
static_img = path.join(
"static", "img", "nobody", "%s%s" % (str(size), ".png")
)
if not path.isfile(static_img):
# We trust this exists!!!
static_img = path.join('static', 'img', 'nobody', '512.png')
static_img = path.join("static", "img", "nobody", "512.png")
# We trust static/ is mapped to /static/
return HttpResponseRedirect('/' + static_img)
return HttpResponseRedirect("/" + static_img)
imgformat = obj.photo.format
photodata = Image.open(BytesIO(obj.photo.data))
@@ -298,31 +314,35 @@ class AvatarImageView(TemplateView):
obj.photo.save()
obj.access_count += 1
obj.save()
if imgformat == 'jpg':
imgformat = 'jpeg'
response = CachingHttpResponse(
uri,
data,
content_type='image/%s' % imgformat)
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
if imgformat == "jpg":
imgformat = "jpeg"
response = CachingHttpResponse(uri, data, content_type="image/%s" % imgformat)
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
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,too-many-return-statements
'''
def get(
self, request, *args, **kwargs
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
"""
Override get from parent class
'''
"""
def redir_default(default=None):
url = reverse_lazy(
'avatar_view',
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
url = (
reverse_lazy("avatar_view", args=[kwargs["digest"]])
+ "?s=%i" % size
+ "&forcedefault=y"
)
if default is not None:
url += '&default=%s' % default
url += "&default=%s" % default
return HttpResponseRedirect(url)
size = get_size(request)
@@ -330,70 +350,75 @@ class GravatarProxyView(View):
default = None
try:
if str(request.GET['default']) != 'None':
default = request.GET['default']
except: # pylint: disable=bare-except
if str(request.GET["default"]) != "None":
default = request.GET["default"]
except Exception: # pylint: disable=bare-except
pass
if str(default) != 'wavatar':
if str(default) != "wavatar":
# 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
if cache.get(gravatar_test_url) == 'default':
gravatar_test_url = (
"https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % 50
)
if cache.get(gravatar_test_url) == "default":
# DEBUG only
# print("Cached Gravatar response: Default.")
return redir_default(default)
try:
testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
data = BytesIO(testdata.read())
if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a':
cache.set(gravatar_test_url, 'default', 60)
if (
hashlib.md5(data.read()).hexdigest()
== "71bc262d627971d13fe6f3180b93062a"
):
cache.set(gravatar_test_url, "default", 60)
return redir_default(default)
except Exception as exc: # pylint: disable=broad-except
print('Gravatar test url fetch failed: %s' % exc)
print("Gravatar test url fetch failed: %s" % exc)
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % size + '&d=%s' % default
gravatar_url = (
"https://secure.gravatar.com/avatar/"
+ kwargs["digest"]
+ "?s=%i" % size
+ "&d=%s" % default
)
try:
if cache.get(gravatar_url) == 'err':
print('Cached Gravatar fetch failed with URL error')
if cache.get(gravatar_url) == "err":
print("Cached Gravatar fetch failed with URL error: %s" % gravatar_url)
return redir_default(default)
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)
cache.set(gravatar_url, 'err', 30)
"Gravatar fetch failed with an unexpected %s HTTP error: %s"
% (exc.code, gravatar_url)
)
cache.set(gravatar_url, "err", 30)
return redir_default(default)
except URLError as exc:
print(
'Gravatar fetch failed with URL error: %s' %
exc.reason)
cache.set(gravatar_url, 'err', 30)
print("Gravatar fetch failed with URL error: %s" % exc.reason)
cache.set(gravatar_url, "err", 30)
return redir_default(default)
except SSLError as exc:
print(
'Gravatar fetch failed with SSL error: %s' %
exc.reason)
cache.set(gravatar_url, 'err', 30)
print("Gravatar fetch failed with SSL error: %s" % exc.reason)
cache.set(gravatar_url, "err", 30)
return redir_default(default)
try:
data = BytesIO(gravatarimagedata.read())
img = Image.open(data)
data.seek(0)
response = HttpResponse(
data.read(),
content_type='image/%s' % file_format(img.format))
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
data.read(), content_type="image/%s" % file_format(img.format)
)
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
except ValueError as exc:
print('Value error: %s' % exc)
print("Value error: %s" % exc)
return redir_default(default)
# We shouldn't reach this point... But make sure we do something
@@ -401,14 +426,17 @@ class GravatarProxyView(View):
class StatsView(TemplateView, JsonResponse):
'''
"""
Return stats
'''
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
"""
def get(
self, request, *args, **kwargs
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
retval = {
'users': User.objects.all().count(),
'mails': ConfirmedEmail.objects.all().count(),
'openids': ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member
"users": User.objects.all().count(),
"mails": ConfirmedEmail.objects.all().count(),
"openids": ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member
}
return JsonResponse(retval)

View File

@@ -2,25 +2,36 @@ autopep8
bcrypt
defusedxml
Django
django-anymail[mailgun]
django-auth-ldap
django-bootstrap4
django-coverage-plugin
django-extensions
django-ipware
django-user-accounts
email-validator
fabric
flake8-respect-noqa
git+https://github.com/ercpe/pydenticon5.git
git+https://github.com/flavono123/identicon.git
git+https://github.com/ofalk/django-openid-auth
git+https://github.com/ofalk/monsterid.git
git+https://github.com/ofalk/Robohash.git@devel
mysqlclient
notsetuptools
pagan
Pillow
pip
psycopg2-binary
py3dns
pydocstyle
pyLibravatar
pylint
PyMySQL
python3-openid
python-coveralls
python-language-server
python-memcached
python3-openid
pytz
rope
setuptools
@@ -28,13 +39,3 @@ six
social-auth-app-django
wheel
yapf
django-anymail[mailgun]
mysqlclient
psycopg2-binary
notsetuptools
git+https://github.com/ofalk/monsterid.git
git+https://github.com/ofalk/Robohash.git@devel
python-memcached
git+https://github.com/ercpe/pydenticon5.git
git+https://github.com/flavono123/identicon.git
pagan

View File

@@ -15,6 +15,7 @@
<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 'export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Download your libravatar data' %}</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>