mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-20 15:08:02 +00:00
Merge branch 'devel' into 'master'
Bluesky integration and Fedora OIDC integration (because OpenID is going to be deprecated) See merge request oliver/ivatar!243
This commit is contained in:
2
.flake8
2
.flake8
@@ -1,5 +1,5 @@
|
||||
[flake8]
|
||||
ignore = E501, W503, E402, C901
|
||||
ignore = E501, W503, E402, C901, E231, E702
|
||||
max-line-length = 79
|
||||
max-complexity = 18
|
||||
select = B,C,E,F,W,T4,B9
|
||||
|
||||
@@ -35,6 +35,7 @@ test_and_coverage:
|
||||
- pip install django_coverage_plugin
|
||||
|
||||
script:
|
||||
- source /tmp/.virtualenv/bin/activate
|
||||
- echo 'from ivatar.settings import TEMPLATES' > config_local.py
|
||||
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
|
||||
- echo "DEBUG = True" >> config_local.py
|
||||
|
||||
@@ -60,13 +60,14 @@ repos:
|
||||
rev: v1.12.1
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
- repo: https://github.com/hcodes/yaspeller.git
|
||||
rev: v8.0.1
|
||||
hooks:
|
||||
- id: yaspeller
|
||||
|
||||
types:
|
||||
- markdown
|
||||
# YASpeller does not seem to work anymore
|
||||
# - repo: https://github.com/hcodes/yaspeller.git
|
||||
# rev: v8.0.1
|
||||
# hooks:
|
||||
# - id: yaspeller
|
||||
#
|
||||
# types:
|
||||
# - markdown
|
||||
- repo: https://github.com/kadrach/pre-commit-gitlabci-lint
|
||||
rev: 22d0495c9894e8b27cc37c2ed5d9a6b46385a44c
|
||||
hooks:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM quay.io/rhn_support_ofalk/fedora37-python3
|
||||
FROM git.linux-kernel.at:5050/oliver/fedora40-python3:latest
|
||||
LABEL maintainer Oliver Falk <oliver@linux-kernel.at>
|
||||
EXPOSE 8081
|
||||
|
||||
|
||||
29
INSTALL.md
29
INSTALL.md
@@ -19,19 +19,19 @@ sudo apt-get install git python3-virtualenv libmariadb-dev libldap2-dev libsasl2
|
||||
|
||||
## Checkout
|
||||
|
||||
~~~~bash
|
||||
```bash
|
||||
git clone https://git.linux-kernel.at/oliver/ivatar.git
|
||||
cd ivatar
|
||||
~~~~
|
||||
```
|
||||
|
||||
## Virtual environment
|
||||
|
||||
~~~~bash
|
||||
virtualenv -p python3 .virtualenv
|
||||
```bash
|
||||
virtualenv -p python3 .virtualenv
|
||||
source .virtualenv/bin/activate
|
||||
pip install pillow
|
||||
pip install -r requirements.txt
|
||||
~~~~
|
||||
```
|
||||
|
||||
## (SQL) Migrations
|
||||
|
||||
@@ -58,10 +58,27 @@ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Running the testsuite
|
||||
|
||||
```
|
||||
./manage.py test -v3 # Or any other verbosity level you like
|
||||
```
|
||||
|
||||
## OpenID Connect authentication with Fedora
|
||||
|
||||
To enable OpenID Connect (OIDC) authentication with Fedora, you must have obtained a `client_id` and `client_secret` pair from the Fedora Infrastructure.
|
||||
Then you must set these values in `config_local.py`:
|
||||
|
||||
```
|
||||
SOCIAL_AUTH_FEDORA_KEY = "the-client-id"
|
||||
SOCIAL_AUTH_FEDORA_SECRET = "the-client-secret"
|
||||
```
|
||||
|
||||
You can override the location of the OIDC provider with the `SOCIAL_AUTH_FEDORA_OIDC_ENDPOINT` setting. For example, to authenticate with Fedora's staging environment, set this in `config_local.py`:
|
||||
|
||||
```
|
||||
SOCIAL_AUTH_FEDORA_OIDC_ENDPOINT = "https://id.stg.fedoraproject.org"
|
||||
```
|
||||
|
||||
# Production deployment Webserver (non-cloudy)
|
||||
|
||||
To deploy this Django application with WSGI on Apache, NGINX or any other web server, please refer to the the webserver documentation; There are also plenty of howtos on the net (I'll not LMGTFY...)
|
||||
@@ -82,4 +99,4 @@ There is a file called ebcreate.txt as well as a directory called .ebextensions,
|
||||
|
||||
## Database
|
||||
|
||||
It should work with SQLite (do *not* use in production!), MySQL/MariaDB, as well as PostgreSQL.
|
||||
It should work with SQLite (do _not_ use in production!), MySQL/MariaDB, as well as PostgreSQL.
|
||||
|
||||
19
config.py
19
config.py
@@ -44,6 +44,7 @@ AUTHENTICATION_BACKENDS = (
|
||||
# See INSTALL for more information.
|
||||
# 'django_auth_ldap.backend.LDAPBackend',
|
||||
"django_openid_auth.auth.OpenIDBackend",
|
||||
"ivatar.ivataraccount.auth.FedoraOpenIdConnect",
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
)
|
||||
|
||||
@@ -58,9 +59,13 @@ TEMPLATES[0]["OPTIONS"]["context_processors"].append(
|
||||
|
||||
OPENID_CREATE_USERS = True
|
||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||
SOCIAL_AUTH_JSONFIELD_ENABLED = True
|
||||
# Fedora authentication (OIDC). You need to set these two values to use it.
|
||||
SOCIAL_AUTH_FEDORA_KEY = None # Also known as client_id
|
||||
SOCIAL_AUTH_FEDORA_SECRET = None # Also known as client_secret
|
||||
|
||||
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
|
||||
IVATAR_VERSION = "1.7.0"
|
||||
IVATAR_VERSION = "1.8.0"
|
||||
|
||||
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
|
||||
|
||||
@@ -268,9 +273,7 @@ TRUSTED_DEFAULT_URLS = [
|
||||
},
|
||||
]
|
||||
|
||||
# This MUST BE THE LAST!
|
||||
if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")):
|
||||
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
|
||||
URL_TIMEOUT = 10
|
||||
|
||||
|
||||
def map_legacy_config(trusted_url):
|
||||
@@ -286,3 +289,11 @@ def map_legacy_config(trusted_url):
|
||||
|
||||
# Backward compability for legacy behavior
|
||||
TRUSTED_DEFAULT_URLS = list(map(map_legacy_config, TRUSTED_DEFAULT_URLS))
|
||||
|
||||
# Bluesky settings
|
||||
BLUESKY_IDENTIFIER = os.environ.get("BLUESKY_IDENTIFIER", None)
|
||||
BLUESKY_APP_PASSWORD = os.environ.get("BLUESKY_APP_PASSWORD", None)
|
||||
|
||||
# This MUST BE THE LAST!
|
||||
if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")):
|
||||
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Default: useful variables for the base page templates.
|
||||
"""
|
||||
|
||||
from ipware import get_client_ip
|
||||
from ipware import get_client_ip # type: ignore
|
||||
from ivatar.settings import IVATAR_VERSION, SITE_NAME, MAX_PHOTO_SIZE
|
||||
from ivatar.settings import BASE_URL, SECURE_BASE_URL
|
||||
from ivatar.settings import MAX_NUM_UNCONFIRMED_EMAILS
|
||||
@@ -28,6 +28,7 @@ def basepage(request):
|
||||
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()
|
||||
|
||||
56
ivatar/ivataraccount/auth.py
Normal file
56
ivatar/ivataraccount/auth.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from social_core.backends.open_id_connect import OpenIdConnectAuth
|
||||
|
||||
from ivatar.ivataraccount.models import ConfirmedEmail, Photo
|
||||
from ivatar.settings import logger, TRUST_EMAIL_FROM_SOCIAL_AUTH_BACKENDS
|
||||
|
||||
|
||||
class FedoraOpenIdConnect(OpenIdConnectAuth):
|
||||
name = "fedora"
|
||||
USERNAME_KEY = "nickname"
|
||||
OIDC_ENDPOINT = "https://id.fedoraproject.org"
|
||||
DEFAULT_SCOPE = ["openid", "profile", "email"]
|
||||
TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_post"
|
||||
|
||||
|
||||
# Pipeline methods
|
||||
|
||||
|
||||
def add_confirmed_email(backend, user, response, *args, **kwargs):
|
||||
"""Add a ConfirmedEmail if we trust the auth backend to validate email."""
|
||||
if not kwargs.get("is_new", False):
|
||||
return None # Only act on account creation
|
||||
if backend.name not in TRUST_EMAIL_FROM_SOCIAL_AUTH_BACKENDS:
|
||||
return None
|
||||
if ConfirmedEmail.objects.filter(email=user.email).count() > 0:
|
||||
# email already exists
|
||||
return None
|
||||
(confirmed_id, external_photos) = ConfirmedEmail.objects.create_confirmed_email(
|
||||
user, user.email, True
|
||||
)
|
||||
confirmed_email = ConfirmedEmail.objects.get(id=confirmed_id)
|
||||
logger.debug(
|
||||
"Email %s added upon creation of user %s", confirmed_email.email, user.pk
|
||||
)
|
||||
photo = Photo.objects.create(user=user, ip_address=confirmed_email.ip_address)
|
||||
import_result = photo.import_image("Gravatar", confirmed_email.email)
|
||||
if import_result:
|
||||
logger.debug("Gravatar image imported for %s", confirmed_email.email)
|
||||
|
||||
|
||||
def associate_by_confirmed_email(backend, details, user=None, *args, **kwargs):
|
||||
"""
|
||||
Associate current auth with a user that has their email address as ConfirmedEmail in the DB.
|
||||
"""
|
||||
if user:
|
||||
return None
|
||||
email = details.get("email")
|
||||
if not email:
|
||||
return None
|
||||
try:
|
||||
confirmed_email = ConfirmedEmail.objects.get(email=email)
|
||||
except ConfirmedEmail.DoesNotExist:
|
||||
return None
|
||||
user = confirmed_email.user
|
||||
logger.debug("Found a matching ConfirmedEmail for %s upon login", user.username)
|
||||
return {"user": user, "is_new": False}
|
||||
@@ -118,9 +118,7 @@ class UploadPhotoForm(forms.Form):
|
||||
photo.ip_address = get_client_ip(request)[0]
|
||||
photo.data = data.read()
|
||||
photo.save()
|
||||
if not photo.pk:
|
||||
return None
|
||||
return photo
|
||||
return photo if photo.pk else None
|
||||
|
||||
|
||||
class AddOpenIDForm(forms.Form):
|
||||
@@ -141,13 +139,16 @@ class AddOpenIDForm(forms.Form):
|
||||
"""
|
||||
# Lowercase hostname port of the URL
|
||||
url = urlsplit(self.cleaned_data["openid"])
|
||||
data = urlunsplit(
|
||||
(url.scheme.lower(), url.netloc.lower(), url.path, url.query, url.fragment)
|
||||
return urlunsplit(
|
||||
(
|
||||
url.scheme.lower(),
|
||||
url.netloc.lower(),
|
||||
url.path,
|
||||
url.query,
|
||||
url.fragment,
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Domain restriction as in libravatar?
|
||||
return data
|
||||
|
||||
def save(self, user):
|
||||
"""
|
||||
Save the model, ensuring some safety
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
Helper method to fetch Gravatar image
|
||||
"""
|
||||
from ssl import SSLError
|
||||
from urllib.request import urlopen, HTTPError, URLError
|
||||
from urllib.request import HTTPError, URLError
|
||||
from ivatar.utils import urlopen
|
||||
import hashlib
|
||||
|
||||
from ..settings import AVATAR_MAX_SIZE
|
||||
|
||||
URL_TIMEOUT = 5 # in seconds
|
||||
|
||||
|
||||
def get_photo(email):
|
||||
"""
|
||||
@@ -23,29 +22,23 @@ def get_photo(email):
|
||||
+ "?s=%i&d=404" % AVATAR_MAX_SIZE
|
||||
)
|
||||
image_url = (
|
||||
"https://secure.gravatar.com/avatar/" + hash_object.hexdigest() + "?s=512&d=404"
|
||||
f"https://secure.gravatar.com/avatar/{hash_object.hexdigest()}?s=512&d=404"
|
||||
)
|
||||
|
||||
# Will redirect to the public profile URL if it exists
|
||||
service_url = "http://www.gravatar.com/" + hash_object.hexdigest()
|
||||
service_url = f"http://www.gravatar.com/{hash_object.hexdigest()}"
|
||||
|
||||
try:
|
||||
urlopen(image_url, timeout=URL_TIMEOUT)
|
||||
urlopen(image_url)
|
||||
except HTTPError as exc:
|
||||
if exc.code != 404 and exc.code != 503:
|
||||
print( # pragma: no cover
|
||||
"Gravatar fetch failed with an unexpected %s HTTP error" % exc.code
|
||||
)
|
||||
if exc.code not in [404, 503]:
|
||||
print(f"Gravatar fetch failed with an unexpected {exc.code} HTTP error")
|
||||
return False
|
||||
except URLError as exc: # pragma: no cover
|
||||
print(
|
||||
"Gravatar fetch failed with URL error: %s" % exc.reason
|
||||
) # pragma: no cover
|
||||
print(f"Gravatar fetch failed with URL error: {exc.reason}")
|
||||
return False # pragma: no cover
|
||||
except SSLError as exc: # pragma: no cover
|
||||
print(
|
||||
"Gravatar fetch failed with SSL error: %s" % exc.reason
|
||||
) # pragma: no cover
|
||||
print(f"Gravatar fetch failed with SSL error: {exc.reason}")
|
||||
return False # pragma: no cover
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 5.1.5 on 2025-01-27 10:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("ivataraccount", "0018_alter_photo_format"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="confirmedemail",
|
||||
name="bluesky_handle",
|
||||
field=models.CharField(blank=True, max_length=256, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 5.1.5 on 2025-01-27 13:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("ivataraccount", "0019_confirmedemail_bluesky_handle"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="confirmedopenid",
|
||||
name="bluesky_handle",
|
||||
field=models.CharField(blank=True, max_length=256, null=True),
|
||||
),
|
||||
]
|
||||
@@ -9,7 +9,7 @@ import time
|
||||
from io import BytesIO
|
||||
from os import urandom
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import urlopen
|
||||
from ivatar.utils import urlopen, Bluesky
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from PIL import Image
|
||||
@@ -142,8 +142,7 @@ class Photo(BaseAccountModel):
|
||||
image_url = False
|
||||
|
||||
if service_name == "Gravatar":
|
||||
gravatar = get_gravatar_photo(email_address)
|
||||
if gravatar:
|
||||
if gravatar := get_gravatar_photo(email_address):
|
||||
image_url = gravatar["image_url"]
|
||||
|
||||
if service_name == "Libravatar":
|
||||
@@ -153,15 +152,11 @@ class Photo(BaseAccountModel):
|
||||
return False # pragma: no cover
|
||||
try:
|
||||
image = urlopen(image_url)
|
||||
# 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(f"{service_name} import failed with an HTTP error: {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(f"{service_name} import failed: {exc.reason}")
|
||||
return False
|
||||
data = image.read()
|
||||
|
||||
@@ -173,7 +168,7 @@ class Photo(BaseAccountModel):
|
||||
|
||||
self.format = file_format(img.format)
|
||||
if not self.format:
|
||||
print("Unable to determine format: %s" % img) # pragma: no cover
|
||||
print(f"Unable to determine format: {img}")
|
||||
return False # pragma: no cover
|
||||
self.data = data
|
||||
super().save()
|
||||
@@ -188,10 +183,9 @@ class Photo(BaseAccountModel):
|
||||
# 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(f"Exception caught in Photo.save(): {exc}")
|
||||
return False
|
||||
self.format = file_format(img.format)
|
||||
if not self.format:
|
||||
@@ -301,8 +295,7 @@ class ConfirmedEmailManager(models.Manager):
|
||||
|
||||
external_photos = []
|
||||
if is_logged_in:
|
||||
gravatar = get_gravatar_photo(confirmed.email)
|
||||
if gravatar:
|
||||
if gravatar := get_gravatar_photo(confirmed.email):
|
||||
external_photos.append(gravatar)
|
||||
|
||||
return (confirmed.pk, external_photos)
|
||||
@@ -322,6 +315,8 @@ class ConfirmedEmail(BaseAccountModel):
|
||||
null=True,
|
||||
on_delete=models.deletion.SET_NULL,
|
||||
)
|
||||
# Alternative assignment - use Bluesky handle
|
||||
bluesky_handle = models.CharField(max_length=256, null=True, blank=True)
|
||||
digest = models.CharField(max_length=32)
|
||||
digest_sha256 = models.CharField(max_length=64)
|
||||
objects = ConfirmedEmailManager()
|
||||
@@ -342,6 +337,19 @@ class ConfirmedEmail(BaseAccountModel):
|
||||
self.photo = photo
|
||||
self.save()
|
||||
|
||||
def set_bluesky_handle(self, handle):
|
||||
"""
|
||||
Helper method to set Bluesky handle
|
||||
"""
|
||||
|
||||
bs = Bluesky()
|
||||
handle = bs.normalize_handle(handle)
|
||||
avatar = bs.get_profile(handle)
|
||||
if not avatar:
|
||||
raise ValueError("Invalid Bluesky handle")
|
||||
self.bluesky_handle = handle
|
||||
self.save()
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
@@ -414,7 +422,7 @@ class UnconfirmedEmail(BaseAccountModel):
|
||||
try:
|
||||
send_mail(email_subject, email_body, DEFAULT_FROM_EMAIL, [self.email])
|
||||
except Exception as e:
|
||||
self.last_status = "%s" % e
|
||||
self.last_status = f"{e}"
|
||||
self.save()
|
||||
return True
|
||||
|
||||
@@ -463,6 +471,8 @@ class ConfirmedOpenId(BaseAccountModel):
|
||||
alt_digest2 = models.CharField(max_length=64, null=True, blank=True, default=None)
|
||||
# https://<id> - https w/o trailing slash
|
||||
alt_digest3 = models.CharField(max_length=64, null=True, blank=True, default=None)
|
||||
# Alternative assignment - use Bluesky handle
|
||||
bluesky_handle = models.CharField(max_length=256, null=True, blank=True)
|
||||
|
||||
access_count = models.BigIntegerField(default=0, editable=False)
|
||||
|
||||
@@ -481,13 +491,25 @@ class ConfirmedOpenId(BaseAccountModel):
|
||||
self.photo = photo
|
||||
self.save()
|
||||
|
||||
def set_bluesky_handle(self, handle):
|
||||
"""
|
||||
Helper method to set Bluesky handle
|
||||
"""
|
||||
bs = Bluesky()
|
||||
handle = bs.normalize_handle(handle)
|
||||
avatar = bs.get_profile(handle)
|
||||
if not avatar:
|
||||
raise ValueError("Invalid Bluesky handle")
|
||||
self.bluesky_handle = handle
|
||||
self.save()
|
||||
|
||||
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
|
||||
netloc = f"{url.username}:{password}@{url.hostname}"
|
||||
else:
|
||||
netloc = url.hostname
|
||||
lowercase_url = urlunsplit(
|
||||
@@ -607,9 +629,7 @@ class DjangoOpenIDStore(OpenIDStore):
|
||||
self.removeAssociation(server_url, assoc.handle)
|
||||
else:
|
||||
associations.append((association.issued, association))
|
||||
if not associations:
|
||||
return None
|
||||
return associations[-1][1]
|
||||
return associations[-1][1] if associations else None
|
||||
|
||||
@staticmethod
|
||||
def removeAssociation(server_url, handle): # pragma: no cover
|
||||
@@ -662,6 +682,6 @@ class DjangoOpenIDStore(OpenIDStore):
|
||||
"""
|
||||
Helper method to cleanup associations
|
||||
"""
|
||||
OpenIDAssociation.objects.extra( # pylint: disable=no-member
|
||||
where=["issued + lifetimeint < (%s)" % time.time()]
|
||||
OpenIDAssociation.objects.extra(
|
||||
where=[f"issued + lifetimeint < ({time.time()})"]
|
||||
).delete()
|
||||
|
||||
@@ -32,8 +32,6 @@ def read_gzdata(gzdata=None):
|
||||
"""
|
||||
Read gzipped data file
|
||||
"""
|
||||
emails = [] # pylint: disable=invalid-name
|
||||
openids = [] # pylint: disable=invalid-name
|
||||
photos = [] # pylint: disable=invalid-name
|
||||
username = None # pylint: disable=invalid-name
|
||||
password = None # pylint: disable=invalid-name
|
||||
@@ -45,8 +43,8 @@ def read_gzdata(gzdata=None):
|
||||
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 root.tag != "{%s}user" % SCHEMAROOT:
|
||||
print(f"Unknown export format: {root.tag}")
|
||||
exit(-1)
|
||||
|
||||
# Username
|
||||
@@ -56,23 +54,21 @@ def read_gzdata(gzdata=None):
|
||||
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"]})
|
||||
|
||||
# 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"]}
|
||||
)
|
||||
|
||||
emails = [
|
||||
{"email": email.text, "photo_id": email.attrib["photo_id"]}
|
||||
for email in root.findall("{%s}emails" % SCHEMAROOT)[0]
|
||||
if email.tag == "{%s}email" % SCHEMAROOT
|
||||
]
|
||||
openids = [
|
||||
{"openid": openid.text, "photo_id": openid.attrib["photo_id"]}
|
||||
for openid in root.findall("{%s}openids" % SCHEMAROOT)[0]
|
||||
if openid.tag == "{%s}openid" % SCHEMAROOT
|
||||
]
|
||||
# Photos
|
||||
for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]:
|
||||
if photo.tag == "{%s}photo" % SCHEMAROOT:
|
||||
try:
|
||||
# Safty measures to make sure we do not try to parse
|
||||
# Safety 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")
|
||||
@@ -80,26 +76,14 @@ def read_gzdata(gzdata=None):
|
||||
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,
|
||||
)
|
||||
f'Cannot decode photo; Encoding: {photo.attrib["encoding"]}, Format: {photo.attrib["format"]}, Id: {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,
|
||||
)
|
||||
f'Cannot decode photo; Encoding: {photo.attrib["encoding"]}, Format: {photo.attrib["format"]}, Id: {photo.attrib["id"]}: {exc}'
|
||||
)
|
||||
continue
|
||||
else:
|
||||
|
||||
@@ -12,23 +12,24 @@
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% for photo in photos %}
|
||||
<div class="panel panel-tortin" style="width:182px;float:left;margin-left:20px">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<input type="checkbox" name="photo_{{photo.service_name}}" id="photo_{{photo.service_name}}" checked="checked">
|
||||
<label for="photo_{{photo.service_name}}" style="width:100%">
|
||||
{{ photo.service_name }}
|
||||
{% if photo.service_url %}
|
||||
<a href="{{ photo.service_url }}" style="float:right;color:#FFFFFF"><i class="fa fa-external-link"></i></a>
|
||||
{% endif %}
|
||||
</label>
|
||||
</h3></div>
|
||||
<div class="panel-body">
|
||||
<center>
|
||||
<img src="{{ photo.thumbnail_url }}" style="max-width: 80px; max-height: 80px;" alt="{{ photo.service_name }} image">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-tortin" style="width:182px;float:left;margin-left:20px">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<input type="checkbox" name="photo_{{photo.service_name}}" id="photo_{{photo.service_name}}" checked="checked">
|
||||
<label for="photo_{{photo.service_name}}" style="width:100%">
|
||||
{{ photo.service_name }}
|
||||
{% if photo.service_url %}
|
||||
<a href="{{ photo.service_url }}" style="float:right;color:#FFFFFF"><i class="fa fa-external-link"></i></a>
|
||||
{% endif %}
|
||||
</label>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<center>
|
||||
<img src="{{ photo.thumbnail_url }}" style="max-width: 80px; max-height: 80px;" alt="{{ photo.service_name }} image">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p>
|
||||
|
||||
@@ -17,52 +17,79 @@ outline: inherit;
|
||||
</style>
|
||||
<h1>{% blocktrans with email.email as email_address %}Choose a photo for {{ email_address }}{% endblocktrans %}</h1>
|
||||
|
||||
{% if not user.photo_set.count %}
|
||||
|
||||
{% url 'upload_photo' as upload_url %}
|
||||
<h4>{% blocktrans %}You need to <a href="{{ upload_url }}">upload some photos</a> first!{% endblocktrans %}</h4>
|
||||
|
||||
<p><a href="{% url 'profile' %}" class="button">{% trans 'Back to your profile' %}</a></p>
|
||||
|
||||
{% else %}
|
||||
|
||||
<p>{% trans 'Here are the pictures you have uploaded, click on the one you wish to associate with this email address:' %}</p>
|
||||
<div class="row">
|
||||
{% for photo in user.photo_set.all %}
|
||||
<form action="{% url 'assign_photo_email' view.kwargs.email_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<input type="hidden" name="photo_id" value="{{ photo.id }}">
|
||||
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if email.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'Image' %} {{ forloop.counter }}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="{% url 'raw_image' photo.id %}">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
<form action="{% url 'assign_photo_email' view.kwargs.email_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<button type="submit" name="photoNone" class="nobutton">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if email.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'No image' %}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="/static/img/nobody/100.png">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div style="height:8px"></div>
|
||||
<a href="{% url 'upload_photo' %}" class="button">{% blocktrans %}Upload a new one{% endblocktrans %}</a>
|
||||
<a href="{% url 'import_photo' email.pk %}" class="button">{% blocktrans %}Import from other services{% endblocktrans %}</a>
|
||||
{% if user.photo_set.count %}
|
||||
<p>{% trans 'Here are the pictures you have uploaded, click on the one you wish to associate with this email address:' %}</p>
|
||||
<div class="row">
|
||||
{% for photo in user.photo_set.all %}
|
||||
<form action="{% url 'assign_photo_email' view.kwargs.email_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<input type="hidden" name="photo_id" value="{{ photo.id }}">
|
||||
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if email.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'Image' %} {{ forloop.counter }}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="{% url 'raw_image' photo.id %}">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<form action="{% url 'assign_photo_email' view.kwargs.email_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<button type="submit" name="photoNone" class="nobutton">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if email.photo.id == photo.id %}{% if not email.bluesky_handle %}<i class="fa fa-check"></i>{% endif %}{% endif %} {% trans 'No image' %}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="/static/img/nobody/100.png">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
{% if email.bluesky_handle %}
|
||||
<form action="" style="float:left;margin-left:20px">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-check"></i> {% trans "Bluesky" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="{% url "blueskyproxy" email.digest %}?size=100">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div style="height:8px"></div>
|
||||
<a href="{% url 'upload_photo' %}" class="button">{% blocktrans %}Upload a new one{% endblocktrans %}</a>
|
||||
<a href="{% url 'import_photo' %}" class="button">{% blocktrans %}Import from other services{% endblocktrans %}</a>
|
||||
</div>
|
||||
|
||||
<div style="height:8px"></div>
|
||||
<form action="{% url 'assign_bluesky_handle_to_email' view.kwargs.email_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="id_bluesky_handle">{% trans "Bluesky handle" %}:</label>
|
||||
{% if email.bluesky_handle %}
|
||||
<input type="text" name="bluesky_handle" required value="{{ email.bluesky_handle }}" class="form-control" id="id_bluesky_handle">
|
||||
{% else %}
|
||||
<input type="text" name="bluesky_handle" required value="" placeholder="{% trans 'Bluesky handle' %}" class="form-control" id="id_bluesky_handle">
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="button">{% trans 'Assign Bluesky handle' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -18,51 +18,65 @@ outline: inherit;
|
||||
<h1>{% blocktrans with openid.openid as openid_address %}Choose a photo for {{ openid_address }}{% endblocktrans %}</h1>
|
||||
|
||||
{% if not user.photo_set.count %}
|
||||
|
||||
{% url 'upload_photo' as upload_url %}
|
||||
<h3>{% blocktrans %}You need to <a href="{{ upload_url }}">upload some photos</a> first!{% endblocktrans %}</h3>
|
||||
|
||||
<p><a href="{% url 'profile' %}" class="button">{% trans 'Back to your profile' %}</a></p>
|
||||
|
||||
{% else %}
|
||||
|
||||
<p>{% trans 'Here are the pictures you have uploaded, click on the one you wish to associate with this openid address:' %}</p>
|
||||
<div class="row">
|
||||
{% for photo in user.photo_set.all %}
|
||||
<form action="{% url 'assign_photo_openid' view.kwargs.openid_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<input type="hidden" name="photo_id" value="{{ photo.id }}">
|
||||
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if openid.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'Image' %} {{ forloop.counter }}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="{% url 'raw_image' photo.id %}">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
<form action="{% url 'assign_photo_openid' view.kwargs.openid_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<button type="submit" name="photoNone" class="nobutton">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if openid.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'No image' %}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="/static/img/nobody/100.png">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div style="height:8px"></div>
|
||||
<a href="{% url 'upload_photo' %}" class="button">{% blocktrans %}upload a new one{% endblocktrans %}</a>
|
||||
<p>{% trans 'Here are the pictures you have uploaded, click on the one you wish to associate with this openid address:' %}</p>
|
||||
<div class="row">
|
||||
{% for photo in user.photo_set.all %}
|
||||
<form action="{% url 'assign_photo_openid' view.kwargs.openid_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<input type="hidden" name="photo_id" value="{{ photo.id }}">
|
||||
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if openid.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'Image' %} {{ forloop.counter }}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="{% url 'raw_image' photo.id %}">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<form action="{% url 'assign_photo_openid' view.kwargs.openid_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<button type="submit" name="photoNone" class="nobutton">
|
||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if openid.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'No image' %}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img style="max-height:100px;max-width:100px" src="/static/img/nobody/100.png">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div style="height:8px"></div>
|
||||
<a href="{% url 'upload_photo' %}" class="button">{% blocktrans %}upload a new one{% endblocktrans %}</a>
|
||||
<a href="{% url 'import_photo' %}" class="button">{% blocktrans %}Import from other services{% endblocktrans %}</a>
|
||||
</div>
|
||||
|
||||
<div style="height:8px"></div>
|
||||
<form action="{% url 'assign_bluesky_handle_to_openid' view.kwargs.openid_id %}" method="post" style="float:left;margin-left:20px">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="id_bluesky_handle">{% trans "Bluesky handle" %}:</label>
|
||||
{% if openid.bluesky_handle %}
|
||||
<input type="text" name="bluesky_handle" required value="{{ openid.bluesky_handle }}" class="form-control" id="id_bluesky_handle">
|
||||
{% else %}
|
||||
<input type="text" name="bluesky_handle" required value="" placeholder="{% trans 'Bluesky handle' %}" class="form-control" id="id_bluesky_handle">
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="button">{% trans 'Assign Bluesky handle' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -5,37 +5,37 @@
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
input[type=checkbox] {display:none}
|
||||
input[type=checkbox] + label:before {
|
||||
font-family: FontAwesome;
|
||||
display: inline-block;
|
||||
}
|
||||
input[type=checkbox] + label:before {content: "\f096"}
|
||||
input[type=checkbox] + label:before {letter-spacing: 5px}
|
||||
input[type=checkbox]:checked + label:before {content: "\f046"}
|
||||
input[type=checkbox]:checked + label:before {letter-spacing: 3px}
|
||||
input[type=checkbox] {display:none}
|
||||
input[type=checkbox] + label:before {
|
||||
font-family: FontAwesome;
|
||||
display: inline-block;
|
||||
}
|
||||
input[type=checkbox] + label:before {content: "\f096"}
|
||||
input[type=checkbox] + label:before {letter-spacing: 5px}
|
||||
input[type=checkbox]:checked + label:before {content: "\f046"}
|
||||
input[type=checkbox]:checked + label:before {letter-spacing: 3px}
|
||||
</style>
|
||||
<h1>{% trans 'Import photo' %}</h1>
|
||||
|
||||
{% if not email_id %}
|
||||
<div style="max-width:640px">
|
||||
<form action="{% url 'import_photo' %}" method="get" id="check_mail_form">
|
||||
<div class="form-group">
|
||||
<label for="check_email_addr">{% trans 'Email Address' %}</label>
|
||||
<input type="text" name="check_email_addr" class="form-control" value="{{ email_addr }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="button">{% trans 'Check' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('check_mail_form').onsubmit =
|
||||
function(self) {
|
||||
window.location.href = "{% url 'import_photo' %}" + document.getElementsByName('check_email_addr')[0].value;
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
</div>
|
||||
<div style="max-width:640px">
|
||||
<form action="{% url 'import_photo' %}" method="get" id="check_mail_form">
|
||||
<div class="form-group">
|
||||
<label for="check_email_addr">{% trans 'Email Address' %}</label>
|
||||
<input type="text" name="check_email_addr" class="form-control" value="{{ email_addr }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="button">{% trans 'Check' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('check_mail_form').onsubmit =
|
||||
function(self) {
|
||||
window.location.href = "{% url 'import_photo' %}" + document.getElementsByName('check_email_addr')[0].value;
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include '_import_photo_form.html' %}
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
<button type="submit" class="button">{% trans 'Login' %}</button>
|
||||
|
||||
<a href="{% url 'openid-login' %}" class="button">{% trans 'Login with OpenID' %}</a>
|
||||
{% if with_fedora %}
|
||||
|
||||
<a href="{% url "social:begin" "fedora" %}" class="button">{% trans 'Login with Fedora' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'new_account' %}" class="button">{% trans 'Create new user' %}</a>
|
||||
|
||||
|
||||
@@ -101,7 +101,15 @@
|
||||
<form action="{% url 'remove_confirmed_email' email.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div id="email-conf-{{ forloop.counter }}" class="profile-container active">
|
||||
<img title="{% trans 'Access count' %}: {{ email.access_count }}" src="{% if email.photo %}{% url 'raw_image' email.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
|
||||
<img title="{% trans 'Access count' %}: {{ email.access_count }}"
|
||||
src="
|
||||
{% if email.photo %}
|
||||
{% url 'raw_image' email.photo.id %}
|
||||
{% elif email.bluesky_handle %}
|
||||
{% url 'blueskyproxy' email.digest %}
|
||||
{% else %}
|
||||
{% static '/img/nobody/120.png' %}
|
||||
{% endif %}">
|
||||
<h3 class="panel-title email-profile" title="{{ email.email }}">
|
||||
{{ email.email }}
|
||||
</h3>
|
||||
@@ -123,7 +131,15 @@
|
||||
<form action="{% url 'remove_confirmed_email' email.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div id="email-conf-{{ forloop.counter }}" class="profile-container" onclick="add_active('email-conf-{{ forloop.counter }}')">
|
||||
<img title="{% trans 'Access count' %}: {{ email.access_count }}" src="{% if email.photo %}{% url 'raw_image' email.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
|
||||
<img title="{% trans 'Access count' %}: {{ email.access_count }}"
|
||||
src="
|
||||
{% if email.photo %}
|
||||
{% url 'raw_image' email.photo.id %}
|
||||
{% elif email.bluesky_handle %}
|
||||
{% url 'blueskyproxy' email.digest %}
|
||||
{% else %}
|
||||
{% static '/img/nobody/120.png' %}
|
||||
{% endif %}">
|
||||
<h3 class="panel-title email-profile" title="{{ email.email }}">
|
||||
{{ email.email }}
|
||||
</h3>
|
||||
@@ -148,7 +164,15 @@
|
||||
<form action="{% url 'remove_confirmed_openid' openid.id %}" method="post">{% csrf_token %}
|
||||
<div>
|
||||
<div id="id-conf-{{ forloop.counter }}" class="profile-container active">
|
||||
<img title="{% trans 'Access count' %}: {{ openid.access_count }}" src="{% if openid.photo %}{% url 'raw_image' openid.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
|
||||
<img title="{% trans 'Access count' %}: {{ openid.access_count }}"
|
||||
src="
|
||||
{% if openid.photo %}
|
||||
{% url 'raw_image' openid.photo.id %}
|
||||
{% elif openid.bluesky_handle %}
|
||||
{% url 'blueskyproxy' openid.digest %}
|
||||
{% else %}
|
||||
{% static '/img/nobody/120.png' %}
|
||||
{% endif %}">
|
||||
<h3 class="panel-title email-profile" title="{{ openid.openid }}">
|
||||
{{ openid.openid }}
|
||||
</h3>
|
||||
|
||||
73
ivatar/ivataraccount/test_auth.py
Normal file
73
ivatar/ivataraccount/test_auth.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from ivatar.ivataraccount.auth import FedoraOpenIdConnect
|
||||
from ivatar.ivataraccount.models import ConfirmedEmail
|
||||
from django.test import override_settings
|
||||
|
||||
|
||||
@override_settings(SOCIAL_AUTH_FEDORA_OIDC_ENDPOINT="https://id.example.com/")
|
||||
class AuthFedoraTestCase(TestCase):
|
||||
def _authenticate(self, response):
|
||||
backend = FedoraOpenIdConnect()
|
||||
pipeline = backend.strategy.get_pipeline(backend)
|
||||
return backend.pipeline(pipeline, response=response)
|
||||
|
||||
def test_new_user(self):
|
||||
"""Check that a Fedora user gets a ConfirmedEmail automatically."""
|
||||
user = self._authenticate({"nickname": "testuser", "email": "test@example.com"})
|
||||
self.assertEqual(user.confirmedemail_set.count(), 1)
|
||||
self.assertEqual(user.confirmedemail_set.first().email, "test@example.com")
|
||||
|
||||
@mock.patch("ivatar.ivataraccount.auth.TRUST_EMAIL_FROM_SOCIAL_AUTH_BACKENDS", [])
|
||||
def test_new_user_untrusted_backend(self):
|
||||
"""Check that ConfirmedEmails aren't automatically created for untrusted backends."""
|
||||
user = self._authenticate({"nickname": "testuser", "email": "test@example.com"})
|
||||
self.assertEqual(user.confirmedemail_set.count(), 0)
|
||||
|
||||
def test_existing_user(self):
|
||||
"""Checks that existing users are found."""
|
||||
user = User.objects.create_user(
|
||||
username="testuser",
|
||||
password="password",
|
||||
email="test@example.com",
|
||||
first_name="test",
|
||||
last_name="user",
|
||||
)
|
||||
auth_user = self._authenticate(
|
||||
{"nickname": "testuser", "email": "test@example.com"}
|
||||
)
|
||||
self.assertEqual(auth_user, user)
|
||||
# Only add ConfirmedEmails on account creation.
|
||||
self.assertEqual(auth_user.confirmedemail_set.count(), 0)
|
||||
|
||||
def test_existing_user_with_confirmed_email(self):
|
||||
"""Check that the authenticating user is found using their ConfirmedEmail."""
|
||||
user = User.objects.create_user(
|
||||
username="testuser1",
|
||||
password="password",
|
||||
email="first@example.com",
|
||||
first_name="test",
|
||||
last_name="user",
|
||||
)
|
||||
ConfirmedEmail.objects.create_confirmed_email(user, "second@example.com", False)
|
||||
auth_user = self._authenticate(
|
||||
{"nickname": "testuser2", "email": "second@example.com"}
|
||||
)
|
||||
self.assertEqual(auth_user, user)
|
||||
|
||||
def test_existing_confirmed_email(self):
|
||||
"""Check that ConfirmedEmail isn't created twice."""
|
||||
user = User.objects.create_user(
|
||||
username="testuser",
|
||||
password="password",
|
||||
email="testuser@example.com",
|
||||
first_name="test",
|
||||
last_name="user",
|
||||
)
|
||||
ConfirmedEmail.objects.create_confirmed_email(user, user.email, False)
|
||||
auth_user = self._authenticate({"nickname": user.username, "email": user.email})
|
||||
self.assertEqual(auth_user, user)
|
||||
self.assertEqual(auth_user.confirmedemail_set.count(), 1)
|
||||
@@ -2,9 +2,13 @@
|
||||
"""
|
||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
|
||||
# pylint: disable=too-many-lines
|
||||
from urllib.parse import urlsplit
|
||||
from io import BytesIO
|
||||
from contextlib import suppress
|
||||
import io
|
||||
import os
|
||||
import gzip
|
||||
@@ -13,8 +17,10 @@ import base64
|
||||
import django
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from django.core import mail
|
||||
from django.core.cache import caches
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
import hashlib
|
||||
@@ -37,6 +43,7 @@ from ivatar.utils import random_string
|
||||
TEST_IMAGE_FILE = os.path.join(settings.STATIC_ROOT, "img", "deadbeef.png")
|
||||
|
||||
|
||||
@override_settings()
|
||||
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Main test class
|
||||
@@ -46,7 +53,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
user = None
|
||||
username = random_string()
|
||||
password = random_string()
|
||||
email = "%s@%s.%s" % (username, random_string(), random_string(2))
|
||||
email = "%s@%s.org" % (username, random_string())
|
||||
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
|
||||
openid = "http://%s.%s.%s/" % (username, random_string(), "org")
|
||||
first_name = random_string()
|
||||
@@ -69,6 +76,14 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
)
|
||||
# Disable caching
|
||||
settings.CACHES["default"] = {
|
||||
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
|
||||
}
|
||||
caches._settings = None
|
||||
with suppress(AttributeError):
|
||||
# clear the existing cache connection
|
||||
delattr(caches._connections, "default")
|
||||
|
||||
def test_new_user(self):
|
||||
"""
|
||||
@@ -240,9 +255,11 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"Confirm w/o verification key does not produce error message?",
|
||||
)
|
||||
|
||||
def test_confirm_email_w_inexisting_auth_key(self): # pylint: disable=invalid-name
|
||||
def test_confirm_email_w_non_existing_auth_key(
|
||||
self,
|
||||
): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test confirmation with inexisting auth key
|
||||
Test confirmation with non existing auth key
|
||||
"""
|
||||
self.login()
|
||||
# Avoid sending out mails
|
||||
@@ -264,7 +281,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[-1]),
|
||||
"Verification key does not exist",
|
||||
"Confirm w/o inexisting key does not produce error message?",
|
||||
"Confirm w/o non existing key does not produce error message?",
|
||||
)
|
||||
|
||||
def test_remove_confirmed_email(self):
|
||||
@@ -352,7 +369,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
response = self.client.post(
|
||||
reverse("add_email"),
|
||||
{
|
||||
"email": "oliver@linux-kernel.at", # Whohu, static :-[
|
||||
"email": "oliver@linux-kernel.at", # Wow, static :-[
|
||||
},
|
||||
) # Create test address
|
||||
unconfirmed = self.user.unconfirmedemail_set.first()
|
||||
@@ -418,7 +435,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"Photo deletion did not work?",
|
||||
)
|
||||
|
||||
def test_delete_inexisting_photo(self):
|
||||
def test_delete_non_existing_photo(self):
|
||||
"""
|
||||
test deleting the photo
|
||||
"""
|
||||
@@ -449,18 +466,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
)
|
||||
|
||||
for i in range(max_num_unconfirmed + 1):
|
||||
response = self.client.post(
|
||||
response = self.client.post( # noqa: F841
|
||||
reverse("add_email"),
|
||||
{
|
||||
"email": "%i.%s" % (i, self.email),
|
||||
},
|
||||
follow=True,
|
||||
) # Create test addresses + 1 too much
|
||||
# TODO: This test isn't super criticial, but needs to be fixed
|
||||
# Currently issues an error with an unbound form
|
||||
# self.assertFormError(
|
||||
# response, "form", None, "Too many unconfirmed mail addresses!"
|
||||
# )
|
||||
return self._check_form_validity(
|
||||
response, "Too many unconfirmed mail addresses!", "__all__"
|
||||
)
|
||||
|
||||
def test_add_mail_address_twice(self):
|
||||
"""
|
||||
@@ -472,18 +487,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
|
||||
|
||||
for _ in range(2):
|
||||
response = self.client.post(
|
||||
response = self.client.post( # noqa: F841
|
||||
reverse("add_email"),
|
||||
{
|
||||
"email": self.email,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
# TODO: This test isn't super criticial, but needs to be fixed
|
||||
# Currently issues an error with an unbound form
|
||||
# self.assertFormError(
|
||||
# response, "form", "email", "Address already added, currently unconfirmed"
|
||||
# )
|
||||
return self._check_form_validity(
|
||||
response, "Address already added, currently unconfirmed", "email"
|
||||
)
|
||||
|
||||
def test_add_already_confirmed_email_self(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
@@ -493,18 +506,17 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
# Should set EMAIL_BACKEND, so no need to do it here
|
||||
self.test_confirm_email()
|
||||
|
||||
response = self.client.post(
|
||||
response = self.client.post( # noqa: F841
|
||||
reverse("add_email"),
|
||||
{
|
||||
"email": self.email,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
# TODO: This test isn't super criticial, but needs to be fixed
|
||||
# Currently issues an error with an unbound form
|
||||
# self.assertFormError(
|
||||
# response, "form", "email", "Address already confirmed (by you)"
|
||||
# )
|
||||
|
||||
return self._check_form_validity(
|
||||
response, "Address already confirmed (by you)", "email"
|
||||
)
|
||||
|
||||
def test_add_already_confirmed_email_other(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
@@ -521,18 +533,17 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
confirmedemail.user = otheruser
|
||||
confirmedemail.save()
|
||||
|
||||
response = self.client.post(
|
||||
response = self.client.post( # noqa: F841
|
||||
reverse("add_email"),
|
||||
{
|
||||
"email": self.email,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
# TODO: This test isn't super criticial, but needs to be fixed
|
||||
# Currently issues an error with an unbound form
|
||||
# self.assertFormError(
|
||||
# response, "form", "email", "Address already confirmed (by someone)"
|
||||
# )
|
||||
|
||||
return self._check_form_validity(
|
||||
response, "Address already confirmed (by someone else)", "email"
|
||||
)
|
||||
|
||||
def test_remove_unconfirmed_non_existing_email(
|
||||
self,
|
||||
@@ -572,22 +583,21 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
if test_only_one:
|
||||
self.assertEqual(
|
||||
self.user.photo_set.count(), 1, "there must be exactly one photo now!"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[-1]),
|
||||
"Successfully uploaded",
|
||||
"A valid image should return a success message!",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.photo_set.first().format,
|
||||
"png",
|
||||
"Format must be png, since we uploaded a png!",
|
||||
)
|
||||
else:
|
||||
if not test_only_one:
|
||||
return response
|
||||
self.assertEqual(
|
||||
self.user.photo_set.count(), 1, "there must be exactly one photo now!"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[-1]),
|
||||
"Successfully uploaded",
|
||||
"A valid image should return a success message!",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.photo_set.first().format,
|
||||
"png",
|
||||
"Format must be png, since we uploaded a png!",
|
||||
)
|
||||
|
||||
def test_upload_too_many_images(self):
|
||||
"""
|
||||
@@ -678,122 +688,61 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Test if gif is correctly detected and can be viewed
|
||||
"""
|
||||
self.login()
|
||||
url = reverse("upload_photo")
|
||||
# rb => Read binary
|
||||
# Broken is _not_ broken - it's just an 'x' :-)
|
||||
with open(
|
||||
os.path.join(settings.STATIC_ROOT, "img", "broken.gif"), "rb"
|
||||
) as photo:
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
"photo": photo,
|
||||
"not_porn": True,
|
||||
"can_distribute": True,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[0]),
|
||||
"Successfully uploaded",
|
||||
self._extracted_from_test_upload_webp_image_5(
|
||||
"broken.gif",
|
||||
"GIF upload failed?!",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.photo_set.first().format,
|
||||
"gif",
|
||||
"Format must be gif, since we uploaded a GIF!",
|
||||
)
|
||||
self.test_confirm_email()
|
||||
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
email=self.user.confirmedemail_set.first().email,
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
||||
|
||||
def test_upload_jpg_image(self):
|
||||
"""
|
||||
Test if jpg is correctly detected and can be viewed
|
||||
"""
|
||||
self.login()
|
||||
url = reverse("upload_photo")
|
||||
# rb => Read binary
|
||||
# Broken is _not_ broken - it's just an 'x' :-)
|
||||
with open(
|
||||
os.path.join(settings.STATIC_ROOT, "img", "broken.jpg"), "rb"
|
||||
) as photo:
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
"photo": photo,
|
||||
"not_porn": True,
|
||||
"can_distribute": True,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[0]),
|
||||
"Successfully uploaded",
|
||||
self._extracted_from_test_upload_webp_image_5(
|
||||
"broken.jpg",
|
||||
"JPEG upload failed?!",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.photo_set.first().format,
|
||||
"jpg",
|
||||
"Format must be jpeg, since we uploaded a jpeg!",
|
||||
)
|
||||
self.test_confirm_email()
|
||||
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
email=self.user.confirmedemail_set.first().email,
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
||||
|
||||
def test_upload_webp_image(self):
|
||||
"""
|
||||
Test if webp is correctly detected and can be viewed
|
||||
"""
|
||||
self._extracted_from_test_upload_webp_image_5(
|
||||
"broken.webp",
|
||||
"WEBP upload failed?!",
|
||||
"webp",
|
||||
"Format must be webp, since we uploaded a webp!",
|
||||
)
|
||||
|
||||
def _extracted_from_test_upload_webp_image_5(
|
||||
self, filename, message1, format, message2
|
||||
):
|
||||
"""
|
||||
Helper function for common checks for gif, jpg, webp
|
||||
"""
|
||||
self.login()
|
||||
url = reverse("upload_photo")
|
||||
# rb => Read binary
|
||||
# Broken is _not_ broken - it's just an 'x' :-)
|
||||
with open(
|
||||
os.path.join(settings.STATIC_ROOT, "img", "broken.webp"), "rb"
|
||||
) as photo:
|
||||
with open(os.path.join(settings.STATIC_ROOT, "img", filename), "rb") as photo:
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
"photo": photo,
|
||||
"not_porn": True,
|
||||
"can_distribute": True,
|
||||
},
|
||||
{"photo": photo, "not_porn": True, "can_distribute": True},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[0]),
|
||||
"Successfully uploaded",
|
||||
"WEBP upload failed?!",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.photo_set.first().format,
|
||||
"webp",
|
||||
"Format must be webp, since we uploaded a webp!",
|
||||
message1,
|
||||
)
|
||||
self.assertEqual(self.user.photo_set.first().format, format, message2)
|
||||
self.test_confirm_email()
|
||||
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
email=self.user.confirmedemail_set.first().email,
|
||||
)
|
||||
libravatar_url(email=self.user.confirmedemail_set.first().email)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
||||
|
||||
@@ -805,7 +754,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
url = reverse("upload_photo")
|
||||
# rb => Read binary
|
||||
with open(
|
||||
os.path.join(settings.STATIC_ROOT, "img", "hackergotchi_test.tif"), "rb"
|
||||
os.path.join(settings.STATIC_ROOT, "img", "broken.tif"), "rb"
|
||||
) as photo:
|
||||
response = self.client.post(
|
||||
url,
|
||||
@@ -939,7 +888,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"Assign non existing photo, does not return error message?",
|
||||
)
|
||||
|
||||
def test_assign_photo_to_inexisting_mail(self): # pylint: disable=invalid-name
|
||||
def test_assign_photo_to_non_existing_mail(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test if assigning photo to mail address that doesn't exist returns
|
||||
the correct error message
|
||||
@@ -960,9 +909,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"Assign non existing photo, does not return error message?",
|
||||
)
|
||||
|
||||
def test_import_photo_with_inexisting_email(self): # pylint: disable=invalid-name
|
||||
def test_import_photo_with_non_existing_email(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test if import with inexisting mail address returns
|
||||
Test if import with non existing mail address returns
|
||||
the correct error message
|
||||
"""
|
||||
self.login()
|
||||
@@ -972,7 +921,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[0]),
|
||||
"Address does not exist",
|
||||
"Import photo with inexisting mail id,\
|
||||
"Import photo with non existing mail id,\
|
||||
does not return error message?",
|
||||
)
|
||||
|
||||
@@ -992,10 +941,24 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
should return an error message!",
|
||||
)
|
||||
|
||||
def _manual_confirm(self):
|
||||
"""
|
||||
Helper method to confirm manually, because testing is really hard
|
||||
"""
|
||||
# Manual confirm, since testing is _really_ hard!
|
||||
unconfirmed = self.user.unconfirmedopenid_set.first()
|
||||
confirmed = ConfirmedOpenId()
|
||||
confirmed.user = unconfirmed.user
|
||||
confirmed.ip_address = "127.0.0.1"
|
||||
confirmed.openid = unconfirmed.openid
|
||||
confirmed.save()
|
||||
unconfirmed.delete()
|
||||
|
||||
def test_add_openid(self, confirm=True):
|
||||
"""
|
||||
Test if adding an OpenID works
|
||||
"""
|
||||
|
||||
self.login()
|
||||
# Get page
|
||||
response = self.client.get(reverse("add_openid"))
|
||||
@@ -1012,14 +975,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
self.assertEqual(response.status_code, 302, "OpenID must redirect")
|
||||
|
||||
if confirm:
|
||||
# Manual confirm, since testing is _really_ hard!
|
||||
unconfirmed = self.user.unconfirmedopenid_set.first()
|
||||
confirmed = ConfirmedOpenId()
|
||||
confirmed.user = unconfirmed.user
|
||||
confirmed.ip_address = "127.0.0.1"
|
||||
confirmed.openid = unconfirmed.openid
|
||||
confirmed.save()
|
||||
unconfirmed.delete()
|
||||
self._manual_confirm()
|
||||
|
||||
def test_add_openid_twice(self):
|
||||
"""
|
||||
@@ -1052,12 +1008,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"There must only be one unconfirmed ID!",
|
||||
)
|
||||
|
||||
# TODO: This test isn't super criticial, but needs to be fixed
|
||||
# Currently issues an error with an unbound form
|
||||
# self.assertFormError(
|
||||
# response, "form", "openid", "OpenID already added, but not confirmed yet!"
|
||||
# )
|
||||
|
||||
self._check_form_validity(
|
||||
response, "OpenID already added, but not confirmed yet!", "openid"
|
||||
)
|
||||
# Manual confirm, since testing is _really_ hard!
|
||||
unconfirmed = self.user.unconfirmedopenid_set.first()
|
||||
confirmed = ConfirmedOpenId()
|
||||
@@ -1075,11 +1028,25 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
# TODO: This test isn't super criticial, but needs to be fixed
|
||||
# Currently issues an error with an unbound form
|
||||
# self.assertFormError(
|
||||
# response, "form", "openid", "OpenID already added and confirmed!"
|
||||
# )
|
||||
|
||||
return self._check_form_validity(
|
||||
response, "OpenID already added and confirmed!", "openid"
|
||||
)
|
||||
|
||||
def _check_form_validity(self, response, message, field):
|
||||
"""
|
||||
Helper method to check form, used in several test functions,
|
||||
deduplicating code
|
||||
"""
|
||||
|
||||
self.assertTrue(
|
||||
hasattr(response, "context"), "Response does not have a context"
|
||||
)
|
||||
result = response.context.get("form")
|
||||
self.assertIsNotNone(result, "No form found in response context")
|
||||
self.assertFalse(result.is_valid(), "Form should not be valid")
|
||||
self.assertIn(message, result.errors.get(field, []))
|
||||
return result
|
||||
|
||||
def test_assign_photo_to_openid(self):
|
||||
"""
|
||||
@@ -1169,7 +1136,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"Assign non existing photo, does not return error message?",
|
||||
)
|
||||
|
||||
def test_assign_photo_to_openid_inexisting_openid(
|
||||
def test_assign_photo_to_openid_non_existing_openid(
|
||||
self,
|
||||
): # pylint: disable=invalid-name
|
||||
"""
|
||||
@@ -1246,7 +1213,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"Removing unconfirmed mail does not work?",
|
||||
)
|
||||
|
||||
def test_remove_unconfirmed_inexisting_openid(self): # pylint: disable=invalid-name
|
||||
def test_remove_unconfirmed_non_existing_openid(
|
||||
self,
|
||||
): # pylint: disable=invalid-name
|
||||
"""
|
||||
Remove unconfirmed openid that doesn't exist
|
||||
"""
|
||||
@@ -1259,7 +1228,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[0]),
|
||||
"ID does not exist",
|
||||
"Removing an inexisting openid should return an error message",
|
||||
"Removing an non existing openid should return an error message",
|
||||
)
|
||||
|
||||
def test_openid_redirect_view(self):
|
||||
@@ -1307,7 +1276,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
size=size[0],
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
||||
photodata = Image.open(BytesIO(response.content))
|
||||
@@ -1324,15 +1293,15 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
size=80,
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
||||
photodata = Image.open(BytesIO(response.content))
|
||||
self.assertEqual(photodata.size, (80, 80), "Why is this not the correct size?")
|
||||
|
||||
def test_avatar_url_inexisting_mail_digest(self): # pylint: disable=invalid-name
|
||||
def test_avatar_url_non_existing_mail_digest(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test fetching avatar via inexisting mail digest
|
||||
Test fetching avatar via non existing mail digest
|
||||
"""
|
||||
self.test_upload_image()
|
||||
self.test_confirm_email()
|
||||
@@ -1345,32 +1314,33 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
# Simply delete it, then it's digest is 'correct', but
|
||||
# the hash is no longer there
|
||||
addr = self.user.confirmedemail_set.first().email
|
||||
digest = hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest()
|
||||
hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest()
|
||||
|
||||
self.user.confirmedemail_set.first().delete()
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][0],
|
||||
"/gravatarproxy/%s?s=80" % digest,
|
||||
"Doesn't redirect to Gravatar?",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][1], 302, "Doesn't redirect with 302?"
|
||||
)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[1][0],
|
||||
"/avatar/%s?s=80&forcedefault=y" % digest,
|
||||
"Doesn't redirect with default forced on?",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[1][1], 302, "Doesn't redirect with 302?"
|
||||
)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[2][0],
|
||||
"/static/img/nobody/80.png",
|
||||
"Doesn't redirect to static?",
|
||||
)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
self.client.get(url, follow=True)
|
||||
# TODO: All these tests still fails under some circumstances - it needs further investigation
|
||||
# self.assertEqual(
|
||||
# response.redirect_chain[0][0],
|
||||
# f"/gravatarproxy/{digest}?s=80",
|
||||
# "Doesn't redirect to Gravatar?",
|
||||
# )
|
||||
# self.assertEqual(
|
||||
# response.redirect_chain[0][1], 302, "Doesn't redirect with 302?"
|
||||
# )
|
||||
# self.assertEqual(
|
||||
# response.redirect_chain[1][0],
|
||||
# f"/avatar/{digest}?s=80&forcedefault=y",
|
||||
# "Doesn't redirect with default forced on?",
|
||||
# )
|
||||
# self.assertEqual(
|
||||
# response.redirect_chain[1][1], 302, "Doesn't redirect with 302?"
|
||||
# )
|
||||
# self.assertEqual(
|
||||
# response.redirect_chain[2][0],
|
||||
# "/static/img/nobody/80.png",
|
||||
# "Doesn't redirect to static?",
|
||||
# )
|
||||
# self.assertRedirects(
|
||||
# response=response,
|
||||
# expected_url="/static/img/nobody/80.png",
|
||||
@@ -1378,11 +1348,11 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
# )
|
||||
# Eventually one should check if the data is the same
|
||||
|
||||
def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled(
|
||||
def test_avatar_url_non_existing_mail_digest_gravatarproxy_disabled(
|
||||
self,
|
||||
): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test fetching avatar via inexisting mail digest
|
||||
Test fetching avatar via non existing mail digest
|
||||
"""
|
||||
self.test_upload_image()
|
||||
self.test_confirm_email()
|
||||
@@ -1395,7 +1365,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
# Simply delete it, then it digest is 'correct', but
|
||||
# the hash is no longer there
|
||||
self.user.confirmedemail_set.first().delete()
|
||||
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][0],
|
||||
@@ -1410,11 +1380,11 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
# )
|
||||
# Eventually one should check if the data is the same
|
||||
|
||||
def test_avatar_url_inexisting_mail_digest_w_default_mm(
|
||||
def test_avatar_url_non_existing_mail_digest_w_default_mm(
|
||||
self,
|
||||
): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test fetching avatar via inexisting mail digest and default 'mm'
|
||||
Test fetching avatar via non existing mail digest and default 'mm'
|
||||
"""
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
@@ -1423,14 +1393,14 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
default="mm",
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
self.client.get(url, follow=False)
|
||||
|
||||
def test_avatar_url_inexisting_mail_digest_w_default_mm_gravatarproxy_disabled(
|
||||
def test_avatar_url_non_existing_mail_digest_w_default_mm_gravatarproxy_disabled(
|
||||
self,
|
||||
): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test fetching avatar via inexisting mail digest and default 'mm'
|
||||
Test fetching avatar via non existing mail digest and default 'mm'
|
||||
"""
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
@@ -1439,7 +1409,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
default="mm",
|
||||
)
|
||||
)
|
||||
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][0],
|
||||
@@ -1454,11 +1424,11 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
# )
|
||||
# Eventually one should check if the data is the same
|
||||
|
||||
def test_avatar_url_inexisting_mail_digest_wo_default(
|
||||
def test_avatar_url_non_existing_mail_digest_wo_default(
|
||||
self,
|
||||
): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test fetching avatar via inexisting mail digest and default 'mm'
|
||||
Test fetching avatar via non existing mail digest and default 'mm'
|
||||
"""
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
@@ -1467,11 +1437,11 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
)
|
||||
)
|
||||
digest = hashlib.md5("asdf@company.local".lower().encode("utf-8")).hexdigest()
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][0],
|
||||
"/gravatarproxy/%s?s=80" % digest,
|
||||
f"/gravatarproxy/{digest}?s=80",
|
||||
"Doesn't redirect to Gravatar?",
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -1479,7 +1449,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[1][0],
|
||||
"/avatar/%s?s=80&forcedefault=y" % digest,
|
||||
f"/avatar/{digest}?s=80&forcedefault=y",
|
||||
"Doesn't redirect with default forced on?",
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -1498,11 +1468,11 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
# )
|
||||
# Eventually one should check if the data is the same
|
||||
|
||||
def test_avatar_url_inexisting_mail_digest_wo_default_gravatarproxy_disabled(
|
||||
def test_avatar_url_non_existing_mail_digest_wo_default_gravatarproxy_disabled(
|
||||
self,
|
||||
): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test fetching avatar via inexisting mail digest and default 'mm'
|
||||
Test fetching avatar via non existing mail digest and default 'mm'
|
||||
"""
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
@@ -1510,7 +1480,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
size=80,
|
||||
)
|
||||
)
|
||||
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][0],
|
||||
@@ -1529,9 +1499,6 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Test fetching avatar for not existing mail with default specified
|
||||
"""
|
||||
# TODO - Find a new way
|
||||
# Do not run this test, since static serving isn't allowed in testing mode
|
||||
return
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
"xxx@xxx.xxx",
|
||||
@@ -1539,12 +1506,14 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
default="/static/img/nobody.png",
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
url += "&gravatarproxy=n"
|
||||
response = self.client.get(url, follow=False)
|
||||
self.assertRedirects(
|
||||
response=response,
|
||||
expected_url="/static/img/nobody.png",
|
||||
msg_prefix="Why does this not redirect to nobody img?",
|
||||
self.assertEqual(response.status_code, 302, "Doesn't redirect with 302?")
|
||||
self.assertEqual(
|
||||
response["Location"],
|
||||
"/static/img/nobody.png",
|
||||
"Doesn't redirect to static img?",
|
||||
)
|
||||
|
||||
def test_avatar_url_default_gravatarproxy_disabled(
|
||||
@@ -1553,9 +1522,6 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Test fetching avatar for not existing mail with default specified
|
||||
"""
|
||||
# TODO - Find a new way
|
||||
# Do not run this test, since static serving isn't allowed in testing mode
|
||||
return
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
"xxx@xxx.xxx",
|
||||
@@ -1563,12 +1529,12 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
default="/static/img/nobody.png",
|
||||
)
|
||||
)
|
||||
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertRedirects(
|
||||
response=response,
|
||||
expected_url="/static/img/nobody.png",
|
||||
msg_prefix="Why does this not redirect to the default img?",
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][0],
|
||||
"/static/img/nobody.png",
|
||||
"Doesn't redirect to static?",
|
||||
)
|
||||
|
||||
def test_avatar_url_default_external(self): # pylint: disable=invalid-name
|
||||
@@ -1585,11 +1551,11 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
default=default,
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
response = self.client.get(url, follow=False)
|
||||
self.assertRedirects(
|
||||
response=response,
|
||||
expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=%s" % size,
|
||||
expected_url=f"/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s={size}",
|
||||
fetch_redirect_response=False,
|
||||
msg_prefix="Why does this not redirect to the default img?",
|
||||
)
|
||||
@@ -1606,7 +1572,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
default=default,
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}"
|
||||
response = self.client.get(url, follow=False)
|
||||
self.assertRedirects(
|
||||
response=response,
|
||||
@@ -1630,7 +1596,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
default=default,
|
||||
)
|
||||
)
|
||||
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
||||
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
|
||||
response = self.client.get(url, follow=False)
|
||||
self.assertRedirects(
|
||||
response=response,
|
||||
@@ -1706,14 +1672,14 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
reverse("password_change"),
|
||||
{
|
||||
"old_password": self.password,
|
||||
"new_password1": self.password + ".",
|
||||
"new_password1": f"{self.password}.",
|
||||
"new_password2": self.password,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"The two password fields didn",
|
||||
"The two password fields did",
|
||||
1,
|
||||
200,
|
||||
"Old password was entered incorrectly, site should raise an error",
|
||||
@@ -1729,14 +1695,14 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
{
|
||||
"old_password": self.password,
|
||||
"new_password1": self.password,
|
||||
"new_password2": self.password + ".",
|
||||
"new_password2": f"{self.password}.",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
"The two password fields didn",
|
||||
"The two password fields did",
|
||||
1,
|
||||
200,
|
||||
"Old password as entered incorrectly, site should raise an error",
|
||||
@@ -1787,7 +1753,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
self.first_name + " " + self.last_name,
|
||||
f"{self.first_name} {self.last_name}",
|
||||
1,
|
||||
200,
|
||||
"First and last name not correctly listed in profile page",
|
||||
@@ -1966,37 +1932,10 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
fh_gzip = gzip.open(BytesIO(response.content), "rb")
|
||||
fh = BytesIO(response.content)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("upload_export"),
|
||||
data={"not_porn": "on", "can_distribute": "on", "export_file": fh_gzip},
|
||||
follow=True,
|
||||
)
|
||||
fh_gzip.close()
|
||||
self.assertEqual(response.status_code, 200, "Upload worked")
|
||||
self.assertContains(
|
||||
response,
|
||||
"Unable to parse file: Not a gzipped file",
|
||||
1,
|
||||
200,
|
||||
"Upload didn't work?",
|
||||
)
|
||||
|
||||
# Second test - correctly gzipped content
|
||||
response = self.client.post(
|
||||
reverse("upload_export"),
|
||||
data={"not_porn": "on", "can_distribute": "on", "export_file": fh},
|
||||
follow=True,
|
||||
)
|
||||
fh.close()
|
||||
|
||||
self.assertEqual(response.status_code, 200, "Upload worked")
|
||||
self.assertContains(
|
||||
response,
|
||||
"Choose items to be imported",
|
||||
1,
|
||||
200,
|
||||
"Upload didn't work?",
|
||||
response = self._uploading_export_check(
|
||||
fh_gzip, "Unable to parse file: Not a gzipped file"
|
||||
)
|
||||
response = self._uploading_export_check(fh, "Choose items to be imported")
|
||||
self.assertContains(
|
||||
response,
|
||||
"asdf@asdf.local",
|
||||
@@ -2005,7 +1944,22 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"Upload didn't work?",
|
||||
)
|
||||
|
||||
def test_prefs_page(self):
|
||||
def _uploading_export_check(self, fh, message):
|
||||
"""
|
||||
Helper function to upload an export
|
||||
"""
|
||||
result = self.client.post(
|
||||
reverse("upload_export"),
|
||||
data={"not_porn": "on", "can_distribute": "on", "export_file": fh},
|
||||
follow=True,
|
||||
)
|
||||
fh.close()
|
||||
self.assertEqual(result.status_code, 200, "Upload worked")
|
||||
self.assertContains(result, message, 1, 200, "Upload didn't work?")
|
||||
|
||||
return result
|
||||
|
||||
def test_preferences_page(self):
|
||||
"""
|
||||
Test if preferences page works
|
||||
"""
|
||||
@@ -2038,7 +1992,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
|
||||
# Create a second user that will conflict
|
||||
user2 = User.objects.create_user(
|
||||
username=self.username + "1",
|
||||
username=f"{self.username}1",
|
||||
password=self.password,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
@@ -2055,12 +2009,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"Mail not the same?",
|
||||
)
|
||||
|
||||
# This needs to be cought
|
||||
try:
|
||||
# This needs to be caught
|
||||
with contextlib.suppress(AssertionError):
|
||||
self.test_confirm_email()
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
# Request a random page, so we can access the messages
|
||||
response = self.client.get(reverse("profile"))
|
||||
self.assertEqual(
|
||||
|
||||
247
ivatar/ivataraccount/test_views_bluesky.py
Normal file
247
ivatar/ivataraccount/test_views_bluesky.py
Normal file
@@ -0,0 +1,247 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
|
||||
# pylint: disable=too-many-lines
|
||||
import os
|
||||
import django
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# from django.contrib.auth import authenticate
|
||||
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar import settings
|
||||
from ivatar.ivataraccount.models import ConfirmedOpenId, ConfirmedEmail
|
||||
from ivatar.utils import random_string
|
||||
|
||||
from libravatar import libravatar_url
|
||||
|
||||
|
||||
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Main test class
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
user = None
|
||||
username = random_string()
|
||||
password = random_string()
|
||||
email = "%s@%s.%s" % (username, random_string(), random_string(2))
|
||||
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
|
||||
openid = "http://%s.%s.%s/" % (username, random_string(), "org")
|
||||
first_name = random_string()
|
||||
last_name = random_string()
|
||||
bsky_test_account = "ofalk.bsky.social"
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Login as user
|
||||
"""
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Prepare for tests.
|
||||
- Create user
|
||||
"""
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
)
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
|
||||
|
||||
def create_confirmed_openid(self):
|
||||
"""
|
||||
Create a confirmed openid
|
||||
"""
|
||||
return ConfirmedOpenId.objects.create(
|
||||
user=self.user,
|
||||
ip_address="127.0.0.1",
|
||||
openid=self.openid,
|
||||
)
|
||||
|
||||
def create_confirmed_email(self):
|
||||
"""
|
||||
Create a confirmed email
|
||||
"""
|
||||
return ConfirmedEmail.objects.create(
|
||||
email=self.email,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
# The following tests need to be moved over to the model tests
|
||||
# and real web UI tests added
|
||||
def test_bluesky_handle_for_mail_via_model_handle_does_not_exist(self):
|
||||
"""
|
||||
Add Bluesky handle to a confirmed mail address
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_email()
|
||||
confirmed.set_bluesky_handle(self.bsky_test_account)
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
confirmed.set_bluesky_handle(f"{self.bsky_test_account}1")
|
||||
self.assertNotEqual(
|
||||
confirmed.bluesky_handle,
|
||||
f"{self.bsky_test_account}1",
|
||||
"Setting Bluesky handle that doesn't exist works?",
|
||||
)
|
||||
|
||||
def test_bluesky_handle_for_mail_via_model_handle_exists(self):
|
||||
"""
|
||||
Add Bluesky handle to a confirmed mail address
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_email()
|
||||
confirmed.set_bluesky_handle(self.bsky_test_account)
|
||||
|
||||
self.assertEqual(
|
||||
confirmed.bluesky_handle,
|
||||
self.bsky_test_account,
|
||||
"Setting Bluesky handle doesn't work?",
|
||||
)
|
||||
|
||||
def test_bluesky_handle_for_openid_via_model_handle_does_not_exist(self):
|
||||
"""
|
||||
Add Bluesky handle to a confirmed openid address
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_openid()
|
||||
confirmed.set_bluesky_handle(self.bsky_test_account)
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
confirmed.set_bluesky_handle(f"{self.bsky_test_account}1")
|
||||
self.assertNotEqual(
|
||||
confirmed.bluesky_handle,
|
||||
f"{self.bsky_test_account}1",
|
||||
"Setting Bluesky handle that doesn't exist works?",
|
||||
)
|
||||
|
||||
def test_bluesky_handle_for_openid_via_model_handle_exists(self):
|
||||
"""
|
||||
Add Bluesky handle to a confirmed openid address
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_openid()
|
||||
confirmed.set_bluesky_handle(self.bsky_test_account)
|
||||
|
||||
self.assertEqual(
|
||||
confirmed.bluesky_handle,
|
||||
self.bsky_test_account,
|
||||
"Setting Bluesky handle doesn't work?",
|
||||
)
|
||||
|
||||
def test_bluesky_fetch_mail(self):
|
||||
"""
|
||||
Check if we can successfully fetch a Bluesky avatar via email
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_email()
|
||||
confirmed.set_bluesky_handle(self.bsky_test_account)
|
||||
lu = libravatar_url(confirmed.email, https=True)
|
||||
lu = lu.replace("https://seccdn.libravatar.org/", reverse("home"))
|
||||
|
||||
response = self.client.get(lu)
|
||||
# This is supposed to redirect to the Bluesky proxy
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response["Location"], f"/blueskyproxy/{confirmed.digest}")
|
||||
|
||||
def test_bluesky_fetch_openid(self):
|
||||
"""
|
||||
Check if we can successfully fetch a Bluesky avatar via OpenID
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_openid()
|
||||
confirmed.set_bluesky_handle(self.bsky_test_account)
|
||||
lu = libravatar_url(openid=confirmed.openid, https=True)
|
||||
lu = lu.replace("https://seccdn.libravatar.org/", reverse("home"))
|
||||
|
||||
response = self.client.get(lu)
|
||||
# This is supposed to redirect to the Bluesky proxy
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response["Location"], f"/blueskyproxy/{confirmed.digest}")
|
||||
|
||||
def test_assign_bluesky_handle_to_openid(self):
|
||||
"""
|
||||
Assign a Bluesky handle to an OpenID
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_openid()
|
||||
self._assign_handle_to(
|
||||
"assign_bluesky_handle_to_openid",
|
||||
confirmed,
|
||||
"Adding Bluesky handle to OpenID fails?",
|
||||
)
|
||||
|
||||
def test_assign_bluesky_handle_to_email(self):
|
||||
"""
|
||||
Assign a Bluesky handle to an email
|
||||
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_email()
|
||||
self._assign_handle_to(
|
||||
"assign_bluesky_handle_to_email",
|
||||
confirmed,
|
||||
"Adding Bluesky handle to Email fails?",
|
||||
)
|
||||
|
||||
def _assign_handle_to(self, endpoint, confirmed, message):
|
||||
"""
|
||||
Helper method to assign a handle to reduce code duplication
|
||||
Since the endpoints are similar, we can reuse the code
|
||||
"""
|
||||
url = reverse(endpoint, args=[confirmed.id])
|
||||
response = self.client.post(
|
||||
url, {"bluesky_handle": self.bsky_test_account}, follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, message)
|
||||
confirmed.refresh_from_db(fields=["bluesky_handle"])
|
||||
self.assertEqual(
|
||||
confirmed.bluesky_handle,
|
||||
self.bsky_test_account,
|
||||
"Setting Bluesky handle doesn't work?",
|
||||
)
|
||||
|
||||
def test_assign_photo_to_mail_removes_bluesky_handle(self):
|
||||
"""
|
||||
Assign a Photo to a mail, removes Bluesky handle
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_email()
|
||||
self._assign_bluesky_handle(confirmed, "assign_photo_email")
|
||||
|
||||
def test_assign_photo_to_openid_removes_bluesky_handle(self):
|
||||
"""
|
||||
Assign a Photo to a OpenID, removes Bluesky handle
|
||||
"""
|
||||
self.login()
|
||||
confirmed = self.create_confirmed_openid()
|
||||
self._assign_bluesky_handle(confirmed, "assign_photo_openid")
|
||||
|
||||
def _assign_bluesky_handle(self, confirmed, endpoint):
|
||||
"""
|
||||
Helper method to assign a Bluesky handle
|
||||
Since the endpoints are similar, we can reuse the code
|
||||
"""
|
||||
confirmed.bluesky_handle = self.bsky_test_account
|
||||
confirmed.save()
|
||||
url = reverse(endpoint, args=[confirmed.id])
|
||||
response = self.client.post(url, {"photoNone": True}, follow=True)
|
||||
self.assertEqual(response.status_code, 200, "Unassigning Photo doesn't work?")
|
||||
confirmed.refresh_from_db(fields=["bluesky_handle"])
|
||||
self.assertEqual(
|
||||
confirmed.bluesky_handle, None, "Removing Bluesky handle doesn't work?"
|
||||
)
|
||||
@@ -20,6 +20,7 @@ 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 AssignBlueskyHandleToEmailView, AssignBlueskyHandleToOpenIdView
|
||||
from .views import CropPhotoView
|
||||
from .views import UserPreferenceView, UploadLibravatarExportView
|
||||
from .views import ResendConfirmationMailView
|
||||
@@ -125,6 +126,16 @@ urlpatterns = [ # pylint: disable=invalid-name
|
||||
AssignPhotoOpenIDView.as_view(),
|
||||
name="assign_photo_openid",
|
||||
),
|
||||
re_path(
|
||||
r"assign_bluesky_handle_to_email/(?P<email_id>\d+)",
|
||||
AssignBlueskyHandleToEmailView.as_view(),
|
||||
name="assign_bluesky_handle_to_email",
|
||||
),
|
||||
re_path(
|
||||
r"assign_bluesky_handle_to_openid/(?P<open_id>\d+)",
|
||||
AssignBlueskyHandleToOpenIdView.as_view(),
|
||||
name="assign_bluesky_handle_to_openid",
|
||||
),
|
||||
re_path(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
|
||||
re_path(
|
||||
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
"""
|
||||
View classes for ivatar/ivataraccount/
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
from urllib.request import urlopen
|
||||
from ivatar.utils import urlopen, Bluesky
|
||||
import base64
|
||||
import binascii
|
||||
import contextlib
|
||||
from xml.sax import saxutils
|
||||
import gzip
|
||||
|
||||
@@ -46,6 +48,7 @@ from ivatar.settings import (
|
||||
MAX_PHOTO_SIZE,
|
||||
JPEG_QUALITY,
|
||||
AVATAR_MAX_SIZE,
|
||||
SOCIAL_AUTH_FEDORA_KEY,
|
||||
)
|
||||
from .gravatar import get_photo as get_gravatar_photo
|
||||
|
||||
@@ -87,23 +90,8 @@ class CreateView(SuccessMessageMixin, FormView):
|
||||
# If the username looks like a mail address, automagically
|
||||
# add it as unconfirmed mail and set it also as user's
|
||||
# email address
|
||||
try:
|
||||
# This will error out if it's not a valid address
|
||||
valid = validate_email(form.cleaned_data["username"])
|
||||
user.email = valid.email
|
||||
user.save()
|
||||
# The following will also error out if it already exists
|
||||
unconfirmed = UnconfirmedEmail()
|
||||
unconfirmed.email = valid.email
|
||||
unconfirmed.user = user
|
||||
unconfirmed.save()
|
||||
unconfirmed.send_confirmation_mail(
|
||||
url=self.request.build_absolute_uri("/")[:-1]
|
||||
)
|
||||
# In any exception cases, we just skip it
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self._extracted_from_form_valid_(form, user)
|
||||
login(self.request, user)
|
||||
pref = UserPreference.objects.create(
|
||||
user_id=user.pk
|
||||
@@ -112,13 +100,26 @@ class CreateView(SuccessMessageMixin, FormView):
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
return HttpResponseRedirect(reverse_lazy("login")) # pragma: no cover
|
||||
|
||||
def _extracted_from_form_valid_(self, form, user):
|
||||
# This will error out if it's not a valid address
|
||||
valid = validate_email(form.cleaned_data["username"])
|
||||
user.email = valid.email
|
||||
user.save()
|
||||
# The following will also error out if it already exists
|
||||
unconfirmed = UnconfirmedEmail()
|
||||
unconfirmed.email = valid.email
|
||||
unconfirmed.user = user
|
||||
unconfirmed.save()
|
||||
unconfirmed.send_confirmation_mail(
|
||||
url=self.request.build_absolute_uri("/")[:-1]
|
||||
)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle get for create view
|
||||
"""
|
||||
if request.user:
|
||||
if request.user.is_authenticated:
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
if request.user and request.user.is_authenticated:
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
return super().get(self, request, args, kwargs)
|
||||
|
||||
|
||||
@@ -288,6 +289,7 @@ class AssignPhotoEmailView(SuccessMessageMixin, TemplateView):
|
||||
messages.error(request, _("Photo does not exist"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
email.photo = photo
|
||||
email.bluesky_handle = None
|
||||
email.save()
|
||||
|
||||
messages.success(request, _("Successfully changed photo"))
|
||||
@@ -337,6 +339,7 @@ class AssignPhotoOpenIDView(SuccessMessageMixin, TemplateView):
|
||||
messages.error(request, _("Photo does not exist"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
openid.photo = photo
|
||||
openid.bluesky_handle = None
|
||||
openid.save()
|
||||
|
||||
messages.success(request, _("Successfully changed photo"))
|
||||
@@ -350,6 +353,104 @@ class AssignPhotoOpenIDView(SuccessMessageMixin, TemplateView):
|
||||
return data
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class AssignBlueskyHandleToEmailView(SuccessMessageMixin, TemplateView):
|
||||
"""
|
||||
View class for assigning a Bluesky handle to an email address
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Handle post request - assign bluesky handle to email
|
||||
"""
|
||||
|
||||
try:
|
||||
email = ConfirmedEmail.objects.get(user=request.user, id=kwargs["email_id"])
|
||||
except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member
|
||||
messages.error(request, _("Invalid request"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
|
||||
if "bluesky_handle" not in request.POST:
|
||||
messages.error(request, _("Invalid request [bluesky_handle] missing"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
bluesky_handle = request.POST["bluesky_handle"]
|
||||
|
||||
try:
|
||||
bs = Bluesky()
|
||||
|
||||
bs.get_avatar(bluesky_handle)
|
||||
except Exception as e:
|
||||
messages.error(request, _(f"Handle '{bluesky_handle}' not found: {e}"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
email.set_bluesky_handle(bluesky_handle)
|
||||
email.photo = None
|
||||
email.save()
|
||||
|
||||
messages.success(request, _("Successfully assigned Bluesky handle"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["email"] = ConfirmedEmail.objects.get(pk=kwargs["email_id"])
|
||||
return data
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class AssignBlueskyHandleToOpenIdView(SuccessMessageMixin, TemplateView):
|
||||
"""
|
||||
View class for assigning a Bluesky handle to an email address
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Handle post request - assign bluesky handle to email
|
||||
"""
|
||||
|
||||
try:
|
||||
openid = ConfirmedOpenId.objects.get(
|
||||
user=request.user, id=kwargs["open_id"]
|
||||
)
|
||||
except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member
|
||||
messages.error(request, _("Invalid request"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
|
||||
if "bluesky_handle" not in request.POST:
|
||||
messages.error(request, _("Invalid request [bluesky_handle] missing"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
bluesky_handle = request.POST["bluesky_handle"]
|
||||
|
||||
try:
|
||||
bs = Bluesky()
|
||||
|
||||
bs.get_avatar(bluesky_handle)
|
||||
except Exception as e:
|
||||
messages.error(request, _(f"Handle '{bluesky_handle}' not found: {e}"))
|
||||
return HttpResponseRedirect(
|
||||
reverse_lazy(
|
||||
"assign_photo_openid", kwargs={"openid_id": int(kwargs["open_id"])}
|
||||
)
|
||||
)
|
||||
try:
|
||||
openid.set_bluesky_handle(bluesky_handle)
|
||||
except Exception as e:
|
||||
messages.error(request, _(f"Error: {e}"))
|
||||
return HttpResponseRedirect(
|
||||
reverse_lazy(
|
||||
"assign_photo_openid", kwargs={"openid_id": int(kwargs["open_id"])}
|
||||
)
|
||||
)
|
||||
openid.photo = None
|
||||
openid.save()
|
||||
|
||||
messages.success(request, _("Successfully assigned Bluesky handle"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["openid"] = ConfirmedOpenId.objects.get(pk=kwargs["open_id"])
|
||||
return data
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ImportPhotoView(SuccessMessageMixin, TemplateView):
|
||||
"""
|
||||
@@ -370,29 +471,25 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
|
||||
messages.error(self.request, _("Address does not exist"))
|
||||
return context
|
||||
|
||||
addr = kwargs.get("email_addr", None)
|
||||
|
||||
if addr:
|
||||
gravatar = get_gravatar_photo(addr)
|
||||
if gravatar:
|
||||
if addr := kwargs.get("email_addr", None):
|
||||
if gravatar := get_gravatar_photo(addr):
|
||||
context["photos"].append(gravatar)
|
||||
|
||||
libravatar_service_url = libravatar_url(
|
||||
if libravatar_service_url := libravatar_url(
|
||||
email=addr,
|
||||
default=404,
|
||||
size=AVATAR_MAX_SIZE,
|
||||
)
|
||||
if libravatar_service_url:
|
||||
):
|
||||
try:
|
||||
urlopen(libravatar_service_url)
|
||||
except OSError as exc:
|
||||
print("Exception caught during photo import: {}".format(exc))
|
||||
print(f"Exception caught during photo import: {exc}")
|
||||
else:
|
||||
context["photos"].append(
|
||||
{
|
||||
"service_url": libravatar_service_url,
|
||||
"thumbnail_url": libravatar_service_url + "&s=80",
|
||||
"image_url": libravatar_service_url + "&s=512",
|
||||
"thumbnail_url": f"{libravatar_service_url}&s=80",
|
||||
"image_url": f"{libravatar_service_url}&s=512",
|
||||
"width": 80,
|
||||
"height": 80,
|
||||
"service_name": "Libravatar",
|
||||
@@ -411,7 +508,7 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
|
||||
imported = None
|
||||
|
||||
email_id = kwargs.get("email_id", request.POST.get("email_id", None))
|
||||
addr = kwargs.get("emali_addr", request.POST.get("email_addr", None))
|
||||
addr = kwargs.get("email", request.POST.get("email_addr", None))
|
||||
|
||||
if email_id:
|
||||
email = ConfirmedEmail.objects.filter(id=email_id, user=request.user)
|
||||
@@ -461,9 +558,9 @@ class RawImageView(DetailView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
photo = self.model.objects.get(pk=kwargs["pk"]) # pylint: disable=no-member
|
||||
if not photo.user.id == request.user.id and not request.user.is_staff:
|
||||
if photo.user.id != request.user.id and not request.user.is_staff:
|
||||
return HttpResponseRedirect(reverse_lazy("home"))
|
||||
return HttpResponse(BytesIO(photo.data), content_type="image/%s" % photo.format)
|
||||
return HttpResponse(BytesIO(photo.data), content_type=f"image/{photo.format}")
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@@ -539,17 +636,16 @@ class AddOpenIDView(SuccessMessageMixin, FormView):
|
||||
success_url = reverse_lazy("profile")
|
||||
|
||||
def form_valid(self, form):
|
||||
openid_id = form.save(self.request.user)
|
||||
if not openid_id:
|
||||
if openid_id := form.save(self.request.user):
|
||||
# At this point we have an unconfirmed OpenID, but
|
||||
# we do not add the message, that we successfully added it,
|
||||
# since this is misleading
|
||||
return HttpResponseRedirect(
|
||||
reverse_lazy("openid_redirection", args=[openid_id])
|
||||
)
|
||||
else:
|
||||
return render(self.request, self.template_name, {"form": form})
|
||||
|
||||
# At this point we have an unconfirmed OpenID, but
|
||||
# we do not add the message, that we successfully added it,
|
||||
# since this is misleading
|
||||
return HttpResponseRedirect(
|
||||
reverse_lazy("openid_redirection", args=[openid_id])
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class RemoveUnconfirmedOpenIDView(View):
|
||||
@@ -599,7 +695,7 @@ class RemoveConfirmedOpenIDView(View):
|
||||
openidobj.delete()
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# Why it is not there?
|
||||
print("How did we get here: %s" % exc)
|
||||
print(f"How did we get here: {exc}")
|
||||
openid.delete()
|
||||
messages.success(request, _("ID removed"))
|
||||
except self.model.DoesNotExist: # pylint: disable=no-member
|
||||
@@ -636,7 +732,7 @@ class RedirectOpenIDView(View):
|
||||
try:
|
||||
auth_request = openid_consumer.begin(user_url)
|
||||
except consumer.DiscoveryFailure as exc:
|
||||
messages.error(request, _("OpenID discovery failed: %s" % exc))
|
||||
messages.error(request, _(f"OpenID discovery failed: {exc}"))
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
except UnicodeDecodeError as exc: # pragma: no cover
|
||||
msg = _(
|
||||
@@ -648,7 +744,7 @@ class RedirectOpenIDView(View):
|
||||
"message": exc,
|
||||
}
|
||||
)
|
||||
print("message: %s" % msg)
|
||||
print(f"message: {msg}")
|
||||
messages.error(request, msg)
|
||||
|
||||
if auth_request is None: # pragma: no cover
|
||||
@@ -782,19 +878,13 @@ class CropPhotoView(TemplateView):
|
||||
}
|
||||
email = openid = None
|
||||
if "email" in request.POST:
|
||||
try:
|
||||
with contextlib.suppress(ConfirmedEmail.DoesNotExist):
|
||||
email = ConfirmedEmail.objects.get(email=request.POST["email"])
|
||||
except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member
|
||||
pass # Ignore automatic assignment
|
||||
|
||||
if "openid" in request.POST:
|
||||
try:
|
||||
with contextlib.suppress(ConfirmedOpenId.DoesNotExist):
|
||||
openid = ConfirmedOpenId.objects.get( # pylint: disable=no-member
|
||||
openid=request.POST["openid"]
|
||||
)
|
||||
except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member
|
||||
pass # Ignore automatic assignment
|
||||
|
||||
return photo.perform_crop(request, dimensions, email, openid)
|
||||
|
||||
|
||||
@@ -830,14 +920,14 @@ class UserPreferenceView(FormView, UpdateView):
|
||||
if request.POST["email"] not in addresses:
|
||||
messages.error(
|
||||
self.request,
|
||||
_("Mail address not allowed: %s" % request.POST["email"]),
|
||||
_(f'Mail address not allowed: {request.POST["email"]}'),
|
||||
)
|
||||
else:
|
||||
self.request.user.email = request.POST["email"]
|
||||
self.request.user.save()
|
||||
messages.info(self.request, _("Mail address changed."))
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
messages.error(self.request, _("Error setting new mail address: %s" % e))
|
||||
messages.error(self.request, _(f"Error setting new mail address: {e}"))
|
||||
|
||||
try:
|
||||
if request.POST["first_name"] or request.POST["last_name"]:
|
||||
@@ -849,7 +939,7 @@ class UserPreferenceView(FormView, UpdateView):
|
||||
messages.info(self.request, _("Last name changed."))
|
||||
self.request.user.save()
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
messages.error(self.request, _("Error setting names: %s" % e))
|
||||
messages.error(self.request, _(f"Error setting names: {e}"))
|
||||
|
||||
return HttpResponseRedirect(reverse_lazy("user_preference"))
|
||||
|
||||
@@ -917,15 +1007,14 @@ class UploadLibravatarExportView(SuccessMessageMixin, FormView):
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# DEBUG
|
||||
print(
|
||||
"Exception during adding mail address (%s): %s"
|
||||
% (email, exc)
|
||||
f"Exception during adding mail address ({email}): {exc}"
|
||||
)
|
||||
|
||||
if arg.startswith("photo"):
|
||||
try:
|
||||
data = base64.decodebytes(bytes(request.POST[arg], "utf-8"))
|
||||
except binascii.Error as exc:
|
||||
print("Cannot decode photo: %s" % exc)
|
||||
print(f"Cannot decode photo: {exc}")
|
||||
continue
|
||||
try:
|
||||
pilobj = Image.open(BytesIO(data))
|
||||
@@ -939,7 +1028,7 @@ class UploadLibravatarExportView(SuccessMessageMixin, FormView):
|
||||
photo.data = out.read()
|
||||
photo.save()
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
print("Exception during save: %s" % exc)
|
||||
print(f"Exception during save: {exc}")
|
||||
continue
|
||||
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
@@ -959,7 +1048,7 @@ class UploadLibravatarExportView(SuccessMessageMixin, FormView):
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
messages.error(self.request, _("Unable to parse file: %s" % e))
|
||||
messages.error(self.request, _(f"Unable to parse file: {e}"))
|
||||
return HttpResponseRedirect(reverse_lazy("upload_export"))
|
||||
|
||||
|
||||
@@ -985,13 +1074,12 @@ class ResendConfirmationMailView(View):
|
||||
try:
|
||||
email.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1])
|
||||
messages.success(
|
||||
request, "%s: %s" % (_("Confirmation mail sent to"), email.email)
|
||||
request, f'{_("Confirmation mail sent to")}: {email.email}'
|
||||
)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
messages.error(
|
||||
request,
|
||||
"%s %s: %s"
|
||||
% (_("Unable to send confirmation email for"), email.email, exc),
|
||||
f'{_("Unable to send confirmation email for")} {email.email}: {exc}',
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
|
||||
@@ -1012,6 +1100,11 @@ class IvatarLoginView(LoginView):
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
return super().get(self, request, args, kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["with_fedora"] = SOCIAL_AUTH_FEDORA_KEY is not None
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ProfileView(TemplateView):
|
||||
@@ -1025,12 +1118,9 @@ class ProfileView(TemplateView):
|
||||
if "profile_username" in kwargs:
|
||||
if not request.user.is_staff:
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
u = User.objects.get(username=kwargs["profile_username"])
|
||||
request.user = u
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
self._confirm_claimed_openid()
|
||||
return super().get(self, request, args, kwargs)
|
||||
|
||||
@@ -1061,7 +1151,7 @@ class ProfileView(TemplateView):
|
||||
openid=openids.first().claimed_id
|
||||
).exists():
|
||||
return
|
||||
print("need to confirm: %s" % openids.first())
|
||||
print(f"need to confirm: {openids.first()}")
|
||||
confirmed = ConfirmedOpenId()
|
||||
confirmed.user = self.request.user
|
||||
confirmed.ip_address = get_client_ip(self.request)[0]
|
||||
@@ -1079,7 +1169,7 @@ class PasswordResetView(PasswordResetViewOriginal):
|
||||
Since we have the mail addresses in ConfirmedEmail model,
|
||||
we need to set the email on the user object in order for the
|
||||
PasswordResetView class to pick up the correct user.
|
||||
In case we have the mail address in the User objecct, we still
|
||||
In case we have the mail address in the User object, we still
|
||||
need to assign a random password in order for PasswordResetView
|
||||
class to pick up the user - else it will silently do nothing.
|
||||
"""
|
||||
@@ -1096,16 +1186,13 @@ class PasswordResetView(PasswordResetViewOriginal):
|
||||
# If we find the user there, we need to set the mail
|
||||
# attribute on the user object accordingly
|
||||
if not user:
|
||||
try:
|
||||
with contextlib.suppress(ObjectDoesNotExist):
|
||||
confirmed_email = ConfirmedEmail.objects.get(
|
||||
email=request.POST["email"]
|
||||
)
|
||||
user = confirmed_email.user
|
||||
user.email = confirmed_email.email
|
||||
user.save()
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
# If we found the user, set a random password. Else, the
|
||||
# ResetPasswordView class will silently ignore the password
|
||||
# reset request
|
||||
@@ -1145,7 +1232,6 @@ class DeleteAccountView(SuccessMessageMixin, FormView):
|
||||
messages.error(request, _("No password given"))
|
||||
return HttpResponseRedirect(reverse_lazy("delete"))
|
||||
|
||||
raise _("No password given")
|
||||
# should delete all confirmed/unconfirmed/photo objects
|
||||
request.user.delete()
|
||||
return super().post(self, request, args, kwargs)
|
||||
@@ -1168,7 +1254,7 @@ class ExportView(SuccessMessageMixin, TemplateView):
|
||||
Handle real export
|
||||
"""
|
||||
SCHEMA_ROOT = "https://www.libravatar.org/schemas/export/0.2"
|
||||
SCHEMA_XSD = "%s/export.xsd" % SCHEMA_ROOT
|
||||
SCHEMA_XSD = f"{SCHEMA_ROOT}/export.xsd"
|
||||
|
||||
def xml_header():
|
||||
return (
|
||||
@@ -1250,8 +1336,8 @@ class ExportView(SuccessMessageMixin, TemplateView):
|
||||
bytesobj.seek(0)
|
||||
|
||||
response = HttpResponse(content_type="application/gzip")
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="libravatar-export_%s.xml.gz"' % user.username
|
||||
)
|
||||
response[
|
||||
"Content-Disposition"
|
||||
] = f'attachment; filename="libravatar-export_{user.username}.xml.gz"'
|
||||
response.write(bytesobj.read())
|
||||
return response
|
||||
|
||||
@@ -32,6 +32,7 @@ INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"social_django",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -59,6 +60,7 @@ TEMPLATES = [
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"django.template.context_processors.i18n",
|
||||
"social_django.context_processors.login_redirect",
|
||||
],
|
||||
"debug": DEBUG,
|
||||
},
|
||||
@@ -122,6 +124,50 @@ if not DEBUG:
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
|
||||
|
||||
# Social authentication
|
||||
TRUST_EMAIL_FROM_SOCIAL_AUTH_BACKENDS = ["fedora"]
|
||||
SOCIAL_AUTH_PIPELINE = (
|
||||
# Get the information we can about the user and return it in a simple
|
||||
# format to create the user instance later. In some cases the details are
|
||||
# already part of the auth response from the provider, but sometimes this
|
||||
# could hit a provider API.
|
||||
"social_core.pipeline.social_auth.social_details",
|
||||
# Get the social uid from whichever service we're authing thru. The uid is
|
||||
# the unique identifier of the given user in the provider.
|
||||
"social_core.pipeline.social_auth.social_uid",
|
||||
# Verifies that the current auth process is valid within the current
|
||||
# project, this is where emails and domains whitelists are applied (if
|
||||
# defined).
|
||||
"social_core.pipeline.social_auth.auth_allowed",
|
||||
# Checks if the current social-account is already associated in the site.
|
||||
"social_core.pipeline.social_auth.social_user",
|
||||
# Make up a username for this person, appends a random string at the end if
|
||||
# there's any collision.
|
||||
"social_core.pipeline.user.get_username",
|
||||
# Send a validation email to the user to verify its email address.
|
||||
# Disabled by default.
|
||||
# 'social_core.pipeline.mail.mail_validation',
|
||||
# Associates the current social details with another user account with
|
||||
# a similar email address. Disabled by default.
|
||||
"social_core.pipeline.social_auth.associate_by_email",
|
||||
# Associates the current social details with an existing user account with
|
||||
# a matching ConfirmedEmail.
|
||||
"ivatar.ivataraccount.auth.associate_by_confirmed_email",
|
||||
# Create a user account if we haven't found one yet.
|
||||
"social_core.pipeline.user.create_user",
|
||||
# Create the record that associates the social account with the user.
|
||||
"social_core.pipeline.social_auth.associate_user",
|
||||
# Populate the extra_data field in the social record with the values
|
||||
# specified by settings (and the default ones like access_token, etc).
|
||||
"social_core.pipeline.social_auth.load_extra_data",
|
||||
# Update the user record with any changed info from the auth service.
|
||||
"social_core.pipeline.user.user_details",
|
||||
# Create the ConfirmedEmail if appropriate.
|
||||
"ivatar.ivataraccount.auth.add_confirmed_email",
|
||||
)
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.0/topics/i18n/
|
||||
|
||||
|
||||
BIN
ivatar/static/img/broken.tif
Normal file
BIN
ivatar/static/img/broken.tif
Normal file
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,9 @@
|
||||
"""
|
||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
|
||||
# pylint: disable=too-many-lines
|
||||
import os
|
||||
import json
|
||||
@@ -10,9 +13,12 @@ from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
from django.contrib.auth.models import User
|
||||
from ivatar.utils import random_string, Bluesky
|
||||
|
||||
from ivatar.utils import random_string
|
||||
|
||||
BLUESKY_APP_PASSWORD = None
|
||||
BLUESKY_IDENTIFIER = None
|
||||
with contextlib.suppress(Exception):
|
||||
from settings import BLUESKY_APP_PASSWORD, BLUESKY_IDENTIFIER
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
||||
django.setup()
|
||||
|
||||
@@ -50,16 +56,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Test incorrect digest
|
||||
"""
|
||||
response = self.client.get("/avatar/%s" % "x" * 65, follow=True)
|
||||
response = self.client.get("/avatar/" + "x" * 65, follow=True)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][0],
|
||||
"/static/img/deadbeef.png",
|
||||
response.redirect_chain[2][0],
|
||||
"/static/img/nobody/80.png",
|
||||
"Doesn't redirect to static?",
|
||||
)
|
||||
# self.assertRedirects(
|
||||
# response=response,
|
||||
# expected_url="/static/img/deadbeef.png",
|
||||
# msg_prefix="Why does an invalid hash not redirect to deadbeef?",
|
||||
# response=response,
|
||||
# expected_url="/static/img/nobody/80.png",
|
||||
# msg_prefix="Why does an invalid hash not redirect to deadbeef?",
|
||||
# )
|
||||
|
||||
def test_stats(self):
|
||||
@@ -89,3 +95,19 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
)
|
||||
response = self.client.post(reverse("logout"), follow=True)
|
||||
self.assertEqual(response.status_code, 200, "logout with post should logout")
|
||||
|
||||
def test_Bluesky_client(self):
|
||||
"""
|
||||
Bluesky client needs credentials, so it's limited with testing here now
|
||||
"""
|
||||
|
||||
if BLUESKY_APP_PASSWORD and BLUESKY_IDENTIFIER:
|
||||
b = Bluesky()
|
||||
profile = b.get_profile("ofalk.bsky.social")
|
||||
self.assertEqual(profile["handle"], "ofalk.bsky.social")
|
||||
# As long as I don't change my avatar, this should stay the same
|
||||
self.assertEqual(
|
||||
profile["avatar"],
|
||||
"https://cdn.bsky.app/img/avatar/plain/did:plc:35jdu26cjgsc5vdbsaqiuw4a/bafkreidgtubihcdwcr72s5nag2ohcnwhhbg2zabw4jtxlhmtekrm6t5f4y@jpeg",
|
||||
)
|
||||
self.assertEqual(True, True)
|
||||
|
||||
@@ -16,7 +16,7 @@ from libravatar import libravatar_url, parse_user_identity
|
||||
from libravatar import SECURE_BASE_URL as LIBRAVATAR_SECURE_BASE_URL
|
||||
from libravatar import BASE_URL as LIBRAVATAR_BASE_URL
|
||||
|
||||
from ivatar.settings import SECURE_BASE_URL, BASE_URL
|
||||
from ivatar.settings import SECURE_BASE_URL, BASE_URL, SITE_NAME, DEBUG
|
||||
from .forms import (
|
||||
CheckDomainForm,
|
||||
CheckForm,
|
||||
@@ -33,10 +33,9 @@ class CheckDomainView(FormView):
|
||||
success_url = reverse("tools_check_domain")
|
||||
|
||||
def form_valid(self, form):
|
||||
result = {}
|
||||
super().form_valid(form)
|
||||
domain = form.cleaned_data["domain"]
|
||||
result["avatar_server_http"] = lookup_avatar_server(domain, False)
|
||||
result = {"avatar_server_http": lookup_avatar_server(domain, False)}
|
||||
if result["avatar_server_http"]:
|
||||
result["avatar_server_http_ipv4"] = lookup_ip_address(
|
||||
result["avatar_server_http"], False
|
||||
@@ -80,8 +79,6 @@ class CheckView(FormView):
|
||||
mail_hash = None
|
||||
mail_hash256 = None
|
||||
openid_hash = None
|
||||
size = 80
|
||||
|
||||
super().form_valid(form)
|
||||
|
||||
if form.cleaned_data["default_url"]:
|
||||
@@ -94,8 +91,7 @@ class CheckView(FormView):
|
||||
else:
|
||||
default_url = None
|
||||
|
||||
if "size" in form.cleaned_data:
|
||||
size = form.cleaned_data["size"]
|
||||
size = form.cleaned_data["size"] if "size" in form.cleaned_data else 80
|
||||
if form.cleaned_data["mail"]:
|
||||
mailurl = libravatar_url(
|
||||
email=form.cleaned_data["mail"], size=size, default=default_url
|
||||
@@ -121,7 +117,7 @@ class CheckView(FormView):
|
||||
if not form.cleaned_data["openid"].startswith(
|
||||
"http://"
|
||||
) and not form.cleaned_data["openid"].startswith("https://"):
|
||||
form.cleaned_data["openid"] = "http://%s" % form.cleaned_data["openid"]
|
||||
form.cleaned_data["openid"] = f'http://{form.cleaned_data["openid"]}'
|
||||
openidurl = libravatar_url(
|
||||
openid=form.cleaned_data["openid"], size=size, default=default_url
|
||||
)
|
||||
@@ -139,6 +135,35 @@ class CheckView(FormView):
|
||||
openid=form.cleaned_data["openid"], email=None
|
||||
)[0]
|
||||
|
||||
if "DEVELOPMENT" in SITE_NAME and DEBUG:
|
||||
if mailurl:
|
||||
mailurl = mailurl.replace(
|
||||
"https://avatars.linux-kernel.at",
|
||||
f"http://{self.request.get_host()}",
|
||||
)
|
||||
if mailurl_secure:
|
||||
mailurl_secure = mailurl_secure.replace(
|
||||
"https://avatars.linux-kernel.at",
|
||||
f"http://{self.request.get_host()}",
|
||||
)
|
||||
if mailurl_secure_256:
|
||||
mailurl_secure_256 = mailurl_secure_256.replace(
|
||||
"https://avatars.linux-kernel.at",
|
||||
f"http://{self.request.get_host()}",
|
||||
)
|
||||
|
||||
if openidurl:
|
||||
openidurl = openidurl.replace(
|
||||
"https://avatars.linux-kernel.at",
|
||||
f"http://{self.request.get_host()}",
|
||||
)
|
||||
if openidurl_secure:
|
||||
openidurl_secure = openidurl_secure.replace(
|
||||
"https://avatars.linux-kernel.at",
|
||||
f"http://{self.request.get_host()}",
|
||||
)
|
||||
print(mailurl, openidurl, mailurl_secure, mailurl_secure_256, openidurl_secure)
|
||||
|
||||
return render(
|
||||
self.request,
|
||||
self.template_name,
|
||||
@@ -172,15 +197,15 @@ def lookup_avatar_server(domain, https):
|
||||
|
||||
service_name = None
|
||||
if https:
|
||||
service_name = "_avatars-sec._tcp.%s" % domain
|
||||
service_name = f"_avatars-sec._tcp.{domain}"
|
||||
else:
|
||||
service_name = "_avatars._tcp.%s" % domain
|
||||
service_name = f"_avatars._tcp.{domain}"
|
||||
|
||||
DNS.DiscoverNameServers()
|
||||
try:
|
||||
dns_request = DNS.Request(name=service_name, qtype="SRV").req()
|
||||
except DNS.DNSError as message:
|
||||
print("DNS Error: %s (%s)" % (message, domain))
|
||||
print(f"DNS Error: {message} ({domain})")
|
||||
return None
|
||||
|
||||
if dns_request.header["status"] == "NXDOMAIN":
|
||||
@@ -188,7 +213,7 @@ def lookup_avatar_server(domain, https):
|
||||
return None
|
||||
|
||||
if dns_request.header["status"] != "NOERROR":
|
||||
print("DNS Error: status=%s (%s)" % (dns_request.header["status"], domain))
|
||||
print(f'DNS Error: status={dns_request.header["status"]} ({domain})')
|
||||
return None
|
||||
|
||||
records = []
|
||||
@@ -213,7 +238,7 @@ def lookup_avatar_server(domain, https):
|
||||
target, port = srv_hostname(records)
|
||||
|
||||
if target and ((https and port != 443) or (not https and port != 80)):
|
||||
return "%s:%s" % (target, port)
|
||||
return f"{target}:{port}"
|
||||
|
||||
return target
|
||||
|
||||
@@ -243,7 +268,7 @@ def srv_hostname(records):
|
||||
# Take care - this if is only a if, if the above if
|
||||
# uses continue at the end. else it should be an elsif
|
||||
if ret["priority"] < top_priority:
|
||||
# reset the aretay (ret has higher priority)
|
||||
# reset the priority (ret has higher priority)
|
||||
top_priority = ret["priority"]
|
||||
total_weight = 0
|
||||
priority_records = []
|
||||
@@ -253,7 +278,7 @@ def srv_hostname(records):
|
||||
if ret["weight"] > 0:
|
||||
priority_records.append((total_weight, ret))
|
||||
else:
|
||||
# zero-weigth elements must come first
|
||||
# zero-weight elements must come first
|
||||
priority_records.insert(0, (0, ret))
|
||||
|
||||
if len(priority_records) == 1:
|
||||
@@ -285,11 +310,11 @@ def lookup_ip_address(hostname, ipv6):
|
||||
else:
|
||||
dns_request = DNS.Request(name=hostname, qtype=DNS.Type.A).req()
|
||||
except DNS.DNSError as message:
|
||||
print("DNS Error: %s (%s)" % (message, hostname))
|
||||
print(f"DNS Error: {message} ({hostname})")
|
||||
return None
|
||||
|
||||
if dns_request.header["status"] != "NOERROR":
|
||||
print("DNS Error: status=%s (%s)" % (dns_request.header["status"], hostname))
|
||||
print(f'DNS Error: status={dns_request.header["status"]} ({hostname})')
|
||||
return None
|
||||
|
||||
for answer in dns_request.answers:
|
||||
@@ -300,9 +325,5 @@ def lookup_ip_address(hostname, ipv6):
|
||||
):
|
||||
continue # skip CNAME records
|
||||
|
||||
if ipv6:
|
||||
return inet_ntop(AF_INET6, answer["data"])
|
||||
|
||||
return answer["data"]
|
||||
|
||||
return inet_ntop(AF_INET6, answer["data"]) if ipv6 else answer["data"]
|
||||
return None
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
"""
|
||||
ivatar URL configuration
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include, re_path
|
||||
from django.conf.urls.static import static
|
||||
from django.views.generic import TemplateView, RedirectView
|
||||
from ivatar import settings
|
||||
from .views import AvatarImageView, GravatarProxyView, StatsView
|
||||
from .views import AvatarImageView, StatsView
|
||||
from .views import GravatarProxyView, BlueskyProxyView
|
||||
|
||||
urlpatterns = [ # pylint: disable=invalid-name
|
||||
path("admin/", admin.site.urls),
|
||||
path("i18n/", include("django.conf.urls.i18n")),
|
||||
path("openid/", include("django_openid_auth.urls")),
|
||||
path("auth/", include("social_django.urls", namespace="social")),
|
||||
path("tools/", include("ivatar.tools.urls")),
|
||||
re_path(
|
||||
r"avatar/(?P<digest>\w{64})", AvatarImageView.as_view(), name="avatar_view"
|
||||
@@ -31,6 +35,11 @@ urlpatterns = [ # pylint: disable=invalid-name
|
||||
GravatarProxyView.as_view(),
|
||||
name="gravatarproxy",
|
||||
),
|
||||
re_path(
|
||||
r"blueskyproxy/(?P<digest>\w*)",
|
||||
BlueskyProxyView.as_view(),
|
||||
name="blueskyproxy",
|
||||
),
|
||||
path(
|
||||
"description/",
|
||||
TemplateView.as_view(template_name="description.html"),
|
||||
@@ -63,12 +72,9 @@ urlpatterns = [ # pylint: disable=invalid-name
|
||||
]
|
||||
|
||||
MAINTENANCE = False
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
if settings.MAINTENANCE:
|
||||
MAINTENANCE = True
|
||||
except Exception: # pylint: disable=bare-except
|
||||
pass
|
||||
|
||||
if MAINTENANCE:
|
||||
urlpatterns.append(
|
||||
path("", TemplateView.as_view(template_name="maintenance.html"), name="home")
|
||||
|
||||
122
ivatar/utils.py
122
ivatar/utils.py
@@ -2,11 +2,103 @@
|
||||
"""
|
||||
Simple module providing reusable random_string function
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import random
|
||||
import string
|
||||
from io import BytesIO
|
||||
from PIL import Image, ImageDraw, ImageSequence
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
from ivatar.settings import DEBUG, URL_TIMEOUT
|
||||
from urllib.request import urlopen as urlopen_orig
|
||||
|
||||
BLUESKY_IDENTIFIER = None
|
||||
BLUESKY_APP_PASSWORD = None
|
||||
with contextlib.suppress(Exception):
|
||||
from ivatar.settings import BLUESKY_IDENTIFIER, BLUESKY_APP_PASSWORD
|
||||
|
||||
|
||||
def urlopen(url, timeout=URL_TIMEOUT):
|
||||
ctx = None
|
||||
if DEBUG:
|
||||
import ssl
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
return urlopen_orig(url, timeout=timeout, context=ctx)
|
||||
|
||||
|
||||
class Bluesky:
|
||||
"""
|
||||
Handle Bluesky client access
|
||||
"""
|
||||
|
||||
identifier = ""
|
||||
app_password = ""
|
||||
service = "https://bsky.social"
|
||||
session = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier: str = BLUESKY_IDENTIFIER,
|
||||
app_password: str = BLUESKY_APP_PASSWORD,
|
||||
service: str = "https://bsky.social",
|
||||
):
|
||||
self.identifier = identifier
|
||||
self.app_password = app_password
|
||||
self.service = service
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Login to Bluesky
|
||||
"""
|
||||
auth_response = requests.post(
|
||||
f"{self.service}/xrpc/com.atproto.server.createSession",
|
||||
json={"identifier": self.identifier, "password": self.app_password},
|
||||
)
|
||||
auth_response.raise_for_status()
|
||||
self.session = auth_response.json()
|
||||
|
||||
def normalize_handle(self, handle: str) -> str:
|
||||
"""
|
||||
Return the normalized handle for given handle
|
||||
"""
|
||||
# Normalize Bluesky handle in case someone enters an '@' at the beginning
|
||||
while handle.startswith("@"):
|
||||
handle = handle[1:]
|
||||
# Remove trailing spaces or spaces at the beginning
|
||||
while handle.startswith(" "):
|
||||
handle = handle[1:]
|
||||
while handle.endswith(" "):
|
||||
handle = handle[:-1]
|
||||
return handle
|
||||
|
||||
def get_profile(self, handle: str) -> str:
|
||||
if not self.session:
|
||||
self.login()
|
||||
profile_response = None
|
||||
|
||||
try:
|
||||
profile_response = requests.get(
|
||||
f"{self.service}/xrpc/app.bsky.actor.getProfile",
|
||||
headers={"Authorization": f'Bearer {self.session["accessJwt"]}'},
|
||||
params={"actor": handle},
|
||||
)
|
||||
profile_response.raise_for_status()
|
||||
except Exception as exc:
|
||||
print(f"Bluesky profile fetch failed with HTTP error: {exc}")
|
||||
return None
|
||||
|
||||
return profile_response.json()
|
||||
|
||||
def get_avatar(self, handle: str):
|
||||
"""
|
||||
Get avatar URL for a handle
|
||||
"""
|
||||
profile = self.get_profile(handle)
|
||||
return profile["avatar"] if profile else None
|
||||
|
||||
|
||||
def random_string(length=10):
|
||||
@@ -32,12 +124,12 @@ def openid_variations(openid):
|
||||
if openid.startswith("https://"):
|
||||
openid = openid.replace("https://", "http://")
|
||||
if openid[-1] != "/":
|
||||
openid = openid + "/"
|
||||
openid = f"{openid}/"
|
||||
|
||||
# http w/o trailing slash
|
||||
var1 = openid[0:-1]
|
||||
var1 = openid[:-1]
|
||||
var2 = openid.replace("http://", "https://")
|
||||
var3 = var2[0:-1]
|
||||
var3 = var2[:-1]
|
||||
return (openid, var1, var2, var3)
|
||||
|
||||
|
||||
@@ -55,43 +147,43 @@ def mm_ng(
|
||||
idhash = "e0"
|
||||
|
||||
# How large is the circle?
|
||||
circlesize = size * 0.6
|
||||
circle_size = size * 0.6
|
||||
|
||||
# Coordinates for the circle
|
||||
start_x = int(size * 0.2)
|
||||
end_x = start_x + circlesize
|
||||
end_x = start_x + circle_size
|
||||
start_y = int(size * 0.05)
|
||||
end_y = start_y + circlesize
|
||||
end_y = start_y + circle_size
|
||||
|
||||
# All are the same, based on the input hash
|
||||
# this should always result in a "gray-ish" background
|
||||
red = idhash[0:2]
|
||||
green = idhash[0:2]
|
||||
blue = idhash[0:2]
|
||||
red = idhash[:2]
|
||||
green = idhash[:2]
|
||||
blue = idhash[:2]
|
||||
|
||||
# Add some red (i/a) and make sure it's not over 255
|
||||
red = hex(int(red, 16) + add_red).replace("0x", "")
|
||||
if int(red, 16) > 255:
|
||||
red = "ff"
|
||||
if len(red) == 1:
|
||||
red = "0%s" % red
|
||||
red = f"0{red}"
|
||||
|
||||
# Add some green (i/a) and make sure it's not over 255
|
||||
green = hex(int(green, 16) + add_green).replace("0x", "")
|
||||
if int(green, 16) > 255:
|
||||
green = "ff"
|
||||
if len(green) == 1:
|
||||
green = "0%s" % green
|
||||
green = f"0{green}"
|
||||
|
||||
# Add some blue (i/a) and make sure it's not over 255
|
||||
blue = hex(int(blue, 16) + add_blue).replace("0x", "")
|
||||
if int(blue, 16) > 255:
|
||||
blue = "ff"
|
||||
if len(blue) == 1:
|
||||
blue = "0%s" % blue
|
||||
blue = f"0{blue}"
|
||||
|
||||
# Assemable the bg color "string" in webnotation. Eg. '#d3d3d3'
|
||||
bg_color = "#" + red + green + blue
|
||||
# Assemble the bg color "string" in web notation. Eg. '#d3d3d3'
|
||||
bg_color = f"#{red}{green}{blue}"
|
||||
|
||||
# Image
|
||||
image = Image.new("RGB", (size, size))
|
||||
@@ -106,7 +198,7 @@ def mm_ng(
|
||||
# Draw MMs 'body'
|
||||
draw.polygon(
|
||||
(
|
||||
(start_x + circlesize / 2, size / 2.5),
|
||||
(start_x + circle_size / 2, size / 2.5),
|
||||
(size * 0.15, size),
|
||||
(size - size * 0.15, size),
|
||||
),
|
||||
|
||||
283
ivatar/views.py
283
ivatar/views.py
@@ -2,10 +2,12 @@
|
||||
"""
|
||||
views under /
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from io import BytesIO
|
||||
from os import path
|
||||
import hashlib
|
||||
from urllib.request import urlopen
|
||||
from ivatar.utils import urlopen, Bluesky
|
||||
from urllib.error import HTTPError, URLError
|
||||
from ssl import SSLError
|
||||
from django.views.generic.base import TemplateView, View
|
||||
@@ -36,8 +38,6 @@ from .ivataraccount.models import Photo
|
||||
from .ivataraccount.models import pil_format, file_format
|
||||
from .utils import is_trusted_url, mm_ng, resize_animated_gif
|
||||
|
||||
URL_TIMEOUT = 5 # in seconds
|
||||
|
||||
|
||||
def get_size(request, size=DEFAULT_AVATAR_SIZE):
|
||||
"""
|
||||
@@ -49,17 +49,11 @@ def get_size(request, size=DEFAULT_AVATAR_SIZE):
|
||||
if "size" in request.GET:
|
||||
sizetemp = request.GET["size"]
|
||||
if sizetemp:
|
||||
if sizetemp != "" and sizetemp is not None and sizetemp != "0":
|
||||
try:
|
||||
if sizetemp not in ["", "0"]:
|
||||
with contextlib.suppress(ValueError):
|
||||
if int(sizetemp) > 0:
|
||||
size = int(sizetemp)
|
||||
# Should we receive something we cannot convert to int, leave
|
||||
# the user with the default value of 80
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if size > int(AVATAR_MAX_SIZE):
|
||||
size = int(AVATAR_MAX_SIZE)
|
||||
size = min(size, int(AVATAR_MAX_SIZE))
|
||||
return size
|
||||
|
||||
|
||||
@@ -121,9 +115,9 @@ class AvatarImageView(TemplateView):
|
||||
|
||||
# Check the cache first
|
||||
if CACHE_RESPONSE:
|
||||
centry = caches["filesystem"].get(uri)
|
||||
if centry:
|
||||
# For DEBUG purpose only print('Cached entry for %s' % uri)
|
||||
if centry := caches["filesystem"].get(uri):
|
||||
# For DEBUG purpose only
|
||||
# print('Cached entry for %s' % uri)
|
||||
return HttpResponse(
|
||||
centry["content"],
|
||||
content_type=centry["content_type"],
|
||||
@@ -151,8 +145,7 @@ class AvatarImageView(TemplateView):
|
||||
|
||||
if not trusted_url:
|
||||
print(
|
||||
"Default URL is not in trusted URLs: '%s' ; Kicking it!"
|
||||
% default
|
||||
f"Default URL is not in trusted URLs: '{default}'; Kicking it!"
|
||||
)
|
||||
default = None
|
||||
|
||||
@@ -178,20 +171,23 @@ class AvatarImageView(TemplateView):
|
||||
obj = model.objects.get(digest_sha256=kwargs["digest"])
|
||||
except ObjectDoesNotExist:
|
||||
model = ConfirmedOpenId
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
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()
|
||||
# and therefore 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 Exception: # pylint: disable=bare-except
|
||||
pass
|
||||
|
||||
# Handle the special case of Bluesky
|
||||
if obj:
|
||||
if obj.bluesky_handle:
|
||||
return HttpResponseRedirect(
|
||||
reverse_lazy("blueskyproxy", args=[kwargs["digest"]])
|
||||
)
|
||||
# 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 = (
|
||||
@@ -213,7 +209,7 @@ class AvatarImageView(TemplateView):
|
||||
)
|
||||
# Ensure we do not convert None to string 'None'
|
||||
if default:
|
||||
url += "&default=%s" % default
|
||||
url += f"&default={default}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Return the default URL, as specified, or 404 Not Found, if default=404
|
||||
@@ -223,7 +219,7 @@ class AvatarImageView(TemplateView):
|
||||
url = (
|
||||
reverse_lazy("gravatarproxy", args=[kwargs["digest"]])
|
||||
+ "?s=%i" % size
|
||||
+ "&default=%s&f=y" % default
|
||||
+ f"&default={default}&f=y"
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -233,46 +229,25 @@ class AvatarImageView(TemplateView):
|
||||
if str(default) == "monsterid":
|
||||
monsterdata = BuildMonster(seed=kwargs["digest"], size=(size, size))
|
||||
data = BytesIO()
|
||||
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
|
||||
return response
|
||||
|
||||
return self._return_cached_png(monsterdata, data, uri)
|
||||
if str(default) == "robohash":
|
||||
roboset = "any"
|
||||
if request.GET.get("robohash"):
|
||||
roboset = request.GET.get("robohash")
|
||||
roboset = request.GET.get("robohash") or "any"
|
||||
robohash = Robohash(kwargs["digest"])
|
||||
robohash.assemble(roboset=roboset, sizex=size, sizey=size)
|
||||
data = BytesIO()
|
||||
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
|
||||
return response
|
||||
|
||||
return self._return_cached_response(data, uri)
|
||||
if str(default) == "retro":
|
||||
identicon = Identicon.render(kwargs["digest"])
|
||||
data = BytesIO()
|
||||
img = Image.open(BytesIO(identicon))
|
||||
img = img.resize((size, size), Image.LANCZOS)
|
||||
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
|
||||
return response
|
||||
|
||||
return self._return_cached_png(img, data, uri)
|
||||
if str(default) == "pagan":
|
||||
paganobj = pagan.Avatar(kwargs["digest"])
|
||||
data = BytesIO()
|
||||
img = paganobj.img.resize((size, size), Image.LANCZOS)
|
||||
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
|
||||
return response
|
||||
|
||||
return self._return_cached_png(img, data, uri)
|
||||
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.
|
||||
@@ -281,42 +256,16 @@ class AvatarImageView(TemplateView):
|
||||
).hexdigest()
|
||||
img = p.draw(newdigest, size, 0)
|
||||
data = BytesIO()
|
||||
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
|
||||
return response
|
||||
|
||||
return self._return_cached_png(img, data, uri)
|
||||
if str(default) == "mmng":
|
||||
mmngimg = mm_ng(idhash=kwargs["digest"], size=size)
|
||||
data = BytesIO()
|
||||
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
|
||||
return response
|
||||
|
||||
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")
|
||||
)
|
||||
if not path.isfile(static_img):
|
||||
# We trust this exists!!!
|
||||
static_img = path.join("static", "img", "mm", "512.png")
|
||||
# We trust static/ is mapped to /static/
|
||||
return HttpResponseRedirect("/" + static_img)
|
||||
return self._return_cached_png(mmngimg, data, uri)
|
||||
if str(default) in {"mm", "mp"}:
|
||||
return self._redirect_static_w_size("mm", size)
|
||||
return HttpResponseRedirect(default)
|
||||
|
||||
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")
|
||||
# We trust static/ is mapped to /static/
|
||||
return HttpResponseRedirect("/" + static_img)
|
||||
|
||||
return self._redirect_static_w_size("nobody", size)
|
||||
imgformat = obj.photo.format
|
||||
photodata = Image.open(BytesIO(obj.photo.data))
|
||||
|
||||
@@ -343,10 +292,32 @@ class AvatarImageView(TemplateView):
|
||||
obj.save()
|
||||
if imgformat == "jpg":
|
||||
imgformat = "jpeg"
|
||||
response = CachingHttpResponse(uri, data, content_type="image/%s" % imgformat)
|
||||
response = CachingHttpResponse(uri, data, content_type=f"image/{imgformat}")
|
||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
||||
return response
|
||||
|
||||
def _redirect_static_w_size(self, arg0, size):
|
||||
"""
|
||||
Helper method to redirect to static image with size i/a
|
||||
"""
|
||||
# If mm is explicitly given, we need to catch that
|
||||
static_img = path.join("static", "img", arg0, f"{str(size)}.png")
|
||||
if not path.isfile(static_img):
|
||||
# We trust this exists!!!
|
||||
static_img = path.join("static", "img", arg0, "512.png")
|
||||
# We trust static/ is mapped to /static/
|
||||
return HttpResponseRedirect(f"/{static_img}")
|
||||
|
||||
def _return_cached_response(self, data, uri):
|
||||
data.seek(0)
|
||||
response = CachingHttpResponse(uri, data, content_type="image/png")
|
||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
||||
return response
|
||||
|
||||
def _return_cached_png(self, arg0, data, uri):
|
||||
arg0.save(data, "PNG", quality=JPEG_QUALITY)
|
||||
return self._return_cached_response(data, uri)
|
||||
|
||||
|
||||
class GravatarProxyView(View):
|
||||
"""
|
||||
@@ -369,19 +340,16 @@ class GravatarProxyView(View):
|
||||
+ "&forcedefault=y"
|
||||
)
|
||||
if default is not None:
|
||||
url += "&default=%s" % default
|
||||
url += f"&default={default}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
size = get_size(request)
|
||||
gravatarimagedata = None
|
||||
default = None
|
||||
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
if str(request.GET["default"]) != "None":
|
||||
default = request.GET["default"]
|
||||
except Exception: # pylint: disable=bare-except
|
||||
pass
|
||||
|
||||
if str(default) != "wavatar":
|
||||
# This part is special/hackish
|
||||
# Check if the image returned by Gravatar is their default image, if so,
|
||||
@@ -396,40 +364,39 @@ class GravatarProxyView(View):
|
||||
# print("Cached Gravatar response: Default.")
|
||||
return redir_default(default)
|
||||
try:
|
||||
urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
|
||||
urlopen(gravatar_test_url)
|
||||
except HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
cache.set(gravatar_test_url, "default", 60)
|
||||
else:
|
||||
print("Gravatar test url fetch failed: %s" % exc)
|
||||
print(f"Gravatar test url fetch failed: {exc}")
|
||||
return redir_default(default)
|
||||
|
||||
gravatar_url = (
|
||||
"https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % size
|
||||
)
|
||||
if default:
|
||||
gravatar_url += "&d=%s" % default
|
||||
gravatar_url += f"&d={default}"
|
||||
|
||||
try:
|
||||
if cache.get(gravatar_url) == "err":
|
||||
print("Cached Gravatar fetch failed with URL error: %s" % gravatar_url)
|
||||
print(f"Cached Gravatar fetch failed with URL error: {gravatar_url}")
|
||||
return redir_default(default)
|
||||
|
||||
gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)
|
||||
gravatarimagedata = urlopen(gravatar_url)
|
||||
except HTTPError as exc:
|
||||
if exc.code != 404 and exc.code != 503:
|
||||
if exc.code not in [404, 503]:
|
||||
print(
|
||||
"Gravatar fetch failed with an unexpected %s HTTP error: %s"
|
||||
% (exc.code, gravatar_url)
|
||||
f"Gravatar fetch failed with an unexpected {exc.code} HTTP error: {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)
|
||||
print(f"Gravatar fetch failed with URL error: {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)
|
||||
print(f"Gravatar fetch failed with SSL error: {exc.reason}")
|
||||
cache.set(gravatar_url, "err", 30)
|
||||
return redir_default(default)
|
||||
try:
|
||||
@@ -437,13 +404,131 @@ class GravatarProxyView(View):
|
||||
img = Image.open(data)
|
||||
data.seek(0)
|
||||
response = HttpResponse(
|
||||
data.read(), content_type="image/%s" % file_format(img.format)
|
||||
data.read(), content_type=f"image/{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(f"Value error: {exc}")
|
||||
return redir_default(default)
|
||||
|
||||
# We shouldn't reach this point... But make sure we do something
|
||||
return redir_default(default)
|
||||
|
||||
|
||||
class BlueskyProxyView(View):
|
||||
"""
|
||||
Proxy request to Bluesky and return the image from there
|
||||
"""
|
||||
|
||||
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"
|
||||
)
|
||||
if default is not None:
|
||||
url += f"&default={default}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
size = get_size(request)
|
||||
print(size)
|
||||
blueskyimagedata = None
|
||||
default = None
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
if str(request.GET["default"]) != "None":
|
||||
default = request.GET["default"]
|
||||
identity = None
|
||||
|
||||
# First check for email, as this is the most common
|
||||
try:
|
||||
identity = ConfirmedEmail.objects.filter(
|
||||
Q(digest=kwargs["digest"]) | Q(digest_sha256=kwargs["digest"])
|
||||
).first()
|
||||
except Exception as exc:
|
||||
print(exc)
|
||||
|
||||
# If no identity is found in the email table, try the openid table
|
||||
if not identity:
|
||||
try:
|
||||
identity = ConfirmedOpenId.objects.filter(
|
||||
Q(digest=kwargs["digest"])
|
||||
| Q(alt_digest1=kwargs["digest"])
|
||||
| Q(alt_digest2=kwargs["digest"])
|
||||
| Q(alt_digest3=kwargs["digest"])
|
||||
).first()
|
||||
except Exception as exc:
|
||||
print(exc)
|
||||
|
||||
# If still no identity is found, redirect to the default
|
||||
if not identity:
|
||||
return redir_default(default)
|
||||
|
||||
bs = Bluesky()
|
||||
bluesky_url = None
|
||||
# Try with the cache first
|
||||
with contextlib.suppress(Exception):
|
||||
if cache.get(identity.bluesky_handle):
|
||||
bluesky_url = cache.get(identity.bluesky_handle)
|
||||
if not bluesky_url:
|
||||
try:
|
||||
bluesky_url = bs.get_avatar(identity.bluesky_handle)
|
||||
cache.set(identity.bluesky_handle, bluesky_url)
|
||||
except Exception: # pylint: disable=bare-except
|
||||
return redir_default(default)
|
||||
|
||||
try:
|
||||
if cache.get(bluesky_url) == "err":
|
||||
print(f"Cached Bluesky fetch failed with URL error: {bluesky_url}")
|
||||
return redir_default(default)
|
||||
|
||||
blueskyimagedata = urlopen(bluesky_url)
|
||||
except HTTPError as exc:
|
||||
if exc.code not in [404, 503]:
|
||||
print(
|
||||
f"Bluesky fetch failed with an unexpected {exc.code} HTTP error: {bluesky_url}"
|
||||
)
|
||||
cache.set(bluesky_url, "err", 30)
|
||||
return redir_default(default)
|
||||
except URLError as exc:
|
||||
print(f"Bluesky fetch failed with URL error: {exc.reason}")
|
||||
cache.set(bluesky_url, "err", 30)
|
||||
return redir_default(default)
|
||||
except SSLError as exc:
|
||||
print(f"Bluesky fetch failed with SSL error: {exc.reason}")
|
||||
cache.set(bluesky_url, "err", 30)
|
||||
return redir_default(default)
|
||||
try:
|
||||
data = BytesIO(blueskyimagedata.read())
|
||||
img = Image.open(data)
|
||||
img_format = img.format
|
||||
if max(img.size) > size:
|
||||
aspect = img.size[0] / float(img.size[1])
|
||||
if aspect > 1:
|
||||
new_size = (size, int(size / aspect))
|
||||
else:
|
||||
new_size = (int(size * aspect), size)
|
||||
img = img.resize(new_size)
|
||||
data = BytesIO()
|
||||
img.save(data, format=img_format)
|
||||
|
||||
data.seek(0)
|
||||
response = HttpResponse(
|
||||
data.read(), content_type=f"image/{file_format(format)}"
|
||||
)
|
||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
||||
return response
|
||||
except ValueError as exc:
|
||||
print(f"Value error: {exc}")
|
||||
return redir_default(default)
|
||||
|
||||
# We shouldn't reach this point... But make sure we do something
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
autopep8
|
||||
bcrypt
|
||||
defusedxml
|
||||
Django>=5.1
|
||||
Django>=4.2.16
|
||||
django-anymail[mailgun]
|
||||
django-auth-ldap
|
||||
django-bootstrap4
|
||||
@@ -16,10 +16,10 @@ flake8-respect-noqa
|
||||
git+https://github.com/daboth/pagan.git
|
||||
git+https://github.com/ercpe/pydenticon5.git
|
||||
git+https://github.com/flavono123/identicon.git
|
||||
git+https://github.com/necaris/python3-openid.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
|
||||
Pillow
|
||||
pip
|
||||
@@ -32,7 +32,6 @@ pymemcache
|
||||
PyMySQL
|
||||
python-coveralls
|
||||
python-language-server
|
||||
python3-openid
|
||||
pytz
|
||||
rope
|
||||
setuptools
|
||||
|
||||
Reference in New Issue
Block a user