mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-11 18:56:23 +00:00
v1.5 massive (code) update
This commit is contained in:
@@ -7,8 +7,12 @@ omit =
|
|||||||
import_libravatar.py
|
import_libravatar.py
|
||||||
requirements.txt
|
requirements.txt
|
||||||
static/admin/*
|
static/admin/*
|
||||||
static/humans.txt
|
**/static/humans.txt
|
||||||
static/img/robots.txt
|
**/static/img/robots.txt
|
||||||
|
ivatar/ivataraccount/read_libravatar_export.py
|
||||||
|
templates/maintenance.html
|
||||||
|
encryption_test.py
|
||||||
|
libravatarproxy.py
|
||||||
|
|
||||||
|
|
||||||
[html]
|
[html]
|
||||||
|
|||||||
6
.flake8
Normal file
6
.flake8
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[flake8]
|
||||||
|
ignore = E501, W503, E402
|
||||||
|
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
74
.pre-commit-config.yaml
Normal 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
229
config.py
@@ -1,6 +1,7 @@
|
|||||||
''' yes
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
Configuration overrides for settings.py
|
Configuration overrides for settings.py
|
||||||
'''
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -14,50 +15,61 @@ from ivatar.settings import INSTALLED_APPS
|
|||||||
from ivatar.settings import TEMPLATES
|
from ivatar.settings import TEMPLATES
|
||||||
|
|
||||||
ADMIN_USERS = []
|
ADMIN_USERS = []
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
INSTALLED_APPS.extend([
|
INSTALLED_APPS.extend(
|
||||||
'django_extensions',
|
[
|
||||||
'django_openid_auth',
|
"django_extensions",
|
||||||
'bootstrap4',
|
"django_openid_auth",
|
||||||
'anymail',
|
"bootstrap4",
|
||||||
'ivatar',
|
"anymail",
|
||||||
'ivatar.ivataraccount',
|
"ivatar",
|
||||||
'ivatar.tools',
|
"ivatar.ivataraccount",
|
||||||
])
|
"ivatar.tools",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
MIDDLEWARE.extend([
|
MIDDLEWARE.extend(
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
[
|
||||||
])
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
|
]
|
||||||
|
)
|
||||||
MIDDLEWARE.insert(
|
MIDDLEWARE.insert(
|
||||||
0, 'ivatar.middleware.MultipleProxyMiddleware',
|
0,
|
||||||
|
"ivatar.middleware.MultipleProxyMiddleware",
|
||||||
)
|
)
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
# Enable this to allow LDAP authentication.
|
# Enable this to allow LDAP authentication.
|
||||||
# See INSTALL for more information.
|
# See INSTALL for more information.
|
||||||
# 'django_auth_ldap.backend.LDAPBackend',
|
# 'django_auth_ldap.backend.LDAPBackend',
|
||||||
'django_openid_auth.auth.OpenIDBackend',
|
"django_openid_auth.auth.OpenIDBackend",
|
||||||
'django.contrib.auth.backends.ModelBackend',
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
)
|
)
|
||||||
|
|
||||||
TEMPLATES[0]['DIRS'].extend([
|
TEMPLATES[0]["DIRS"].extend(
|
||||||
os.path.join(BASE_DIR, 'templates'),
|
[
|
||||||
])
|
os.path.join(BASE_DIR, "templates"),
|
||||||
TEMPLATES[0]['OPTIONS']['context_processors'].append(
|
]
|
||||||
'ivatar.context_processors.basepage',
|
)
|
||||||
|
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
|
||||||
|
"ivatar.context_processors.basepage",
|
||||||
)
|
)
|
||||||
|
|
||||||
OPENID_CREATE_USERS = True
|
OPENID_CREATE_USERS = True
|
||||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||||
|
|
||||||
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
|
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
|
||||||
IVATAR_VERSION = '1.4'
|
IVATAR_VERSION = "1.5"
|
||||||
|
|
||||||
SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/')
|
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
|
||||||
BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/')
|
|
||||||
|
|
||||||
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_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
||||||
|
|
||||||
MAX_NUM_PHOTOS = 5
|
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
|
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
||||||
|
|
||||||
BOOTSTRAP4 = {
|
BOOTSTRAP4 = {
|
||||||
'include_jquery': False,
|
"include_jquery": False,
|
||||||
'javascript_in_head': False,
|
"javascript_in_head": False,
|
||||||
'css_url': {
|
"css_url": {
|
||||||
'href': '/static/css/bootstrap.min.css',
|
"href": "/static/css/bootstrap.min.css",
|
||||||
'integrity':
|
"integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB",
|
||||||
'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB',
|
"crossorigin": "anonymous",
|
||||||
'crossorigin': 'anonymous',
|
|
||||||
},
|
},
|
||||||
'javascript_url': {
|
"javascript_url": {
|
||||||
'url': '/static/js/bootstrap.min.js',
|
"url": "/static/js/bootstrap.min.js",
|
||||||
'integrity': '',
|
"integrity": "",
|
||||||
'crossorigin': 'anonymous',
|
"crossorigin": "anonymous",
|
||||||
},
|
},
|
||||||
'popper_url': {
|
"popper_url": {
|
||||||
'url': '/static/js/popper.min.js',
|
"url": "/static/js/popper.min.js",
|
||||||
'integrity':
|
"integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49",
|
||||||
'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49',
|
"crossorigin": "anonymous",
|
||||||
'crossorigin': 'anonymous',
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if 'EMAIL_BACKEND' in os.environ:
|
if "EMAIL_BACKEND" in os.environ:
|
||||||
EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover
|
EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover
|
||||||
else:
|
else:
|
||||||
if 'test' in sys.argv or 'collectstatic' in sys.argv:
|
if "test" in sys.argv or "collectstatic" in sys.argv:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
ANYMAIL = { # pragma: no cover
|
ANYMAIL = { # pragma: no cover
|
||||||
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
|
"MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"],
|
||||||
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
|
"MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"],
|
||||||
}
|
}
|
||||||
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
|
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover
|
||||||
except Exception as exc: # pragma: nocover
|
except Exception: # pragma: nocover # pylint: disable=broad-except
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||||
|
|
||||||
SERVER_EMAIL = os.environ.get('SERVER_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')
|
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ivatar.settings import DATABASES
|
from ivatar.settings import DATABASES
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
DATABASES = [] # pragma: no cover
|
DATABASES = [] # pragma: no cover
|
||||||
|
|
||||||
if 'default' not in DATABASES:
|
if "default" not in DATABASES:
|
||||||
DATABASES['default'] = { # pragma: no cover
|
DATABASES["default"] = { # pragma: no cover
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if 'MYSQL_DATABASE' in os.environ:
|
if "MYSQL_DATABASE" in os.environ:
|
||||||
DATABASES['default'] = { # pragma: no cover
|
DATABASES["default"] = { # pragma: no cover
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
"ENGINE": "django.db.backends.mysql",
|
||||||
'NAME': os.environ['MYSQL_DATABASE'],
|
"NAME": os.environ["MYSQL_DATABASE"],
|
||||||
'USER': os.environ['MYSQL_USER'],
|
"USER": os.environ["MYSQL_USER"],
|
||||||
'PASSWORD': os.environ['MYSQL_PASSWORD'],
|
"PASSWORD": os.environ["MYSQL_PASSWORD"],
|
||||||
'HOST': 'mysql',
|
"HOST": "mysql",
|
||||||
}
|
}
|
||||||
|
|
||||||
if 'POSTGRESQL_DATABASE' in os.environ:
|
if "POSTGRESQL_DATABASE" in os.environ:
|
||||||
DATABASES['default'] = { # pragma: no cover
|
DATABASES["default"] = { # pragma: no cover
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
'NAME': os.environ['POSTGRESQL_DATABASE'],
|
"NAME": os.environ["POSTGRESQL_DATABASE"],
|
||||||
'USER': os.environ['POSTGRESQL_USER'],
|
"USER": os.environ["POSTGRESQL_USER"],
|
||||||
'PASSWORD': os.environ['POSTGRESQL_PASSWORD'],
|
"PASSWORD": os.environ["POSTGRESQL_PASSWORD"],
|
||||||
'HOST': 'postgresql',
|
"HOST": "postgresql",
|
||||||
}
|
}
|
||||||
|
|
||||||
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer"
|
||||||
|
|
||||||
USE_X_FORWARDED_HOST = True
|
USE_X_FORWARDED_HOST = True
|
||||||
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',]
|
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [
|
||||||
|
"avatars.linux-kernel.at",
|
||||||
|
"localhost",
|
||||||
|
]
|
||||||
|
|
||||||
DEFAULT_AVATAR_SIZE = 80
|
DEFAULT_AVATAR_SIZE = 80
|
||||||
|
|
||||||
LANGUAGES = (
|
LANGUAGES = (
|
||||||
('de', _('Deutsch')),
|
("de", _("Deutsch")),
|
||||||
('en', _('English')),
|
("en", _("English")),
|
||||||
('ca', _('Català')),
|
("ca", _("Català")),
|
||||||
('cs', _('Česky')),
|
("cs", _("Česky")),
|
||||||
('es', _('Español')),
|
("es", _("Español")),
|
||||||
('eu', _('Basque')),
|
("eu", _("Basque")),
|
||||||
('fr', _('Français')),
|
("fr", _("Français")),
|
||||||
('it', _('Italiano')),
|
("it", _("Italiano")),
|
||||||
('ja', _('日本語')),
|
("ja", _("日本語")),
|
||||||
('nl', _('Nederlands')),
|
("nl", _("Nederlands")),
|
||||||
('pt', _('Português')),
|
("pt", _("Português")),
|
||||||
('ru', _('Русский')),
|
("ru", _("Русский")),
|
||||||
('sq', _('Shqip')),
|
("sq", _("Shqip")),
|
||||||
('tr', _('Türkçe')),
|
("tr", _("Türkçe")),
|
||||||
('uk', _('Українська')),
|
("uk", _("Українська")),
|
||||||
)
|
)
|
||||||
|
|
||||||
MESSAGE_TAGS = {
|
MESSAGE_TAGS = {
|
||||||
message_constants.DEBUG: 'debug',
|
message_constants.DEBUG: "debug",
|
||||||
message_constants.INFO: 'info',
|
message_constants.INFO: "info",
|
||||||
message_constants.SUCCESS: 'success',
|
message_constants.SUCCESS: "success",
|
||||||
message_constants.WARNING: 'warning',
|
message_constants.WARNING: "warning",
|
||||||
message_constants.ERROR: 'danger',
|
message_constants.ERROR: "danger",
|
||||||
}
|
}
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
"default": {
|
||||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
|
||||||
'LOCATION': [
|
"LOCATION": [
|
||||||
'127.0.0.1:11211',
|
"127.0.0.1:11211",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'filesystem': {
|
"filesystem": {
|
||||||
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
|
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
|
||||||
'LOCATION': '/var/tmp/ivatar_cache',
|
"LOCATION": "/var/tmp/ivatar_cache",
|
||||||
'TIMEOUT': 900, # 15 minutes
|
"TIMEOUT": 900, # 15 minutes
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# This is 5 minutes caching for generated/resized images,
|
# This is 5 minutes caching for generated/resized images,
|
||||||
@@ -197,5 +210,5 @@ CACHE_IMAGES_MAX_AGE = 5 * 60
|
|||||||
CACHE_RESPONSE = True
|
CACHE_RESPONSE = True
|
||||||
|
|
||||||
# This MUST BE THE LAST!
|
# 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
|
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
'''
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
Module init
|
Module init
|
||||||
'''
|
"""
|
||||||
|
|
||||||
app_label = __name__ # pylint: disable=invalid-name
|
app_label = __name__ # pylint: disable=invalid-name
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'''
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
Default: useful variables for the base page templates.
|
Default: useful variables for the base page templates.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
from ipware import get_client_ip
|
from ipware import get_client_ip
|
||||||
from ivatar.settings import IVATAR_VERSION, SITE_NAME, MAX_PHOTO_SIZE
|
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):
|
def basepage(request):
|
||||||
'''
|
"""
|
||||||
Our contextprocessor adds additional context variables
|
Our contextprocessor adds additional context variables
|
||||||
in order to be used in the templates
|
in order to be used in the templates
|
||||||
'''
|
"""
|
||||||
context = {}
|
context = {}
|
||||||
if 'openid_identifier' in request.GET:
|
if "openid_identifier" in request.GET:
|
||||||
context['openid_identifier'] = \
|
context["openid_identifier"] = request.GET[
|
||||||
request.GET['openid_identifier'] # pragma: no cover
|
"openid_identifier"
|
||||||
|
] # pragma: no cover
|
||||||
client_ip = get_client_ip(request)[0]
|
client_ip = get_client_ip(request)[0]
|
||||||
context['client_ip'] = client_ip
|
context["client_ip"] = client_ip
|
||||||
context['ivatar_version'] = IVATAR_VERSION
|
context["ivatar_version"] = IVATAR_VERSION
|
||||||
context['site_name'] = SITE_NAME
|
context["site_name"] = SITE_NAME
|
||||||
context['site_url'] = request.build_absolute_uri('/')[:-1]
|
context["site_url"] = request.build_absolute_uri("/")[:-1]
|
||||||
context['max_file_size'] = MAX_PHOTO_SIZE
|
context["max_file_size"] = MAX_PHOTO_SIZE
|
||||||
context['BASE_URL'] = BASE_URL
|
context["BASE_URL"] = BASE_URL
|
||||||
context['SECURE_BASE_URL'] = SECURE_BASE_URL
|
context["SECURE_BASE_URL"] = SECURE_BASE_URL
|
||||||
context['max_emails'] = False
|
context["max_emails"] = False
|
||||||
if request.user:
|
if request.user:
|
||||||
if not request.user.is_anonymous:
|
if not request.user.is_anonymous:
|
||||||
unconfirmed = request.user.unconfirmedemail_set.count()
|
unconfirmed = request.user.unconfirmedemail_set.count()
|
||||||
if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS:
|
if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS:
|
||||||
context['max_emails'] = True
|
context["max_emails"] = True
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'''
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
Our models for ivatar.ivataraccount
|
Our models for ivatar.ivataraccount
|
||||||
'''
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -37,48 +38,49 @@ from .gravatar import get_photo as get_gravatar_photo
|
|||||||
|
|
||||||
|
|
||||||
def file_format(image_type):
|
def file_format(image_type):
|
||||||
'''
|
"""
|
||||||
Helper method returning a short image type
|
Helper method returning a short image type
|
||||||
'''
|
"""
|
||||||
if image_type == 'JPEG':
|
if image_type == "JPEG":
|
||||||
return 'jpg'
|
return "jpg"
|
||||||
elif image_type == 'PNG':
|
elif image_type == "PNG":
|
||||||
return 'png'
|
return "png"
|
||||||
elif image_type == 'GIF':
|
elif image_type == "GIF":
|
||||||
return 'gif'
|
return "gif"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def pil_format(image_type):
|
def pil_format(image_type):
|
||||||
'''
|
"""
|
||||||
Helper method returning the 'encoder name' for PIL
|
Helper method returning the 'encoder name' for PIL
|
||||||
'''
|
"""
|
||||||
if image_type == 'jpg' or image_type == 'jpeg':
|
if image_type == "jpg" or image_type == "jpeg":
|
||||||
return 'JPEG'
|
return "JPEG"
|
||||||
elif image_type == 'png':
|
elif image_type == "png":
|
||||||
return 'PNG'
|
return "PNG"
|
||||||
elif image_type == 'gif':
|
elif image_type == "gif":
|
||||||
return 'GIF'
|
return "GIF"
|
||||||
|
|
||||||
logger.info('Unsupported file format: %s', image_type)
|
logger.info("Unsupported file format: %s", image_type)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class UserPreference(models.Model):
|
class UserPreference(models.Model):
|
||||||
'''
|
"""
|
||||||
Holds the user users preferences
|
Holds the user users preferences
|
||||||
'''
|
"""
|
||||||
|
|
||||||
THEMES = (
|
THEMES = (
|
||||||
('default', 'Default theme'),
|
("default", "Default theme"),
|
||||||
('clime', 'climes theme'),
|
("clime", "climes theme"),
|
||||||
('green', 'green theme'),
|
("green", "green theme"),
|
||||||
('red', 'red theme'),
|
("red", "red theme"),
|
||||||
)
|
)
|
||||||
|
|
||||||
theme = models.CharField(
|
theme = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=THEMES,
|
choices=THEMES,
|
||||||
default='default',
|
default="default",
|
||||||
)
|
)
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
@@ -88,13 +90,14 @@ class UserPreference(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class BaseAccountModel(models.Model):
|
||||||
'''
|
"""
|
||||||
Base, abstract model, holding fields we use in all cases
|
Base, abstract model, holding fields we use in all cases
|
||||||
'''
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.deletion.CASCADE,
|
on_delete=models.deletion.CASCADE,
|
||||||
@@ -103,40 +106,43 @@ class BaseAccountModel(models.Model):
|
|||||||
add_date = models.DateTimeField(default=timezone.now)
|
add_date = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
'''
|
"""
|
||||||
Class attributes
|
Class attributes
|
||||||
'''
|
"""
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Photo(BaseAccountModel):
|
class Photo(BaseAccountModel):
|
||||||
'''
|
"""
|
||||||
Model holding the photos and information about them
|
Model holding the photos and information about them
|
||||||
'''
|
"""
|
||||||
|
|
||||||
ip_address = models.GenericIPAddressField(unpack_ipv4=True)
|
ip_address = models.GenericIPAddressField(unpack_ipv4=True)
|
||||||
data = models.BinaryField()
|
data = models.BinaryField()
|
||||||
format = models.CharField(max_length=3)
|
format = models.CharField(max_length=3)
|
||||||
access_count = models.BigIntegerField(default=0, editable=False)
|
access_count = models.BigIntegerField(default=0, editable=False)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
'''
|
"""
|
||||||
Class attributes
|
Class attributes
|
||||||
'''
|
"""
|
||||||
verbose_name = _('photo')
|
|
||||||
verbose_name_plural = _('photos')
|
verbose_name = _("photo")
|
||||||
|
verbose_name_plural = _("photos")
|
||||||
|
|
||||||
def import_image(self, service_name, email_address):
|
def import_image(self, service_name, email_address):
|
||||||
'''
|
"""
|
||||||
Allow to import image from other (eg. Gravatar) service
|
Allow to import image from other (eg. Gravatar) service
|
||||||
'''
|
"""
|
||||||
image_url = False
|
image_url = False
|
||||||
|
|
||||||
if service_name == 'Gravatar':
|
if service_name == "Gravatar":
|
||||||
gravatar = get_gravatar_photo(email_address)
|
gravatar = get_gravatar_photo(email_address)
|
||||||
if gravatar:
|
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)
|
image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE)
|
||||||
|
|
||||||
if not image_url:
|
if not image_url:
|
||||||
@@ -146,13 +152,12 @@ class Photo(BaseAccountModel):
|
|||||||
# No idea how to test this
|
# No idea how to test this
|
||||||
# pragma: no cover
|
# pragma: no cover
|
||||||
except HTTPError as exc:
|
except HTTPError as exc:
|
||||||
print('%s import failed with an HTTP error: %s' %
|
print("%s import failed with an HTTP error: %s" % (service_name, exc.code))
|
||||||
(service_name, exc.code))
|
|
||||||
return False
|
return False
|
||||||
# No idea how to test this
|
# No idea how to test this
|
||||||
# pragma: no cover
|
# pragma: no cover
|
||||||
except URLError as exc:
|
except URLError as exc:
|
||||||
print('%s import failed: %s' % (service_name, exc.reason))
|
print("%s import failed: %s" % (service_name, exc.reason))
|
||||||
return False
|
return False
|
||||||
data = image.read()
|
data = image.read()
|
||||||
|
|
||||||
@@ -164,35 +169,36 @@ class Photo(BaseAccountModel):
|
|||||||
|
|
||||||
self.format = file_format(img.format)
|
self.format = file_format(img.format)
|
||||||
if not self.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
|
return False # pragma: no cover
|
||||||
self.data = data
|
self.data = data
|
||||||
super().save()
|
super().save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None,
|
def save(
|
||||||
update_fields=None):
|
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||||
'''
|
):
|
||||||
|
"""
|
||||||
Override save from parent, taking care about the image
|
Override save from parent, taking care about the image
|
||||||
'''
|
"""
|
||||||
# Use PIL to read the file format
|
# Use PIL to read the file format
|
||||||
try:
|
try:
|
||||||
img = Image.open(BytesIO(self.data))
|
img = Image.open(BytesIO(self.data))
|
||||||
# Testing? Ideas anyone?
|
# Testing? Ideas anyone?
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
# For debugging only
|
# For debugging only
|
||||||
print('Exception caught in Photo.save(): %s' % exc)
|
print("Exception caught in Photo.save(): %s" % exc)
|
||||||
return False
|
return False
|
||||||
self.format = file_format(img.format)
|
self.format = file_format(img.format)
|
||||||
if not self.format:
|
if not self.format:
|
||||||
print('Format not recognized')
|
print("Format not recognized")
|
||||||
return False
|
return False
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def perform_crop(self, request, dimensions, email, openid):
|
def perform_crop(self, request, dimensions, email, openid):
|
||||||
'''
|
"""
|
||||||
Helper to crop the image
|
Helper to crop the image
|
||||||
'''
|
"""
|
||||||
if request.user.photo_set.count() == 1:
|
if request.user.photo_set.count() == 1:
|
||||||
# This is the first photo, assign to all confirmed addresses
|
# This is the first photo, assign to all confirmed addresses
|
||||||
for addr in request.user.confirmedemail_set.all():
|
for addr in request.user.confirmedemail_set.all():
|
||||||
@@ -217,34 +223,40 @@ class Photo(BaseAccountModel):
|
|||||||
img = Image.open(BytesIO(self.data))
|
img = Image.open(BytesIO(self.data))
|
||||||
|
|
||||||
# This should be anyway checked during save...
|
# This should be anyway checked during save...
|
||||||
dimensions['a'], \
|
dimensions["a"], dimensions["b"] = img.size # pylint: disable=invalid-name
|
||||||
dimensions['b'] = img.size # pylint: disable=invalid-name
|
if dimensions["a"] > MAX_PIXELS or dimensions["b"] > MAX_PIXELS:
|
||||||
if dimensions['a'] > MAX_PIXELS or dimensions['b'] > MAX_PIXELS:
|
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
_('Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s' % {
|
_(
|
||||||
max_pixels: MAX_PIXELS,
|
"Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s"
|
||||||
}))
|
% {
|
||||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
"max_pixels": MAX_PIXELS,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||||
|
|
||||||
if dimensions['w'] == 0 and dimensions['h'] == 0:
|
if dimensions["w"] == 0 and dimensions["h"] == 0:
|
||||||
dimensions['w'], dimensions['h'] = dimensions['a'], dimensions['b']
|
dimensions["w"], dimensions["h"] = dimensions["a"], dimensions["b"]
|
||||||
min_from_w_h = min(dimensions['w'], dimensions['h'])
|
min_from_w_h = min(dimensions["w"], dimensions["h"])
|
||||||
dimensions['w'], dimensions['h'] = min_from_w_h, min_from_w_h
|
dimensions["w"], dimensions["h"] = min_from_w_h, min_from_w_h
|
||||||
elif ((dimensions['w'] < 0)
|
elif (
|
||||||
or ((dimensions['x'] + dimensions['w']) > dimensions['a'])
|
(dimensions["w"] < 0)
|
||||||
or (dimensions['h'] < 0)
|
or ((dimensions["x"] + dimensions["w"]) > dimensions["a"])
|
||||||
or ((dimensions['y'] + dimensions['h']) > dimensions['b'])):
|
or (dimensions["h"] < 0)
|
||||||
messages.error(
|
or ((dimensions["y"] + dimensions["h"]) > dimensions["b"])
|
||||||
request,
|
):
|
||||||
_('Crop outside of original image bounding box'))
|
messages.error(request, _("Crop outside of original image bounding box"))
|
||||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||||
|
|
||||||
cropped = img.crop((
|
cropped = img.crop(
|
||||||
dimensions['x'],
|
(
|
||||||
dimensions['y'],
|
dimensions["x"],
|
||||||
dimensions['x'] + dimensions['w'],
|
dimensions["y"],
|
||||||
dimensions['y'] + dimensions['h']))
|
dimensions["x"] + dimensions["w"],
|
||||||
|
dimensions["y"] + dimensions["h"],
|
||||||
|
)
|
||||||
|
)
|
||||||
# cropped.load()
|
# cropped.load()
|
||||||
# Resize the image only if it's larger than the specified max width.
|
# Resize the image only if it's larger than the specified max width.
|
||||||
cropped_w, cropped_h = cropped.size
|
cropped_w, cropped_h = cropped.size
|
||||||
@@ -260,26 +272,26 @@ class Photo(BaseAccountModel):
|
|||||||
self.data = data.read()
|
self.data = data.read()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||||
|
|
||||||
def __str__(self):
|
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
|
# pylint: disable=too-few-public-methods
|
||||||
class ConfirmedEmailManager(models.Manager):
|
class ConfirmedEmailManager(models.Manager):
|
||||||
'''
|
"""
|
||||||
Manager for our confirmed email addresses model
|
Manager for our confirmed email addresses model
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_confirmed_email(user, email_address, is_logged_in):
|
def create_confirmed_email(user, email_address, is_logged_in):
|
||||||
'''
|
"""
|
||||||
Helper method to create confirmed email address
|
Helper method to create confirmed email address
|
||||||
'''
|
"""
|
||||||
confirmed = ConfirmedEmail()
|
confirmed = ConfirmedEmail()
|
||||||
confirmed.user = user
|
confirmed.user = user
|
||||||
confirmed.ip_address = '0.0.0.0'
|
confirmed.ip_address = "0.0.0.0"
|
||||||
confirmed.email = email_address
|
confirmed.email = email_address
|
||||||
confirmed.save()
|
confirmed.save()
|
||||||
|
|
||||||
@@ -293,14 +305,15 @@ class ConfirmedEmailManager(models.Manager):
|
|||||||
|
|
||||||
|
|
||||||
class ConfirmedEmail(BaseAccountModel):
|
class ConfirmedEmail(BaseAccountModel):
|
||||||
'''
|
"""
|
||||||
Model holding our confirmed email addresses, as well as the relation
|
Model holding our confirmed email addresses, as well as the relation
|
||||||
to the assigned photo
|
to the assigned photo
|
||||||
'''
|
"""
|
||||||
|
|
||||||
email = models.EmailField(unique=True, max_length=MAX_LENGTH_EMAIL)
|
email = models.EmailField(unique=True, max_length=MAX_LENGTH_EMAIL)
|
||||||
photo = models.ForeignKey(
|
photo = models.ForeignKey(
|
||||||
Photo,
|
Photo,
|
||||||
related_name='emails',
|
related_name="emails",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.deletion.SET_NULL,
|
on_delete=models.deletion.SET_NULL,
|
||||||
@@ -311,123 +324,129 @@ class ConfirmedEmail(BaseAccountModel):
|
|||||||
access_count = models.BigIntegerField(default=0, editable=False)
|
access_count = models.BigIntegerField(default=0, editable=False)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
'''
|
"""
|
||||||
Class attributes
|
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):
|
def set_photo(self, photo):
|
||||||
'''
|
"""
|
||||||
Helper method to set photo
|
Helper method to set photo
|
||||||
'''
|
"""
|
||||||
self.photo = photo
|
self.photo = photo
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None,
|
def save(
|
||||||
update_fields=None):
|
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||||
'''
|
):
|
||||||
|
"""
|
||||||
Override save from parent, add digest
|
Override save from parent, add digest
|
||||||
'''
|
"""
|
||||||
self.digest = hashlib.md5(
|
self.digest = hashlib.md5(
|
||||||
self.email.strip().lower().encode('utf-8')
|
self.email.strip().lower().encode("utf-8")
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
self.digest_sha256 = hashlib.sha256(
|
self.digest_sha256 = hashlib.sha256(
|
||||||
self.email.strip().lower().encode('utf-8')
|
self.email.strip().lower().encode("utf-8")
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class UnconfirmedEmail(BaseAccountModel):
|
||||||
'''
|
"""
|
||||||
Model holding unconfirmed email addresses as well as the verification key
|
Model holding unconfirmed email addresses as well as the verification key
|
||||||
'''
|
"""
|
||||||
|
|
||||||
email = models.EmailField(max_length=MAX_LENGTH_EMAIL)
|
email = models.EmailField(max_length=MAX_LENGTH_EMAIL)
|
||||||
verification_key = models.CharField(max_length=64)
|
verification_key = models.CharField(max_length=64)
|
||||||
last_send_date = models.DateTimeField(null=True, blank=True)
|
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 Meta: # pylint: disable=too-few-public-methods
|
||||||
'''
|
"""
|
||||||
Class attributes
|
Class attributes
|
||||||
'''
|
"""
|
||||||
verbose_name = _('unconfirmed email')
|
|
||||||
verbose_name_plural = _('unconfirmed emails')
|
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None,
|
verbose_name = _("unconfirmed email")
|
||||||
update_fields=None):
|
verbose_name_plural = _("unconfirmed emails")
|
||||||
|
|
||||||
|
def save(
|
||||||
|
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||||
|
):
|
||||||
if not self.verification_key:
|
if not self.verification_key:
|
||||||
hash_object = hashlib.new('sha256')
|
hash_object = hashlib.new("sha256")
|
||||||
hash_object.update(
|
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
|
) # pylint: disable=no-member
|
||||||
self.verification_key = hash_object.hexdigest()
|
self.verification_key = hash_object.hexdigest()
|
||||||
super(UnconfirmedEmail, self).save(
|
super(UnconfirmedEmail, self).save(
|
||||||
force_insert,
|
force_insert, force_update, using, update_fields
|
||||||
force_update,
|
)
|
||||||
using,
|
|
||||||
update_fields)
|
|
||||||
|
|
||||||
def send_confirmation_mail(self, url=SECURE_BASE_URL):
|
def send_confirmation_mail(self, url=SECURE_BASE_URL):
|
||||||
'''
|
"""
|
||||||
Send confirmation mail to that mail address
|
Send confirmation mail to that mail address
|
||||||
'''
|
"""
|
||||||
link = url + \
|
link = url + reverse(
|
||||||
reverse(
|
"confirm_email", kwargs={"verification_key": self.verification_key}
|
||||||
'confirm_email',
|
)
|
||||||
kwargs={'verification_key': self.verification_key})
|
email_subject = _("Confirm your email address on %s") % SITE_NAME
|
||||||
email_subject = _('Confirm your email address on %s') % \
|
email_body = render_to_string(
|
||||||
SITE_NAME
|
"email_confirmation.txt",
|
||||||
email_body = render_to_string('email_confirmation.txt', {
|
{
|
||||||
'verification_link': link,
|
"verification_link": link,
|
||||||
'site_name': SITE_NAME,
|
"site_name": SITE_NAME,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
self.last_send_date = timezone.now()
|
self.last_send_date = timezone.now()
|
||||||
self.last_status = 'OK'
|
self.last_status = "OK"
|
||||||
# if settings.DEBUG:
|
# if settings.DEBUG:
|
||||||
# print('DEBUG: %s' % link)
|
# print('DEBUG: %s' % link)
|
||||||
try:
|
try:
|
||||||
send_mail(
|
send_mail(email_subject, email_body, DEFAULT_FROM_EMAIL, [self.email])
|
||||||
email_subject, email_body, DEFAULT_FROM_EMAIL,
|
|
||||||
[self.email])
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.last_status = "%s" % e
|
self.last_status = "%s" % e
|
||||||
self.save()
|
self.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class UnconfirmedOpenId(BaseAccountModel):
|
||||||
'''
|
"""
|
||||||
Model holding unconfirmed OpenIDs
|
Model holding unconfirmed OpenIDs
|
||||||
'''
|
"""
|
||||||
|
|
||||||
openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL)
|
openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
'''
|
"""
|
||||||
Meta class
|
Meta class
|
||||||
'''
|
"""
|
||||||
verbose_name = _('unconfirmed OpenID')
|
|
||||||
verbose_name_plural = ('unconfirmed_OpenIDs')
|
verbose_name = _("unconfirmed OpenID")
|
||||||
|
verbose_name_plural = "unconfirmed_OpenIDs"
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class ConfirmedOpenId(BaseAccountModel):
|
||||||
'''
|
"""
|
||||||
Model holding confirmed OpenIDs, as well as the relation to
|
Model holding confirmed OpenIDs, as well as the relation to
|
||||||
the assigned photo
|
the assigned photo
|
||||||
'''
|
"""
|
||||||
|
|
||||||
openid = models.URLField(unique=True, max_length=MAX_LENGTH_URL)
|
openid = models.URLField(unique=True, max_length=MAX_LENGTH_URL)
|
||||||
photo = models.ForeignKey(
|
photo = models.ForeignKey(
|
||||||
Photo,
|
Photo,
|
||||||
related_name='openids',
|
related_name="openids",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.deletion.SET_NULL,
|
on_delete=models.deletion.SET_NULL,
|
||||||
@@ -444,25 +463,27 @@ class ConfirmedOpenId(BaseAccountModel):
|
|||||||
access_count = models.BigIntegerField(default=0, editable=False)
|
access_count = models.BigIntegerField(default=0, editable=False)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
'''
|
"""
|
||||||
Meta class
|
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):
|
def set_photo(self, photo):
|
||||||
'''
|
"""
|
||||||
Helper method to save photo
|
Helper method to save photo
|
||||||
'''
|
"""
|
||||||
self.photo = photo
|
self.photo = photo
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None,
|
def save(
|
||||||
update_fields=None):
|
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||||
|
):
|
||||||
url = urlsplit(self.openid)
|
url = urlsplit(self.openid)
|
||||||
if url.username: # pragma: no cover
|
if url.username: # pragma: no cover
|
||||||
password = url.password or ''
|
password = url.password or ""
|
||||||
netloc = url.username + ':' + password + '@' + url.hostname
|
netloc = url.username + ":" + password + "@" + url.hostname
|
||||||
else:
|
else:
|
||||||
netloc = url.hostname
|
netloc = url.hostname
|
||||||
lowercase_url = urlunsplit(
|
lowercase_url = urlunsplit(
|
||||||
@@ -470,37 +491,44 @@ class ConfirmedOpenId(BaseAccountModel):
|
|||||||
)
|
)
|
||||||
self.openid = lowercase_url
|
self.openid = lowercase_url
|
||||||
|
|
||||||
self.digest = hashlib.sha256(openid_variations(lowercase_url)[0].encode('utf-8')).hexdigest()
|
self.digest = hashlib.sha256(
|
||||||
self.alt_digest1 = hashlib.sha256(openid_variations(lowercase_url)[1].encode('utf-8')).hexdigest()
|
openid_variations(lowercase_url)[0].encode("utf-8")
|
||||||
self.alt_digest2 = hashlib.sha256(openid_variations(lowercase_url)[2].encode('utf-8')).hexdigest()
|
).hexdigest()
|
||||||
self.alt_digest3 = hashlib.sha256(openid_variations(lowercase_url)[3].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)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class OpenIDNonce(models.Model):
|
||||||
'''
|
"""
|
||||||
Model holding OpenID Nonces
|
Model holding OpenID Nonces
|
||||||
See also: https://github.com/edx/django-openid-auth/
|
See also: https://github.com/edx/django-openid-auth/
|
||||||
'''
|
"""
|
||||||
|
|
||||||
server_url = models.CharField(max_length=255)
|
server_url = models.CharField(max_length=255)
|
||||||
timestamp = models.IntegerField()
|
timestamp = models.IntegerField()
|
||||||
salt = models.CharField(max_length=128)
|
salt = models.CharField(max_length=128)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s (%i) (timestamp: %i)' % (
|
return "%s (%i) (timestamp: %i)" % (self.server_url, self.pk, self.timestamp)
|
||||||
self.server_url,
|
|
||||||
self.pk,
|
|
||||||
self.timestamp)
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDAssociation(models.Model):
|
class OpenIDAssociation(models.Model):
|
||||||
'''
|
"""
|
||||||
Model holding the relation/association about OpenIDs
|
Model holding the relation/association about OpenIDs
|
||||||
'''
|
"""
|
||||||
|
|
||||||
server_url = models.TextField(max_length=2047)
|
server_url = models.TextField(max_length=2047)
|
||||||
handle = models.CharField(max_length=255)
|
handle = models.CharField(max_length=255)
|
||||||
secret = models.TextField(max_length=255) # stored base64 encoded
|
secret = models.TextField(max_length=255) # stored base64 encoded
|
||||||
@@ -509,56 +537,62 @@ class OpenIDAssociation(models.Model):
|
|||||||
assoc_type = models.TextField(max_length=64)
|
assoc_type = models.TextField(max_length=64)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s (%i) (%s, lifetime: %i)' % (
|
return "%s (%i) (%s, lifetime: %i)" % (
|
||||||
self.server_url,
|
self.server_url,
|
||||||
self.pk,
|
self.pk,
|
||||||
self.assoc_type,
|
self.assoc_type,
|
||||||
self.lifetime)
|
self.lifetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DjangoOpenIDStore(OpenIDStore):
|
class DjangoOpenIDStore(OpenIDStore):
|
||||||
'''
|
"""
|
||||||
The Python openid library needs an OpenIDStore subclass to persist data
|
The Python openid library needs an OpenIDStore subclass to persist data
|
||||||
related to OpenID authentications. This one uses our Django models.
|
related to OpenID authentications. This one uses our Django models.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def storeAssociation(server_url, association): # pragma: no cover
|
def storeAssociation(server_url, association): # pragma: no cover
|
||||||
'''
|
"""
|
||||||
Helper method to store associations
|
Helper method to store associations
|
||||||
'''
|
"""
|
||||||
assoc = OpenIDAssociation(
|
assoc = OpenIDAssociation(
|
||||||
server_url=server_url,
|
server_url=server_url,
|
||||||
handle=association.handle,
|
handle=association.handle,
|
||||||
secret=base64.encodebytes(association.secret),
|
secret=base64.encodebytes(association.secret),
|
||||||
issued=association.issued,
|
issued=association.issued,
|
||||||
lifetime=association.issued,
|
lifetime=association.issued,
|
||||||
assoc_type=association.assoc_type)
|
assoc_type=association.assoc_type,
|
||||||
|
)
|
||||||
assoc.save()
|
assoc.save()
|
||||||
|
|
||||||
def getAssociation(self, server_url, handle=None): # pragma: no cover
|
def getAssociation(self, server_url, handle=None): # pragma: no cover
|
||||||
'''
|
"""
|
||||||
Helper method to get associations
|
Helper method to get associations
|
||||||
'''
|
"""
|
||||||
assocs = []
|
assocs = []
|
||||||
if handle is not None:
|
if handle is not None:
|
||||||
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
||||||
server_url=server_url,
|
server_url=server_url, handle=handle
|
||||||
handle=handle)
|
)
|
||||||
else:
|
else:
|
||||||
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
||||||
server_url=server_url)
|
server_url=server_url
|
||||||
|
)
|
||||||
if not assocs:
|
if not assocs:
|
||||||
return None
|
return None
|
||||||
associations = []
|
associations = []
|
||||||
for assoc in assocs:
|
for assoc in assocs:
|
||||||
if isinstance(assoc.secret, str):
|
if isinstance(assoc.secret, str):
|
||||||
assoc.secret = assoc.secret.split("b'")[1].split("'")[0]
|
assoc.secret = assoc.secret.split("b'")[1].split("'")[0]
|
||||||
assoc.secret = bytes(assoc.secret, 'utf-8')
|
assoc.secret = bytes(assoc.secret, "utf-8")
|
||||||
association = OIDAssociation(assoc.handle,
|
association = OIDAssociation(
|
||||||
base64.decodebytes(assoc.secret),
|
assoc.handle,
|
||||||
assoc.issued, assoc.lifetime,
|
base64.decodebytes(assoc.secret),
|
||||||
assoc.assoc_type)
|
assoc.issued,
|
||||||
|
assoc.lifetime,
|
||||||
|
assoc.assoc_type,
|
||||||
|
)
|
||||||
expires = 0
|
expires = 0
|
||||||
try:
|
try:
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
@@ -575,12 +609,14 @@ class DjangoOpenIDStore(OpenIDStore):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def removeAssociation(server_url, handle): # pragma: no cover
|
def removeAssociation(server_url, handle): # pragma: no cover
|
||||||
'''
|
"""
|
||||||
Helper method to remove associations
|
Helper method to remove associations
|
||||||
'''
|
"""
|
||||||
assocs = list(
|
assocs = list(
|
||||||
OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
||||||
server_url=server_url, handle=handle))
|
server_url=server_url, handle=handle
|
||||||
|
)
|
||||||
|
)
|
||||||
assocs_exist = len(assocs) > 0
|
assocs_exist = len(assocs) > 0
|
||||||
for assoc in assocs:
|
for assoc in assocs:
|
||||||
assoc.delete()
|
assoc.delete()
|
||||||
@@ -588,9 +624,9 @@ class DjangoOpenIDStore(OpenIDStore):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def useNonce(server_url, timestamp, salt): # pragma: no cover
|
def useNonce(server_url, timestamp, salt): # pragma: no cover
|
||||||
'''
|
"""
|
||||||
Helper method to 'use' nonces
|
Helper method to 'use' nonces
|
||||||
'''
|
"""
|
||||||
# Has nonce expired?
|
# Has nonce expired?
|
||||||
if abs(timestamp - time.time()) > oidnonce.SKEW:
|
if abs(timestamp - time.time()) > oidnonce.SKEW:
|
||||||
return False
|
return False
|
||||||
@@ -598,27 +634,30 @@ class DjangoOpenIDStore(OpenIDStore):
|
|||||||
nonce = OpenIDNonce.objects.get( # pylint: disable=no-member
|
nonce = OpenIDNonce.objects.get( # pylint: disable=no-member
|
||||||
server_url__exact=server_url,
|
server_url__exact=server_url,
|
||||||
timestamp__exact=timestamp,
|
timestamp__exact=timestamp,
|
||||||
salt__exact=salt)
|
salt__exact=salt,
|
||||||
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
nonce = OpenIDNonce.objects.create( # pylint: disable=no-member
|
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
|
return True
|
||||||
nonce.delete()
|
nonce.delete()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cleanupNonces(): # pragma: no cover
|
def cleanupNonces(): # pragma: no cover
|
||||||
'''
|
"""
|
||||||
Helper method to cleanup nonces
|
Helper method to cleanup nonces
|
||||||
'''
|
"""
|
||||||
timestamp = int(time.time()) - oidnonce.SKEW
|
timestamp = int(time.time()) - oidnonce.SKEW
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete()
|
OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cleanupAssociations(): # pragma: no cover
|
def cleanupAssociations(): # pragma: no cover
|
||||||
'''
|
"""
|
||||||
Helper method to cleanup associations
|
Helper method to cleanup associations
|
||||||
'''
|
"""
|
||||||
OpenIDAssociation.objects.extra( # pylint: disable=no-member
|
OpenIDAssociation.objects.extra( # pylint: disable=no-member
|
||||||
where=['issued + lifetimeint < (%s)' % time.time()]).delete()
|
where=["issued + lifetimeint < (%s)" % time.time()]
|
||||||
|
).delete()
|
||||||
|
|||||||
@@ -1,84 +1,122 @@
|
|||||||
'''
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
Reading libravatar export
|
Reading libravatar export
|
||||||
'''
|
"""
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
|
import os
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import gzip
|
import gzip
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
import base64
|
import base64
|
||||||
from PIL import Image
|
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):
|
def read_gzdata(gzdata=None):
|
||||||
'''
|
"""
|
||||||
Read gzipped data file
|
Read gzipped data file
|
||||||
'''
|
"""
|
||||||
emails = [] # pylint: disable=invalid-name
|
emails = [] # pylint: disable=invalid-name
|
||||||
openids = [] # pylint: disable=invalid-name
|
openids = [] # pylint: disable=invalid-name
|
||||||
photos = [] # pylint: disable=invalid-name
|
photos = [] # pylint: disable=invalid-name
|
||||||
username = None # pylint: disable=invalid-name
|
username = None # pylint: disable=invalid-name
|
||||||
password = None # pylint: disable=invalid-name
|
password = None # pylint: disable=invalid-name
|
||||||
|
|
||||||
if not gzdata:
|
if not gzdata:
|
||||||
return False
|
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()
|
content = fh.read()
|
||||||
fh.close()
|
fh.close()
|
||||||
root = xml.etree.ElementTree.fromstring(content)
|
root = xml.etree.ElementTree.fromstring(content)
|
||||||
if not root.tag == '{%s}user' % SCHEMAROOT:
|
if not root.tag == "{%s}user" % SCHEMAROOT:
|
||||||
print('Unknown export format: %s' % root.tag)
|
print("Unknown export format: %s" % root.tag)
|
||||||
exit(-1)
|
exit(-1)
|
||||||
|
|
||||||
# Username
|
# Username
|
||||||
for item in root.findall('{%s}account' % SCHEMAROOT)[0].items():
|
for item in root.findall("{%s}account" % SCHEMAROOT)[0].items():
|
||||||
if item[0] == 'username':
|
if item[0] == "username":
|
||||||
username = item[1]
|
username = item[1]
|
||||||
if item[0] == 'password':
|
if item[0] == "password":
|
||||||
password = item[1]
|
password = item[1]
|
||||||
|
|
||||||
# Emails
|
# Emails
|
||||||
for email in root.findall('{%s}emails' % SCHEMAROOT)[0]:
|
for email in root.findall("{%s}emails" % SCHEMAROOT)[0]:
|
||||||
if email.tag == '{%s}email' % SCHEMAROOT:
|
if email.tag == "{%s}email" % SCHEMAROOT:
|
||||||
emails.append({'email': email.text, 'photo_id': email.attrib['photo_id']})
|
emails.append({"email": email.text, "photo_id": email.attrib["photo_id"]})
|
||||||
|
|
||||||
# OpenIDs
|
# OpenIDs
|
||||||
for openid in root.findall('{%s}openids' % SCHEMAROOT)[0]:
|
for openid in root.findall("{%s}openids" % SCHEMAROOT)[0]:
|
||||||
if openid.tag == '{%s}openid' % SCHEMAROOT:
|
if openid.tag == "{%s}openid" % SCHEMAROOT:
|
||||||
openids.append({'openid': openid.text, 'photo_id': openid.attrib['photo_id']})
|
openids.append(
|
||||||
|
{"openid": openid.text, "photo_id": openid.attrib["photo_id"]}
|
||||||
|
)
|
||||||
|
|
||||||
# Photos
|
# Photos
|
||||||
for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]:
|
for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]:
|
||||||
if photo.tag == '{%s}photo' % SCHEMAROOT:
|
if photo.tag == "{%s}photo" % SCHEMAROOT:
|
||||||
try:
|
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:
|
except binascii.Error as exc:
|
||||||
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
|
print(
|
||||||
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
|
"Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s"
|
||||||
|
% (
|
||||||
|
photo.attrib["encoding"],
|
||||||
|
photo.attrib["format"],
|
||||||
|
photo.attrib["id"],
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
Image.open(BytesIO(data))
|
Image.open(BytesIO(data))
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
|
print(
|
||||||
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
|
"Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s"
|
||||||
|
% (
|
||||||
|
photo.attrib["encoding"],
|
||||||
|
photo.attrib["format"],
|
||||||
|
photo.attrib["id"],
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
# If it is a working image, we can use it
|
# If it is a working image, we can use it
|
||||||
photo.text.replace('\n', '')
|
photo.text.replace("\n", "")
|
||||||
photos.append({
|
photos.append(
|
||||||
'data': photo.text,
|
{
|
||||||
'format': photo.attrib['format'],
|
"data": photo.text,
|
||||||
'id': photo.attrib['id'],
|
"format": photo.attrib["format"],
|
||||||
})
|
"id": photo.attrib["id"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'emails': emails,
|
"emails": emails,
|
||||||
'openids': openids,
|
"openids": openids,
|
||||||
'photos': photos,
|
"photos": photos,
|
||||||
'username': username,
|
"username": username,
|
||||||
'password': password,
|
"password": password,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% 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>
|
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
|
||||||
{% for email in emails %}
|
{% for email in emails %}
|
||||||
<div class="checkbox">
|
<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>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
20
ivatar/ivataraccount/templates/export.html
Normal file
20
ivatar/ivataraccount/templates/export.html
Normal 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>
|
||||||
|
<a href="{% url 'profile' %}" class="button">{% trans 'Cancel' %}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,119 +1,155 @@
|
|||||||
'''
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
URLs for ivatar.ivataraccount
|
URLs for ivatar.ivataraccount
|
||||||
'''
|
"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.conf.urls import url
|
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 LogoutView
|
||||||
from django.contrib.auth.views import PasswordResetDoneView,\
|
from django.contrib.auth.views import (
|
||||||
PasswordResetConfirmView, PasswordResetCompleteView
|
PasswordResetDoneView,
|
||||||
|
PasswordResetConfirmView,
|
||||||
|
PasswordResetCompleteView,
|
||||||
|
)
|
||||||
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
|
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
|
|
||||||
from . views import ProfileView, PasswordResetView
|
from .views import ProfileView, PasswordResetView
|
||||||
from . views import CreateView, PasswordSetView, AddEmailView
|
from .views import CreateView, PasswordSetView, AddEmailView
|
||||||
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView
|
from .views import RemoveUnconfirmedEmailView, ConfirmEmailView
|
||||||
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
|
from .views import RemoveConfirmedEmailView, AssignPhotoEmailView
|
||||||
from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
|
from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
|
||||||
from . views import ImportPhotoView, RawImageView, DeletePhotoView
|
from .views import ImportPhotoView, RawImageView, DeletePhotoView
|
||||||
from . views import UploadPhotoView, AssignPhotoOpenIDView
|
from .views import UploadPhotoView, AssignPhotoOpenIDView
|
||||||
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
|
from .views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
|
||||||
from . views import CropPhotoView
|
from .views import CropPhotoView
|
||||||
from . views import UserPreferenceView, UploadLibravatarExportView
|
from .views import UserPreferenceView, UploadLibravatarExportView
|
||||||
from . views import ResendConfirmationMailView
|
from .views import ResendConfirmationMailView
|
||||||
from . views import IvatarLoginView
|
from .views import IvatarLoginView
|
||||||
from . views import DeleteAccountView
|
from .views import DeleteAccountView
|
||||||
|
from .views import ExportView
|
||||||
|
|
||||||
# Define URL patterns, self documenting
|
# Define URL patterns, self documenting
|
||||||
# To see the fancy, colorful evaluation of these use:
|
# To see the fancy, colorful evaluation of these use:
|
||||||
# ./manager show_urls
|
# ./manager show_urls
|
||||||
urlpatterns = [ # pylint: disable=invalid-name
|
urlpatterns = [ # pylint: disable=invalid-name
|
||||||
path('new/', CreateView.as_view(), name='new_account'),
|
path("new/", CreateView.as_view(), name="new_account"),
|
||||||
path('login/', IvatarLoginView.as_view(), name='login'),
|
path("login/", IvatarLoginView.as_view(), name="login"),
|
||||||
|
path("logout/", LogoutView.as_view(next_page="/"), name="logout"),
|
||||||
path(
|
path(
|
||||||
'logout/', LogoutView.as_view(next_page='/'),
|
"password_change/",
|
||||||
name='logout'),
|
PasswordChangeView.as_view(template_name="password_change.html"),
|
||||||
|
name="password_change",
|
||||||
path('password_change/',
|
),
|
||||||
PasswordChangeView.as_view(template_name='password_change.html'),
|
path(
|
||||||
name='password_change'),
|
"password_change/done/",
|
||||||
path('password_change/done/',
|
PasswordChangeDoneView.as_view(template_name="password_change_done.html"),
|
||||||
PasswordChangeDoneView.as_view(template_name='password_change_done.html'),
|
name="password_change_done",
|
||||||
name='password_change_done'),
|
),
|
||||||
|
path(
|
||||||
path('password_reset/',
|
"password_reset/",
|
||||||
PasswordResetView.as_view(template_name='password_reset.html'),
|
PasswordResetView.as_view(template_name="password_reset.html"),
|
||||||
name='password_reset'),
|
name="password_reset",
|
||||||
path('password_reset/done/',
|
),
|
||||||
PasswordResetDoneView.as_view(
|
path(
|
||||||
template_name='password_reset_submitted.html'),
|
"password_reset/done/",
|
||||||
name='password_reset_done'),
|
PasswordResetDoneView.as_view(template_name="password_reset_submitted.html"),
|
||||||
path('reset/<uidb64>/<token>/',
|
name="password_reset_done",
|
||||||
PasswordResetConfirmView.as_view(
|
),
|
||||||
template_name='password_change.html'),
|
path(
|
||||||
name='password_reset_confirm'),
|
"reset/<uidb64>/<token>/",
|
||||||
path('reset/done/',
|
PasswordResetConfirmView.as_view(template_name="password_change.html"),
|
||||||
PasswordResetCompleteView.as_view(
|
name="password_reset_confirm",
|
||||||
template_name='password_change_done.html'),
|
),
|
||||||
name='password_reset_complete'),
|
path(
|
||||||
|
"reset/done/",
|
||||||
path('export/', login_required(
|
PasswordResetCompleteView.as_view(template_name="password_change_done.html"),
|
||||||
TemplateView.as_view(template_name='export.html')
|
name="password_reset_complete",
|
||||||
), name='export'),
|
),
|
||||||
path('delete/', DeleteAccountView.as_view(), name='delete'),
|
path(
|
||||||
path('profile/', ProfileView.as_view(), name='profile'),
|
"export/",
|
||||||
url('profile/(?P<profile_username>.+)', ProfileView.as_view(), name='profile_with_profile_username'),
|
ExportView.as_view(),
|
||||||
path('add_email/', AddEmailView.as_view(), name='add_email'),
|
name="export",
|
||||||
path('add_openid/', AddOpenIDView.as_view(), name='add_openid'),
|
),
|
||||||
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'),
|
path("delete/", DeleteAccountView.as_view(), name="delete"),
|
||||||
path('password_set/', PasswordSetView.as_view(), name='password_set'),
|
path("profile/", ProfileView.as_view(), name="profile"),
|
||||||
url(
|
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(),
|
RemoveUnconfirmedOpenIDView.as_view(),
|
||||||
name='remove_unconfirmed_openid'),
|
name="remove_unconfirmed_openid",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'remove_confirmed_openid/(?P<openid_id>\d+)',
|
r"remove_confirmed_openid/(?P<openid_id>\d+)",
|
||||||
RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'),
|
RemoveConfirmedOpenIDView.as_view(),
|
||||||
|
name="remove_confirmed_openid",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'openid_redirection/(?P<openid_id>\d+)',
|
r"openid_redirection/(?P<openid_id>\d+)",
|
||||||
RedirectOpenIDView.as_view(), name='openid_redirection'),
|
RedirectOpenIDView.as_view(),
|
||||||
|
name="openid_redirection",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'confirm_openid/(?P<openid_id>\w+)',
|
r"confirm_openid/(?P<openid_id>\w+)",
|
||||||
ConfirmOpenIDView.as_view(), name='confirm_openid'),
|
ConfirmOpenIDView.as_view(),
|
||||||
|
name="confirm_openid",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'confirm_email/(?P<verification_key>\w+)',
|
r"confirm_email/(?P<verification_key>\w+)",
|
||||||
ConfirmEmailView.as_view(), name='confirm_email'),
|
ConfirmEmailView.as_view(),
|
||||||
|
name="confirm_email",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'remove_unconfirmed_email/(?P<email_id>\d+)',
|
r"remove_unconfirmed_email/(?P<email_id>\d+)",
|
||||||
RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'),
|
RemoveUnconfirmedEmailView.as_view(),
|
||||||
|
name="remove_unconfirmed_email",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'remove_confirmed_email/(?P<email_id>\d+)',
|
r"remove_confirmed_email/(?P<email_id>\d+)",
|
||||||
RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'),
|
RemoveConfirmedEmailView.as_view(),
|
||||||
|
name="remove_confirmed_email",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'assign_photo_email/(?P<email_id>\d+)',
|
r"assign_photo_email/(?P<email_id>\d+)",
|
||||||
AssignPhotoEmailView.as_view(), name='assign_photo_email'),
|
AssignPhotoEmailView.as_view(),
|
||||||
|
name="assign_photo_email",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'assign_photo_openid/(?P<openid_id>\d+)',
|
r"assign_photo_openid/(?P<openid_id>\d+)",
|
||||||
AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'),
|
AssignPhotoOpenIDView.as_view(),
|
||||||
|
name="assign_photo_openid",
|
||||||
|
),
|
||||||
|
url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
|
||||||
url(
|
url(
|
||||||
r'import_photo/$',
|
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
|
||||||
ImportPhotoView.as_view(), name='import_photo'),
|
ImportPhotoView.as_view(),
|
||||||
|
name="import_photo",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)',
|
r"import_photo/(?P<email_id>\d+)",
|
||||||
ImportPhotoView.as_view(), name='import_photo'),
|
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(
|
url(
|
||||||
r'import_photo/(?P<email_id>\d+)',
|
r"upload_export/(?P<save>save)$",
|
||||||
ImportPhotoView.as_view(), name='import_photo'),
|
UploadLibravatarExportView.as_view(),
|
||||||
|
name="upload_export",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'delete_photo/(?P<pk>\d+)',
|
r"resend_confirmation_mail/(?P<email_id>\d+)",
|
||||||
DeletePhotoView.as_view(), name='delete_photo'),
|
ResendConfirmationMailView.as_view(),
|
||||||
url(r'raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
|
name="resend_confirmation_mail",
|
||||||
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'),
|
|
||||||
]
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Middleware classes
|
Middleware classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
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
|
Middleware to rewrite proxy headers for deployments
|
||||||
with multiple proxies
|
with multiple proxies
|
||||||
@@ -14,5 +19,7 @@ class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-publi
|
|||||||
Rewrites the proxy headers so that forwarded server is
|
Rewrites the proxy headers so that forwarded server is
|
||||||
used if available.
|
used if available.
|
||||||
"""
|
"""
|
||||||
if 'HTTP_X_FORWARDED_SERVER' in request.META:
|
if "HTTP_X_FORWARDED_SERVER" in request.META:
|
||||||
request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER']
|
request.META["HTTP_X_FORWARDED_HOST"] = request.META[
|
||||||
|
"HTTP_X_FORWARDED_SERVER"
|
||||||
|
]
|
||||||
|
|||||||
@@ -2,25 +2,36 @@ autopep8
|
|||||||
bcrypt
|
bcrypt
|
||||||
defusedxml
|
defusedxml
|
||||||
Django
|
Django
|
||||||
|
django-anymail[mailgun]
|
||||||
django-auth-ldap
|
django-auth-ldap
|
||||||
django-bootstrap4
|
django-bootstrap4
|
||||||
django-coverage-plugin
|
django-coverage-plugin
|
||||||
django-extensions
|
django-extensions
|
||||||
django-ipware
|
django-ipware
|
||||||
django-user-accounts
|
django-user-accounts
|
||||||
|
email-validator
|
||||||
fabric
|
fabric
|
||||||
flake8-respect-noqa
|
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/django-openid-auth
|
||||||
|
git+https://github.com/ofalk/monsterid.git
|
||||||
|
git+https://github.com/ofalk/Robohash.git@devel
|
||||||
|
mysqlclient
|
||||||
|
notsetuptools
|
||||||
|
pagan
|
||||||
Pillow
|
Pillow
|
||||||
pip
|
pip
|
||||||
|
psycopg2-binary
|
||||||
py3dns
|
py3dns
|
||||||
pydocstyle
|
pydocstyle
|
||||||
pyLibravatar
|
pyLibravatar
|
||||||
pylint
|
pylint
|
||||||
PyMySQL
|
PyMySQL
|
||||||
python3-openid
|
|
||||||
python-coveralls
|
python-coveralls
|
||||||
python-language-server
|
python-language-server
|
||||||
|
python-memcached
|
||||||
|
python3-openid
|
||||||
pytz
|
pytz
|
||||||
rope
|
rope
|
||||||
setuptools
|
setuptools
|
||||||
@@ -28,13 +39,3 @@ six
|
|||||||
social-auth-app-django
|
social-auth-app-django
|
||||||
wheel
|
wheel
|
||||||
yapf
|
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
|
|
||||||
|
|||||||
@@ -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 '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 '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 '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_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 '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>
|
<li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>
|
||||||
|
|||||||
Reference in New Issue
Block a user