mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-14 20:18:02 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9f73bc012 | ||
|
|
df0400375d | ||
|
|
50569afc25 | ||
|
|
927083eb58 | ||
|
|
a2eea54235 | ||
|
|
01bcc1ee11 | ||
|
|
16f809d8a6 | ||
|
|
f01e49d495 | ||
|
|
fd696ed74c | ||
|
|
021a8de4d8 | ||
|
|
cbdaed28da | ||
|
|
95410f6e43 | ||
|
|
2be7309625 | ||
|
|
6deea2758f | ||
|
|
2bb1f5f26d | ||
|
|
3878554dd9 | ||
|
|
9478177c83 | ||
|
|
47837f4516 | ||
|
|
2276ea962f | ||
|
|
ae3c6beed4 | ||
|
|
ff9af3de9b | ||
|
|
7e46df0c15 | ||
|
|
e1547d14c5 | ||
|
|
8f5bc9653b | ||
|
|
5dbbff49d0 | ||
|
|
5c8da703cb | ||
|
|
3aeb1ba454 | ||
|
|
9e189b3fd2 | ||
|
|
b8292b5404 | ||
|
|
5730c2dabf | ||
|
|
dddd24e57f | ||
|
|
a6c5899f44 | ||
|
|
ba6f46c6eb | ||
|
|
ddfc1e7824 | ||
|
|
2761e801df | ||
|
|
555a8b0523 | ||
|
|
6d984a486a | ||
|
|
9dceb7a696 | ||
|
|
64575a9b99 | ||
|
|
a94954d58c | ||
|
|
d2e4162b6b | ||
|
|
4afee63137 | ||
|
|
d486fdef2c | ||
|
|
e945ae2b4d | ||
|
|
9565ccc54e | ||
|
|
e68c75d74d |
1
.buildpacks
Normal file
1
.buildpacks
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/heroku/heroku-buildpack-python
|
||||||
4
.env
4
.env
@@ -1,6 +1,8 @@
|
|||||||
if [ ! -d .virtualenv ]; then
|
if [ ! -d .virtualenv ]; then
|
||||||
if [ ! "$(which virtualenv)" == "" ]; then
|
if [ ! "$(which virtualenv)" == "" ]; then
|
||||||
virtualenv -p python3 .virtualenv
|
if [ -f .env ]; then
|
||||||
|
virtualenv -p python3 .virtualenv
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -f .virtualenv/bin/activate ]; then
|
if [ -f .virtualenv/bin/activate ]; then
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,3 +20,4 @@ falko_gravatar.jpg
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
dump_all*.sql
|
dump_all*.sql
|
||||||
dist/
|
dist/
|
||||||
|
.env.local
|
||||||
|
|||||||
124
.gitlab-ci.yml
124
.gitlab-ci.yml
@@ -1,11 +1,16 @@
|
|||||||
default:
|
image:
|
||||||
image:
|
name: quay.io/rhn_support_ofalk/fedora35-python3
|
||||||
name: quay.io/rhn_support_ofalk/fedora35-python3
|
entrypoint:
|
||||||
entrypoint: [ '/bin/sh', '-c' ]
|
- "/bin/sh"
|
||||||
|
- "-c"
|
||||||
|
|
||||||
before_script:
|
test_and_coverage:
|
||||||
|
stage: build
|
||||||
|
coverage: "/^TOTAL.*\\s+(\\d+\\%)$/"
|
||||||
|
before_script:
|
||||||
- virtualenv -p python3 /tmp/.virtualenv
|
- virtualenv -p python3 /tmp/.virtualenv
|
||||||
- source /tmp/.virtualenv/bin/activate
|
- source /tmp/.virtualenv/bin/activate
|
||||||
|
- pip install -U pip
|
||||||
- pip install Pillow
|
- pip install Pillow
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install python-coveralls
|
- pip install python-coveralls
|
||||||
@@ -13,66 +18,93 @@ before_script:
|
|||||||
- pip install pycco
|
- pip install pycco
|
||||||
- pip install django_coverage_plugin
|
- pip install django_coverage_plugin
|
||||||
|
|
||||||
test_and_coverage:
|
|
||||||
stage: test
|
|
||||||
coverage: '/^TOTAL.*\s+(\d+\%)$/'
|
|
||||||
script:
|
script:
|
||||||
- echo 'from ivatar.settings import TEMPLATES' > config_local.py
|
- echo 'from ivatar.settings import TEMPLATES' > config_local.py
|
||||||
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
|
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
|
||||||
- echo "DEBUG = True" >> config_local.py
|
- echo "DEBUG = True" >> config_local.py
|
||||||
- python manage.py collectstatic --noinput
|
- echo "from config import CACHES" >> config_local.py
|
||||||
- coverage run --source . manage.py test -v3
|
- echo "CACHES['default'] = CACHES['filesystem']" >> config_local.py
|
||||||
- coverage report --fail-under=70
|
- python manage.py collectstatic --noinput
|
||||||
- coverage html
|
- coverage run --source . manage.py test -v3
|
||||||
|
- coverage report --fail-under=70
|
||||||
|
- coverage html
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- htmlcov/
|
- htmlcov/
|
||||||
|
|
||||||
pycco:
|
pycco:
|
||||||
stage: test
|
stage: test
|
||||||
|
before_script:
|
||||||
|
- virtualenv -p python3 /tmp/.virtualenv
|
||||||
|
- source /tmp/.virtualenv/bin/activate
|
||||||
|
- pip install -U pip
|
||||||
|
- pip install Pillow
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- pip install python-coveralls
|
||||||
|
- pip install coverage
|
||||||
|
- pip install pycco
|
||||||
|
- pip install django_coverage_plugin
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- /bin/true
|
- "/bin/true"
|
||||||
- find ivatar/ -type f -name "*.py"|grep -v __pycache__|grep -v __init__.py|grep -v /migrations/ | xargs pycco -p -d pycco -i -s
|
- find ivatar/ -type f -name "*.py"|grep -v __pycache__|grep -v __init__.py|grep
|
||||||
|
-v /migrations/ | xargs pycco -p -d pycco -i -s
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- pycco/
|
- pycco/
|
||||||
expire_in: 14 days
|
expire_in: 14 days
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
before_script:
|
|
||||||
- /bin/true
|
|
||||||
- /bin/true
|
|
||||||
stage: deploy
|
stage: deploy
|
||||||
dependencies:
|
dependencies:
|
||||||
- test_and_coverage
|
- test_and_coverage
|
||||||
- pycco
|
- pycco
|
||||||
script:
|
script:
|
||||||
- mv htmlcov/ public/
|
- mv htmlcov/ public/
|
||||||
- mv pycco/ public/
|
- mv pycco/ public/
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
expire_in: 14 days
|
expire_in: 14 days
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
build-image:
|
build-image:
|
||||||
image: docker
|
image: docker
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- devel
|
||||||
services:
|
services:
|
||||||
- docker:dind
|
- docker:dind
|
||||||
before_script:
|
before_script:
|
||||||
- docker info
|
- docker info
|
||||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ls -lah
|
- ls -lah
|
||||||
- |
|
- |
|
||||||
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
|
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
|
||||||
tag=""
|
tag=""
|
||||||
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
|
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
|
||||||
else
|
else
|
||||||
tag=":$CI_COMMIT_REF_SLUG"
|
tag=":$CI_COMMIT_REF_SLUG"
|
||||||
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
|
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
|
||||||
fi
|
fi
|
||||||
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
|
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
|
||||||
- docker push "$CI_REGISTRY_IMAGE${tag}"
|
- docker push "$CI_REGISTRY_IMAGE${tag}"
|
||||||
|
semgrep:
|
||||||
|
extends: semgrep-sast
|
||||||
|
stage: test
|
||||||
|
allow_failure: true
|
||||||
|
image: registry.gitlab.com/gitlab-org/security-products/analyzers/semgrep:latest
|
||||||
|
variables:
|
||||||
|
CI_PROJECT_DIR: "/tmp/app"
|
||||||
|
SECURE_LOG_LEVEL: "debug"
|
||||||
|
script:
|
||||||
|
- rm -rf .virtualenv
|
||||||
|
- /analyzer run
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- gl-sast-report.json
|
||||||
|
- semgrep.sarif
|
||||||
|
|
||||||
|
include:
|
||||||
|
- template: Jobs/SAST.gitlab-ci.yml
|
||||||
|
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
|
||||||
|
- template: Jobs/Secret-Detection.gitlab-ci.yml
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: check-useless-excludes
|
- id: check-useless-excludes
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v2.6.2
|
rev: v3.0.0-alpha.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
files: \.(css|js|md|markdown|json)
|
files: \.(css|js|md|markdown|json)
|
||||||
- repo: https://github.com/python/black
|
- repo: https://github.com/python/black
|
||||||
rev: 22.3.0
|
rev: 22.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.2.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: check-ast
|
- id: check-ast
|
||||||
@@ -37,8 +37,8 @@ repos:
|
|||||||
- id: requirements-txt-fixer
|
- id: requirements-txt-fixer
|
||||||
- id: sort-simple-yaml
|
- id: sort-simple-yaml
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 3.9.2
|
rev: 6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
- repo: local
|
- repo: local
|
||||||
|
|||||||
19
Dockerfile
19
Dockerfile
@@ -1,17 +1,22 @@
|
|||||||
FROM quay.io/rhn_support_ofalk/fedora35-python3
|
FROM quay.io/rhn_support_ofalk/fedora37-python3
|
||||||
LABEL maintainer Oliver Falk <oliver@linux-kernel.at>
|
LABEL maintainer Oliver Falk <oliver@linux-kernel.at>
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|
||||||
RUN pip3 install pip --upgrade
|
|
||||||
|
|
||||||
ADD . /opt/ivatar-devel
|
ADD . /opt/ivatar-devel
|
||||||
|
|
||||||
WORKDIR /opt/ivatar-devel
|
WORKDIR /opt/ivatar-devel
|
||||||
|
|
||||||
RUN pip3 install Pillow && pip3 install -r requirements.txt && pip3 install python-coveralls coverage pycco django_coverage_plugin
|
RUN pip3 install pip --upgrade \
|
||||||
|
&& virtualenv .virtualenv \
|
||||||
|
&& source .virtualenv/bin/activate \
|
||||||
|
&& pip3 install Pillow \
|
||||||
|
&& pip3 install -r requirements.txt \
|
||||||
|
&& pip3 install python-coveralls coverage pycco django_coverage_plugin
|
||||||
|
|
||||||
RUN echo "DEBUG = True" >> /opt/ivatar-devel/config_local.py
|
RUN echo "DEBUG = True" >> /opt/ivatar-devel/config_local.py
|
||||||
RUN echo "EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'" >> /opt/ivatar-devel/config_local.py
|
RUN echo "EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'" >> /opt/ivatar-devel/config_local.py
|
||||||
RUN python3 manage.py migrate && python3 manage.py collectstatic --noinput
|
RUN source .virtualenv/bin/activate \
|
||||||
RUN echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@local.tld', 'admin')" | python manage.py shell
|
&& python3 manage.py migrate \
|
||||||
ENTRYPOINT python3 ./manage.py runserver 0:8081
|
&& python3 manage.py collectstatic --noinput \
|
||||||
|
&& echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@local.tld', 'admin')" | python manage.py shell
|
||||||
|
ENTRYPOINT source .virtualenv/bin/activate && python3 ./manage.py runserver 0:8081
|
||||||
|
|||||||
10
MANIFEST.in
Normal file
10
MANIFEST.in
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
include *.py
|
||||||
|
include *.md
|
||||||
|
include COPYING
|
||||||
|
include LICENSE
|
||||||
|
recursive-include templates *
|
||||||
|
recursive-include ivatar *
|
||||||
|
exclude .virtualenv
|
||||||
|
exclude libravatar.egg-info
|
||||||
|
global-exclude *.py[co]
|
||||||
|
global-exclude __pycache__
|
||||||
2
attic/debug_toolbar_resources.txt
Normal file
2
attic/debug_toolbar_resources.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
https://django-debug-toolbar.readthedocs.io/en/latest/installation.html
|
||||||
|
https://stackoverflow.com/questions/6548947/how-can-django-debug-toolbar-be-set-to-work-for-just-some-users/6549317#6549317
|
||||||
49
attic/encryption_test.py
Executable file
49
attic/encryption_test.py
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import django
|
||||||
|
import timeit
|
||||||
|
|
||||||
|
os.environ.setdefault(
|
||||||
|
"DJANGO_SETTINGS_MODULE", "ivatar.settings"
|
||||||
|
) # pylint: disable=wrong-import-position
|
||||||
|
django.setup() # pylint: disable=wrong-import-position
|
||||||
|
|
||||||
|
from ivatar.ivataraccount.models import ConfirmedEmail, APIKey
|
||||||
|
from simplecrypt import decrypt
|
||||||
|
from binascii import unhexlify
|
||||||
|
|
||||||
|
digest = None
|
||||||
|
digest_sha256 = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_digest_sha256():
|
||||||
|
digest_sha256 = ConfirmedEmail.objects.first().encrypted_digest_sha256(
|
||||||
|
secret_key=APIKey.objects.first()
|
||||||
|
)
|
||||||
|
return digest_sha256
|
||||||
|
|
||||||
|
|
||||||
|
def get_digest():
|
||||||
|
digest = ConfirmedEmail.objects.first().encrypted_digest(
|
||||||
|
secret_key=APIKey.objects.first()
|
||||||
|
)
|
||||||
|
return digest
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_digest():
|
||||||
|
return decrypt(APIKey.objects.first().secret_key, unhexlify(digest))
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_digest_256():
|
||||||
|
return decrypt(APIKey.objects.first().secret_key, unhexlify(digest_sha256))
|
||||||
|
|
||||||
|
|
||||||
|
digest = get_digest()
|
||||||
|
digest_sha256 = get_digest_sha256()
|
||||||
|
|
||||||
|
print("Encrypt digest: %s" % timeit.timeit(get_digest, number=1))
|
||||||
|
print("Encrypt digest_sha256: %s" % timeit.timeit(get_digest_sha256, number=1))
|
||||||
|
print("Decrypt digest: %s" % timeit.timeit(decrypt_digest, number=1))
|
||||||
|
print("Decrypt digest_sha256: %s" % timeit.timeit(decrypt_digest_256, number=1))
|
||||||
7
attic/example_mysql_config
Normal file
7
attic/example_mysql_config
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
DATABASES['default'] = {
|
||||||
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
|
'NAME': 'libravatar',
|
||||||
|
'USER': 'libravatar',
|
||||||
|
'PASSWORD': 'libravatar',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
}
|
||||||
13
config.py
13
config.py
@@ -60,7 +60,7 @@ OPENID_CREATE_USERS = True
|
|||||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||||
|
|
||||||
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
|
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
|
||||||
IVATAR_VERSION = "1.6.2"
|
IVATAR_VERSION = "1.7.0"
|
||||||
|
|
||||||
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
|
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ MESSAGE_TAGS = {
|
|||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
|
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
|
||||||
"LOCATION": [
|
"LOCATION": [
|
||||||
"127.0.0.1:11211",
|
"127.0.0.1:11211",
|
||||||
],
|
],
|
||||||
@@ -232,15 +232,18 @@ TRUSTED_DEFAULT_URLS = [
|
|||||||
"host_equals": "avatars.dicebear.com",
|
"host_equals": "avatars.dicebear.com",
|
||||||
"path_prefix": "/api/",
|
"path_prefix": "/api/",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"schemes": ["https"],
|
||||||
|
"host_equals": "api.dicebear.com",
|
||||||
|
"path_prefix": "/",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"schemes": ["https"],
|
"schemes": ["https"],
|
||||||
"host_equals": "badges.fedoraproject.org",
|
"host_equals": "badges.fedoraproject.org",
|
||||||
"path_prefix": "/static/img/",
|
"path_prefix": "/static/img/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"schemes": [
|
"schemes": ["http"],
|
||||||
"http",
|
|
||||||
],
|
|
||||||
"host_equals": "www.planet-libre.org",
|
"host_equals": "www.planet-libre.org",
|
||||||
"path_prefix": "/themes/planetlibre/images/",
|
"path_prefix": "/themes/planetlibre/images/",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ def file_format(image_type):
|
|||||||
"""
|
"""
|
||||||
Helper method returning a short image type
|
Helper method returning a short image type
|
||||||
"""
|
"""
|
||||||
if image_type == "JPEG":
|
if image_type in ("JPEG", "MPO"):
|
||||||
return "jpg"
|
return "jpg"
|
||||||
elif image_type == "PNG":
|
elif image_type == "PNG":
|
||||||
return "png"
|
return "png"
|
||||||
elif image_type == "GIF":
|
elif image_type == "GIF":
|
||||||
return "gif"
|
return "gif"
|
||||||
|
elif image_type == "WEBP":
|
||||||
|
return "webp"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -54,12 +56,14 @@ def pil_format(image_type):
|
|||||||
"""
|
"""
|
||||||
Helper method returning the 'encoder name' for PIL
|
Helper method returning the 'encoder name' for PIL
|
||||||
"""
|
"""
|
||||||
if image_type == "jpg" or image_type == "jpeg":
|
if image_type in ("jpg", "jpeg", "mpo"):
|
||||||
return "JPEG"
|
return "JPEG"
|
||||||
elif image_type == "png":
|
elif image_type == "png":
|
||||||
return "PNG"
|
return "PNG"
|
||||||
elif image_type == "gif":
|
elif image_type == "gif":
|
||||||
return "GIF"
|
return "GIF"
|
||||||
|
elif image_type == "webp":
|
||||||
|
return "WEBP"
|
||||||
|
|
||||||
logger.info("Unsupported file format: %s", image_type)
|
logger.info("Unsupported file format: %s", image_type)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ outline: inherit;
|
|||||||
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
|
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
|
||||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">{% ifequal email.photo.id photo.id %}<i class="fa fa-check"></i>{% endifequal %} {% trans 'Image' %} {{ forloop.counter }}</h3>
|
<h3 class="panel-title">{% if email.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'Image' %} {{ forloop.counter }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body" style="height:130px">
|
<div class="panel-body" style="height:130px">
|
||||||
<center>
|
<center>
|
||||||
@@ -49,7 +49,7 @@ outline: inherit;
|
|||||||
<button type="submit" name="photoNone" class="nobutton">
|
<button type="submit" name="photoNone" class="nobutton">
|
||||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">{% ifequal email.photo.id photo.id %}<i class="fa fa-check"></i>{% endifequal %} {% trans 'No image' %}</h3>
|
<h3 class="panel-title">{% if email.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'No image' %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body" style="height:130px">
|
<div class="panel-body" style="height:130px">
|
||||||
<center>
|
<center>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ outline: inherit;
|
|||||||
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
|
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
|
||||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">{% ifequal openid.photo.id photo.id %}<i class="fa fa-check"></i>{% endifequal %} {% trans 'Image' %} {{ forloop.counter }}</h3>
|
<h3 class="panel-title">{% if openid.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'Image' %} {{ forloop.counter }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body" style="height:130px">
|
<div class="panel-body" style="height:130px">
|
||||||
<center>
|
<center>
|
||||||
@@ -49,7 +49,7 @@ outline: inherit;
|
|||||||
<button type="submit" name="photoNone" class="nobutton">
|
<button type="submit" name="photoNone" class="nobutton">
|
||||||
<div class="panel panel-tortin" style="width:132px;margin:0">
|
<div class="panel panel-tortin" style="width:132px;margin:0">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">{% ifequal openid.photo.id photo.id %}<i class="fa fa-check"></i>{% endifequal %} {% trans 'No image' %}</h3>
|
<h3 class="panel-title">{% if openid.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'No image' %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body" style="height:130px">
|
<div class="panel-body" style="height:130px">
|
||||||
<center>
|
<center>
|
||||||
|
|||||||
@@ -748,6 +748,47 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
response = self.client.get(url, follow=True)
|
response = self.client.get(url, follow=True)
|
||||||
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
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.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:
|
||||||
|
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",
|
||||||
|
"WEBP upload failed?!",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.user.photo_set.first().format,
|
||||||
|
"webp",
|
||||||
|
"Format must be webp, since we uploaded a webp!",
|
||||||
|
)
|
||||||
|
self.test_confirm_email()
|
||||||
|
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
|
||||||
|
urlobj = urlsplit(
|
||||||
|
libravatar_url(
|
||||||
|
email=self.user.confirmedemail_set.first().email,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||||
|
response = self.client.get(url, follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
||||||
|
|
||||||
def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name
|
def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name
|
||||||
"""
|
"""
|
||||||
Test if unsupported format is correctly detected
|
Test if unsupported format is correctly detected
|
||||||
@@ -1292,16 +1333,37 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
# Simply delete it, then it's digest is 'correct', but
|
# Simply delete it, then it's digest is 'correct', but
|
||||||
# the hash is no longer there
|
# the hash is no longer there
|
||||||
addr = self.user.confirmedemail_set.first().email
|
addr = self.user.confirmedemail_set.first().email
|
||||||
hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest()
|
digest = hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
self.user.confirmedemail_set.first().delete()
|
self.user.confirmedemail_set.first().delete()
|
||||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||||
response = self.client.get(url, follow=True)
|
response = self.client.get(url, follow=True)
|
||||||
self.assertRedirects(
|
self.assertEqual(
|
||||||
response=response,
|
response.redirect_chain[0][0],
|
||||||
expected_url="/static/img/nobody/80.png",
|
"/gravatarproxy/%s?s=80" % digest,
|
||||||
msg_prefix="Why does this not redirect to Gravatar?",
|
"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?",
|
||||||
|
)
|
||||||
|
# self.assertRedirects(
|
||||||
|
# response=response,
|
||||||
|
# expected_url="/static/img/nobody/80.png",
|
||||||
|
# msg_prefix="Why does this not redirect to Gravatar?",
|
||||||
|
# )
|
||||||
# Eventually one should check if the data is the same
|
# Eventually one should check if the data is the same
|
||||||
|
|
||||||
def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled(
|
def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled(
|
||||||
@@ -1323,11 +1385,17 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
self.user.confirmedemail_set.first().delete()
|
self.user.confirmedemail_set.first().delete()
|
||||||
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
||||||
response = self.client.get(url, follow=True)
|
response = self.client.get(url, follow=True)
|
||||||
self.assertRedirects(
|
self.assertEqual(
|
||||||
response=response,
|
response.redirect_chain[0][0],
|
||||||
expected_url="/static/img/nobody/80.png",
|
"/static/img/nobody/80.png",
|
||||||
msg_prefix="Why does this not redirect to the default img?",
|
"Doesn't redirect to static?",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# self.assertRedirects(
|
||||||
|
# response=response,
|
||||||
|
# expected_url="/static/img/nobody/80.png",
|
||||||
|
# msg_prefix="Why does this not redirect to the default img?",
|
||||||
|
# )
|
||||||
# Eventually one should check if the data is the same
|
# Eventually one should check if the data is the same
|
||||||
|
|
||||||
def test_avatar_url_inexisting_mail_digest_w_default_mm(
|
def test_avatar_url_inexisting_mail_digest_w_default_mm(
|
||||||
@@ -1361,11 +1429,17 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
)
|
)
|
||||||
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
||||||
response = self.client.get(url, follow=True)
|
response = self.client.get(url, follow=True)
|
||||||
self.assertRedirects(
|
self.assertEqual(
|
||||||
response=response,
|
response.redirect_chain[0][0],
|
||||||
expected_url="/static/img/mm/80.png",
|
"/static/img/mm/80.png",
|
||||||
msg_prefix="Why does this not redirect to the default img?",
|
"Doesn't redirect to static?",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# self.assertRedirects(
|
||||||
|
# response=response,
|
||||||
|
# expected_url="/static/img/mm/80.png",
|
||||||
|
# msg_prefix="Why does this not redirect to the default img?",
|
||||||
|
# )
|
||||||
# Eventually one should check if the data is the same
|
# Eventually one should check if the data is the same
|
||||||
|
|
||||||
def test_avatar_url_inexisting_mail_digest_wo_default(
|
def test_avatar_url_inexisting_mail_digest_wo_default(
|
||||||
@@ -1380,13 +1454,36 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
size=80,
|
size=80,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
digest = hashlib.md5("asdf@company.local".lower().encode("utf-8")).hexdigest()
|
||||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||||
response = self.client.get(url, follow=True)
|
response = self.client.get(url, follow=True)
|
||||||
self.assertRedirects(
|
self.assertEqual(
|
||||||
response=response,
|
response.redirect_chain[0][0],
|
||||||
expected_url="/static/img/nobody/80.png",
|
"/gravatarproxy/%s?s=80" % digest,
|
||||||
msg_prefix="Why does this not redirect to the default img?",
|
"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?",
|
||||||
|
)
|
||||||
|
|
||||||
|
# self.assertRedirects(
|
||||||
|
# response=response,
|
||||||
|
# expected_url="/static/img/nobody/80.png",
|
||||||
|
# msg_prefix="Why does this not redirect to the default img?",
|
||||||
|
# )
|
||||||
# Eventually one should check if the data is the same
|
# 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_inexisting_mail_digest_wo_default_gravatarproxy_disabled(
|
||||||
@@ -1403,17 +1500,26 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
)
|
)
|
||||||
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
|
||||||
response = self.client.get(url, follow=True)
|
response = self.client.get(url, follow=True)
|
||||||
self.assertRedirects(
|
self.assertEqual(
|
||||||
response=response,
|
response.redirect_chain[0][0],
|
||||||
expected_url="/static/img/nobody/80.png",
|
"/static/img/nobody/80.png",
|
||||||
msg_prefix="Why does this not redirect to the default img?",
|
"Doesn't redirect to static?",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# self.assertRedirects(
|
||||||
|
# response=response,
|
||||||
|
# expected_url="/static/img/nobody/80.png",
|
||||||
|
# msg_prefix="Why does this not redirect to the default img?",
|
||||||
|
# )
|
||||||
# Eventually one should check if the data is the same
|
# Eventually one should check if the data is the same
|
||||||
|
|
||||||
def test_avatar_url_default(self): # pylint: disable=invalid-name
|
def test_avatar_url_default(self): # pylint: disable=invalid-name
|
||||||
"""
|
"""
|
||||||
Test fetching avatar for not existing mail with default specified
|
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(
|
urlobj = urlsplit(
|
||||||
libravatar_url(
|
libravatar_url(
|
||||||
"xxx@xxx.xxx",
|
"xxx@xxx.xxx",
|
||||||
@@ -1422,7 +1528,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||||
response = self.client.get(url, follow=True)
|
response = self.client.get(url, follow=False)
|
||||||
self.assertRedirects(
|
self.assertRedirects(
|
||||||
response=response,
|
response=response,
|
||||||
expected_url="/static/img/nobody.png",
|
expected_url="/static/img/nobody.png",
|
||||||
@@ -1435,6 +1541,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
Test fetching avatar for not existing mail with default specified
|
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(
|
urlobj = urlsplit(
|
||||||
libravatar_url(
|
libravatar_url(
|
||||||
"xxx@xxx.xxx",
|
"xxx@xxx.xxx",
|
||||||
@@ -1889,4 +1998,61 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
Test if preferences page works
|
Test if preferences page works
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.login()
|
||||||
self.client.get(reverse("user_preference"))
|
self.client.get(reverse("user_preference"))
|
||||||
|
|
||||||
|
def test_delete_user(self):
|
||||||
|
"""
|
||||||
|
Test if deleting user profile works
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
self.client.get(reverse("delete"))
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("delete"),
|
||||||
|
data={"password": self.password},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200, "Deletion worked")
|
||||||
|
self.assertEqual(User.objects.count(), 0, "No user there any more")
|
||||||
|
|
||||||
|
def test_confirm_already_confirmed(self):
|
||||||
|
"""
|
||||||
|
Try to confirm a mail address that has been confirmed (by another user)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add mail address (stays unconfirmed)
|
||||||
|
self.test_add_email()
|
||||||
|
|
||||||
|
# Create a second user that will conflict
|
||||||
|
user2 = User.objects.create_user(
|
||||||
|
username=self.username + "1",
|
||||||
|
password=self.password,
|
||||||
|
first_name=self.first_name,
|
||||||
|
last_name=self.last_name,
|
||||||
|
)
|
||||||
|
ConfirmedEmail.objects.create(
|
||||||
|
email=self.email,
|
||||||
|
user=user2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Just to be sure
|
||||||
|
self.assertEqual(
|
||||||
|
self.user.unconfirmedemail_set.first().email,
|
||||||
|
user2.confirmedemail_set.first().email,
|
||||||
|
"Mail not the same?",
|
||||||
|
)
|
||||||
|
|
||||||
|
# This needs to be cought
|
||||||
|
try:
|
||||||
|
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(
|
||||||
|
str(list(response.context[0]["messages"])[0]),
|
||||||
|
"This mail address has been taken already and cannot be confirmed",
|
||||||
|
"This should return an error message!",
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
URLs for ivatar.ivataraccount
|
URLs for ivatar.ivataraccount
|
||||||
"""
|
"""
|
||||||
from django.urls import path
|
from django.urls import path, re_path
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from django.contrib.auth.views import LogoutView
|
from django.contrib.auth.views import LogoutView
|
||||||
from django.contrib.auth.views import (
|
from django.contrib.auth.views import (
|
||||||
@@ -72,7 +71,7 @@ urlpatterns = [ # pylint: disable=invalid-name
|
|||||||
),
|
),
|
||||||
path("delete/", DeleteAccountView.as_view(), name="delete"),
|
path("delete/", DeleteAccountView.as_view(), name="delete"),
|
||||||
path("profile/", ProfileView.as_view(), name="profile"),
|
path("profile/", ProfileView.as_view(), name="profile"),
|
||||||
url(
|
re_path(
|
||||||
"profile/(?P<profile_username>.+)",
|
"profile/(?P<profile_username>.+)",
|
||||||
ProfileView.as_view(),
|
ProfileView.as_view(),
|
||||||
name="profile_with_profile_username",
|
name="profile_with_profile_username",
|
||||||
@@ -81,73 +80,77 @@ urlpatterns = [ # pylint: disable=invalid-name
|
|||||||
path("add_openid/", AddOpenIDView.as_view(), name="add_openid"),
|
path("add_openid/", AddOpenIDView.as_view(), name="add_openid"),
|
||||||
path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"),
|
path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"),
|
||||||
path("password_set/", PasswordSetView.as_view(), name="password_set"),
|
path("password_set/", PasswordSetView.as_view(), name="password_set"),
|
||||||
url(
|
re_path(
|
||||||
r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
|
r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
|
||||||
RemoveUnconfirmedOpenIDView.as_view(),
|
RemoveUnconfirmedOpenIDView.as_view(),
|
||||||
name="remove_unconfirmed_openid",
|
name="remove_unconfirmed_openid",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"remove_confirmed_openid/(?P<openid_id>\d+)",
|
r"remove_confirmed_openid/(?P<openid_id>\d+)",
|
||||||
RemoveConfirmedOpenIDView.as_view(),
|
RemoveConfirmedOpenIDView.as_view(),
|
||||||
name="remove_confirmed_openid",
|
name="remove_confirmed_openid",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"openid_redirection/(?P<openid_id>\d+)",
|
r"openid_redirection/(?P<openid_id>\d+)",
|
||||||
RedirectOpenIDView.as_view(),
|
RedirectOpenIDView.as_view(),
|
||||||
name="openid_redirection",
|
name="openid_redirection",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"confirm_openid/(?P<openid_id>\w+)",
|
r"confirm_openid/(?P<openid_id>\w+)",
|
||||||
ConfirmOpenIDView.as_view(),
|
ConfirmOpenIDView.as_view(),
|
||||||
name="confirm_openid",
|
name="confirm_openid",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"confirm_email/(?P<verification_key>\w+)",
|
r"confirm_email/(?P<verification_key>\w+)",
|
||||||
ConfirmEmailView.as_view(),
|
ConfirmEmailView.as_view(),
|
||||||
name="confirm_email",
|
name="confirm_email",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"remove_unconfirmed_email/(?P<email_id>\d+)",
|
r"remove_unconfirmed_email/(?P<email_id>\d+)",
|
||||||
RemoveUnconfirmedEmailView.as_view(),
|
RemoveUnconfirmedEmailView.as_view(),
|
||||||
name="remove_unconfirmed_email",
|
name="remove_unconfirmed_email",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"remove_confirmed_email/(?P<email_id>\d+)",
|
r"remove_confirmed_email/(?P<email_id>\d+)",
|
||||||
RemoveConfirmedEmailView.as_view(),
|
RemoveConfirmedEmailView.as_view(),
|
||||||
name="remove_confirmed_email",
|
name="remove_confirmed_email",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"assign_photo_email/(?P<email_id>\d+)",
|
r"assign_photo_email/(?P<email_id>\d+)",
|
||||||
AssignPhotoEmailView.as_view(),
|
AssignPhotoEmailView.as_view(),
|
||||||
name="assign_photo_email",
|
name="assign_photo_email",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"assign_photo_openid/(?P<openid_id>\d+)",
|
r"assign_photo_openid/(?P<openid_id>\d+)",
|
||||||
AssignPhotoOpenIDView.as_view(),
|
AssignPhotoOpenIDView.as_view(),
|
||||||
name="assign_photo_openid",
|
name="assign_photo_openid",
|
||||||
),
|
),
|
||||||
url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
|
re_path(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
|
||||||
url(
|
re_path(
|
||||||
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
|
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
|
||||||
ImportPhotoView.as_view(),
|
ImportPhotoView.as_view(),
|
||||||
name="import_photo",
|
name="import_photo",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"import_photo/(?P<email_id>\d+)",
|
r"import_photo/(?P<email_id>\d+)",
|
||||||
ImportPhotoView.as_view(),
|
ImportPhotoView.as_view(),
|
||||||
name="import_photo",
|
name="import_photo",
|
||||||
),
|
),
|
||||||
url(r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"),
|
re_path(
|
||||||
url(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
|
r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"
|
||||||
url(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
|
),
|
||||||
url(r"pref/$", UserPreferenceView.as_view(), name="user_preference"),
|
re_path(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
|
||||||
url(r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"),
|
re_path(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
|
||||||
url(
|
re_path(r"pref/$", UserPreferenceView.as_view(), name="user_preference"),
|
||||||
|
re_path(
|
||||||
|
r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
r"upload_export/(?P<save>save)$",
|
r"upload_export/(?P<save>save)$",
|
||||||
UploadLibravatarExportView.as_view(),
|
UploadLibravatarExportView.as_view(),
|
||||||
name="upload_export",
|
name="upload_export",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"resend_confirmation_mail/(?P<email_id>\d+)",
|
r"resend_confirmation_mail/(?P<email_id>\d+)",
|
||||||
ResendConfirmationMailView.as_view(),
|
ResendConfirmationMailView.as_view(),
|
||||||
name="resend_confirmation_mail",
|
name="resend_confirmation_mail",
|
||||||
|
|||||||
@@ -207,6 +207,13 @@ class ConfirmEmailView(SuccessMessageMixin, TemplateView):
|
|||||||
messages.error(request, _("Verification key does not exist"))
|
messages.error(request, _("Verification key does not exist"))
|
||||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||||
|
|
||||||
|
if ConfirmedEmail.objects.filter(email=unconfirmed.email).count() > 0:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
_("This mail address has been taken already and cannot be confirmed"),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||||
|
|
||||||
# TODO: Check for a reasonable expiration time in unconfirmed email
|
# TODO: Check for a reasonable expiration time in unconfirmed email
|
||||||
|
|
||||||
(confirmed_id, external_photos) = ConfirmedEmail.objects.create_confirmed_email(
|
(confirmed_id, external_photos) = ConfirmedEmail.objects.create_confirmed_email(
|
||||||
|
|||||||
BIN
ivatar/static/img/broken.webp
Normal file
BIN
ivatar/static/img/broken.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -37,6 +37,7 @@ class Tester(TestCase):
|
|||||||
self.assertEqual(pil_format("jpeg"), "JPEG")
|
self.assertEqual(pil_format("jpeg"), "JPEG")
|
||||||
self.assertEqual(pil_format("png"), "PNG")
|
self.assertEqual(pil_format("png"), "PNG")
|
||||||
self.assertEqual(pil_format("gif"), "GIF")
|
self.assertEqual(pil_format("gif"), "GIF")
|
||||||
|
self.assertEqual(pil_format("webp"), "WEBP")
|
||||||
self.assertEqual(pil_format("abc"), None)
|
self.assertEqual(pil_format("abc"), None)
|
||||||
|
|
||||||
def test_userprefs_str(self):
|
def test_userprefs_str(self):
|
||||||
|
|||||||
@@ -50,11 +50,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
Test incorrect digest
|
Test incorrect digest
|
||||||
"""
|
"""
|
||||||
response = self.client.get("/avatar/%s" % "x" * 65, follow=True)
|
response = self.client.get("/avatar/%s" % "x" * 65, follow=True)
|
||||||
self.assertRedirects(
|
self.assertEqual(
|
||||||
response=response,
|
response.redirect_chain[0][0],
|
||||||
expected_url="/static/img/deadbeef.png",
|
"/static/img/deadbeef.png",
|
||||||
msg_prefix="Why does an invalid hash not redirect to deadbeef?",
|
"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?",
|
||||||
|
# )
|
||||||
|
|
||||||
def test_stats(self):
|
def test_stats(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
0
ivatar/tools/__init__.py
Normal file
0
ivatar/tools/__init__.py
Normal file
@@ -48,16 +48,109 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
password=self.password,
|
password=self.password,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_check(self):
|
def test_check_mail(self):
|
||||||
"""
|
"""
|
||||||
Test check page
|
Test check page
|
||||||
"""
|
"""
|
||||||
|
self.login()
|
||||||
response = self.client.get(reverse("tools_check"))
|
response = self.client.get(reverse("tools_check"))
|
||||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("tools_check"),
|
||||||
|
data={"mail": "test@test.com", "size": "85"},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
'value="test@test.com"',
|
||||||
|
1,
|
||||||
|
200,
|
||||||
|
"Value not set again!?",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"b642b4217b34b1e8d3bd915fc65c4452",
|
||||||
|
3,
|
||||||
|
200,
|
||||||
|
"Wrong md5 hash!?",
|
||||||
|
)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a",
|
||||||
|
3,
|
||||||
|
200,
|
||||||
|
"Wrong sha256 hash!?",
|
||||||
|
)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
'value="85"',
|
||||||
|
1,
|
||||||
|
200,
|
||||||
|
"Size should be set based on post params!?",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_openid(self):
|
||||||
|
"""
|
||||||
|
Test check page
|
||||||
|
"""
|
||||||
|
self.login()
|
||||||
|
response = self.client.get(reverse("tools_check"))
|
||||||
|
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("tools_check"),
|
||||||
|
data={"openid": "https://test.com", "size": "85"},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
'value="https://test.com"',
|
||||||
|
1,
|
||||||
|
200,
|
||||||
|
"Value not set again!?",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"396936bd0bf0603d6784b65d03e96dae90566c36b62661f28d4116c516524bcc",
|
||||||
|
3,
|
||||||
|
200,
|
||||||
|
"Wrong sha256 hash!?",
|
||||||
|
)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
'value="85"',
|
||||||
|
1,
|
||||||
|
200,
|
||||||
|
"Size should be set based on post params!?",
|
||||||
|
)
|
||||||
|
|
||||||
def test_check_domain(self):
|
def test_check_domain(self):
|
||||||
"""
|
"""
|
||||||
Test check domain page
|
Test check domain page
|
||||||
"""
|
"""
|
||||||
|
self.login()
|
||||||
response = self.client.get(reverse("tools_check_domain"))
|
response = self.client.get(reverse("tools_check_domain"))
|
||||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("tools_check_domain"),
|
||||||
|
data={"domain": "linux-kernel.at"},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"http://avatars.linux-kernel.at",
|
||||||
|
2,
|
||||||
|
200,
|
||||||
|
"Not responing with right URL!?",
|
||||||
|
)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"https://avatars.linux-kernel.at",
|
||||||
|
2,
|
||||||
|
200,
|
||||||
|
"Not responing with right URL!?",
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
ivatar/tools URL configuration
|
ivatar/tools URL configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.urls import path, re_path
|
||||||
from .views import CheckView, CheckDomainView
|
from .views import CheckView, CheckDomainView
|
||||||
|
|
||||||
urlpatterns = [ # pylint: disable=invalid-name
|
urlpatterns = [ # pylint: disable=invalid-name
|
||||||
url("check/", CheckView.as_view(), name="tools_check"),
|
path("check/", CheckView.as_view(), name="tools_check"),
|
||||||
url("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"),
|
path("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"),
|
||||||
url("check_domain$", CheckDomainView.as_view(), name="tools_check_domain"),
|
re_path("check_domain$", CheckDomainView.as_view(), name="tools_check_domain"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
ivatar URL configuration
|
ivatar URL configuration
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include, re_path
|
||||||
from django.conf.urls import url
|
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.views.generic import TemplateView, RedirectView
|
from django.views.generic import TemplateView, RedirectView
|
||||||
from ivatar import settings
|
from ivatar import settings
|
||||||
@@ -13,65 +12,72 @@ from .views import AvatarImageView, GravatarProxyView, StatsView
|
|||||||
urlpatterns = [ # pylint: disable=invalid-name
|
urlpatterns = [ # pylint: disable=invalid-name
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("i18n/", include("django.conf.urls.i18n")),
|
path("i18n/", include("django.conf.urls.i18n")),
|
||||||
url("openid/", include("django_openid_auth.urls")),
|
path("openid/", include("django_openid_auth.urls")),
|
||||||
url("tools/", include("ivatar.tools.urls")),
|
path("tools/", include("ivatar.tools.urls")),
|
||||||
url(r"avatar/(?P<digest>\w{64})", AvatarImageView.as_view(), name="avatar_view"),
|
re_path(
|
||||||
url(r"avatar/(?P<digest>\w{32})", AvatarImageView.as_view(), name="avatar_view"),
|
r"avatar/(?P<digest>\w{64})", AvatarImageView.as_view(), name="avatar_view"
|
||||||
url(r"avatar/$", AvatarImageView.as_view(), name="avatar_view"),
|
),
|
||||||
url(
|
re_path(
|
||||||
|
r"avatar/(?P<digest>\w{32})", AvatarImageView.as_view(), name="avatar_view"
|
||||||
|
),
|
||||||
|
re_path(r"avatar/$", AvatarImageView.as_view(), name="avatar_view"),
|
||||||
|
re_path(
|
||||||
r"avatar/(?P<digest>\w*)",
|
r"avatar/(?P<digest>\w*)",
|
||||||
RedirectView.as_view(url="/static/img/deadbeef.png"),
|
RedirectView.as_view(url="/static/img/deadbeef.png"),
|
||||||
name="invalid_hash",
|
name="invalid_hash",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"gravatarproxy/(?P<digest>\w*)",
|
r"gravatarproxy/(?P<digest>\w*)",
|
||||||
GravatarProxyView.as_view(),
|
GravatarProxyView.as_view(),
|
||||||
name="gravatarproxy",
|
name="gravatarproxy",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
"description/",
|
"description/",
|
||||||
TemplateView.as_view(template_name="description.html"),
|
TemplateView.as_view(template_name="description.html"),
|
||||||
name="description",
|
name="description",
|
||||||
),
|
),
|
||||||
# The following two are TODO TODO TODO TODO TODO
|
# The following two are TODO TODO TODO TODO TODO
|
||||||
url(
|
path(
|
||||||
"run_your_own/",
|
"run_your_own/",
|
||||||
TemplateView.as_view(template_name="run_your_own.html"),
|
TemplateView.as_view(template_name="run_your_own.html"),
|
||||||
name="run_your_own",
|
name="run_your_own",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
"features/",
|
"features/",
|
||||||
TemplateView.as_view(template_name="features.html"),
|
TemplateView.as_view(template_name="features.html"),
|
||||||
name="features",
|
name="features",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
"security/",
|
"security/",
|
||||||
TemplateView.as_view(template_name="security.html"),
|
TemplateView.as_view(template_name="security.html"),
|
||||||
name="security",
|
name="security",
|
||||||
),
|
),
|
||||||
url("privacy/", TemplateView.as_view(template_name="privacy.html"), name="privacy"),
|
path(
|
||||||
url("contact/", TemplateView.as_view(template_name="contact.html"), name="contact"),
|
"privacy/", TemplateView.as_view(template_name="privacy.html"), name="privacy"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"contact/", TemplateView.as_view(template_name="contact.html"), name="contact"
|
||||||
|
),
|
||||||
path("talk_to_us/", RedirectView.as_view(url="/contact"), name="talk_to_us"),
|
path("talk_to_us/", RedirectView.as_view(url="/contact"), name="talk_to_us"),
|
||||||
url("stats/", StatsView.as_view(), name="stats"),
|
path("stats/", StatsView.as_view(), name="stats"),
|
||||||
]
|
]
|
||||||
|
|
||||||
MAINTENANCE = False
|
MAINTENANCE = False
|
||||||
try:
|
try:
|
||||||
if settings.MAINTENANCE:
|
if settings.MAINTENANCE:
|
||||||
MAINTENANCE = True
|
MAINTENANCE = True
|
||||||
except: # pylint: disable=bare-except
|
except Exception: # pylint: disable=bare-except
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if MAINTENANCE:
|
if MAINTENANCE:
|
||||||
urlpatterns.append(
|
urlpatterns.append(
|
||||||
url("", TemplateView.as_view(template_name="maintenance.html"), name="home")
|
path("", TemplateView.as_view(template_name="maintenance.html"), name="home")
|
||||||
)
|
)
|
||||||
urlpatterns.insert(3, url("accounts/", RedirectView.as_view(url="/")))
|
urlpatterns.insert(3, path("accounts/", RedirectView.as_view(url="/")))
|
||||||
else:
|
else:
|
||||||
urlpatterns.append(
|
urlpatterns.append(
|
||||||
url("", TemplateView.as_view(template_name="home.html"), name="home")
|
path("", TemplateView.as_view(template_name="home.html"), name="home")
|
||||||
)
|
)
|
||||||
urlpatterns.insert(3, url("accounts/", include("ivatar.ivataraccount.urls")))
|
urlpatterns.insert(3, path("accounts/", include("ivatar.ivataraccount.urls")))
|
||||||
|
|
||||||
|
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ Simple module providing reusable random_string function
|
|||||||
"""
|
"""
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from PIL import Image, ImageDraw
|
from io import BytesIO
|
||||||
|
from PIL import Image, ImageDraw, ImageSequence
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
@@ -158,3 +159,25 @@ def is_trusted_url(url, url_filters):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def resize_animated_gif(input_pil: Image, size: list) -> BytesIO:
|
||||||
|
def _thumbnail_frames(image):
|
||||||
|
for frame in ImageSequence.Iterator(image):
|
||||||
|
new_frame = frame.copy()
|
||||||
|
new_frame.thumbnail(size)
|
||||||
|
yield new_frame
|
||||||
|
|
||||||
|
frames = list(_thumbnail_frames(input_pil))
|
||||||
|
output = BytesIO()
|
||||||
|
output_image = frames[0]
|
||||||
|
output_image.save(
|
||||||
|
output,
|
||||||
|
format="gif",
|
||||||
|
save_all=True,
|
||||||
|
optimize=False,
|
||||||
|
append_images=frames[1:],
|
||||||
|
disposal=input_pil.disposal_method,
|
||||||
|
**input_pil.info,
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
|||||||
from .ivataraccount.models import UnconfirmedEmail, UnconfirmedOpenId
|
from .ivataraccount.models import UnconfirmedEmail, UnconfirmedOpenId
|
||||||
from .ivataraccount.models import Photo
|
from .ivataraccount.models import Photo
|
||||||
from .ivataraccount.models import pil_format, file_format
|
from .ivataraccount.models import pil_format, file_format
|
||||||
from .utils import is_trusted_url, mm_ng
|
from .utils import is_trusted_url, mm_ng, resize_animated_gif
|
||||||
|
|
||||||
URL_TIMEOUT = 5 # in seconds
|
URL_TIMEOUT = 5 # in seconds
|
||||||
|
|
||||||
@@ -151,7 +151,8 @@ class AvatarImageView(TemplateView):
|
|||||||
|
|
||||||
if not trusted_url:
|
if not trusted_url:
|
||||||
print(
|
print(
|
||||||
"Default URL is not in trusted URLs: '%s' ; Kicking it!" % default
|
"Default URL is not in trusted URLs: '%s' ; Kicking it!"
|
||||||
|
% default
|
||||||
)
|
)
|
||||||
default = None
|
default = None
|
||||||
|
|
||||||
@@ -318,14 +319,23 @@ class AvatarImageView(TemplateView):
|
|||||||
|
|
||||||
imgformat = obj.photo.format
|
imgformat = obj.photo.format
|
||||||
photodata = Image.open(BytesIO(obj.photo.data))
|
photodata = Image.open(BytesIO(obj.photo.data))
|
||||||
# If the image is smaller than what was requested, we need
|
|
||||||
# to use the function resize
|
|
||||||
if photodata.size[0] < size or photodata.size[1] < size:
|
|
||||||
photodata = photodata.resize((size, size), Image.ANTIALIAS)
|
|
||||||
else:
|
|
||||||
photodata.thumbnail((size, size), Image.ANTIALIAS)
|
|
||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
photodata.save(data, pil_format(imgformat), quality=JPEG_QUALITY)
|
|
||||||
|
# Animated GIFs need additional handling
|
||||||
|
if imgformat == "gif" and photodata.is_animated:
|
||||||
|
# Debug only
|
||||||
|
# print("Object is animated and has %i frames" % photodata.n_frames)
|
||||||
|
data = resize_animated_gif(photodata, (size, size))
|
||||||
|
else:
|
||||||
|
# If the image is smaller than what was requested, we need
|
||||||
|
# to use the function resize
|
||||||
|
if photodata.size[0] < size or photodata.size[1] < size:
|
||||||
|
photodata = photodata.resize((size, size), Image.ANTIALIAS)
|
||||||
|
else:
|
||||||
|
photodata.thumbnail((size, size), Image.ANTIALIAS)
|
||||||
|
photodata.save(data, pil_format(imgformat), quality=JPEG_QUALITY)
|
||||||
|
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
obj.photo.access_count += 1
|
obj.photo.access_count += 1
|
||||||
obj.photo.save()
|
obj.photo.save()
|
||||||
|
|||||||
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ['setuptools>=40.8.0', 'wheel']
|
||||||
|
build-backend = 'setuptools.build_meta:__legacy__'
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
autopep8
|
autopep8
|
||||||
bcrypt
|
bcrypt
|
||||||
defusedxml
|
defusedxml
|
||||||
Django < 4.0
|
Django
|
||||||
django-anymail[mailgun]
|
django-anymail[mailgun]
|
||||||
django-auth-ldap
|
django-auth-ldap
|
||||||
django-bootstrap4
|
django-bootstrap4
|
||||||
@@ -27,10 +27,10 @@ py3dns
|
|||||||
pydocstyle
|
pydocstyle
|
||||||
pyLibravatar
|
pyLibravatar
|
||||||
pylint
|
pylint
|
||||||
|
pymemcache
|
||||||
PyMySQL
|
PyMySQL
|
||||||
python-coveralls
|
python-coveralls
|
||||||
python-language-server
|
python-language-server
|
||||||
python-memcached
|
|
||||||
python3-openid
|
python3-openid
|
||||||
pytz
|
pytz
|
||||||
rope
|
rope
|
||||||
|
|||||||
33
setup.cfg
Normal file
33
setup.cfg
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[metadata]
|
||||||
|
name = libravatar
|
||||||
|
version = 1.7.0
|
||||||
|
description = A Django application implementing libravatar.org
|
||||||
|
long_description = file: README.md
|
||||||
|
url = https://libravatar.org
|
||||||
|
author = Oliver Falk
|
||||||
|
author_email = oliver@linux-kernel.at
|
||||||
|
license = GPLv3
|
||||||
|
classifiers =
|
||||||
|
Environment :: Web Environment
|
||||||
|
Framework :: Django
|
||||||
|
Framework :: Django :: 3.2
|
||||||
|
Framework :: Django :: 4.0
|
||||||
|
Framework :: Django :: 4.1
|
||||||
|
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
||||||
|
Operating System :: OS Independent
|
||||||
|
Programming Language :: Python
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3 :: Only
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
|
Programming Language :: Python :: 3.10
|
||||||
|
Programming Language :: Python :: 3.11
|
||||||
|
Topic :: Internet :: WWW/HTTP
|
||||||
|
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
|
||||||
|
[options]
|
||||||
|
include_package_data = true
|
||||||
|
packages = find:
|
||||||
|
python_requires = >=3.8
|
||||||
|
install_requires =
|
||||||
|
Django >= 3.2
|
||||||
6
setup.py
Normal file
6
setup.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
packages=find_packages(),
|
||||||
|
)
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<button type="submit" class="button">{% trans 'Login' %}</button>
|
<button type="submit" class="button">{% trans 'Login' %}</button>
|
||||||
<input type="hidden" name="next" value="{{ request.build_absolute_uri }}{% url 'profile' %}" />
|
<input type="hidden" name="next" value="{% url 'profile' %}" />
|
||||||
|
|
||||||
<button type="reset" class="button" onclick="window.history.back();">{% trans 'Cancel' %}</button>
|
<button type="reset" class="button" onclick="window.history.back();">{% trans 'Cancel' %}</button>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user