Compare commits
350 Commits
apikeys
...
ai-playgro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56cceb5724 | ||
|
|
5a005d6845 | ||
|
|
493f9405dd | ||
|
|
a641572e4b | ||
|
|
30f94610bd | ||
|
|
55b7466eb5 | ||
|
|
8a70ea1131 | ||
|
|
b69f08694a | ||
|
|
ed27493abc | ||
|
|
f7d72c18fb | ||
|
|
7a1e38ab50 | ||
|
|
9d390a5b19 | ||
|
|
52576bbf18 | ||
|
|
d720fcfa50 | ||
|
|
c6e1583e7e | ||
|
|
5114b4d5d0 | ||
|
|
f81d6bb84c | ||
|
|
16dd861953 | ||
|
|
4316c2bcc6 | ||
|
|
b711594c1f | ||
|
|
0d16b1f518 | ||
|
|
4abbfeaa36 | ||
|
|
13f13f7443 | ||
|
|
f5c8cda222 | ||
|
|
59c8db6aec | ||
|
|
ebfcd67512 | ||
|
|
0df2af4f4b | ||
|
|
797ec91320 | ||
|
|
85c06cd42c | ||
|
|
aa742ea181 | ||
|
|
deeaab7e23 | ||
|
|
0832ac9fe0 | ||
|
|
b3f580e51b | ||
|
|
12bc7e5af4 | ||
|
|
e44a84e9ae | ||
|
|
a1d13ba3ce | ||
|
|
aa3e1e48dc | ||
|
|
919ed4d485 | ||
|
|
cb7328fe23 | ||
|
|
1892e9585e | ||
|
|
7d0d2f931b | ||
|
|
5dcd69f332 | ||
|
|
1560a5d1de | ||
|
|
1f17526fac | ||
|
|
c36d0fb808 | ||
|
|
771a386bf4 | ||
|
|
c109b70901 | ||
|
|
b8996e7b00 | ||
|
|
184f3eb7f7 | ||
|
|
27e3751776 | ||
|
|
e3b0782082 | ||
|
|
5a0aa4838c | ||
|
|
bbacd413ca | ||
|
|
99b4fdcbcd | ||
|
|
c948f515e0 | ||
|
|
8dd29e872d | ||
|
|
4f5f498da8 | ||
|
|
6c4d928a1f | ||
|
|
498ab7aa69 | ||
|
|
e90604a8d3 | ||
|
|
4a892b0c4c | ||
|
|
04a39f7693 | ||
|
|
715725f7c9 | ||
|
|
b86e211adc | ||
|
|
6b24a7732b | ||
|
|
488206f15b | ||
|
|
b12b5df17a | ||
|
|
60ee28270a | ||
|
|
fe1113df06 | ||
|
|
ad79c414da | ||
|
|
3aaaac51f0 | ||
|
|
56780cba69 | ||
|
|
dcbd2c5df5 | ||
|
|
0a0a96ab6e | ||
|
|
22f9dac816 | ||
|
|
433bf4d3e2 | ||
|
|
6e0bc487cd | ||
|
|
6cd5b64553 | ||
|
|
154e965fe3 | ||
|
|
072783bfd5 | ||
|
|
259d370d9a | ||
|
|
e1705cef36 | ||
|
|
94b0fc2068 | ||
|
|
ffab741445 | ||
|
|
7559ddad6e | ||
|
|
dc30267ff4 | ||
|
|
3fad7497a1 | ||
|
|
4a615c933b | ||
|
|
6c25f6ea12 | ||
|
|
cea4d5eb24 | ||
|
|
e878224ba6 | ||
|
|
483dcf8cf7 | ||
|
|
07c7f42f01 | ||
|
|
b0d09e3ad4 | ||
|
|
a2972ac61f | ||
|
|
1fa5dddce5 | ||
|
|
2cb868b129 | ||
|
|
0e295401df | ||
|
|
ecc87033cc | ||
|
|
2df0cdf892 | ||
|
|
cf5c058bfb | ||
|
|
c2145e144f | ||
|
|
549289a36a | ||
|
|
8b0fc31f6a | ||
|
|
4eedb3e628 | ||
|
|
049271acdd | ||
|
|
1a859af31f | ||
|
|
2fe8af6fab | ||
|
|
3a61d519ba | ||
|
|
8dff034f9e | ||
|
|
b58c35e98b | ||
|
|
4f239119d6 | ||
|
|
b7efc60cc0 | ||
|
|
9faf308264 | ||
|
|
b3cfccb9c0 | ||
|
|
5a1dfbc459 | ||
|
|
df0400375d | ||
|
|
50569afc25 | ||
|
|
4385fcc034 | ||
|
|
927083eb58 | ||
|
|
fa4ce5e079 | ||
|
|
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 | ||
|
|
f359532c30 | ||
|
|
93e8b3f07f | ||
|
|
66bf945770 | ||
|
|
a76d5b9225 | ||
|
|
1aa40fecab | ||
|
|
f4fe49b3b4 | ||
|
|
e1923f92c2 | ||
|
|
49780739f8 | ||
|
|
71d69dde53 | ||
|
|
f4af809e6d | ||
|
|
9221da5805 | ||
|
|
b71ac2d7e3 | ||
|
|
4ac3ca5dc2 | ||
|
|
bc6b7313ec | ||
|
|
0b5271424c | ||
|
|
899e8db661 | ||
|
|
cf65ea2c6a | ||
|
|
ce18bb58bd | ||
|
|
27e11f8051 | ||
|
|
2a8fe01027 | ||
|
|
99ff61cf34 | ||
|
|
8fa4a9c88b | ||
|
|
2695f932ee | ||
|
|
58957c7fc2 | ||
|
|
2578e804b6 | ||
|
|
515a4a3b0b | ||
|
|
a492995836 | ||
|
|
67ac0ad973 | ||
|
|
ad39324650 | ||
|
|
125c797c36 | ||
|
|
ef02feed3b | ||
|
|
714ae58509 | ||
|
|
f651a5a6d8 | ||
|
|
1a10861d2f | ||
|
|
fe22748821 | ||
|
|
42825ef7ae | ||
|
|
462b318fcb | ||
|
|
c0a2a6ef67 | ||
|
|
01c1bd3a8d | ||
|
|
b64f939344 | ||
|
|
ddaf6a6d8a | ||
|
|
b49bf9d918 | ||
|
|
c6016b4984 | ||
|
|
fe5f91bd66 | ||
|
|
7f10c239f9 | ||
|
|
fd218abe0f | ||
|
|
70a2771f34 | ||
|
|
00aa1a45cb | ||
|
|
16c2245c99 | ||
|
|
fb65fd76d9 | ||
|
|
4d85fed519 | ||
|
|
23985a13a8 | ||
|
|
40a5b06e25 | ||
|
|
978da813e6 | ||
|
|
a9cff27ef7 | ||
|
|
6c8763752c | ||
|
|
e50615fcdb | ||
|
|
99b2379ea5 | ||
|
|
f10b13862c | ||
|
|
fa4f5da45d | ||
|
|
337ced827a | ||
|
|
502bc2cc03 | ||
|
|
d0f65d435a | ||
|
|
c54c57231b | ||
|
|
34291e5f87 | ||
|
|
a085e8b22f | ||
|
|
cbe0af27c0 | ||
|
|
c93d0eb86c | ||
|
|
6e084ea080 | ||
|
|
e115e2c3ab | ||
|
|
0c3686beef | ||
|
|
05d0e05545 | ||
|
|
0ccd3fa7c1 | ||
|
|
8989a83deb | ||
|
|
a1c1da81e1 | ||
|
|
521f6db235 | ||
|
|
b93569a279 | ||
|
|
ab56bf720a | ||
|
|
56f90412bf | ||
|
|
e260e6ff2f | ||
|
|
ff9bfdefb5 | ||
|
|
09a8c60ad0 | ||
|
|
547b570f5d | ||
|
|
ee13fb545a | ||
|
|
e2a24296c4 | ||
|
|
de6ba7b1c4 | ||
|
|
71a24737b4 | ||
|
|
e260c7410b | ||
|
|
030ea6fd33 | ||
|
|
c3422ccc78 | ||
|
|
6aa870963d | ||
|
|
355af2351d | ||
|
|
7ab63887a7 | ||
|
|
26768aacb2 | ||
|
|
a3f7575726 | ||
|
|
44f3c2bcba | ||
|
|
b7d3c7655a | ||
|
|
f37fc4de09 | ||
|
|
7b01e2eef2 | ||
|
|
f8a5fc55e0 | ||
|
|
8ba3a55756 | ||
|
|
d663bceead | ||
|
|
7c7de1e711 | ||
|
|
f288a97bad | ||
|
|
3d3aa4f48e | ||
|
|
328914698c | ||
|
|
5cec4cbdb1 | ||
|
|
9fdbf81f71 | ||
|
|
1f04bf0f18 | ||
|
|
205ba0c934 | ||
|
|
7ca34aea1b | ||
|
|
32ed22704c | ||
|
|
38d2f0dd92 | ||
|
|
52e5673834 | ||
|
|
12d69576af | ||
|
|
7ef4fecb4f | ||
|
|
539e0b6ce0 | ||
|
|
d7b5b0de27 | ||
|
|
7cfd283c85 | ||
|
|
41ee907292 | ||
|
|
b2f06256db | ||
|
|
a3489188c3 | ||
|
|
0c2f039ff4 | ||
|
|
48d1b7d47d | ||
|
|
ce01f8dec1 | ||
|
|
d2701deb0f | ||
|
|
9defe7617a | ||
|
|
dcb3627179 | ||
|
|
fbb099d4f6 | ||
|
|
d9f4a4f634 | ||
|
|
e9f1bfc7f4 | ||
|
|
7a53a18222 | ||
|
|
aa26ac802c | ||
|
|
552da91044 | ||
|
|
73fca0b953 | ||
|
|
ca9f83984a | ||
|
|
0f60e20d99 | ||
|
|
5733448830 | ||
|
|
b52244b167 | ||
|
|
a346bc6285 | ||
|
|
b3280376f2 | ||
|
|
c5f493178c | ||
|
|
64dc146fae | ||
|
|
334da91881 | ||
|
|
5c66a0a3c1 | ||
|
|
7d9f2ead9a | ||
|
|
519dbe9a6b | ||
|
|
ab0ea07d7b | ||
|
|
8450f42a20 | ||
|
|
7adaa7c48b | ||
|
|
9a40beda13 | ||
|
|
f924a8e9c0 | ||
|
|
57f4cc46aa | ||
|
|
413e714000 | ||
|
|
ba4c1a8432 | ||
|
|
635951ff4d | ||
|
|
33169da930 | ||
|
|
92035d7e15 | ||
|
|
b4b81499ca | ||
|
|
8f7f6983cd | ||
|
|
ffbd0f9148 | ||
|
|
64edc0aa61 | ||
|
|
3bee6ed05f | ||
|
|
652f11289b | ||
|
|
03e495de24 | ||
|
|
8fb601ece1 | ||
|
|
c83453077e | ||
|
|
49219f1eec | ||
|
|
625cd290c0 | ||
|
|
e74365e788 | ||
|
|
02eaad4a5b | ||
|
|
31bc906fda | ||
|
|
feab5f6156 | ||
|
|
0262ad850f | ||
|
|
f13927f859 | ||
|
|
30991b4b57 | ||
|
|
91d1193e23 | ||
|
|
c5eeda5748 | ||
|
|
2535cefa6c | ||
|
|
ea900c0109 | ||
|
|
a58b193b8c | ||
|
|
060a664d26 | ||
|
|
6bc0390b48 | ||
|
|
b74ab99ef7 | ||
|
|
b463f9f95a | ||
|
|
476bddb21c | ||
|
|
07ba1c5a05 |
1
.buildpacks
Normal file
@@ -0,0 +1 @@
|
||||
https://github.com/heroku/heroku-buildpack-python
|
||||
@@ -5,6 +5,15 @@ omit =
|
||||
node_modules/*
|
||||
.virtualenv/*
|
||||
import_libravatar.py
|
||||
requirements.txt
|
||||
static/admin/*
|
||||
**/static/humans.txt
|
||||
**/static/img/robots.txt
|
||||
ivatar/ivataraccount/read_libravatar_export.py
|
||||
templates/maintenance.html
|
||||
encryption_test.py
|
||||
libravatarproxy.py
|
||||
|
||||
|
||||
[html]
|
||||
extra_css = coverage_extra_style.css
|
||||
|
||||
11
.env
Normal file
@@ -0,0 +1,11 @@
|
||||
if [ ! -d .virtualenv ]; then
|
||||
if [ ! "$(which virtualenv)" == "" ]; then
|
||||
if [ -f .env ]; then
|
||||
virtualenv -p python3 .virtualenv
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -f .virtualenv/bin/activate ]; then
|
||||
source .virtualenv/bin/activate
|
||||
AUTOENV_ENABLE_LEAVE=True
|
||||
fi
|
||||
1
.env.leave
Normal file
@@ -0,0 +1 @@
|
||||
deactivate
|
||||
6
.flake8
Normal file
@@ -0,0 +1,6 @@
|
||||
[flake8]
|
||||
ignore = E501, W503, E402, C901, E231, E702
|
||||
max-line-length = 79
|
||||
max-complexity = 18
|
||||
select = B,C,E,F,W,T4,B9
|
||||
exclude = .git,__pycache__,.virtualenv
|
||||
11
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
__pycache__
|
||||
/db.sqlite3
|
||||
/static/
|
||||
/static/*
|
||||
**.*.swp
|
||||
.coverage
|
||||
htmlcov/
|
||||
@@ -13,3 +13,12 @@ db.sqlite3.SAVE
|
||||
node_modules/
|
||||
config_local.py
|
||||
locale/*/LC_MESSAGES/django.mo
|
||||
.DS_Store
|
||||
.idea/
|
||||
contacts.csv
|
||||
falko_gravatar.jpg
|
||||
*.egg-info
|
||||
dump_all*.sql
|
||||
dist/
|
||||
.env.local
|
||||
tmp/
|
||||
|
||||
101
.gitlab-ci.yml
@@ -1,8 +1,32 @@
|
||||
image: docker.io/ofalk/fedora31-python3
|
||||
image:
|
||||
name: quay.io/rhn_support_ofalk/fedora36-python3
|
||||
entrypoint:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
|
||||
before_script:
|
||||
# Cache pip deps to speed up builds
|
||||
cache:
|
||||
paths:
|
||||
- .pipcache
|
||||
variables:
|
||||
PIP_CACHE_DIR: .pipcache
|
||||
|
||||
test_and_coverage:
|
||||
stage: build
|
||||
coverage: "/^TOTAL.*\\s+(\\d+\\%)$/"
|
||||
services:
|
||||
- postgres:latest
|
||||
variables:
|
||||
POSTGRES_DB: django_db
|
||||
POSTGRES_USER: django_user
|
||||
POSTGRES_PASSWORD: django_password
|
||||
POSTGRES_HOST: postgres
|
||||
DATABASE_URL: "postgres://django_user:django_password@postgres/django_db"
|
||||
PYTHONUNBUFFERED: 1
|
||||
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
|
||||
@@ -10,34 +34,43 @@ before_script:
|
||||
- pip install pycco
|
||||
- pip install django_coverage_plugin
|
||||
|
||||
test_and_coverage:
|
||||
stage: test
|
||||
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
|
||||
- echo "from config import CACHES" >> config_local.py
|
||||
- echo "CACHES['default'] = CACHES['filesystem']" >> config_local.py
|
||||
- python manage.py sqldsn
|
||||
- python manage.py collectstatic --noinput
|
||||
- coverage run --source . manage.py test -v3
|
||||
- coverage run --source . manage.py test -v3 --noinput
|
||||
- coverage report --fail-under=70
|
||||
- coverage html
|
||||
artifacts:
|
||||
paths:
|
||||
- htmlcov/
|
||||
|
||||
pycco:
|
||||
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:
|
||||
- /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
|
||||
- "/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
|
||||
artifacts:
|
||||
paths:
|
||||
- pycco/
|
||||
expire_in: 14 days
|
||||
|
||||
pages:
|
||||
before_script:
|
||||
- /bin/true
|
||||
- /bin/true
|
||||
stage: deploy
|
||||
dependencies:
|
||||
- test_and_coverage
|
||||
@@ -51,3 +84,47 @@ pages:
|
||||
expire_in: 14 days
|
||||
only:
|
||||
- master
|
||||
#build-image:
|
||||
# image: docker
|
||||
# only:
|
||||
# - master
|
||||
# - devel
|
||||
# services:
|
||||
# - docker:dind
|
||||
# before_script:
|
||||
# - docker info
|
||||
# - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
||||
# script:
|
||||
# - ls -lah
|
||||
# - |
|
||||
# if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
|
||||
# tag=""
|
||||
# echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
|
||||
# else
|
||||
# tag=":$CI_COMMIT_REF_SLUG"
|
||||
# echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
|
||||
# fi
|
||||
# - docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
|
||||
# - docker push "$CI_REGISTRY_IMAGE${tag}"
|
||||
semgrep:
|
||||
stage: test
|
||||
allow_failure: true
|
||||
image: registry.gitlab.com/gitlab-org/security-products/analyzers/semgrep:latest
|
||||
only:
|
||||
- master
|
||||
- devel
|
||||
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
|
||||
|
||||
5
.gitlab/issue_templates/example-template.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Dscribe your issue
|
||||
|
||||
# What have you tried to far?
|
||||
|
||||
# Links / Pointer / Resources
|
||||
75
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
fail_fast: true
|
||||
repos:
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-useless-excludes
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.0.0-alpha.4
|
||||
hooks:
|
||||
- id: prettier
|
||||
files: \.(css|js|md|markdown|json)
|
||||
- repo: https://github.com/python/black
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-ast
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: check-symlinks
|
||||
- id: check-vcs-permalinks
|
||||
- id: check-xml
|
||||
- id: check-yaml
|
||||
args:
|
||||
- --unsafe
|
||||
- id: end-of-file-fixer
|
||||
- id: fix-encoding-pragma
|
||||
- id: forbid-new-submodules
|
||||
- id: no-commit-to-branch
|
||||
args:
|
||||
- --branch
|
||||
- gh-pages
|
||||
- id: requirements-txt-fixer
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: shfmt
|
||||
name: shfmt
|
||||
minimum_pre_commit_version: 2.4.0
|
||||
language: golang
|
||||
additional_dependencies:
|
||||
- mvdan.cc/sh/v3/cmd/shfmt@v3.1.1
|
||||
entry: shfmt
|
||||
args:
|
||||
- -w
|
||||
- -i
|
||||
- '4'
|
||||
types:
|
||||
- shell
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v1.12.1
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
# 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:
|
||||
- id: gitlabci-lint
|
||||
args: ["https://git.linux-kernel.at"]
|
||||
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM git.linux-kernel.at:5050/oliver/fedora40-python3:latest
|
||||
LABEL maintainer Oliver Falk <oliver@linux-kernel.at>
|
||||
EXPOSE 8081
|
||||
|
||||
ADD . /opt/ivatar-devel
|
||||
|
||||
WORKDIR /opt/ivatar-devel
|
||||
|
||||
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 "EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'" >> /opt/ivatar-devel/config_local.py
|
||||
RUN source .virtualenv/bin/activate \
|
||||
&& python3 manage.py migrate \
|
||||
&& 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
|
||||
37
INSTALL.md
@@ -1,6 +1,6 @@
|
||||
# Installation
|
||||
|
||||
## Prequisits
|
||||
## Prerequisites
|
||||
|
||||
Python 3.x + virtualenv
|
||||
|
||||
@@ -10,20 +10,28 @@ Python 3.x + virtualenv
|
||||
yum install python34-virtualenv.noarch
|
||||
```
|
||||
|
||||
### Debian 11
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install git python3-virtualenv libmariadb-dev libldap2-dev libsasl2-dev
|
||||
```
|
||||
|
||||
## Checkout
|
||||
|
||||
~~~~bash
|
||||
```bash
|
||||
git clone https://git.linux-kernel.at/oliver/ivatar.git
|
||||
cd ivatar
|
||||
~~~~
|
||||
```
|
||||
|
||||
## Virtual environment
|
||||
|
||||
~~~~bash
|
||||
```bash
|
||||
virtualenv -p python3 .virtualenv
|
||||
source .virtualenv/bin/activate
|
||||
pip install pillow
|
||||
pip install -r requirements.txt
|
||||
~~~~
|
||||
```
|
||||
|
||||
## (SQL) Migrations
|
||||
|
||||
@@ -50,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...)
|
||||
@@ -74,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.
|
||||
|
||||
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
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__
|
||||
16
README.md
@@ -1,20 +1,16 @@
|
||||
ivatar / libravatar
|
||||
===================
|
||||
# ivatar / libravatar
|
||||
|
||||
Pipeline and coverage status
|
||||
============================
|
||||
# Pipeline and coverage status
|
||||
|
||||
[](https://git.linux-kernel.at/oliver/ivatar/commits/master)
|
||||
[](http://git.linux-kernel.at/oliver/ivatar/commits/master)
|
||||
|
||||
Reports / code documentation
|
||||
============================
|
||||
# Reports / code documentation
|
||||
|
||||
- [Coverage HTML report](http://oliver.git.linux-kernel.at/ivatar)
|
||||
- [Code documentation (autogenerated, pycco)](http://oliver.git.linux-kernel.at/ivatar/pycco/)
|
||||
- [Coverage HTML report](http://oliver.git.linux-kernel.at/ivatar)
|
||||
- [Code documentation (autogenerated, pycco)](http://oliver.git.linux-kernel.at/ivatar/pycco/)
|
||||
|
||||
Authors and contributors
|
||||
========================
|
||||
# Authors and contributors
|
||||
|
||||
Lead developer/Owner: Oliver Falk (aka ofalk or falko) - https://git.linux-kernel.at/oliver
|
||||
|
||||
|
||||
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
@@ -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
@@ -0,0 +1,7 @@
|
||||
DATABASES['default'] = {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'libravatar',
|
||||
'USER': 'libravatar',
|
||||
'PASSWORD': 'libravatar',
|
||||
'HOST': 'localhost',
|
||||
}
|
||||
343
config.py
@@ -1,11 +1,12 @@
|
||||
''' yes
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Configuration overrides for settings.py
|
||||
'''
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.messages import constants as message_constants
|
||||
from ivatar.settings import BASE_DIR
|
||||
|
||||
@@ -14,50 +15,66 @@ from ivatar.settings import INSTALLED_APPS
|
||||
from ivatar.settings import TEMPLATES
|
||||
|
||||
ADMIN_USERS = []
|
||||
ALLOWED_HOSTS = ['*']
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
INSTALLED_APPS.extend([
|
||||
'django_extensions',
|
||||
'django_openid_auth',
|
||||
'bootstrap4',
|
||||
'anymail',
|
||||
'ivatar',
|
||||
'ivatar.ivataraccount',
|
||||
'ivatar.tools',
|
||||
])
|
||||
INSTALLED_APPS.extend(
|
||||
[
|
||||
"django_extensions",
|
||||
"django_openid_auth",
|
||||
"bootstrap4",
|
||||
"anymail",
|
||||
"ivatar",
|
||||
"ivatar.ivataraccount",
|
||||
"ivatar.tools",
|
||||
]
|
||||
)
|
||||
|
||||
MIDDLEWARE.extend([
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
])
|
||||
MIDDLEWARE.extend(
|
||||
[
|
||||
"ivatar.middleware.CustomLocaleMiddleware",
|
||||
]
|
||||
)
|
||||
MIDDLEWARE.insert(
|
||||
0, 'ivatar.middleware.MultipleProxyMiddleware',
|
||||
0,
|
||||
"ivatar.middleware.MultipleProxyMiddleware",
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
# Enable this to allow LDAP authentication.
|
||||
# See INSTALL for more information.
|
||||
# 'django_auth_ldap.backend.LDAPBackend',
|
||||
'django_openid_auth.auth.OpenIDBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
"django_openid_auth.auth.OpenIDBackend",
|
||||
"ivatar.ivataraccount.auth.FedoraOpenIdConnect",
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
)
|
||||
|
||||
TEMPLATES[0]['DIRS'].extend([
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
])
|
||||
TEMPLATES[0]['OPTIONS']['context_processors'].append(
|
||||
'ivatar.context_processors.basepage',
|
||||
TEMPLATES[0]["DIRS"].extend(
|
||||
[
|
||||
os.path.join(BASE_DIR, "templates"),
|
||||
]
|
||||
)
|
||||
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
|
||||
"ivatar.context_processors.basepage",
|
||||
)
|
||||
|
||||
OPENID_CREATE_USERS = True
|
||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||
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.3'
|
||||
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
|
||||
IVATAR_VERSION = "1.8.0"
|
||||
|
||||
SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/')
|
||||
BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/')
|
||||
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
|
||||
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('profile')
|
||||
SECURE_BASE_URL = os.environ.get(
|
||||
"SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/"
|
||||
)
|
||||
BASE_URL = os.environ.get("BASE_URL", "http://avatars.linux-kernel.at/avatar/")
|
||||
|
||||
LOGIN_REDIRECT_URL = reverse_lazy("profile")
|
||||
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
||||
|
||||
MAX_NUM_PHOTOS = 5
|
||||
@@ -75,119 +92,237 @@ MIN_LENGTH_EMAIL = 6 # eg. x@x.xx
|
||||
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
||||
|
||||
BOOTSTRAP4 = {
|
||||
'include_jquery': False,
|
||||
'javascript_in_head': False,
|
||||
'css_url': {
|
||||
'href': '/static/css/bootstrap.min.css',
|
||||
'integrity':
|
||||
'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB',
|
||||
'crossorigin': 'anonymous',
|
||||
"include_jquery": False,
|
||||
"javascript_in_head": False,
|
||||
"css_url": {
|
||||
"href": "/static/css/bootstrap.min.css",
|
||||
"integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB",
|
||||
"crossorigin": "anonymous",
|
||||
},
|
||||
'javascript_url': {
|
||||
'url': '/static/js/bootstrap.min.js',
|
||||
'integrity': '',
|
||||
'crossorigin': 'anonymous',
|
||||
"javascript_url": {
|
||||
"url": "/static/js/bootstrap.min.js",
|
||||
"integrity": "",
|
||||
"crossorigin": "anonymous",
|
||||
},
|
||||
'popper_url': {
|
||||
'url': '/static/js/popper.min.js',
|
||||
'integrity':
|
||||
'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49',
|
||||
'crossorigin': 'anonymous',
|
||||
"popper_url": {
|
||||
"url": "/static/js/popper.min.js",
|
||||
"integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49",
|
||||
"crossorigin": "anonymous",
|
||||
},
|
||||
}
|
||||
|
||||
if 'EMAIL_BACKEND' in os.environ:
|
||||
EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover
|
||||
if "EMAIL_BACKEND" in os.environ:
|
||||
EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover
|
||||
else:
|
||||
if 'test' in sys.argv or 'collectstatic' in sys.argv:
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||
if "test" in sys.argv or "collectstatic" in sys.argv:
|
||||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
else:
|
||||
try:
|
||||
ANYMAIL = { # pragma: no cover
|
||||
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
|
||||
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
|
||||
"MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"],
|
||||
"MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"],
|
||||
}
|
||||
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
|
||||
except Exception as exc:
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover
|
||||
except Exception: # pragma: nocover # pylint: disable=broad-except
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
|
||||
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'ivatar@mg.linux-kernel.at')
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'ivatar@mg.linux-kernel.at')
|
||||
SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "ivatar@mg.linux-kernel.at")
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at")
|
||||
|
||||
try:
|
||||
from ivatar.settings import DATABASES
|
||||
except ImportError: # pragma: no cover
|
||||
DATABASES = [] # pragma: no cover
|
||||
|
||||
if 'default' not in DATABASES:
|
||||
DATABASES['default'] = { # pragma: no cover
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
if "default" not in DATABASES:
|
||||
DATABASES["default"] = { # pragma: no cover
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
}
|
||||
|
||||
if 'MYSQL_DATABASE' in os.environ:
|
||||
DATABASES['default'] = { # pragma: no cover
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': os.environ['MYSQL_DATABASE'],
|
||||
'USER': os.environ['MYSQL_USER'],
|
||||
'PASSWORD': os.environ['MYSQL_PASSWORD'],
|
||||
'HOST': 'mysql',
|
||||
if "MYSQL_DATABASE" in os.environ:
|
||||
DATABASES["default"] = { # pragma: no cover
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"NAME": os.environ["MYSQL_DATABASE"],
|
||||
"USER": os.environ["MYSQL_USER"],
|
||||
"PASSWORD": os.environ["MYSQL_PASSWORD"],
|
||||
"HOST": "mysql",
|
||||
}
|
||||
|
||||
if 'POSTGRESQL_DATABASE' in os.environ:
|
||||
DATABASES['default'] = { # pragma: no cover
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.environ['POSTGRESQL_DATABASE'],
|
||||
'USER': os.environ['POSTGRESQL_USER'],
|
||||
'PASSWORD': os.environ['POSTGRESQL_PASSWORD'],
|
||||
'HOST': 'postgresql',
|
||||
if "POSTGRESQL_DATABASE" in os.environ:
|
||||
DATABASES["default"] = { # pragma: no cover
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ["POSTGRESQL_DATABASE"],
|
||||
"USER": os.environ["POSTGRESQL_USER"],
|
||||
"PASSWORD": os.environ["POSTGRESQL_PASSWORD"],
|
||||
"HOST": "postgresql",
|
||||
}
|
||||
|
||||
if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')):
|
||||
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
|
||||
# CI/CD config has different naming
|
||||
if "POSTGRES_DB" in os.environ:
|
||||
DATABASES["default"] = { # pragma: no cover
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ["POSTGRES_DB"],
|
||||
"USER": os.environ["POSTGRES_USER"],
|
||||
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
|
||||
"HOST": os.environ["POSTGRES_HOST"],
|
||||
"TEST": {
|
||||
"NAME": os.environ["POSTGRES_DB"],
|
||||
},
|
||||
}
|
||||
|
||||
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||
SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer"
|
||||
|
||||
USE_X_FORWARDED_HOST = True
|
||||
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',]
|
||||
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [
|
||||
"avatars.linux-kernel.at",
|
||||
"localhost",
|
||||
]
|
||||
|
||||
DEFAULT_AVATAR_SIZE = 80
|
||||
|
||||
LANGUAGES = (
|
||||
('de', _('Deutsch')),
|
||||
('en', _('English')),
|
||||
('ca', _('Català')),
|
||||
('cs', _('Česky')),
|
||||
('es', _('Español')),
|
||||
('eu', _('Basque')),
|
||||
('fr', _('Français')),
|
||||
('it', _('Italiano')),
|
||||
('ja', _('日本語')),
|
||||
('nl', _('Nederlands')),
|
||||
('pt', _('Português')),
|
||||
('ru', _('Русский')),
|
||||
('sq', _('Shqip')),
|
||||
('tr', _('Türkçe')),
|
||||
('uk', _('Українська')),
|
||||
("de", _("Deutsch")),
|
||||
("en", _("English")),
|
||||
("ca", _("Català")),
|
||||
("cs", _("Česky")),
|
||||
("es", _("Español")),
|
||||
("eu", _("Basque")),
|
||||
("fr", _("Français")),
|
||||
("it", _("Italiano")),
|
||||
("ja", _("日本語")),
|
||||
("nl", _("Nederlands")),
|
||||
("pt", _("Português")),
|
||||
("ru", _("Русский")),
|
||||
("sq", _("Shqip")),
|
||||
("tr", _("Türkçe")),
|
||||
("uk", _("Українська")),
|
||||
)
|
||||
|
||||
MESSAGE_TAGS = {
|
||||
message_constants.DEBUG: 'debug',
|
||||
message_constants.INFO: 'info',
|
||||
message_constants.SUCCESS: 'success',
|
||||
message_constants.WARNING: 'warning',
|
||||
message_constants.ERROR: 'danger',
|
||||
message_constants.DEBUG: "debug",
|
||||
message_constants.INFO: "info",
|
||||
message_constants.SUCCESS: "success",
|
||||
message_constants.WARNING: "warning",
|
||||
message_constants.ERROR: "danger",
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': [
|
||||
'127.0.0.1:11211',
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
|
||||
"LOCATION": [
|
||||
"127.0.0.1:11211",
|
||||
],
|
||||
}
|
||||
# "OPTIONS": {"MAX_ENTRIES": 1000000},
|
||||
},
|
||||
"filesystem": {
|
||||
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
|
||||
"LOCATION": "/var/tmp/ivatar_cache",
|
||||
"TIMEOUT": 900, # 15 minutes
|
||||
"OPTIONS": {"MAX_ENTRIES": 1000000},
|
||||
},
|
||||
}
|
||||
|
||||
# This is 5 minutes caching for generated/resized images,
|
||||
# so the sites don't hit ivatar so much
|
||||
# so the sites don't hit ivatar so much - it's what's set in the HTTP header
|
||||
CACHE_IMAGES_MAX_AGE = 5 * 60
|
||||
|
||||
CACHE_RESPONSE = True
|
||||
|
||||
# Trusted URLs for default redirection
|
||||
TRUSTED_DEFAULT_URLS = [
|
||||
{"schemes": ["https"], "host_equals": "ui-avatars.com", "path_prefix": "/api/"},
|
||||
{
|
||||
"schemes": ["http", "https"],
|
||||
"host_equals": "gravatar.com",
|
||||
"path_prefix": "/avatar/",
|
||||
},
|
||||
{
|
||||
"schemes": ["http", "https"],
|
||||
"host_suffix": ".gravatar.com",
|
||||
"path_prefix": "/avatar/",
|
||||
},
|
||||
{
|
||||
"schemes": ["http", "https"],
|
||||
"host_equals": "www.gravatar.org",
|
||||
"path_prefix": "/avatar/",
|
||||
},
|
||||
{
|
||||
"schemes": ["https"],
|
||||
"host_equals": "avatars.dicebear.com",
|
||||
"path_prefix": "/api/",
|
||||
},
|
||||
{
|
||||
"schemes": ["https"],
|
||||
"host_equals": "api.dicebear.com",
|
||||
"path_prefix": "/",
|
||||
},
|
||||
{
|
||||
"schemes": ["https"],
|
||||
"host_equals": "badges.fedoraproject.org",
|
||||
"path_prefix": "/static/img/",
|
||||
},
|
||||
{
|
||||
"schemes": ["http"],
|
||||
"host_equals": "www.planet-libre.org",
|
||||
"path_prefix": "/themes/planetlibre/images/",
|
||||
},
|
||||
{"schemes": ["https"], "host_equals": "www.azuracast.com", "path_prefix": "/img/"},
|
||||
{
|
||||
"schemes": ["https"],
|
||||
"host_equals": "reps.mozilla.org",
|
||||
"path_prefix": "/static/base/img/remo/",
|
||||
},
|
||||
]
|
||||
|
||||
URL_TIMEOUT = 10
|
||||
|
||||
|
||||
def map_legacy_config(trusted_url):
|
||||
"""
|
||||
For backward compability with the legacy configuration
|
||||
for trusting URLs. Adapts them to fit the new config.
|
||||
"""
|
||||
if isinstance(trusted_url, str):
|
||||
return {"url_prefix": trusted_url}
|
||||
|
||||
return 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)
|
||||
|
||||
# Celery Configuration
|
||||
# Try Redis first, fallback to memory broker for development
|
||||
try:
|
||||
import redis
|
||||
|
||||
redis.Redis(host="localhost", port=6379, db=0).ping()
|
||||
CELERY_BROKER_URL = "redis://localhost:6379/0"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Fallback to memory broker for development
|
||||
CELERY_BROKER_URL = "memory://"
|
||||
print("Warning: Redis not available, using memory broker for development")
|
||||
|
||||
CELERY_RESULT_BACKEND = "django-db"
|
||||
CELERY_ACCEPT_CONTENT = ["json"]
|
||||
CELERY_TASK_SERIALIZER = "json"
|
||||
CELERY_RESULT_SERIALIZER = "json"
|
||||
CELERY_TIMEZONE = "UTC"
|
||||
CELERY_TASK_TRACK_STARTED = True
|
||||
CELERY_TASK_TIME_LIMIT = 300 # 5 minutes
|
||||
CELERY_TASK_SOFT_TIME_LIMIT = 240 # 4 minutes
|
||||
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
||||
CELERY_TASK_ACKS_LATE = True
|
||||
CELERY_RESULT_EXPIRES = 3600 # 1 hour
|
||||
CELERY_WORKER_CONCURRENCY = (
|
||||
1 # Max 1 parallel avatar generation task for local development
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
0
create_nobody_from_svg_with_inkscape.sh
Normal file → Executable file
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Import the whole libravatar export
|
||||
'''
|
||||
"""
|
||||
|
||||
import os
|
||||
from os.path import isfile, isdir, join
|
||||
@@ -9,13 +10,18 @@ import sys
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import django
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ivatar.settings") # pylint: disable=wrong-import-position
|
||||
|
||||
os.environ.setdefault(
|
||||
"DJANGO_SETTINGS_MODULE", "ivatar.settings"
|
||||
) # pylint: disable=wrong-import-position
|
||||
django.setup() # pylint: disable=wrong-import-position
|
||||
from django.contrib.auth.models import User
|
||||
from PIL import Image
|
||||
from django_openid_auth.models import UserOpenID
|
||||
from ivatar.settings import JPEG_QUALITY
|
||||
from ivatar.ivataraccount.read_libravatar_export import read_gzdata as libravatar_read_gzdata
|
||||
from ivatar.ivataraccount.read_libravatar_export import (
|
||||
read_gzdata as libravatar_read_gzdata,
|
||||
)
|
||||
from ivatar.ivataraccount.models import ConfirmedEmail
|
||||
from ivatar.ivataraccount.models import ConfirmedOpenId
|
||||
from ivatar.ivataraccount.models import Photo
|
||||
@@ -26,54 +32,63 @@ if len(sys.argv) < 2:
|
||||
exit(-255)
|
||||
|
||||
if not isdir(sys.argv[1]):
|
||||
print("First argument to '%s' must be a directory containing the exports" % sys.argv[0])
|
||||
print(
|
||||
"First argument to '%s' must be a directory containing the exports"
|
||||
% sys.argv[0]
|
||||
)
|
||||
exit(-255)
|
||||
|
||||
PATH = sys.argv[1]
|
||||
for file in os.listdir(PATH):
|
||||
if not file.endswith('.xml.gz'):
|
||||
if not file.endswith(".xml.gz"):
|
||||
continue
|
||||
if isfile(join(PATH, file)):
|
||||
fh = open(join(PATH, file), 'rb')
|
||||
fh = open(join(PATH, file), "rb")
|
||||
items = libravatar_read_gzdata(fh.read())
|
||||
print('Adding user "%s"' % items['username'])
|
||||
(user, created) = User.objects.get_or_create(username=items['username'])
|
||||
user.password = items['password']
|
||||
print('Adding user "%s"' % items["username"])
|
||||
(user, created) = User.objects.get_or_create(username=items["username"])
|
||||
user.password = items["password"]
|
||||
user.save()
|
||||
|
||||
saved_photos = {}
|
||||
for photo in items['photos']:
|
||||
photo_id = photo['id']
|
||||
data = base64.decodebytes(bytes(photo['data'], 'utf-8'))
|
||||
for photo in items["photos"]:
|
||||
photo_id = photo["id"]
|
||||
data = base64.decodebytes(bytes(photo["data"], "utf-8"))
|
||||
pilobj = Image.open(BytesIO(data))
|
||||
out = BytesIO()
|
||||
pilobj.save(out, pilobj.format, quality=JPEG_QUALITY)
|
||||
out.seek(0)
|
||||
photo = Photo()
|
||||
photo.user = user
|
||||
photo.ip_address = '0.0.0.0'
|
||||
photo.ip_address = "0.0.0.0"
|
||||
photo.format = file_format(pilobj.format)
|
||||
photo.data = out.read()
|
||||
photo.save()
|
||||
saved_photos[photo_id] = photo
|
||||
|
||||
for email in items['emails']:
|
||||
for email in items["emails"]:
|
||||
try:
|
||||
ConfirmedEmail.objects.get_or_create(email=email['email'], user=user,
|
||||
photo=saved_photos.get(email['photo_id']))
|
||||
except django.db.utils.IntegrityError:
|
||||
print('%s not unique?' % email['email'])
|
||||
|
||||
for openid in items['openids']:
|
||||
try:
|
||||
ConfirmedOpenId.objects.get_or_create(openid=openid['openid'], user=user,
|
||||
photo=saved_photos.get(openid['photo_id'])) # pylint: disable=no-member
|
||||
UserOpenID.objects.get_or_create(
|
||||
user_id=user.id,
|
||||
claimed_id=openid['openid'],
|
||||
display_id=openid['openid'],
|
||||
ConfirmedEmail.objects.get_or_create(
|
||||
email=email["email"],
|
||||
user=user,
|
||||
photo=saved_photos.get(email["photo_id"]),
|
||||
)
|
||||
except django.db.utils.IntegrityError:
|
||||
print('%s not unique?' % openid['openid'])
|
||||
print("%s not unique?" % email["email"])
|
||||
|
||||
for openid in items["openids"]:
|
||||
try:
|
||||
ConfirmedOpenId.objects.get_or_create(
|
||||
openid=openid["openid"],
|
||||
user=user,
|
||||
photo=saved_photos.get(openid["photo_id"]),
|
||||
) # pylint: disable=no-member
|
||||
UserOpenID.objects.get_or_create(
|
||||
user_id=user.id,
|
||||
claimed_id=openid["openid"],
|
||||
display_id=openid["openid"],
|
||||
)
|
||||
except django.db.utils.IntegrityError:
|
||||
print("%s not unique?" % openid["openid"])
|
||||
|
||||
fh.close()
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Module init
|
||||
'''
|
||||
"""
|
||||
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ("celery_app",)
|
||||
|
||||
app_label = __name__ # pylint: disable=invalid-name
|
||||
|
||||
461
ivatar/ai_service.py
Normal file
@@ -0,0 +1,461 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AI service module for text-to-image avatar generation
|
||||
Supports Stable Diffusion (local and API) for professional avatar generation
|
||||
"""
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AIServiceError(Exception):
|
||||
"""Custom exception for AI service errors"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StableDiffusionService:
|
||||
"""
|
||||
Service for generating images using Stable Diffusion (local or API)
|
||||
"""
|
||||
|
||||
# Model-specific token limits
|
||||
TOKEN_LIMITS = {
|
||||
"stable_diffusion": 77, # CLIP tokenizer limit
|
||||
"stable_diffusion_v2": 77,
|
||||
"stable_diffusion_xl": 77,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.api_url = getattr(settings, "STABLE_DIFFUSION_API_URL", None)
|
||||
self.api_key = getattr(settings, "STABLE_DIFFUSION_API_KEY", None)
|
||||
self.timeout = getattr(settings, "STABLE_DIFFUSION_TIMEOUT", 60)
|
||||
self._pipe = None # Cache for local model
|
||||
self._tokenizer = None # Cache for tokenizer
|
||||
|
||||
def generate_image(
|
||||
self, prompt, size=(512, 512), quality="medium", allow_nsfw=False
|
||||
):
|
||||
"""
|
||||
Generate an image from text prompt using Stable Diffusion
|
||||
|
||||
Args:
|
||||
prompt (str): Text description of the desired image
|
||||
size (tuple): Image dimensions (width, height)
|
||||
quality (str): Generation quality ('low', 'medium', 'high')
|
||||
allow_nsfw (bool): Whether to allow potentially NSFW content
|
||||
|
||||
Returns:
|
||||
PIL.Image: Generated image
|
||||
|
||||
Raises:
|
||||
AIServiceError: If generation fails or prompt is too long
|
||||
"""
|
||||
# Validate prompt length first
|
||||
validation = self.validate_prompt(prompt)
|
||||
if not validation["valid"]:
|
||||
raise AIServiceError(validation["warning"])
|
||||
|
||||
try:
|
||||
if self.api_url and self.api_key:
|
||||
return self._generate_via_api(prompt, size, quality, allow_nsfw)
|
||||
else:
|
||||
return self._generate_locally(prompt, size, quality, allow_nsfw)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate image: {e}")
|
||||
raise AIServiceError(f"Image generation failed: {str(e)}")
|
||||
|
||||
def validate_prompt(self, prompt, model="stable_diffusion"):
|
||||
"""
|
||||
Validate prompt length against model token limits
|
||||
|
||||
Args:
|
||||
prompt (str): Text prompt to validate
|
||||
model (str): Model name to check limits for
|
||||
|
||||
Returns:
|
||||
dict: Validation result with 'valid', 'token_count', 'limit', 'warning'
|
||||
"""
|
||||
try:
|
||||
token_count = self._count_tokens(prompt)
|
||||
limit = self.TOKEN_LIMITS.get(model, 77)
|
||||
|
||||
is_valid = token_count <= limit
|
||||
warning = None
|
||||
|
||||
if not is_valid:
|
||||
warning = f"Prompt too long: {token_count} tokens (limit: {limit}). Please shorten your prompt."
|
||||
|
||||
return {
|
||||
"valid": is_valid,
|
||||
"token_count": token_count,
|
||||
"limit": limit,
|
||||
"warning": warning,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Token counting failed: {e}")
|
||||
return {
|
||||
"valid": True, # Allow generation if counting fails
|
||||
"token_count": 0,
|
||||
"limit": 77,
|
||||
"warning": None,
|
||||
}
|
||||
|
||||
def _count_tokens(self, prompt):
|
||||
"""
|
||||
Count tokens in a prompt using CLIP tokenizer
|
||||
"""
|
||||
try:
|
||||
if self._tokenizer is None:
|
||||
from transformers import CLIPTokenizer
|
||||
|
||||
self._tokenizer = CLIPTokenizer.from_pretrained(
|
||||
"openai/clip-vit-base-patch32"
|
||||
)
|
||||
|
||||
tokens = self._tokenizer(prompt, return_tensors="pt", truncation=False)[
|
||||
"input_ids"
|
||||
]
|
||||
return tokens.shape[1]
|
||||
|
||||
except ImportError:
|
||||
# Fallback: more accurate estimation
|
||||
# CLIP tokenizer typically produces ~1.3 tokens per word for English
|
||||
words = len(prompt.split())
|
||||
return int(words * 1.3)
|
||||
except Exception as e:
|
||||
logger.warning(f"Token counting error: {e}")
|
||||
# Fallback: more accurate estimation
|
||||
words = len(prompt.split())
|
||||
return int(words * 1.3)
|
||||
|
||||
def _is_black_image(self, image):
|
||||
"""
|
||||
Check if an image is completely black (common NSFW response from APIs)
|
||||
|
||||
Args:
|
||||
image (PIL.Image): Image to check
|
||||
|
||||
Returns:
|
||||
bool: True if image is completely black
|
||||
"""
|
||||
# Convert to RGB if necessary
|
||||
if image.mode != "RGB":
|
||||
image = image.convert("RGB")
|
||||
|
||||
# Get image data
|
||||
pixels = list(image.getdata())
|
||||
|
||||
# Check if all pixels are black (0, 0, 0)
|
||||
black_pixels = sum(1 for r, g, b in pixels if r == 0 and g == 0 and b == 0)
|
||||
total_pixels = len(pixels)
|
||||
|
||||
# Consider it a black image if more than 95% of pixels are black
|
||||
return (black_pixels / total_pixels) > 0.95
|
||||
|
||||
def _generate_via_api(self, prompt, size, quality, allow_nsfw=False):
|
||||
"""
|
||||
Generate image via Stable Diffusion API (Replicate, Hugging Face, etc.)
|
||||
"""
|
||||
# Enhanced prompt for avatar generation
|
||||
enhanced_prompt = f"""professional avatar portrait, {prompt}, high quality, detailed, clean background, centered composition, profile picture style, photorealistic"""
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
payload = {
|
||||
"prompt": enhanced_prompt,
|
||||
"width": size[0],
|
||||
"height": size[1],
|
||||
"num_inference_steps": 25
|
||||
if quality == "high"
|
||||
else (20 if quality == "medium" else 15),
|
||||
"guidance_scale": 7.5, # Balanced for quality and speed
|
||||
"negative_prompt": "blurry, low quality, distorted, ugly, deformed, bad anatomy",
|
||||
}
|
||||
|
||||
# Add NSFW safety setting if supported by the API
|
||||
if allow_nsfw:
|
||||
payload["safety_tolerance"] = 2 # Some APIs support this
|
||||
payload["nsfw"] = True # Some APIs support this
|
||||
|
||||
response = requests.post(
|
||||
self.api_url, json=payload, headers=headers, timeout=self.timeout
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_msg = f"Stable Diffusion API request failed: {response.status_code}"
|
||||
try:
|
||||
error_detail = response.json()
|
||||
error_msg += f" - {error_detail}"
|
||||
|
||||
# Check for NSFW content detection
|
||||
if isinstance(error_detail, dict):
|
||||
error_text = str(error_detail).lower()
|
||||
if (
|
||||
"nsfw" in error_text
|
||||
or "inappropriate" in error_text
|
||||
or "black image" in error_text
|
||||
):
|
||||
if allow_nsfw:
|
||||
# If user allowed NSFW but still got blocked, provide a different message
|
||||
raise AIServiceError(
|
||||
"Content warning: The AI service still detected inappropriate content even with relaxed settings. Please try a different prompt or contact support if you believe this is an error."
|
||||
)
|
||||
else:
|
||||
raise AIServiceError(
|
||||
"Content warning: The AI service detected potentially inappropriate content in your prompt. Please modify your description to be more appropriate for all ages and try again."
|
||||
)
|
||||
elif isinstance(error_detail, str):
|
||||
if (
|
||||
"nsfw" in error_detail.lower()
|
||||
or "inappropriate" in error_detail.lower()
|
||||
or "black image" in error_detail.lower()
|
||||
):
|
||||
if allow_nsfw:
|
||||
raise AIServiceError(
|
||||
"Content warning: The AI service still detected inappropriate content even with relaxed settings. Please try a different prompt or contact support if you believe this is an error."
|
||||
)
|
||||
else:
|
||||
raise AIServiceError(
|
||||
"Content warning: The AI service detected potentially inappropriate content in your prompt. Please modify your description to be more appropriate for all ages and try again."
|
||||
)
|
||||
|
||||
except AIServiceError:
|
||||
# Re-raise our custom NSFW error
|
||||
raise
|
||||
except Exception: # pylint: disable=broad-except
|
||||
error_msg += f" - {response.text}"
|
||||
|
||||
# Also check response text for NSFW warnings
|
||||
if (
|
||||
"nsfw" in response.text.lower()
|
||||
or "inappropriate" in response.text.lower()
|
||||
or "black image" in response.text.lower()
|
||||
):
|
||||
if allow_nsfw:
|
||||
raise AIServiceError(
|
||||
"Content warning: The AI service still detected inappropriate content even with relaxed settings. Please try a different prompt or contact support if you believe this is an error."
|
||||
)
|
||||
else:
|
||||
raise AIServiceError(
|
||||
"Content warning: The AI service detected potentially inappropriate content in your prompt. Please modify your description to be more appropriate for all ages and try again."
|
||||
)
|
||||
|
||||
raise AIServiceError(error_msg)
|
||||
|
||||
result = response.json()
|
||||
|
||||
if "image" in result:
|
||||
# Decode base64 image
|
||||
image_data = base64.b64decode(result["image"])
|
||||
image = Image.open(BytesIO(image_data))
|
||||
|
||||
# Check if the image is completely black (common NSFW response)
|
||||
if not allow_nsfw and self._is_black_image(image):
|
||||
raise AIServiceError(
|
||||
"Content warning: The AI service detected potentially inappropriate content in your prompt and returned a black image. Please modify your description to be more appropriate for all ages and try again."
|
||||
)
|
||||
|
||||
return image
|
||||
else:
|
||||
raise AIServiceError("No image data in API response")
|
||||
|
||||
def _generate_locally(self, prompt, size, quality, allow_nsfw=False):
|
||||
"""
|
||||
Generate image using local Stable Diffusion installation
|
||||
This requires diffusers library and a local model
|
||||
"""
|
||||
try:
|
||||
from diffusers import StableDiffusionPipeline
|
||||
import torch
|
||||
|
||||
# Enhanced prompt for avatar generation
|
||||
enhanced_prompt = f"""professional avatar portrait, {prompt}, high quality, detailed, clean background, centered composition, profile picture style, photorealistic"""
|
||||
|
||||
# Use cached model if available, otherwise load it
|
||||
if self._pipe is None:
|
||||
logger.info("Loading Stable Diffusion model (first time or cache miss)")
|
||||
self._pipe = StableDiffusionPipeline.from_pretrained(
|
||||
"runwayml/stable-diffusion-v1-5",
|
||||
torch_dtype=torch.float16
|
||||
if torch.cuda.is_available()
|
||||
else torch.float32,
|
||||
)
|
||||
|
||||
if torch.cuda.is_available():
|
||||
self._pipe = self._pipe.to("cuda")
|
||||
else:
|
||||
logger.info("Using cached Stable Diffusion model")
|
||||
|
||||
pipe = self._pipe
|
||||
|
||||
# Disable safety checker if NSFW override is enabled
|
||||
if allow_nsfw:
|
||||
pipe.safety_checker = None
|
||||
pipe.requires_safety_checker = False
|
||||
|
||||
# Generate image with optimized settings for speed
|
||||
image = pipe(
|
||||
enhanced_prompt,
|
||||
height=size[1],
|
||||
width=size[0],
|
||||
num_inference_steps=25
|
||||
if quality == "high"
|
||||
else (20 if quality == "medium" else 15),
|
||||
guidance_scale=7.5, # Balanced for quality and speed
|
||||
negative_prompt="blurry, low quality, distorted, ugly, deformed, bad anatomy",
|
||||
).images[0]
|
||||
|
||||
return image
|
||||
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"diffusers library not installed, falling back to placeholder"
|
||||
)
|
||||
return self._generate_placeholder(prompt, size)
|
||||
except Exception as e:
|
||||
logger.error(f"Local Stable Diffusion generation failed: {e}")
|
||||
return self._generate_placeholder(prompt, size)
|
||||
|
||||
def _generate_placeholder(self, prompt, size):
|
||||
"""
|
||||
Generate a placeholder image when Stable Diffusion is not available
|
||||
"""
|
||||
logger.info("Generating placeholder image")
|
||||
|
||||
# Create a more sophisticated placeholder
|
||||
img = Image.new("RGBA", size, color=(240, 248, 255, 255))
|
||||
|
||||
from PIL import ImageDraw, ImageFont
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
try:
|
||||
font = ImageFont.load_default()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
font = None
|
||||
|
||||
# Add title
|
||||
title = "AI Avatar (Stable Diffusion)"
|
||||
draw.text((10, 10), title, fill="darkblue", font=font)
|
||||
|
||||
# Add prompt
|
||||
prompt_text = f"Prompt: {prompt[:50]}..."
|
||||
draw.text((10, 40), prompt_text, fill="black", font=font)
|
||||
|
||||
# Add note
|
||||
note = "Install Stable Diffusion for real generation"
|
||||
draw.text((10, 70), note, fill="darkgreen", font=font)
|
||||
|
||||
# Create a more sophisticated avatar placeholder
|
||||
center_x, center_y = size[0] // 2, size[1] // 2 + 20
|
||||
radius = min(size) // 4
|
||||
|
||||
# Face circle
|
||||
draw.ellipse(
|
||||
[
|
||||
center_x - radius,
|
||||
center_y - radius,
|
||||
center_x + radius,
|
||||
center_y + radius,
|
||||
],
|
||||
outline="purple",
|
||||
width=3,
|
||||
fill=(255, 240, 245),
|
||||
)
|
||||
|
||||
# Eyes
|
||||
eye_radius = radius // 4
|
||||
draw.ellipse(
|
||||
[
|
||||
center_x - radius // 2 - eye_radius,
|
||||
center_y - radius // 2 - eye_radius,
|
||||
center_x - radius // 2 + eye_radius,
|
||||
center_y - radius // 2 + eye_radius,
|
||||
],
|
||||
fill="blue",
|
||||
)
|
||||
draw.ellipse(
|
||||
[
|
||||
center_x + radius // 2 - eye_radius,
|
||||
center_y - radius // 2 - eye_radius,
|
||||
center_x + radius // 2 + eye_radius,
|
||||
center_y - radius // 2 + eye_radius,
|
||||
],
|
||||
fill="blue",
|
||||
)
|
||||
|
||||
# Smile
|
||||
smile_y = center_y + radius // 3
|
||||
draw.arc(
|
||||
[
|
||||
center_x - radius // 2,
|
||||
smile_y - radius // 4,
|
||||
center_x + radius // 2,
|
||||
smile_y + radius // 4,
|
||||
],
|
||||
0,
|
||||
180,
|
||||
fill="red",
|
||||
width=3,
|
||||
)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def validate_avatar_prompt(prompt, model="stable_diffusion"):
|
||||
"""
|
||||
Convenience function to validate avatar prompts
|
||||
|
||||
Args:
|
||||
prompt (str): Text description of the avatar
|
||||
model (str): AI model to use
|
||||
|
||||
Returns:
|
||||
dict: Validation result with 'valid', 'token_count', 'limit', 'warning'
|
||||
"""
|
||||
if model == "stable_diffusion":
|
||||
service = StableDiffusionService()
|
||||
return service.validate_prompt(prompt, model)
|
||||
else:
|
||||
# For other models, assume they're valid
|
||||
return {"valid": True, "token_count": 0, "limit": 0, "warning": None}
|
||||
|
||||
|
||||
def generate_avatar_image(
|
||||
prompt,
|
||||
model="stable_diffusion",
|
||||
size=(512, 512),
|
||||
quality="medium",
|
||||
allow_nsfw=False,
|
||||
):
|
||||
"""
|
||||
Convenience function to generate avatar images
|
||||
|
||||
Args:
|
||||
prompt (str): Text description of the avatar
|
||||
model (str): AI model to use (currently only 'stable_diffusion')
|
||||
size (tuple): Image dimensions
|
||||
quality (str): Generation quality ('low', 'medium', 'high')
|
||||
allow_nsfw (bool): Whether to allow potentially NSFW content
|
||||
|
||||
Returns:
|
||||
PIL.Image: Generated avatar image
|
||||
"""
|
||||
if model == "stable_diffusion":
|
||||
service = StableDiffusionService()
|
||||
return service.generate_image(prompt, size, quality, allow_nsfw)
|
||||
else:
|
||||
raise AIServiceError(
|
||||
f"Unsupported model: {model}. Only 'stable_diffusion' is currently supported."
|
||||
)
|
||||
56
ivatar/celery.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Celery configuration for ivatar
|
||||
"""
|
||||
|
||||
import os
|
||||
from celery import Celery
|
||||
from django.conf import settings
|
||||
|
||||
# Set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ivatar.settings")
|
||||
|
||||
app = Celery("ivatar")
|
||||
|
||||
# Using a string here means the worker doesn't have to serialize
|
||||
# the configuration object to child processes.
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
|
||||
# Load task modules from all registered Django apps.
|
||||
app.autodiscover_tasks()
|
||||
|
||||
# Celery configuration
|
||||
app.conf.update(
|
||||
# Task routing - use default queue for simplicity
|
||||
task_default_queue="default",
|
||||
task_routes={
|
||||
"ivatar.tasks.generate_avatar_task": {"queue": "default"},
|
||||
"ivatar.tasks.update_queue_positions": {"queue": "default"},
|
||||
"ivatar.tasks.cleanup_old_tasks": {"queue": "default"},
|
||||
},
|
||||
# Worker configuration
|
||||
worker_prefetch_multiplier=1,
|
||||
task_acks_late=True,
|
||||
# Result backend
|
||||
result_backend="django-db",
|
||||
result_expires=3600, # 1 hour
|
||||
# Task time limits
|
||||
task_time_limit=300, # 5 minutes
|
||||
task_soft_time_limit=240, # 4 minutes
|
||||
# Task serialization
|
||||
task_serializer="json",
|
||||
accept_content=["json"],
|
||||
result_serializer="json",
|
||||
# Timezone
|
||||
timezone="UTC",
|
||||
enable_utc=True,
|
||||
)
|
||||
|
||||
# Set worker concurrency from Django settings
|
||||
if hasattr(settings, "CELERY_WORKER_CONCURRENCY"):
|
||||
app.conf.worker_concurrency = settings.CELERY_WORKER_CONCURRENCY
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
def debug_task(self):
|
||||
print(f"Request: {self.request!r}")
|
||||
@@ -1,35 +1,38 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
def basepage(request):
|
||||
'''
|
||||
"""
|
||||
Our contextprocessor adds additional context variables
|
||||
in order to be used in the templates
|
||||
'''
|
||||
"""
|
||||
context = {}
|
||||
if 'openid_identifier' in request.GET:
|
||||
context['openid_identifier'] = \
|
||||
request.GET['openid_identifier'] # pragma: no cover
|
||||
if "openid_identifier" in request.GET:
|
||||
context["openid_identifier"] = request.GET[
|
||||
"openid_identifier"
|
||||
] # pragma: no cover
|
||||
client_ip = get_client_ip(request)[0]
|
||||
context['client_ip'] = client_ip
|
||||
context['ivatar_version'] = IVATAR_VERSION
|
||||
context['site_name'] = SITE_NAME
|
||||
context['site_url'] = request.build_absolute_uri('/')[:-1]
|
||||
context['max_file_size'] = MAX_PHOTO_SIZE
|
||||
context['BASE_URL'] = BASE_URL
|
||||
context['SECURE_BASE_URL'] = SECURE_BASE_URL
|
||||
context['max_emails'] = False
|
||||
context["client_ip"] = client_ip
|
||||
context["ivatar_version"] = IVATAR_VERSION
|
||||
context["site_name"] = SITE_NAME
|
||||
context["site_url"] = request.build_absolute_uri("/")[:-1]
|
||||
context["max_file_size"] = MAX_PHOTO_SIZE
|
||||
context["BASE_URL"] = BASE_URL
|
||||
context["SECURE_BASE_URL"] = SECURE_BASE_URL
|
||||
context["max_emails"] = False
|
||||
|
||||
if request.user:
|
||||
if not request.user.is_anonymous:
|
||||
unconfirmed = request.user.unconfirmedemail_set.count()
|
||||
if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS:
|
||||
context['max_emails'] = True
|
||||
context["max_emails"] = True
|
||||
|
||||
return context
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Module init
|
||||
'''
|
||||
"""
|
||||
app_label = __name__ # pylint: disable=invalid-name
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Register models in admin
|
||||
'''
|
||||
"""
|
||||
from django.contrib import admin
|
||||
|
||||
from . models import Photo, ConfirmedEmail, UnconfirmedEmail
|
||||
from . models import ConfirmedOpenId, UnconfirmedOpenId
|
||||
from . models import OpenIDNonce, OpenIDAssociation
|
||||
from . models import UserPreference
|
||||
from .models import Photo, ConfirmedEmail, UnconfirmedEmail
|
||||
from .models import ConfirmedOpenId, UnconfirmedOpenId
|
||||
from .models import OpenIDNonce, OpenIDAssociation
|
||||
from .models import UserPreference
|
||||
|
||||
# Register models in admin
|
||||
admin.site.register(Photo)
|
||||
|
||||
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}
|
||||
@@ -1,163 +1,172 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Classes for our ivatar.ivataraccount.forms
|
||||
'''
|
||||
"""
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ipware import get_client_ip
|
||||
|
||||
from ivatar import settings
|
||||
from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL
|
||||
from ivatar.settings import MIN_LENGTH_URL, MAX_LENGTH_URL
|
||||
from . models import UnconfirmedEmail, ConfirmedEmail, Photo
|
||||
from . models import UnconfirmedOpenId, ConfirmedOpenId
|
||||
from . models import UserPreference
|
||||
from .models import UnconfirmedEmail, ConfirmedEmail, Photo
|
||||
from .models import UnconfirmedOpenId, ConfirmedOpenId
|
||||
from .models import UserPreference
|
||||
|
||||
|
||||
MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5
|
||||
|
||||
|
||||
class AddEmailForm(forms.Form):
|
||||
'''
|
||||
"""
|
||||
Form to handle adding email addresses
|
||||
'''
|
||||
"""
|
||||
|
||||
email = forms.EmailField(
|
||||
label=_('Email'),
|
||||
label=_("Email"),
|
||||
min_length=MIN_LENGTH_EMAIL,
|
||||
max_length=MAX_LENGTH_EMAIL,
|
||||
)
|
||||
|
||||
def clean_email(self):
|
||||
'''
|
||||
"""
|
||||
Enforce lowercase email
|
||||
'''
|
||||
"""
|
||||
# TODO: Domain restriction as in libravatar?
|
||||
return self.cleaned_data['email'].lower()
|
||||
return self.cleaned_data["email"].lower()
|
||||
|
||||
def save(self, request):
|
||||
'''
|
||||
"""
|
||||
Save the model, ensuring some safety
|
||||
'''
|
||||
"""
|
||||
user = request.user
|
||||
# Enforce the maximum number of unconfirmed emails a user can have
|
||||
num_unconfirmed = user.unconfirmedemail_set.count()
|
||||
|
||||
max_num_unconfirmed_emails = getattr(
|
||||
settings,
|
||||
'MAX_NUM_UNCONFIRMED_EMAILS',
|
||||
MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT)
|
||||
settings, "MAX_NUM_UNCONFIRMED_EMAILS", MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
||||
)
|
||||
|
||||
if num_unconfirmed >= max_num_unconfirmed_emails:
|
||||
self.add_error(None, _('Too many unconfirmed mail addresses!'))
|
||||
self.add_error(None, _("Too many unconfirmed mail addresses!"))
|
||||
return False
|
||||
|
||||
# Check whether or not a confirmation email has been
|
||||
# sent by this user already
|
||||
if UnconfirmedEmail.objects.filter( # pylint: disable=no-member
|
||||
user=user, email=self.cleaned_data['email']).exists():
|
||||
self.add_error(
|
||||
'email',
|
||||
_('Address already added, currently unconfirmed'))
|
||||
user=user, email=self.cleaned_data["email"]
|
||||
).exists():
|
||||
self.add_error("email", _("Address already added, currently unconfirmed"))
|
||||
return False
|
||||
|
||||
# Check whether or not the email is already confirmed by someone
|
||||
if ConfirmedEmail.objects.filter(
|
||||
email=self.cleaned_data['email']).exists():
|
||||
self.add_error(
|
||||
'email',
|
||||
_('Address already confirmed (by someone else)'))
|
||||
# Check whether or not the email is already confirmed (by someone)
|
||||
check_mail = ConfirmedEmail.objects.filter(email=self.cleaned_data["email"])
|
||||
if check_mail.exists():
|
||||
msg = _("Address already confirmed (by someone else)")
|
||||
if check_mail.first().user == request.user:
|
||||
msg = _("Address already confirmed (by you)")
|
||||
self.add_error("email", msg)
|
||||
return False
|
||||
|
||||
unconfirmed = UnconfirmedEmail()
|
||||
unconfirmed.email = self.cleaned_data['email']
|
||||
unconfirmed.email = self.cleaned_data["email"]
|
||||
unconfirmed.user = user
|
||||
unconfirmed.save()
|
||||
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri('/')[:-1])
|
||||
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1])
|
||||
return True
|
||||
|
||||
|
||||
class UploadPhotoForm(forms.Form):
|
||||
'''
|
||||
"""
|
||||
Form handling photo upload
|
||||
'''
|
||||
"""
|
||||
|
||||
photo = forms.FileField(
|
||||
label=_('Photo'),
|
||||
error_messages={'required': _('You must choose an image to upload.')})
|
||||
label=_("Photo"),
|
||||
error_messages={"required": _("You must choose an image to upload.")},
|
||||
)
|
||||
not_porn = forms.BooleanField(
|
||||
label=_('suitable for all ages (i.e. no offensive content)'),
|
||||
label=_("suitable for all ages (i.e. no offensive content)"),
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('We only host "G-rated" images and so this field must be checked.')
|
||||
})
|
||||
"required": _(
|
||||
'We only host "G-rated" images and so this field must be checked.'
|
||||
)
|
||||
},
|
||||
)
|
||||
can_distribute = forms.BooleanField(
|
||||
label=_('can be freely copied'),
|
||||
label=_("can be freely copied"),
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('This field must be checked since we need to be able to distribute photos to third parties.')
|
||||
})
|
||||
"required": _(
|
||||
"This field must be checked since we need to be able to distribute photos to third parties."
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def save(request, data):
|
||||
'''
|
||||
"""
|
||||
Save the model and assign it to the current user
|
||||
'''
|
||||
"""
|
||||
# Link this file to the user's profile
|
||||
photo = Photo()
|
||||
photo.user = request.user
|
||||
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):
|
||||
'''
|
||||
"""
|
||||
Form to handle adding OpenID
|
||||
'''
|
||||
"""
|
||||
|
||||
openid = forms.URLField(
|
||||
label=_('OpenID'),
|
||||
label=_("OpenID"),
|
||||
min_length=MIN_LENGTH_URL,
|
||||
max_length=MAX_LENGTH_URL,
|
||||
initial='http://'
|
||||
initial="http://",
|
||||
)
|
||||
|
||||
def clean_openid(self):
|
||||
'''
|
||||
"""
|
||||
Enforce restrictions
|
||||
'''
|
||||
"""
|
||||
# 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))
|
||||
|
||||
# TODO: Domain restriction as in libravatar?
|
||||
return data
|
||||
url = urlsplit(self.cleaned_data["openid"])
|
||||
return urlunsplit(
|
||||
(
|
||||
url.scheme.lower(),
|
||||
url.netloc.lower(),
|
||||
url.path,
|
||||
url.query,
|
||||
url.fragment,
|
||||
)
|
||||
)
|
||||
|
||||
def save(self, user):
|
||||
'''
|
||||
"""
|
||||
Save the model, ensuring some safety
|
||||
'''
|
||||
"""
|
||||
if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
|
||||
openid=self.cleaned_data['openid']).exists():
|
||||
self.add_error('openid', _('OpenID already added and confirmed!'))
|
||||
openid=self.cleaned_data["openid"]
|
||||
).exists():
|
||||
self.add_error("openid", _("OpenID already added and confirmed!"))
|
||||
return False
|
||||
|
||||
if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member
|
||||
openid=self.cleaned_data['openid']).exists():
|
||||
self.add_error(
|
||||
'openid',
|
||||
_('OpenID already added, but not confirmed yet!'))
|
||||
openid=self.cleaned_data["openid"]
|
||||
).exists():
|
||||
self.add_error("openid", _("OpenID already added, but not confirmed yet!"))
|
||||
return False
|
||||
|
||||
unconfirmed = UnconfirmedOpenId()
|
||||
unconfirmed.openid = self.cleaned_data['openid']
|
||||
unconfirmed.openid = self.cleaned_data["openid"]
|
||||
unconfirmed.user = user
|
||||
unconfirmed.save()
|
||||
|
||||
@@ -165,40 +174,133 @@ class AddOpenIDForm(forms.Form):
|
||||
|
||||
|
||||
class UpdatePreferenceForm(forms.ModelForm):
|
||||
'''
|
||||
"""
|
||||
Form for updating user preferences
|
||||
'''
|
||||
"""
|
||||
|
||||
class Meta: # pylint: disable=too-few-public-methods
|
||||
'''
|
||||
"""
|
||||
Meta class for UpdatePreferenceForm
|
||||
'''
|
||||
"""
|
||||
|
||||
model = UserPreference
|
||||
fields = ['theme']
|
||||
fields = ["theme"]
|
||||
|
||||
|
||||
class UploadLibravatarExportForm(forms.Form):
|
||||
'''
|
||||
"""
|
||||
Form handling libravatar user export upload
|
||||
'''
|
||||
"""
|
||||
|
||||
export_file = forms.FileField(
|
||||
label=_('Export file'),
|
||||
error_messages={'required': _('You must choose an export file to upload.')})
|
||||
label=_("Export file"),
|
||||
error_messages={"required": _("You must choose an export file to upload.")},
|
||||
)
|
||||
not_porn = forms.BooleanField(
|
||||
label=_('suitable for all ages (i.e. no offensive content)'),
|
||||
label=_("suitable for all ages (i.e. no offensive content)"),
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('We only host "G-rated" images and so this field must be checked.')
|
||||
})
|
||||
"required": _(
|
||||
'We only host "G-rated" images and so this field must be checked.'
|
||||
)
|
||||
},
|
||||
)
|
||||
can_distribute = forms.BooleanField(
|
||||
label=_('can be freely copied'),
|
||||
label=_("can be freely copied"),
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('This field must be checked since we need to be able to\
|
||||
distribute photos to third parties.')
|
||||
})
|
||||
"required": _(
|
||||
"This field must be checked since we need to be able to\
|
||||
distribute photos to third parties."
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class GenerateAvatarForm(forms.Form):
|
||||
"""
|
||||
Form for generating avatars using AI text-to-image
|
||||
"""
|
||||
|
||||
MODEL_CHOICES = [
|
||||
("stable_diffusion", "Stable Diffusion"),
|
||||
# Future models can be added here
|
||||
]
|
||||
|
||||
prompt = forms.CharField(
|
||||
label=_("Avatar Description"),
|
||||
max_length=500,
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"rows": 3,
|
||||
"placeholder": _(
|
||||
'Describe the avatar you want to create, e.g., "A friendly robot with blue eyes"'
|
||||
),
|
||||
"id": "id_prompt",
|
||||
"data-token-limit": "77",
|
||||
"data-model": "stable_diffusion",
|
||||
}
|
||||
),
|
||||
help_text=_(
|
||||
"Describe the avatar you want to generate. Be specific about appearance, style, and mood.<br><small class='text-muted'>Stable Diffusion has a 77-token limit. Keep your description concise for best results.</small>"
|
||||
),
|
||||
)
|
||||
|
||||
model = forms.ChoiceField(
|
||||
label=_("AI Model"),
|
||||
choices=MODEL_CHOICES,
|
||||
initial="stable_diffusion",
|
||||
help_text=_("Select the AI model to use for generation."),
|
||||
)
|
||||
|
||||
quality = forms.ChoiceField(
|
||||
label=_("Generation Quality"),
|
||||
choices=[
|
||||
("low", _("Low (faster, lower quality)")),
|
||||
("medium", _("Medium (balanced)")),
|
||||
("high", _("High (slower, better quality)")),
|
||||
],
|
||||
initial="medium",
|
||||
help_text=_("Higher quality takes longer but produces better results."),
|
||||
)
|
||||
|
||||
not_porn = forms.BooleanField(
|
||||
label=_("Suitable for all ages (no offensive content)"),
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": _(
|
||||
'We only host "G-rated" images and so this field must be checked.'
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
can_distribute = forms.BooleanField(
|
||||
label=_("Can be freely copied"),
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": _(
|
||||
"This field must be checked since we need to be able to distribute photos to third parties."
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
def clean_prompt(self):
|
||||
"""Validate prompt length against token limits"""
|
||||
prompt = self.cleaned_data.get("prompt", "")
|
||||
model = self.cleaned_data.get("model", "stable_diffusion")
|
||||
|
||||
if prompt:
|
||||
from ivatar.ai_service import validate_avatar_prompt
|
||||
|
||||
validation = validate_avatar_prompt(prompt, model)
|
||||
|
||||
if not validation["valid"]:
|
||||
raise forms.ValidationError(validation["warning"])
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
class DeleteAccountForm(forms.Form):
|
||||
password = forms.CharField(label=_('Password'), required=False, widget=forms.PasswordInput())
|
||||
password = forms.CharField(
|
||||
label=_("Password"), required=False, widget=forms.PasswordInput()
|
||||
)
|
||||
|
||||
@@ -1,53 +1,51 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
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
|
||||
from ..settings import AVATAR_MAX_SIZE
|
||||
|
||||
|
||||
def get_photo(email):
|
||||
'''
|
||||
"""
|
||||
Fetch photo from Gravatar, given an email address
|
||||
'''
|
||||
hash_object = hashlib.new('md5')
|
||||
hash_object.update(email.lower().encode('utf-8'))
|
||||
thumbnail_url = 'https://secure.gravatar.com/avatar/' + \
|
||||
hash_object.hexdigest() + '?s=%i&d=404' % AVATAR_MAX_SIZE
|
||||
image_url = 'https://secure.gravatar.com/avatar/' + hash_object.hexdigest(
|
||||
) + '?s=512&d=404'
|
||||
"""
|
||||
hash_object = hashlib.new("md5")
|
||||
hash_object.update(email.lower().encode("utf-8"))
|
||||
thumbnail_url = (
|
||||
"https://secure.gravatar.com/avatar/"
|
||||
+ hash_object.hexdigest()
|
||||
+ "?s=%i&d=404" % AVATAR_MAX_SIZE
|
||||
)
|
||||
image_url = (
|
||||
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 {
|
||||
'thumbnail_url': thumbnail_url,
|
||||
'image_url': image_url,
|
||||
'width': AVATAR_MAX_SIZE,
|
||||
'height': AVATAR_MAX_SIZE,
|
||||
'service_url': service_url,
|
||||
'service_name': 'Gravatar'
|
||||
"thumbnail_url": thumbnail_url,
|
||||
"image_url": image_url,
|
||||
"width": AVATAR_MAX_SIZE,
|
||||
"height": AVATAR_MAX_SIZE,
|
||||
"service_url": service_url,
|
||||
"service_name": "Gravatar",
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-08 14:01
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ivatar.ivataraccount.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0009_alter_user_last_name_max_length'),
|
||||
('ivataraccount', '0014_auto_20190218_1602'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='APIKey',
|
||||
fields=[
|
||||
('public_key', models.CharField(default=ivatar.ivataraccount.models.random_api_key, max_length=32, unique=True)),
|
||||
('secret_key', models.CharField(default=ivatar.ivataraccount.models.random_api_key, max_length=32)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-11 10:16
|
||||
|
||||
from django.db import migrations
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.models import Permission
|
||||
from ivatar.ivataraccount.models import APIKey
|
||||
|
||||
def apply_migration(apps, schema_editor):
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
(group, created) = Group.objects.get_or_create(name='API-User')
|
||||
content_type = ContentType.objects.get_for_model(APIKey)
|
||||
(permission, created) = Permission.objects.get_or_create(
|
||||
codename='can_use_api',
|
||||
name='Can use the API',
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
def revert_migration(apps, schema_editor):
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
Group.objects.get(name='API-User').delete()
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
permission = Permission.objects.get(codename='can_use_api').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ivataraccount', '0015_apikey'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(apply_migration, revert_migration)
|
||||
]
|
||||
23
ivatar/ivataraccount/migrations/0016_auto_20210413_0904.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-13 09:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ivataraccount', '0015_auto_20200225_0934'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='unconfirmedemail',
|
||||
name='last_send_date',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unconfirmedemail',
|
||||
name='last_status',
|
||||
field=models.TextField(blank=True, max_length=2047, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-11 11:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def apply_migration(apps, schema_editor):
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
group = Group.objects.get(name='API-User')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
permission = Permission.objects.get(codename='can_use_api')
|
||||
group.permissions.add(permission)
|
||||
group.save()
|
||||
|
||||
def revert_migration(apps, schema_editor):
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
group = Group.objects.get(name='API-User')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
permission = Permission.objects.get(codename='can_use_api')
|
||||
group.permissions.remove(permission)
|
||||
group.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ivataraccount', '0016_auto_20190311_1016'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(apply_migration, revert_migration)
|
||||
]
|
||||
48
ivatar/ivataraccount/migrations/0017_auto_20210528_1314.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.2.3 on 2021-05-28 13:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ivataraccount', '0016_auto_20210413_0904'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='confirmedemail',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='confirmedopenid',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='openidassociation',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='openidnonce',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unconfirmedemail',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unconfirmedopenid',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
19
ivatar/ivataraccount/migrations/0018_alter_photo_format.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 5.0 on 2024-05-31 15:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("ivataraccount", "0017_auto_20210528_1314"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="photo",
|
||||
name="format",
|
||||
field=models.CharField(max_length=4),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 5.2.1 on 2025-09-17 10:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("ivataraccount", "0020_confirmedopenid_bluesky_handle"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="photo",
|
||||
name="ai_generated",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether this photo was generated by AI"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="photo",
|
||||
name="ai_model",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="The AI model used for generation",
|
||||
max_length=50,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="photo",
|
||||
name="ai_prompt",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="The prompt used to generate this image",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="photo",
|
||||
name="ai_quality",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="The quality setting used",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
80
ivatar/ivataraccount/migrations/0022_add_generation_task.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 5.2.1 on 2025-09-17 10:25
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("ivataraccount", "0021_add_ai_generation_fields"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GenerationTask",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"ip_address",
|
||||
models.GenericIPAddressField(null=True, unpack_ipv4=True),
|
||||
),
|
||||
("add_date", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("prompt", models.TextField()),
|
||||
("model", models.CharField(max_length=50)),
|
||||
("quality", models.CharField(max_length=20)),
|
||||
("allow_nsfw", models.BooleanField(default=False)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("pending", "Pending"),
|
||||
("processing", "Processing"),
|
||||
("completed", "Completed"),
|
||||
("failed", "Failed"),
|
||||
("cancelled", "Cancelled"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("progress", models.IntegerField(default=0)),
|
||||
("queue_position", models.IntegerField(default=0)),
|
||||
("task_id", models.CharField(blank=True, max_length=255, null=True)),
|
||||
("error_message", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"generated_photo",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="ivataraccount.photo",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Generation Task",
|
||||
"verbose_name_plural": "Generation Tasks",
|
||||
"ordering": ["-add_date"],
|
||||
},
|
||||
),
|
||||
]
|
||||
22
ivatar/ivataraccount/migrations/0023_add_ai_invalid_field.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 5.2.1 on 2025-09-17 10:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("ivataraccount", "0022_add_generation_task"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="photo",
|
||||
name="ai_invalid",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether this AI-generated image is invalid (black, etc.)",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +1,37 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Reading libravatar export
|
||||
'''
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import os
|
||||
from io import BytesIO
|
||||
import gzip
|
||||
import xml.etree.ElementTree
|
||||
import base64
|
||||
from PIL import Image
|
||||
import django
|
||||
import sys
|
||||
|
||||
sys.path.append(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..",
|
||||
"..",
|
||||
)
|
||||
)
|
||||
|
||||
SCHEMAROOT = 'https://www.libravatar.org/schemas/export/0.2'
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar.settings import SCHEMAROOT
|
||||
|
||||
|
||||
def read_gzdata(gzdata=None):
|
||||
'''
|
||||
"""
|
||||
Read gzipped data file
|
||||
'''
|
||||
emails = [] # pylint: disable=invalid-name
|
||||
openids = [] # pylint: disable=invalid-name
|
||||
"""
|
||||
photos = [] # pylint: disable=invalid-name
|
||||
username = None # pylint: disable=invalid-name
|
||||
password = None # pylint: disable=invalid-name
|
||||
@@ -26,59 +39,68 @@ def read_gzdata(gzdata=None):
|
||||
if not gzdata:
|
||||
return False
|
||||
|
||||
fh = gzip.open(BytesIO(gzdata), 'rb') # pylint: disable=invalid-name
|
||||
fh = gzip.open(BytesIO(gzdata), "rb") # pylint: disable=invalid-name
|
||||
content = fh.read()
|
||||
fh.close()
|
||||
root = xml.etree.ElementTree.fromstring(content)
|
||||
if not root.tag == '{%s}user' % SCHEMAROOT:
|
||||
print('Unknown export format: %s' % root.tag)
|
||||
if root.tag != "{%s}user" % SCHEMAROOT:
|
||||
print(f"Unknown export format: {root.tag}")
|
||||
exit(-1)
|
||||
|
||||
# Username
|
||||
for item in root.findall('{%s}account' % SCHEMAROOT)[0].items():
|
||||
if item[0] == 'username':
|
||||
for item in root.findall("{%s}account" % SCHEMAROOT)[0].items():
|
||||
if item[0] == "username":
|
||||
username = item[1]
|
||||
if item[0] == 'password':
|
||||
if item[0] == "password":
|
||||
password = item[1]
|
||||
|
||||
# Emails
|
||||
for email in root.findall('{%s}emails' % SCHEMAROOT)[0]:
|
||||
if email.tag == '{%s}email' % SCHEMAROOT:
|
||||
emails.append({'email': email.text, 'photo_id': email.attrib['photo_id']})
|
||||
|
||||
# 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:
|
||||
for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]:
|
||||
if photo.tag == "{%s}photo" % SCHEMAROOT:
|
||||
try:
|
||||
data = base64.decodebytes(bytes(photo.text, 'utf-8'))
|
||||
# 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")
|
||||
photo.text = photo.text.lstrip("b'")
|
||||
data = base64.decodebytes(bytes(photo.text, "utf-8"))
|
||||
except binascii.Error as exc:
|
||||
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
|
||||
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
|
||||
print(
|
||||
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))
|
||||
print(
|
||||
f'Cannot decode photo; Encoding: {photo.attrib["encoding"]}, Format: {photo.attrib["format"]}, Id: {photo.attrib["id"]}: {exc}'
|
||||
)
|
||||
continue
|
||||
else:
|
||||
# If it is a working image, we can use it
|
||||
photo.text.replace('\n', '')
|
||||
photos.append({
|
||||
'data': photo.text,
|
||||
'format': photo.attrib['format'],
|
||||
'id': photo.attrib['id'],
|
||||
})
|
||||
photo.text.replace("\n", "")
|
||||
photos.append(
|
||||
{
|
||||
"data": photo.text,
|
||||
"format": photo.attrib["format"],
|
||||
"id": photo.attrib["id"],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
'emails': emails,
|
||||
'openids': openids,
|
||||
'photos': photos,
|
||||
'username': username,
|
||||
'password': password,
|
||||
"emails": emails,
|
||||
"openids": openids,
|
||||
"photos": photos,
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
@@ -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 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 %}
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p>
|
||||
|
||||
@@ -17,15 +17,17 @@
|
||||
{% if form.email.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ form.email.errors }}</div>
|
||||
{% endif %}
|
||||
<div style="max-width:640px">
|
||||
<div class="form-container">
|
||||
<form action="{% url 'add_email' %}" name="addemail" method="post" id="form-addemail">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_email">{% trans 'Email' %}:</label>
|
||||
<input type="text" name="email" autofocus required class="form-control" id="id_email">
|
||||
<label for="id_email" class="form-label">{% trans 'Email' %}</label>
|
||||
<input type="email" name="email" autofocus required class="form-control" id="id_email" placeholder="{% trans 'Enter your email address' %}">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Add' %}</button>
|
||||
</div>
|
||||
<button type="submit" class="button">{% trans 'Add' %}</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -4,65 +4,81 @@
|
||||
{% block title %}{% blocktrans with email.email as email_address %}Choose a photo for {{ email_address }}{% endblocktrans %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.nobutton {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
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 %}
|
||||
{% 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="photo-grid">
|
||||
{% for photo in user.photo_set.all %}
|
||||
<form action="{% url 'assign_photo_email' view.kwargs.email_id %}" method="post" class="photo-card">{% 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 panel-tortin">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</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">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="photo-grid">
|
||||
<form action="{% url 'assign_photo_email' view.kwargs.email_id %}" method="post" class="photo-card">{% csrf_token %}
|
||||
<button type="submit" name="photoNone" class="nobutton">
|
||||
<div class="panel panel-tortin">
|
||||
<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>
|
||||
</div>
|
||||
<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="{% url 'assign_photo_email' view.kwargs.email_id %}" method="post" class="photo-card">{% csrf_token %}
|
||||
<input type="hidden" name="photo_id" value="bluesky">
|
||||
<button type="submit" name="photoBluesky" class="nobutton">
|
||||
<div class="panel panel-tortin">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if email.bluesky_handle %}<i class="fa fa-check"></i>{% endif %} {% 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>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<a href="{% url 'upload_photo' %}" class="btn btn-primary">{% blocktrans %}Upload a new one{% endblocktrans %}</a>
|
||||
<a href="{% url 'import_photo' %}" class="btn btn-secondary">{% blocktrans %}Import from other services{% endblocktrans %}</a>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<form action="{% url 'assign_bluesky_handle_to_email' view.kwargs.email_id %}" method="post">{% 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="btn btn-primary">{% trans 'Assign Bluesky Handle' %}</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>
|
||||
{% endif %}
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -4,65 +4,78 @@
|
||||
{% block title %}{% blocktrans with openid.openid as openid_address %}Choose a photo for {{ openid_address }}{% endblocktrans %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.nobutton {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
</style>
|
||||
<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 %}
|
||||
{% if user.photo_set.count %}
|
||||
<p>{% trans 'Here are the pictures you have uploaded, click on the one you wish to associate with this openid address:' %}</p>
|
||||
<div class="photo-grid">
|
||||
{% for photo in user.photo_set.all %}
|
||||
<form action="{% url 'assign_photo_openid' view.kwargs.openid_id %}" method="post" class="photo-card">{% 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 panel-tortin">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</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">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="photo-grid">
|
||||
<form action="{% url 'assign_photo_openid' view.kwargs.openid_id %}" method="post" class="photo-card">{% csrf_token %}
|
||||
<button type="submit" name="photoNone" class="nobutton">
|
||||
<div class="panel panel-tortin">
|
||||
<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>
|
||||
</div>
|
||||
<h3 class="panel-title">{% if not openid.photo and not openid.bluesky_handle %}<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>
|
||||
{% if openid.bluesky_handle %}
|
||||
<form action="" class="photo-card">
|
||||
<div class="panel panel-tortin">
|
||||
<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" openid.digest %}?size=100">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<a href="{% url 'upload_photo' %}" class="btn btn-primary">{% blocktrans %}upload a new one{% endblocktrans %}</a>
|
||||
<a href="{% url 'import_photo' %}" class="btn btn-secondary">{% blocktrans %}Import from other services{% endblocktrans %}</a>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<form action="{% url 'assign_bluesky_handle_to_openid' view.kwargs.openid_id %}" method="post">{% 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="btn btn-primary">{% trans 'Assign Bluesky Handle' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div style="height:8px"></div>
|
||||
<a href="{% url 'upload_photo' %}" class="button">{% blocktrans %}upload a new one{% endblocktrans %}</a>
|
||||
|
||||
{% endif %}
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
|
||||
318
ivatar/ivataraccount/templates/avatar_gallery.html
Normal file
@@ -0,0 +1,318 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block title %}{% trans 'Avatar Gallery' %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.avatar-gallery-card {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.avatar-gallery-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.avatar-image-container {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.gallery-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.gallery-section h4 {
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Fix button display issues - override base CSS */
|
||||
.btn {
|
||||
display: inline-block !important;
|
||||
padding: 0.375rem 0.75rem !important;
|
||||
margin-bottom: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0.2rem;
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: #007bff !important;
|
||||
border-color: #007bff !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
color: #fff !important;
|
||||
background-color: #007bff !important;
|
||||
border-color: #007bff !important;
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
color: #6c757d !important;
|
||||
border-color: #6c757d !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover {
|
||||
color: #fff !important;
|
||||
background-color: #6c757d !important;
|
||||
border-color: #6c757d !important;
|
||||
}
|
||||
|
||||
/* Ensure FontAwesome icons display */
|
||||
.fa {
|
||||
font-family: "FontAwesome" !important;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Suppress Firefox font warnings */
|
||||
@font-face {
|
||||
font-family: "FontAwesome";
|
||||
src: url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),
|
||||
url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),
|
||||
url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),
|
||||
url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>{% trans 'Avatar Gallery' %}</h2>
|
||||
<p class="lead">
|
||||
{% trans 'Browse recently generated avatars for inspiration. Click on any avatar to reuse its prompt.' %}
|
||||
</p>
|
||||
|
||||
<!-- User's Own Avatars -->
|
||||
{% if user_avatars %}
|
||||
<div class="mb-4 gallery-section">
|
||||
<h4><i class="fa fa-user"></i> {% trans 'Your Recent Avatars' %}</h4>
|
||||
<div class="row">
|
||||
{% for avatar in user_avatars %}
|
||||
<div class="col-md-3 col-sm-4 col-6 mb-3">
|
||||
<div class="card h-100 avatar-gallery-card">
|
||||
<div class="card-body p-2">
|
||||
<div class="avatar-image-container text-center d-flex justify-content-center align-items-center">
|
||||
<img src="{% url 'raw_image' avatar.pk %}"
|
||||
alt="Avatar"
|
||||
class="img-fluid rounded avatar-image"
|
||||
style="width: 120px; height: 120px; object-fit: contain;">
|
||||
</div>
|
||||
<div class="card-text">
|
||||
<small class="text-muted">
|
||||
{{ avatar.add_date|date:"M d, Y" }}
|
||||
</small>
|
||||
<p class="small mb-2" style="height: 40px; overflow: hidden;">
|
||||
{{ avatar.ai_prompt|truncatechars:60 }}
|
||||
</p>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'reuse_prompt' avatar.pk %}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="fa fa-recycle"></i> {% trans 'Reuse' %}
|
||||
</a>
|
||||
<a href="{% url 'avatar_preview' avatar.pk %}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fa fa-eye"></i> {% trans 'View' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Community Gallery -->
|
||||
<div class="mb-4 gallery-section">
|
||||
<h4><i class="fa fa-users"></i> {% trans 'Community Gallery' %}</h4>
|
||||
<p class="text-muted">
|
||||
{% trans 'Recently generated avatars from all users (last 30)' %}
|
||||
</p>
|
||||
|
||||
{% if avatars %}
|
||||
<div class="row">
|
||||
{% for avatar in avatars %}
|
||||
<div class="col-md-3 col-sm-4 col-6 mb-3">
|
||||
<div class="card h-100 avatar-gallery-card">
|
||||
<div class="card-body p-2">
|
||||
<div class="avatar-image-container text-center d-flex justify-content-center align-items-center">
|
||||
<img src="{% url 'raw_image' avatar.pk %}"
|
||||
alt="Avatar"
|
||||
class="img-fluid rounded avatar-image"
|
||||
style="width: 120px; height: 120px; object-fit: contain;">
|
||||
</div>
|
||||
<div class="card-text">
|
||||
<small class="text-muted">
|
||||
{{ avatar.add_date|date:"M d, Y" }}
|
||||
{% if avatar.user == user %}
|
||||
<span class="badge badge-info">{% trans 'Yours' %}</span>
|
||||
{% endif %}
|
||||
</small>
|
||||
<p class="small mb-2" style="height: 40px; overflow: hidden;">
|
||||
{{ avatar.ai_prompt|truncatechars:60 }}
|
||||
</p>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'reuse_prompt' avatar.pk %}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="fa fa-recycle"></i> {% trans 'Reuse' %}
|
||||
</a>
|
||||
<a href="{% url 'avatar_preview' avatar.pk %}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fa fa-eye"></i> {% trans 'View' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Gallery pagination">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">{% trans 'First' %}</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">{% trans 'Previous' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">
|
||||
{% trans 'Page' %} {{ page_obj.number }} {% trans 'of' %} {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">{% trans 'Next' %}</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">{% trans 'Last' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fa fa-image fa-3x text-muted mb-3"></i>
|
||||
<h5>{% trans 'No Avatars Yet' %}</h5>
|
||||
<p class="text-muted">
|
||||
{% trans 'No AI-generated avatars have been created yet. Be the first to generate one!' %}
|
||||
</p>
|
||||
<a href="{% url 'generate_avatar' %}" class="btn btn-primary">
|
||||
<i class="fa fa-magic"></i> {% trans 'Generate Avatar' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="text-center mb-4">
|
||||
<a href="{% url 'generate_avatar' %}" class="btn btn-primary btn-lg">
|
||||
<i class="fa fa-magic"></i> {% trans 'Generate New Avatar' %}
|
||||
</a>
|
||||
<a href="{% url 'profile' %}" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="fa fa-user"></i> {% trans 'Back to Profile' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tips -->
|
||||
<div class="mt-4">
|
||||
<h5>{% trans 'Gallery Tips' %}</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Click "Reuse" to copy a prompt and modify it for your own avatar' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Click "View" to see the full avatar and assign it to your emails' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Use the gallery to find inspiration for your own avatar descriptions' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Your own avatars are shown at the top for quick access' %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add click animation to reuse buttons
|
||||
const reuseButtons = document.querySelectorAll('a[href*="reuse_prompt"]');
|
||||
reuseButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> {% trans "Loading..." %}';
|
||||
this.classList.add('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
// Add click animation to view buttons
|
||||
const viewButtons = document.querySelectorAll('a[href*="avatar_preview"]');
|
||||
viewButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> {% trans "Loading..." %}';
|
||||
this.classList.add('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
198
ivatar/ivataraccount/templates/avatar_preview.html
Normal file
@@ -0,0 +1,198 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block title %}{% trans 'Avatar Preview' %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<h2>{% trans 'Avatar Preview' %}</h2>
|
||||
<p class="lead">
|
||||
{% trans 'Here\'s your generated avatar. You can refine it or assign it to your email addresses.' %}
|
||||
</p>
|
||||
|
||||
<!-- Avatar Display -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body text-center">
|
||||
<h4>{% trans 'Generated Avatar' %}</h4>
|
||||
<div class="avatar-preview-container mb-3">
|
||||
<img src="{{ photo_url }}" alt="Generated Avatar" class="img-fluid rounded" style="max-width: 400px; max-height: 400px;">
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
{% trans 'Generated on' %} {{ photo.add_date|date:"F d, Y \a\t H:i" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Refinement Form -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa fa-magic"></i> {% trans 'Refine Avatar' %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
{% trans 'Not satisfied with the result? Modify your description and generate a new avatar.' %}
|
||||
</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Custom form rendering for better control -->
|
||||
<div class="form-group">
|
||||
<label for="id_prompt">{{ form.prompt.label }}</label>
|
||||
<textarea name="prompt"
|
||||
id="id_prompt"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
maxlength="500"
|
||||
data-token-limit="77"
|
||||
data-model="stable_diffusion"
|
||||
placeholder="{{ form.prompt.field.widget.attrs.placeholder }}"
|
||||
required>{{ form.prompt.value|default:'' }}</textarea>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.prompt.help_text|safe }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_model">{{ form.model.label }}</label>
|
||||
<select name="model" id="id_model" class="form-control">
|
||||
{% for value, label in form.model.field.choices %}
|
||||
<option value="{{ value }}" {% if value == form.model.value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">{{ form.model.help_text }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_quality">{{ form.quality.label }}</label>
|
||||
<select name="quality" id="id_quality" class="form-control">
|
||||
{% for value, label in form.quality.field.choices %}
|
||||
<option value="{{ value }}" {% if value == form.quality.value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">{{ form.quality.help_text }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_not_porn">{{ form.not_porn.label }}</label>
|
||||
<input type="checkbox" name="not_porn" class="form-control" required id="id_not_porn" {% if form.not_porn.value %}checked{% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_can_distribute">{{ form.can_distribute.label }}</label>
|
||||
<input type="checkbox" name="can_distribute" class="form-control" required id="id_can_distribute" {% if form.can_distribute.value %}checked{% endif %}>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-refresh"></i> {% trans 'Regenerate Avatar' %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assignment Options -->
|
||||
{% if confirmed_emails %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa fa-envelope"></i> {% trans 'Assign to Email Addresses' %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
{% trans 'Assign this avatar to one or more of your confirmed email addresses.' %}
|
||||
</p>
|
||||
|
||||
<div class="list-group">
|
||||
{% for email in confirmed_emails %}
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{{ email.email }}</strong>
|
||||
{% if email.photo_id == photo.pk %}
|
||||
<span class="badge badge-success ml-2">{% trans 'Currently assigned' %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{% if email.photo_id != photo.pk %}
|
||||
<a href="{% url 'assign_photo_email' email.pk %}?photo_id={{ photo.pk }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="fa fa-link"></i> {% trans 'Assign' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-success">
|
||||
<i class="fa fa-check"></i> {% trans 'Assigned' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-body text-center">
|
||||
<h5>{% trans 'No Email Addresses' %}</h5>
|
||||
<p class="text-muted">
|
||||
{% trans 'You need to add and confirm email addresses before you can assign avatars to them.' %}
|
||||
</p>
|
||||
<a href="{% url 'add_email' %}" class="btn btn-primary">
|
||||
<i class="fa fa-plus"></i> {% trans 'Add Email Address' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<a href="{% url 'generate_avatar' %}" class="btn btn-outline-primary btn-block">
|
||||
<i class="fa fa-plus"></i> {% trans 'Generate New Avatar' %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<a href="{% url 'profile' %}" class="btn btn-outline-secondary btn-block">
|
||||
<i class="fa fa-user"></i> {% trans 'Back to Profile' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tips -->
|
||||
<div class="mt-4">
|
||||
<h5>{% trans 'Tips for Better Results' %}</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Be more specific: "A friendly robot with blue LED eyes and silver metallic body"' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Add style keywords: "cartoon style", "realistic", "anime", "pixel art"' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Include mood: "cheerful", "serious", "mysterious", "professional"' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Specify lighting: "soft lighting", "dramatic shadows", "bright and clear"' %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% endblock %}
|
||||
@@ -23,7 +23,7 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}
|
||||
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
|
||||
{% for email in emails %}
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email }}</label>
|
||||
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email.email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email.email }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<button type="submit" class="btn btn-danger">{% trans 'Yes, delete all of my stuff' %}</button>
|
||||
|
||||
<button type="cancel" class="button" href="{% url 'profile' %}">{% trans 'Cancel' %}</button>
|
||||
<a href="{% url 'profile' %}" class="btn btn-secondary">{% trans 'Cancel' %}</a>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
20
ivatar/ivataraccount/templates/export.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Export your data' %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{% trans 'Export your data' %}</h1>
|
||||
|
||||
<p>{% trans 'Libravatar will now export all of your personal data to a compressed XML file.' %}</p>
|
||||
|
||||
<form action="{% url 'export' %}" method="post" name="export">{% csrf_token %}
|
||||
|
||||
<p><button type="submit" class="button">{% trans 'Export' %}</button>
|
||||
<a href="{% url 'profile' %}" class="button">{% trans 'Cancel' %}</a>
|
||||
</p>
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock content %}
|
||||
145
ivatar/ivataraccount/templates/generate_avatar.html
Normal file
@@ -0,0 +1,145 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block title %}{% trans 'Generate AI Avatar' %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<h2>{% trans 'Generate AI Avatar' %}</h2>
|
||||
<p class="lead">
|
||||
{% trans 'Create a unique avatar using artificial intelligence. Describe what you want and our AI will generate it for you.' %}
|
||||
</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Custom form rendering for better control -->
|
||||
<div class="form-group">
|
||||
<label for="id_prompt">{{ form.prompt.label }}</label>
|
||||
<textarea name="prompt"
|
||||
id="id_prompt"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
maxlength="500"
|
||||
data-token-limit="77"
|
||||
data-model="stable_diffusion"
|
||||
placeholder="{{ form.prompt.field.widget.attrs.placeholder }}"
|
||||
required>{{ form.prompt.value|default:'' }}</textarea>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.prompt.help_text|safe }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_model">{{ form.model.label }}</label>
|
||||
<select name="model" id="id_model" class="form-control">
|
||||
{% for value, label in form.model.field.choices %}
|
||||
<option value="{{ value }}" {% if value == form.model.value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">{{ form.model.help_text }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_quality">{{ form.quality.label }}</label>
|
||||
<select name="quality" id="id_quality" class="form-control">
|
||||
{% for value, label in form.quality.field.choices %}
|
||||
<option value="{{ value }}" {% if value == form.quality.value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">{{ form.quality.help_text }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_not_porn">{{ form.not_porn.label }}</label>
|
||||
<input type="checkbox" name="not_porn" class="form-control" required id="id_not_porn" {% if form.not_porn.value %}checked{% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_can_distribute">{{ form.can_distribute.label }}</label>
|
||||
<input type="checkbox" name="can_distribute" class="form-control" required id="id_can_distribute" {% if form.can_distribute.value %}checked{% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="fa fa-magic"></i> {% trans 'Generate Avatar' %}
|
||||
</button>
|
||||
<a href="{% url 'profile' %}" class="btn btn-secondary btn-lg">
|
||||
{% trans 'Cancel' %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4>{% trans 'Tips for Better Results' %}</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Be specific about appearance, style, and mood' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Include details like hair color, clothing, or facial expressions' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Try different art styles: "cartoon", "realistic", "anime", "pixel art"' %}
|
||||
</li>
|
||||
<li><i class="fa fa-lightbulb-o text-warning"></i>
|
||||
{% trans 'Keep descriptions appropriate for all ages' %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4>{% trans 'Example Prompts' %}</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6>{% trans 'Character Avatar' %}</h6>
|
||||
<p class="text-muted small">
|
||||
"A friendly robot with blue eyes and a silver body, cartoon style"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6>{% trans 'Abstract Avatar' %}</h6>
|
||||
<p class="text-muted small">
|
||||
"Colorful geometric shapes forming a face, modern art style"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 alert alert-info">
|
||||
<h5><i class="fa fa-info-circle"></i> {% trans 'Important Notes' %}</h5>
|
||||
<ul class="mb-0">
|
||||
<li>{% trans 'Avatar generation may take 30-60 seconds depending on server load' %}</li>
|
||||
<li>{% trans 'Generated avatars are automatically saved to your account' %}</li>
|
||||
<li>{% trans 'You can assign the generated avatar to any of your email addresses' %}</li>
|
||||
<li>{% trans 'All generated content must be appropriate for all ages' %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% endblock %}
|
||||
405
ivatar/ivataraccount/templates/generation_status.html
Normal file
@@ -0,0 +1,405 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans 'Avatar Generation Status' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<h2>{% trans 'Avatar Generation Status' %}</h2>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa fa-magic"></i> {% trans 'Generation Task' %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>{% trans 'Prompt:' %}</strong></p>
|
||||
<p class="text-muted">{{ task.prompt }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>{% trans 'Model:' %}</strong> {{ task.model|title }}</p>
|
||||
<p><strong>{% trans 'Quality:' %}</strong> {{ task.quality|title }}</p>
|
||||
<p><strong>{% trans 'Status:' %}</strong>
|
||||
<span class="badge badge-{% if task.status == 'completed' %}success{% elif task.status == 'failed' %}danger{% elif task.status == 'processing' %}warning{% else %}secondary{% endif %} status-badge">
|
||||
{{ task.get_status_display }}
|
||||
{% if task.status == 'processing' %}
|
||||
<i class="fa fa-spinner fa-spin ml-1"></i>
|
||||
{% elif task.status == 'pending' %}
|
||||
<i class="fa fa-clock ml-1"></i>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="live-indicator ml-2">
|
||||
<i class="fa fa-circle text-success"></i>
|
||||
<small class="text-muted">Live</small>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if task.status == 'processing' %}
|
||||
<div class="mt-3">
|
||||
<div class="progress-container">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar"
|
||||
style="width: {{ task.progress }}%"
|
||||
aria-valuenow="{{ task.progress }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
{{ task.progress }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-info mt-2">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">
|
||||
<i class="fa fa-magic"></i> {% trans 'Generating your avatar...' %}
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<small class="text-muted">
|
||||
<i class="fa fa-refresh fa-spin"></i>
|
||||
<span class="last-updated">{% trans 'Updated just now' %}</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif task.status == 'pending' %}
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-info">
|
||||
<i class="fa fa-clock"></i>
|
||||
{% trans 'Your avatar is in the queue. Position:' %} <strong>{{ queue_position }}</strong>
|
||||
{% if queue_length > 1 %}
|
||||
{% trans 'out of' %} {{ queue_length }} {% trans 'tasks' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||
0%
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">{% trans 'Waiting in queue...' %}</small>
|
||||
</div>
|
||||
{% elif task.status == 'completed' %}
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-success">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
{% trans 'Avatar generated successfully!' %}
|
||||
</div>
|
||||
{% if task.generated_photo %}
|
||||
<div class="text-center">
|
||||
<a href="{% url 'avatar_preview' task.generated_photo.pk %}" class="btn btn-primary btn-lg">
|
||||
<i class="fa fa-eye"></i> {% trans 'View Avatar' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif task.status == 'failed' %}
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
{% trans 'Avatar generation failed.' %}
|
||||
{% if task.error_message %}
|
||||
<br><small>{{ task.error_message }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="{% url 'generate_avatar' %}" class="btn btn-primary">
|
||||
<i class="fa fa-redo"></i> {% trans 'Try Again' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa fa-list"></i> {% trans 'Queue Information' %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-primary">{{ processing_count }}</h3>
|
||||
<p class="text-muted">{% trans 'Currently Processing' %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-warning">{{ queue_length }}</h3>
|
||||
<p class="text-muted">{% trans 'Pending Tasks' %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-info">2</h3>
|
||||
<p class="text-muted">{% trans 'Max Parallel Jobs' %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Your Recent Tasks -->
|
||||
{% if user_tasks %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa fa-history"></i> {% trans 'Your Recent Tasks' %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Prompt' %}</th>
|
||||
<th>{% trans 'Status' %}</th>
|
||||
<th>{% trans 'Created' %}</th>
|
||||
<th>{% trans 'Actions' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user_task in user_tasks %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-truncate d-inline-block" style="max-width: 200px;" title="{{ user_task.prompt }}">
|
||||
{{ user_task.prompt }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-{% if user_task.status == 'completed' %}success{% elif user_task.status == 'failed' %}danger{% elif user_task.status == 'processing' %}warning{% else %}secondary{% endif %}">
|
||||
{{ user_task.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ user_task.add_date|date:"M d, H:i" }}</td>
|
||||
<td>
|
||||
{% if user_task.status == 'completed' and user_task.generated_photo %}
|
||||
<a href="{% url 'avatar_preview' user_task.generated_photo.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
{% elif user_task.status == 'failed' %}
|
||||
<a href="{% url 'generate_avatar' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fa fa-redo"></i>
|
||||
</a>
|
||||
{% elif user_task.status == 'pending' or user_task.status == 'processing' %}
|
||||
<a href="{% url 'generation_status' user_task.pk %}" class="btn btn-sm btn-outline-info">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="{% url 'generate_avatar' %}" class="btn btn-primary btn-lg">
|
||||
<i class="fa fa-plus-circle"></i> {% trans 'Generate New Avatar' %}
|
||||
</a>
|
||||
<a href="{% url 'profile' %}" class="btn btn-secondary btn-lg">
|
||||
<i class="fa fa-user"></i> {% trans 'Back to Profile' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const taskId = {{ task.pk }};
|
||||
let refreshInterval;
|
||||
|
||||
function updateStatus() {
|
||||
fetch(`/accounts/api/task_status/${taskId}/`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error('Error fetching status:', data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update status badge with live indicators
|
||||
const statusBadge = document.querySelector('.status-badge');
|
||||
if (statusBadge) {
|
||||
statusBadge.textContent = data.status_display;
|
||||
statusBadge.className = 'badge badge-' +
|
||||
(data.status === 'completed' ? 'success' :
|
||||
data.status === 'failed' ? 'danger' :
|
||||
data.status === 'processing' ? 'warning' : 'secondary') + ' status-badge';
|
||||
|
||||
// Add appropriate icons
|
||||
const existingIcon = statusBadge.querySelector('i');
|
||||
if (existingIcon) existingIcon.remove();
|
||||
|
||||
if (data.status === 'processing') {
|
||||
statusBadge.innerHTML += ' <i class="fa fa-spinner fa-spin ml-1"></i>';
|
||||
} else if (data.status === 'pending') {
|
||||
statusBadge.innerHTML += ' <i class="fa fa-clock ml-1"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
// Update live indicator
|
||||
const liveIndicator = document.querySelector('.live-indicator i');
|
||||
if (liveIndicator) {
|
||||
liveIndicator.classList.remove('text-success', 'text-warning', 'text-danger');
|
||||
if (data.status === 'completed') {
|
||||
liveIndicator.classList.add('text-success');
|
||||
} else if (data.status === 'failed') {
|
||||
liveIndicator.classList.add('text-danger');
|
||||
} else {
|
||||
liveIndicator.classList.add('text-warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Update last updated timestamp
|
||||
const lastUpdated = document.querySelector('.last-updated');
|
||||
if (lastUpdated) {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString();
|
||||
lastUpdated.textContent = `Updated at ${timeString}`;
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.querySelector('.progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = data.progress + '%';
|
||||
progressBar.setAttribute('aria-valuenow', data.progress);
|
||||
progressBar.textContent = data.progress + '%';
|
||||
}
|
||||
|
||||
// Update queue information
|
||||
const queueInfo = document.querySelector('.alert-info');
|
||||
if (queueInfo && data.status === 'pending') {
|
||||
queueInfo.innerHTML = `
|
||||
<i class="fa fa-clock"></i>
|
||||
Your avatar is in the queue. Position: <strong>${data.queue_position}</strong>
|
||||
${data.queue_length > 1 ? `out of ${data.queue_length} tasks` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
// Update queue stats
|
||||
const processingCount = document.querySelector('.text-primary');
|
||||
const pendingCount = document.querySelector('.text-warning');
|
||||
if (processingCount) processingCount.textContent = data.processing_count;
|
||||
if (pendingCount) pendingCount.textContent = data.queue_length;
|
||||
|
||||
// Handle completion
|
||||
if (data.status === 'completed') {
|
||||
clearInterval(refreshInterval);
|
||||
if (data.generated_photo_id) {
|
||||
// Redirect to avatar preview
|
||||
setTimeout(() => {
|
||||
window.location.href = `/accounts/avatar_preview/${data.generated_photo_id}/`;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle failure
|
||||
if (data.status === 'failed') {
|
||||
clearInterval(refreshInterval);
|
||||
const errorDiv = document.querySelector('.alert-danger');
|
||||
if (errorDiv && data.error_message) {
|
||||
errorDiv.innerHTML = `
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
Avatar generation failed.
|
||||
<br><small>${data.error_message}</small>
|
||||
`;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Start auto-refresh if task is pending or processing
|
||||
const taskStatus = '{{ task.status }}';
|
||||
if (taskStatus === 'pending' || taskStatus === 'processing') {
|
||||
refreshInterval = setInterval(updateStatus, 1000); // Update every 1 second for live updates
|
||||
}
|
||||
|
||||
// Add visual feedback for status changes
|
||||
const statusBadge = document.querySelector('.badge');
|
||||
if (statusBadge && taskStatus === 'processing') {
|
||||
statusBadge.classList.add('pulse');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.progress-bar-animated {
|
||||
animation: progress-bar-stripes 1s linear infinite;
|
||||
}
|
||||
|
||||
.live-indicator i {
|
||||
animation: live-pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes live-pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-weight: 500;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Enhanced progress bar */
|
||||
.progress {
|
||||
height: 25px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: linear-gradient(45deg, #007bff, #0056b3);
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
@keyframes progress-bar-stripes {
|
||||
0% { background-position: 1rem 0; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -5,28 +5,28 @@
|
||||
|
||||
{% 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">
|
||||
<div style="max-width:640px">
|
||||
<form action="{% url 'import_photo' %}" method="get" id="check_mail_form">
|
||||
<div class="form-group">
|
||||
<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">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="button">{% trans 'Check' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('check_mail_form').onsubmit =
|
||||
@@ -35,7 +35,7 @@ input[type=checkbox]:checked + label:before {letter-spacing: 3px}
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include '_import_photo_form.html' %}
|
||||
|
||||
@@ -18,24 +18,28 @@
|
||||
{% if form.password.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ form.password.errors }}</div>
|
||||
{% endif %}
|
||||
<div style="max-width:700px">
|
||||
<div class="form-container">
|
||||
<form action="{% url 'login' %}" method="post" name="login">
|
||||
{% csrf_token %}
|
||||
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
||||
<div class="form-group">
|
||||
<label for="id_username">{% trans 'Username' %}:</label>
|
||||
<input type="text" name="username" autofocus required class="form-control" id="id_username">
|
||||
<label for="id_username" class="form-label">{% trans 'Username' %}</label>
|
||||
<input type="text" name="username" autofocus required class="form-control" id="id_username" placeholder="{% trans 'Enter your username' %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_password">{% trans 'Password' %}:</label>
|
||||
<input type="password" name="password" class="form-control" required id="id_password">
|
||||
<label for="id_password" class="form-label">{% trans 'Password' %}</label>
|
||||
<input type="password" name="password" class="form-control" required id="id_password" placeholder="{% trans 'Enter your password' %}">
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Login' %}</button>
|
||||
<a href="{% url 'openid-login' %}" class="btn btn-secondary">{% trans 'Login with OpenID' %}</a>
|
||||
{% if with_fedora %}
|
||||
<a href="{% url "social:begin" "fedora" %}" class="btn btn-secondary">{% trans 'Login with Fedora' %}</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'new_account' %}" class="btn btn-secondary">{% trans 'Create new user' %}</a>
|
||||
<a href="{% url 'password_reset' %}" class="btn btn-secondary">{% trans 'Password reset' %}</a>
|
||||
</div>
|
||||
<button type="submit" class="button">{% trans 'Login' %}</button>
|
||||
|
||||
<a href="{% url 'openid-login' %}" class="button">{% trans 'Login with OpenID' %}</a>
|
||||
|
||||
<a href="{% url 'new_account' %}" class="button">{% trans 'Create new user' %}</a>
|
||||
|
||||
<a href="{% url 'password_reset' %}" class="button">{% trans 'Password reset' %}</a>
|
||||
</form>
|
||||
</div>
|
||||
<div style="height:40px"></div>
|
||||
|
||||
@@ -16,22 +16,25 @@
|
||||
{% if form.password2.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ form.password2.errors }}</div>
|
||||
{% endif %}
|
||||
<div class="form-container">
|
||||
<form action="{% url 'new_account' %}" method="post" name="newaccount">
|
||||
{% csrf_token %}
|
||||
<div style="max-width:640px">
|
||||
<div class="form-group">
|
||||
<label for="id_username">{% trans 'Username' %}:</label>
|
||||
<input type="text" name="username" autofocus required class="form-control" id="id_username">
|
||||
<label for="id_username" class="form-label">{% trans 'Username' %}</label>
|
||||
<input type="text" name="username" autofocus required class="form-control" id="id_username" placeholder="{% trans 'Choose a username' %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_password1">{% trans 'Password' %}:</label>
|
||||
<input type="password" name="password1" class="form-control" required id="id_password1">
|
||||
<label for="id_password1" class="form-label">{% trans 'Password' %}</label>
|
||||
<input type="password" name="password1" class="form-control" required id="id_password1" placeholder="{% trans 'Enter a secure password' %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_password2">{% trans 'Password confirmation' %}:</label>
|
||||
<input type="password" name="password2" class="form-control" required id="id_password2">
|
||||
<label for="id_password2" class="form-label">{% trans 'Password confirmation' %}</label>
|
||||
<input type="password" name="password2" class="form-control" required id="id_password2" placeholder="{% trans 'Confirm your password' %}">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Create account' %}</button>
|
||||
<a href="/accounts/login/" class="btn btn-secondary">{% trans 'Login' %}</a>
|
||||
</div>
|
||||
<button type="submit" class="button">{% trans 'Create account' %}</button> or <a href="/accounts/login/" class="button">{% trans 'Login' %}</a>
|
||||
</form>
|
||||
</div>
|
||||
<div style="height:40px"></div>
|
||||
|
||||
@@ -8,17 +8,18 @@
|
||||
<h1>{% trans 'Reset password' %}</h1>
|
||||
|
||||
<p>{% trans 'To continue with the password reset, enter one of the email addresses associated with your account.' %}</p>
|
||||
<div style="max-width:640px">
|
||||
<div class="form-container">
|
||||
<form action="" method="post" name="reset">{% csrf_token %}
|
||||
|
||||
{{ form.email.errors }}
|
||||
<div class="form-group">
|
||||
<label for="id_email">{% trans 'Email' %}:</label>
|
||||
<input type="text" name="email" autofocus required class="form-control" id="id_email">
|
||||
<label for="id_email" class="form-label">{% trans 'Email' %}</label>
|
||||
<input type="email" name="email" autofocus required class="form-control" id="id_email" placeholder="{% trans 'Enter your email address' %}">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button">{% trans 'Reset my password' %}</button>
|
||||
<button type="cancel" class="button" href="{% url 'profile' %}">{% trans 'Cancel' %}</button>
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Reset my password' %}</button>
|
||||
<a href="{% url 'profile' %}" class="btn btn-secondary">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,35 @@
|
||||
{% block content %}
|
||||
<h1>{% trans 'Account settings' %}</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_email">{% trans 'Your email' %}:</label>
|
||||
<input type="text" name="email" disabled class="form-control" value="{{ user.email }}" id="id_email" style="max-width:600px;">
|
||||
<div class="form-container">
|
||||
<label for="id_username" class="form-label">{% trans 'Username' %}</label>
|
||||
<input type="text" name="username" class="form-control" id="id_username" disabled value="{{ user.username }}">
|
||||
|
||||
<form action="{% url 'user_preference' %}" method="post">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="id_first_name" class="form-label">{% trans 'Firstname' %}</label>
|
||||
<input type="text" name="first_name" class="form-control" id="id_first_name" value="{{ user.first_name }}" placeholder="{% trans 'Enter your first name' %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_last_name" class="form-label">{% trans 'Lastname' %}</label>
|
||||
<input type="text" name="last_name" class="form-control" id="id_last_name" value="{{ user.last_name }}" placeholder="{% trans 'Enter your last name' %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_email" class="form-label">{% trans 'E-mail address' %}</label>
|
||||
<select name="email" class="form-control" id="id_email">
|
||||
<option value="{{ user.email }}" selected>{{ user.email }}</option>
|
||||
{% for confirmed_email in user.confirmedemail_set.all %}
|
||||
{% if user.email != confirmed_email.email %}
|
||||
<option value="{{ confirmed_email.email }}">{{ confirmed_email.email }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="theme" value="{{ user.userpreference.theme }}"/>
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Language stuff not yet fully implemented; Esp. translations are only half-way there
|
||||
@@ -22,7 +48,8 @@
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<div class="radio">
|
||||
<input type="radio" name="language" value="{{ language.code }}" id="language-{{ language.code }}" {% if language.code == LANGUAGE_CODE %}checked{% endif %}>
|
||||
<input type="radio" name="language" value="{{ language.code }}" id="language-{{ language.code }}"
|
||||
{% if language.code == LANGUAGE_CODE %}checked{% endif %}>
|
||||
<label for="language-{{ language.code }}">{{ language.name_local }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -33,48 +60,11 @@
|
||||
|
||||
-->
|
||||
|
||||
{% if perms.ivataraccount.can_us_api %}
|
||||
<h2>{% trans 'API Key' %}</h2>
|
||||
{% if user.apikey %}
|
||||
<div id="copiedToClipboard" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">{% trans 'Copied' %}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{% trans 'Key has been copied to the clipboard' %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function copyToClipboard(fieldId) {
|
||||
var copyText = document.getElementById(fieldId);
|
||||
copyText.select();
|
||||
document.execCommand("copy");
|
||||
$("#copiedToClipboard").modal("show");
|
||||
setTimeout(function() {$('#copiedToClipboard').modal('hide');}, 2000);
|
||||
}
|
||||
</script>
|
||||
<div class="form-group"><label for="public_key">{% trans 'Public key' %}</label>
|
||||
<input type="text" name="public_key" id="public_key" value="{{ user.apikey.public_key }}" style="width:400px;" class="form-control" readonly onclick="copyToClipboard('public_key')">
|
||||
</div>
|
||||
<div class="form-group"><label for="secret_key">{% trans 'Secret key' %}</label>
|
||||
<input type="text" name="secret_key" id="secret_key" value="{{ user.apikey.secret_key }}" style="width:400px;" class="form-control" readonly onclick="copyToClipboard('secret_key')">
|
||||
{% else %}
|
||||
<form action="{% url 'generate_api_key' %}" method="post">{% csrf_token %}
|
||||
<button type="submit" class="btn btn-default">{% trans 'Generate API key' %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div style="height:40px"></div>
|
||||
<div style="height:100px"></div>
|
||||
|
||||
<!-- <p><a href="{% url 'export' %}" class="button">{% trans 'Export your data' %}</a></p> -->
|
||||
|
||||
<p><a href="{% url 'delete' %}" class="button">{% trans 'Permanently delete your account' %}</a></p>
|
||||
<!-- TODO: Better coloring of the button -->
|
||||
<p><a href="{% url 'delete' %}" class="button" style="background:red; color:white;">{% trans 'Permanently delete your account' %}</a></p>
|
||||
<div style="height:2rem"></div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -6,7 +6,16 @@
|
||||
{% block title %}{% trans 'Your Profile' %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script type="text/javascript">
|
||||
function add_active(id){
|
||||
var elems = document.querySelector(".active");
|
||||
if(elems !== null){
|
||||
elems.classList.remove("active");
|
||||
}
|
||||
element = document.getElementById(id);
|
||||
element.classList.add("active");
|
||||
}
|
||||
</script>
|
||||
<h1>
|
||||
{% trans 'Your Profile' %} -
|
||||
{% if user.first_name and user.last_name %}
|
||||
@@ -17,44 +26,44 @@
|
||||
</h1>
|
||||
|
||||
<style>
|
||||
.action-item:hover span {
|
||||
.action-item:hover span {
|
||||
display: inline !important;
|
||||
}
|
||||
@media screen and (max-width: 320px) {
|
||||
}
|
||||
@media screen and (max-width: 320px) {
|
||||
.action-item, .btn {
|
||||
padding-left: 0.3em;
|
||||
padding-right: 0.3em;
|
||||
}
|
||||
}
|
||||
.thumbnail {
|
||||
max-width:80px;
|
||||
max-height:80px;
|
||||
}
|
||||
.nobutton {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
.button {
|
||||
}
|
||||
.thumbnail {
|
||||
max-width:80px;
|
||||
max-height:80px;
|
||||
}
|
||||
.nobutton {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
.button {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.container{
|
||||
}
|
||||
.container{
|
||||
display: grid;
|
||||
}
|
||||
.btn-group{
|
||||
}
|
||||
.btn-group{
|
||||
display: inline-flex;
|
||||
}
|
||||
.input-group-addon{
|
||||
}
|
||||
.input-group-addon{
|
||||
width: auto;
|
||||
height: 3rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
@media only screen and (max-width: 470px) {
|
||||
}
|
||||
@media only screen and (max-width: 470px) {
|
||||
.button {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
@@ -65,100 +74,203 @@ outline: inherit;
|
||||
.btn-group{
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 470px) {
|
||||
p {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
h3{
|
||||
line-height: 3.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<noscript>
|
||||
<style type="text/css">
|
||||
.profile-container > ul{
|
||||
display:block;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
|
||||
{% if user.confirmedemail_set.count or user.confirmedopenid_set.count %}
|
||||
<h3>{% trans 'You have the following confirmed identities:' %}</h3>
|
||||
<div class="row">
|
||||
<div class="row profileid">
|
||||
{% for email in user.confirmedemail_set.all %}
|
||||
{% if user.confirmedemail_set.all|length == 1%}
|
||||
<form action="{% url 'remove_confirmed_email' email.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel" style="width:172px;margin-left:20px;float:left">
|
||||
<div class="panel-heading" style="padding-right:0">
|
||||
<h3 class="panel-title" title="{{ email.email }}" style="display:inline-flex"><a href="{% url 'assign_photo_email' email.id %}"><i class="fa fa-edit"></i></a>
|
||||
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this email address?' %}')"><i class="fa fa-trash"></i></button>
|
||||
{{ email.email|truncatechars:12 }}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img title="{% trans 'Access count' %}: {{ email.access_count }}" style="max-height:100px;max-width:100px" src="{% if email.photo %}{% url 'raw_image' email.photo.id %}{% else %}{% static '/img/nobody/80.png' %}{% endif %}">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
{% 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>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url 'assign_photo_email' email.id %}">
|
||||
Change Profile Picture
|
||||
</a>
|
||||
</li>
|
||||
<li class="email-delete">
|
||||
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this email address?' %}')">
|
||||
Delete Email Address
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<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 %}
|
||||
{% 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>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url 'assign_photo_email' email.id %}">
|
||||
Change Profile Picture
|
||||
</a>
|
||||
</li>
|
||||
<li class="email-delete">
|
||||
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this email address?' %}')">
|
||||
Delete Email Address
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for openid in user.confirmedopenid_set.all %}
|
||||
{% if user.confirmedopenid_set.all|length == 1 %}
|
||||
<form action="{% url 'remove_confirmed_openid' openid.id %}" method="post">{% csrf_token %}
|
||||
<div class="panel" style="width:172px;margin-left:20px;float:left">
|
||||
<div class="panel-heading" style="padding-right:0">
|
||||
<h3 class="panel-title" title="{{ openid.openid }}" style="display:inline-flex"><a href="{% url 'assign_photo_openid' openid.pk %}"><i class="fa fa-edit"></i></a>
|
||||
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this OpenID?' %}')"><i class="fa fa-trash"></i></button>
|
||||
{{ openid.openid|cut:"http://"|cut:"https://"|truncatechars:12 }}</h3>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img title="{% trans 'Access count' %}: {{ openid.access_count }}" style="max-height:100px;max-width:100px" src="{% if openid.photo %}{% url 'raw_image' openid.photo.id %}{% else %}{% static '/img/nobody/80.png' %}{% endif %}">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
<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 %}
|
||||
{% 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>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url 'assign_photo_openid' openid.pk %}">
|
||||
Change OpenID Picture
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this OpenID?' %}')">
|
||||
Delete OpenID
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{% url 'remove_confirmed_openid' openid.id %}" method="post">{% csrf_token %}
|
||||
<div>
|
||||
<div id="id-conf-{{ forloop.counter }}" class="profile-container" onclick="add_active('id-conf-{{ forloop.counter }}')">
|
||||
<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 %}">
|
||||
<h3 class="panel-title email-profile" title="{{ openid.openid }}">
|
||||
{{ openid.openid }}
|
||||
</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url 'assign_photo_openid' openid.pk %}">
|
||||
Change OpenID Picture
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this OpenID?' %}')">
|
||||
Delete OpenID
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.unconfirmedemail_set.count or user.unconfirmedopenid_set.count %}
|
||||
<h3>{% trans 'You have the following unconfirmed email addresses and OpenIDs:' %}</h3>
|
||||
{% if user.unconfirmedemail_set.count or user.unconfirmedopenid_set.count %}
|
||||
<h3>{% trans 'You have the following unconfirmed email addresses and OpenIDs:' %}</h3>
|
||||
{% for email in user.unconfirmedemail_set.all %}
|
||||
<form class="unconfirmed-mail-form" action="{% url 'remove_unconfirmed_email' email.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="btn-group form-group" role="group">
|
||||
<button type="submit" class="button" onclick="return confirm('{% trans 'Are you sure that you want to delete this email address?' %}')"><i class="fa fa-trash"></i></button>
|
||||
<a href="{% url 'resend_confirmation_mail' email.pk %}" class="button"><i class="fa fa-envelope"></i></a>
|
||||
<span class="input-group-addon" style="width: auto;">{{ email.email }}</span>
|
||||
</div>
|
||||
</form>
|
||||
{% csrf_token %}
|
||||
<div class="btn-group form-group" role="group">
|
||||
<button type="submit" class="button" onclick="return confirm('{% trans 'Are you sure that you want to delete this email address?' %}')"><i class="fa fa-trash"></i></button>
|
||||
<a href="{% url 'resend_confirmation_mail' email.pk %}" class="button"><i class="fa fa-envelope"></i></a>
|
||||
<span class="input-group-addon" style="width: auto;">{{ email.email }}</span>
|
||||
</div>
|
||||
</form>
|
||||
{# TODO: (expires in xx hours) #}
|
||||
{% endfor %}
|
||||
{% for openid in user.unconfirmedopenid_set.all %}
|
||||
<form action="{% url 'remove_unconfirmed_openid' openid.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="btn-group form-group" role="group">
|
||||
<button type="submit" class="button" onclick="return confirm('{% trans 'Are you sure that you want to delete this OpenID?' %}')"><i class="fa fa-trash"></i></button>
|
||||
<span class="input-group-addon">{{ openid.openid }}</span>
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
<div class="btn-group form-group" role="group">
|
||||
<button type="submit" class="button" onclick="return confirm('{% trans 'Are you sure that you want to delete this OpenID?' %}')"><i class="fa fa-trash"></i></button>
|
||||
<span class="input-group-addon">{{ openid.openid }}</span>
|
||||
</div>
|
||||
</form>
|
||||
{# TODO: (expires in xx hours) #}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
{% if not max_emails %}<a href="{% url 'add_email' %}" class="button" >{% trans 'Add a new email address' %}</a> {% endif %}
|
||||
<a href="{% url 'add_openid' %}" class="button">{% trans 'Add a new OpenID' %}</a></p>
|
||||
</p>
|
||||
{% if user.photo_set.count %}
|
||||
<h3>{% trans 'Here are the photos you have uploaded/imported:' %}</h3>
|
||||
<div class="row">
|
||||
{% endif %}
|
||||
<p style="padding-top:5px;">
|
||||
{% if not max_emails %}<a href="{% url 'add_email' %}" class="button" >{% trans 'Add a new email address' %}</a> {% endif %}
|
||||
<a href="{% url 'add_openid' %}" class="button">{% trans 'Add a new OpenID' %}</a></p>
|
||||
</p>
|
||||
{% if user.photo_set.count %}
|
||||
<h3>{% trans 'Here are the photos you have uploaded/imported:' %}</h3>
|
||||
<div class="row">
|
||||
{% for photo in user.photo_set.all %}
|
||||
<div class="panel panel-tortin" style="width:132px;margin-left:20px;float:left">
|
||||
<div class="panel panel-tortin" style="width:132px;margin-left:20px;float:left">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><a href="{% url 'delete_photo' photo.pk %}" onclick="return confirm('{% trans 'Are you sure that you want to delete this image?' %}')"><i class="fa fa-trash"></i></a> {% trans 'Image' %} {{ forloop.counter }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" style="height:130px">
|
||||
<center>
|
||||
<img title="{% trans 'Access count' %}: {{ photo.access_count }}" style="max-height:100px;max-width:100px" src="{% url 'raw_image' photo.id %}">
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not max_photos %}
|
||||
<p>
|
||||
<a href="{% url 'upload_photo' %}" class="button">{% trans 'Upload a new photo' %}</a>
|
||||
<a href="{% url 'import_photo' %}" class="button">{% trans 'Import photo from other services' %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
{% if not max_photos %}
|
||||
<p>
|
||||
<a href="{% url 'upload_photo' %}" class="button">{% trans 'Upload a new photo' %}</a>
|
||||
<a href="{% url 'import_photo' %}" class="button">{% trans 'Import photo from other services' %}</a>
|
||||
</p>
|
||||
{% else %}
|
||||
{% trans "You've reached the maximum number of allowed images!" %}<br/>
|
||||
{% trans "No further images can be uploaded." %}
|
||||
{% endif %}
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
|
||||
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)
|
||||
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 = "libravatar.org"
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Login as user
|
||||
"""
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Prepare for tests.
|
||||
- Create user
|
||||
"""
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
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?"
|
||||
)
|
||||
@@ -1,121 +1,197 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
URLs for ivatar.ivataraccount
|
||||
'''
|
||||
from django.urls import path
|
||||
from django.conf.urls import url
|
||||
"""
|
||||
from django.urls import path, re_path
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.contrib.auth.views import PasswordResetDoneView,\
|
||||
PasswordResetConfirmView, PasswordResetCompleteView
|
||||
from django.contrib.auth.views import (
|
||||
PasswordResetDoneView,
|
||||
PasswordResetConfirmView,
|
||||
PasswordResetCompleteView,
|
||||
)
|
||||
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from . views import ProfileView, PasswordResetView
|
||||
from . views import CreateView, PasswordSetView, AddEmailView
|
||||
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView
|
||||
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
|
||||
from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
|
||||
from . views import ImportPhotoView, RawImageView, DeletePhotoView
|
||||
from . views import UploadPhotoView, AssignPhotoOpenIDView
|
||||
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
|
||||
from . views import CropPhotoView
|
||||
from . views import UserPreferenceView, UploadLibravatarExportView
|
||||
from . views import ResendConfirmationMailView
|
||||
from . views import IvatarLoginView
|
||||
from . views import DeleteAccountView
|
||||
from . views import GenerateAPIKey
|
||||
from .views import ProfileView, PasswordResetView
|
||||
from .views import CreateView, PasswordSetView, AddEmailView
|
||||
from .views import RemoveUnconfirmedEmailView, ConfirmEmailView
|
||||
from .views import RemoveConfirmedEmailView, AssignPhotoEmailView
|
||||
from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
|
||||
from .views import ImportPhotoView, RawImageView, DeletePhotoView
|
||||
from .views import UploadPhotoView, AssignPhotoOpenIDView
|
||||
from .views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
|
||||
from .views import AssignBlueskyHandleToEmailView, AssignBlueskyHandleToOpenIdView
|
||||
from .views import CropPhotoView
|
||||
from .views import UserPreferenceView, UploadLibravatarExportView
|
||||
from .views import ResendConfirmationMailView
|
||||
from .views import IvatarLoginView
|
||||
from .views import DeleteAccountView
|
||||
from .views import ExportView
|
||||
from .views import (
|
||||
GenerateAvatarView,
|
||||
AvatarPreviewView,
|
||||
AvatarGalleryView,
|
||||
ReusePromptView,
|
||||
GenerationStatusView,
|
||||
task_status_api,
|
||||
)
|
||||
|
||||
# Define URL patterns, self documenting
|
||||
# To see the fancy, colorful evaluation of these use:
|
||||
# ./manager show_urls
|
||||
urlpatterns = [ # pylint: disable=invalid-name
|
||||
path('new/', CreateView.as_view(), name='new_account'),
|
||||
path('login/', IvatarLoginView.as_view(), name='login'),
|
||||
path("new/", CreateView.as_view(), name="new_account"),
|
||||
path("login/", IvatarLoginView.as_view(), name="login"),
|
||||
path("logout/", LogoutView.as_view(next_page="/"), name="logout"),
|
||||
path(
|
||||
'logout/', LogoutView.as_view(next_page='/'),
|
||||
name='logout'),
|
||||
|
||||
path('password_change/',
|
||||
PasswordChangeView.as_view(template_name='password_change.html'),
|
||||
name='password_change'),
|
||||
path('password_change/done/',
|
||||
PasswordChangeDoneView.as_view(template_name='password_change_done.html'),
|
||||
name='password_change_done'),
|
||||
|
||||
path('password_reset/',
|
||||
PasswordResetView.as_view(template_name='password_reset.html'),
|
||||
name='password_reset'),
|
||||
path('password_reset/done/',
|
||||
PasswordResetDoneView.as_view(
|
||||
template_name='password_reset_submitted.html'),
|
||||
name='password_reset_done'),
|
||||
path('reset/<uidb64>/<token>/',
|
||||
PasswordResetConfirmView.as_view(
|
||||
template_name='password_change.html'),
|
||||
name='password_reset_confirm'),
|
||||
path('reset/done/',
|
||||
PasswordResetCompleteView.as_view(
|
||||
template_name='password_change_done.html'),
|
||||
name='password_reset_complete'),
|
||||
|
||||
path('export/', login_required(
|
||||
TemplateView.as_view(template_name='export.html')
|
||||
), name='export'),
|
||||
path('delete/', DeleteAccountView.as_view(), name='delete'),
|
||||
path('profile/', ProfileView.as_view(), name='profile'),
|
||||
url('profile/(?P<profile_username>.+)', ProfileView.as_view(), name='profile_with_profile_username'),
|
||||
path('add_email/', AddEmailView.as_view(), name='add_email'),
|
||||
path('add_openid/', AddOpenIDView.as_view(), name='add_openid'),
|
||||
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'),
|
||||
path('password_set/', PasswordSetView.as_view(), name='password_set'),
|
||||
url(
|
||||
r'remove_unconfirmed_openid/(?P<openid_id>\d+)',
|
||||
"password_change/",
|
||||
PasswordChangeView.as_view(template_name="password_change.html"),
|
||||
name="password_change",
|
||||
),
|
||||
path(
|
||||
"password_change/done/",
|
||||
PasswordChangeDoneView.as_view(template_name="password_change_done.html"),
|
||||
name="password_change_done",
|
||||
),
|
||||
path(
|
||||
"password_reset/",
|
||||
PasswordResetView.as_view(template_name="password_reset.html"),
|
||||
name="password_reset",
|
||||
),
|
||||
path(
|
||||
"password_reset/done/",
|
||||
PasswordResetDoneView.as_view(template_name="password_reset_submitted.html"),
|
||||
name="password_reset_done",
|
||||
),
|
||||
path(
|
||||
"reset/<uidb64>/<token>/",
|
||||
PasswordResetConfirmView.as_view(template_name="password_change.html"),
|
||||
name="password_reset_confirm",
|
||||
),
|
||||
path(
|
||||
"reset/done/",
|
||||
PasswordResetCompleteView.as_view(template_name="password_change_done.html"),
|
||||
name="password_reset_complete",
|
||||
),
|
||||
path(
|
||||
"export/",
|
||||
ExportView.as_view(),
|
||||
name="export",
|
||||
),
|
||||
path("delete/", DeleteAccountView.as_view(), name="delete"),
|
||||
path("profile/", ProfileView.as_view(), name="profile"),
|
||||
re_path(
|
||||
"profile/(?P<profile_username>.+)",
|
||||
ProfileView.as_view(),
|
||||
name="profile_with_profile_username",
|
||||
),
|
||||
path("generate_avatar/", GenerateAvatarView.as_view(), name="generate_avatar"),
|
||||
re_path(
|
||||
r"generation_status/(?P<task_id>\d+)/",
|
||||
GenerationStatusView.as_view(),
|
||||
name="generation_status",
|
||||
),
|
||||
re_path(
|
||||
r"api/task_status/(?P<task_id>\d+)/", task_status_api, name="task_status_api"
|
||||
),
|
||||
re_path(
|
||||
r"avatar_preview/(?P<photo_id>\d+)/",
|
||||
AvatarPreviewView.as_view(),
|
||||
name="avatar_preview",
|
||||
),
|
||||
path("avatar_gallery/", AvatarGalleryView.as_view(), name="avatar_gallery"),
|
||||
re_path(
|
||||
r"reuse_prompt/(?P<photo_id>\d+)/",
|
||||
ReusePromptView.as_view(),
|
||||
name="reuse_prompt",
|
||||
),
|
||||
path("add_email/", AddEmailView.as_view(), name="add_email"),
|
||||
path("add_openid/", AddOpenIDView.as_view(), name="add_openid"),
|
||||
path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"),
|
||||
path("password_set/", PasswordSetView.as_view(), name="password_set"),
|
||||
re_path(
|
||||
r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
|
||||
RemoveUnconfirmedOpenIDView.as_view(),
|
||||
name='remove_unconfirmed_openid'),
|
||||
url(
|
||||
r'remove_confirmed_openid/(?P<openid_id>\d+)',
|
||||
RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'),
|
||||
url(
|
||||
r'openid_redirection/(?P<openid_id>\d+)',
|
||||
RedirectOpenIDView.as_view(), name='openid_redirection'),
|
||||
url(
|
||||
r'confirm_openid/(?P<openid_id>\w+)',
|
||||
ConfirmOpenIDView.as_view(), name='confirm_openid'),
|
||||
url(
|
||||
r'confirm_email/(?P<verification_key>\w+)',
|
||||
ConfirmEmailView.as_view(), name='confirm_email'),
|
||||
url(
|
||||
r'remove_unconfirmed_email/(?P<email_id>\d+)',
|
||||
RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'),
|
||||
url(
|
||||
r'remove_confirmed_email/(?P<email_id>\d+)',
|
||||
RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'),
|
||||
url(
|
||||
r'assign_photo_email/(?P<email_id>\d+)',
|
||||
AssignPhotoEmailView.as_view(), name='assign_photo_email'),
|
||||
url(
|
||||
r'assign_photo_openid/(?P<openid_id>\d+)',
|
||||
AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'),
|
||||
url(
|
||||
r'import_photo/$',
|
||||
ImportPhotoView.as_view(), name='import_photo'),
|
||||
url(
|
||||
r'import_photo/(?P<email_addr>[\w.]+@[\w.]+.[\w.]+)',
|
||||
ImportPhotoView.as_view(), name='import_photo'),
|
||||
url(
|
||||
r'import_photo/(?P<email_id>\d+)',
|
||||
ImportPhotoView.as_view(), name='import_photo'),
|
||||
url(
|
||||
r'delete_photo/(?P<pk>\d+)',
|
||||
DeletePhotoView.as_view(), name='delete_photo'),
|
||||
url(r'raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
|
||||
url(r'crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'),
|
||||
url(r'pref/$', UserPreferenceView.as_view(), name='user_preference'),
|
||||
url(r'upload_export/$', UploadLibravatarExportView.as_view(), name='upload_export'),
|
||||
url(r'upload_export/(?P<save>save)$',
|
||||
UploadLibravatarExportView.as_view(), name='upload_export'),
|
||||
url(r'resend_confirmation_mail/(?P<email_id>\d+)',
|
||||
ResendConfirmationMailView.as_view(), name='resend_confirmation_mail'),
|
||||
url(r'generate_api_key/$', GenerateAPIKey.as_view(), name='generate_api_key'),
|
||||
name="remove_unconfirmed_openid",
|
||||
),
|
||||
re_path(
|
||||
r"remove_confirmed_openid/(?P<openid_id>\d+)",
|
||||
RemoveConfirmedOpenIDView.as_view(),
|
||||
name="remove_confirmed_openid",
|
||||
),
|
||||
re_path(
|
||||
r"openid_redirection/(?P<openid_id>\d+)",
|
||||
RedirectOpenIDView.as_view(),
|
||||
name="openid_redirection",
|
||||
),
|
||||
re_path(
|
||||
r"confirm_openid/(?P<openid_id>\w+)",
|
||||
ConfirmOpenIDView.as_view(),
|
||||
name="confirm_openid",
|
||||
),
|
||||
re_path(
|
||||
r"confirm_email/(?P<verification_key>\w+)",
|
||||
ConfirmEmailView.as_view(),
|
||||
name="confirm_email",
|
||||
),
|
||||
re_path(
|
||||
r"remove_unconfirmed_email/(?P<email_id>\d+)",
|
||||
RemoveUnconfirmedEmailView.as_view(),
|
||||
name="remove_unconfirmed_email",
|
||||
),
|
||||
re_path(
|
||||
r"remove_confirmed_email/(?P<email_id>\d+)",
|
||||
RemoveConfirmedEmailView.as_view(),
|
||||
name="remove_confirmed_email",
|
||||
),
|
||||
re_path(
|
||||
r"assign_photo_email/(?P<email_id>\d+)",
|
||||
AssignPhotoEmailView.as_view(),
|
||||
name="assign_photo_email",
|
||||
),
|
||||
re_path(
|
||||
r"assign_photo_openid/(?P<openid_id>\d+)",
|
||||
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.]+)",
|
||||
ImportPhotoView.as_view(),
|
||||
name="import_photo",
|
||||
),
|
||||
re_path(
|
||||
r"import_photo/(?P<email_id>\d+)",
|
||||
ImportPhotoView.as_view(),
|
||||
name="import_photo",
|
||||
),
|
||||
re_path(
|
||||
r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"
|
||||
),
|
||||
re_path(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
|
||||
re_path(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
|
||||
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)$",
|
||||
UploadLibravatarExportView.as_view(),
|
||||
name="upload_export",
|
||||
),
|
||||
re_path(
|
||||
r"resend_confirmation_mail/(?P<email_id>\d+)",
|
||||
ResendConfirmationMailView.as_view(),
|
||||
name="resend_confirmation_mail",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,18 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Middleware classes
|
||||
"""
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.middleware.locale import LocaleMiddleware
|
||||
|
||||
|
||||
class CustomLocaleMiddleware(LocaleMiddleware):
|
||||
"""
|
||||
Middleware that extends LocaleMiddleware to skip Vary header processing for image URLs
|
||||
"""
|
||||
|
||||
def process_response(self, request, response):
|
||||
# Check if this is an image-related URL
|
||||
path = request.path
|
||||
if any(
|
||||
path.startswith(prefix)
|
||||
for prefix in ["/avatar/", "/gravatarproxy/", "/blueskyproxy/"]
|
||||
):
|
||||
# Delete Vary from header if exists
|
||||
if "Vary" in response:
|
||||
del response["Vary"]
|
||||
|
||||
# Extract hash from URL path for ETag
|
||||
# URLs are like /avatar/{hash}, /gravatarproxy/{hash}, /blueskyproxy/{hash}
|
||||
path_parts = path.strip("/").split("/")
|
||||
if len(path_parts) >= 2:
|
||||
hash_value = path_parts[1] # Get the hash part
|
||||
response["Etag"] = f'"{hash_value}"'
|
||||
else:
|
||||
# Fallback to content hash if we can't extract from URL
|
||||
response["Etag"] = f'"{hash(response.content)}"'
|
||||
|
||||
# Skip the parent's process_response to avoid adding Accept-Language to Vary
|
||||
return response
|
||||
|
||||
# For all other URLs, use the parent's behavior
|
||||
return super().process_response(request, response)
|
||||
|
||||
|
||||
class MultipleProxyMiddleware(
|
||||
MiddlewareMixin
|
||||
): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
Middleware to rewrite proxy headers for deployments
|
||||
with multiple proxies
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
def process_request(self, request): # pylint: disable=no-self-use
|
||||
"""
|
||||
Rewrites the proxy headers so that forwarded server is
|
||||
used if available.
|
||||
"""
|
||||
if 'HTTP_X_FORWARDED_SERVER' in request.META:
|
||||
request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER']
|
||||
if "HTTP_X_FORWARDED_SERVER" in request.META:
|
||||
request.META["HTTP_X_FORWARDED_HOST"] = request.META[
|
||||
"HTTP_X_FORWARDED_SERVER"
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Django settings for ivatar project.
|
||||
"""
|
||||
@@ -6,7 +7,7 @@ import os
|
||||
import logging
|
||||
|
||||
log_level = logging.DEBUG # pylint: disable=invalid-name
|
||||
logger = logging.getLogger('ivatar') # pylint: disable=invalid-name
|
||||
logger = logging.getLogger("ivatar") # pylint: disable=invalid-name
|
||||
logger.setLevel(log_level)
|
||||
|
||||
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
@@ -14,7 +15,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '=v(+-^t#ahv^a&&e)uf36g8algj$d1@6ou^w(r0@%)#8mlc*zk'
|
||||
SECRET_KEY = "=v(+-^t#ahv^a&&e)uf36g8algj$d1@6ou^w(r0@%)#8mlc*zk"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
@@ -25,52 +26,58 @@ ALLOWED_HOSTS = []
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"social_django",
|
||||
"django_celery_results",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'ivatar.urls'
|
||||
ROOT_URLCONF = "ivatar.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [os.path.join(BASE_DIR, "templates")],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"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,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'ivatar.wsgi.application'
|
||||
WSGI_APPLICATION = "ivatar.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
"ATOMIC_REQUESTS": True,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,26 +87,93 @@ DATABASES = {
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa
|
||||
"OPTIONS": {
|
||||
"min_length": 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa
|
||||
},
|
||||
]
|
||||
|
||||
# Password Hashing (more secure)
|
||||
PASSWORD_HASHERS = [
|
||||
# This isn't working in older Python environments
|
||||
# "django.contrib.auth.hashers.Argon2PasswordHasher",
|
||||
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
||||
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
||||
]
|
||||
|
||||
# Security Settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
CSRF_COOKIE_SECURE = not DEBUG
|
||||
SESSION_COOKIE_SECURE = not DEBUG
|
||||
|
||||
if not DEBUG:
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SECURE_HSTS_SECONDS = 31536000 # 1 year
|
||||
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/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@@ -109,13 +183,10 @@ USE_TZ = True
|
||||
|
||||
|
||||
# Static files configuration (esp. req. during dev.)
|
||||
PROJECT_ROOT = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
os.pardir
|
||||
)
|
||||
)
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
|
||||
from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import # noqa
|
||||
|
||||
586
ivatar/static/css/surly-badges.local.css
Normal file
@@ -0,0 +1,586 @@
|
||||
#surly-badge {
|
||||
font-family: sans-serif !important;
|
||||
font-weight: 400 !important;
|
||||
width: 134px !important;
|
||||
height: 164px !important;
|
||||
text-align: center !important;
|
||||
display: -webkit-box !important;
|
||||
display: -ms-flexbox !important;
|
||||
display: flex !important;
|
||||
-webkit-box-orient: vertical !important;
|
||||
-webkit-box-direction: normal !important;
|
||||
-ms-flex-direction: column !important;
|
||||
flex-direction: column !important;
|
||||
-webkit-box-align: center !important;
|
||||
-ms-flex-align: center !important;
|
||||
align-items: center !important;
|
||||
position: relative !important;
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: top center !important;
|
||||
-webkit-box-sizing: content-box !important;
|
||||
box-sizing: content-box !important;
|
||||
padding: 8px 15px 0 !important;
|
||||
}
|
||||
|
||||
#surly-badge p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 164 150'%3E%3Cpath style='fill:none;stroke:%23fff;stroke-width:2;stroke-miterlimit:10;' d='M16.05,22.74c0-7.61,6.16-13.76,13.76-13.76 M16.05,22.74v127.15 M29.02,140.85l-12.52,8.2 M28.47,141.01 h105.78 M134.25,141.01c7.61,0,13.76-6.16,13.76-13.76 M148.01,21.03v106.22 M29.81,8.97h106.2'/%3E%3Cpath style='stroke:%233273f6;stroke-width:2;stroke-miterlimit:10;' d='M148.01,18.02V-0.02 M138.99,9h18.04'/%3E%3C/svg%3E");
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__header-text {
|
||||
color: #3273f6 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__header-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__tag {
|
||||
background-color: #3273f6 !important;
|
||||
border-bottom: 1px solid #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__tag-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__tag::before {
|
||||
border-color: transparent #7f8ca5 transparent transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__tag::after {
|
||||
border-color: transparent transparent transparent #7f8ca5 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__footer-link {
|
||||
color: #3273f6 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__footer-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-blue .surly-badge__footer-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 164 150'%3E%3Cpath style='fill:none;stroke:%23fff;stroke-width:2;stroke-miterlimit:10;' d='M16.05,22.74c0-7.61,6.16-13.76,13.76-13.76 M16.05,22.74v127.15 M29.02,140.85l-12.52,8.2 M28.47,141.01 h105.78 M134.25,141.01c7.61,0,13.76-6.16,13.76-13.76 M148.01,21.03v106.22 M29.81,8.97h106.2'/%3E%3Cpath style='stroke:%23ff715e;stroke-width:2;stroke-miterlimit:10;' d='M148.01,18.02V-0.02 M138.99,9h18.04'/%3E%3C/svg%3E");
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__header-text {
|
||||
color: #3273f6 !important;
|
||||
background: #ff715e !important;
|
||||
background-image: -webkit-gradient(
|
||||
linear,
|
||||
left top,
|
||||
right top,
|
||||
from(#ff715e),
|
||||
to(#00a8ff)
|
||||
) !important;
|
||||
background-image: -o-linear-gradient(
|
||||
left,
|
||||
#ff715e 0%,
|
||||
#00a8ff 100%
|
||||
) !important;
|
||||
background-image: linear-gradient(90deg, #ff715e 0%, #00a8ff 100%) !important;
|
||||
background-size: 100% !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-moz-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
-moz-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__header-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__tag {
|
||||
background: #ff715e !important;
|
||||
background: -webkit-gradient(
|
||||
linear,
|
||||
left top,
|
||||
right top,
|
||||
from(#ff715e),
|
||||
to(#00a8ff)
|
||||
) !important;
|
||||
background: -o-linear-gradient(left, #ff715e 0%, #00a8ff 100%) !important;
|
||||
background: linear-gradient(90deg, #ff715e 0%, #00a8ff 100%) !important;
|
||||
border-bottom: 1px solid #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__tag-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__tag::before {
|
||||
border-color: transparent #914339 transparent transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__tag::after {
|
||||
border-color: transparent transparent transparent #3b7696 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__footer-link {
|
||||
color: #ff715e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__footer-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-gradient .surly-badge__footer-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 164 150'%3E%3Cpath style='fill:none;stroke:%23fff;stroke-width:2;stroke-miterlimit:10;' d='M16.05,22.74c0-7.61,6.16-13.76,13.76-13.76 M16.05,22.74v127.15 M29.02,140.85l-12.52,8.2 M28.47,141.01 h105.78 M134.25,141.01c7.61,0,13.76-6.16,13.76-13.76 M148.01,21.03v106.22 M29.81,8.97h106.2'/%3E%3Cpath style='stroke:%23ff715e;stroke-width:2;stroke-miterlimit:10;' d='M148.01,18.02V-0.02 M138.99,9h18.04'/%3E%3C/svg%3E");
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__header-text {
|
||||
color: #ff715e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__header-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__tag {
|
||||
background-color: #ff715e !important;
|
||||
border-bottom: 1px solid #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__tag-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__tag::before {
|
||||
border-color: transparent #914339 transparent transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__tag::after {
|
||||
border-color: transparent transparent transparent #914339 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__footer-link {
|
||||
color: #ff715e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__footer-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-red .surly-badge__footer-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 164 150'%3E%3Cpath style='fill:none;stroke:%23fff;stroke-width:2;stroke-miterlimit:10;' d='M16.05,22.74c0-7.61,6.16-13.76,13.76-13.76 M16.05,22.74v127.15 M29.02,140.85l-12.52,8.2 M28.47,141.01 h105.78 M134.25,141.01c7.61,0,13.76-6.16,13.76-13.76 M148.01,21.03v106.22 M29.81,8.97h106.2'/%3E%3Cpath style='stroke:%23fff;stroke-width:2;stroke-miterlimit:10;' d='M148.01,18.02V-0.02 M138.99,9h18.04'/%3E%3C/svg%3E");
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__header-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__header-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__tag {
|
||||
background-color: #fff !important;
|
||||
border-bottom: 1px solid #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__tag-text {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__tag::before {
|
||||
border-color: transparent #707070 transparent transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__tag::after {
|
||||
border-color: transparent transparent transparent #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__footer-link {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__footer-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_black-white .surly-badge__footer-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 164 150'%3E%3Cpath style='fill:none;stroke:%232e2e2e;stroke-width:2;stroke-miterlimit:10;' d='M16.05,22.74c0-7.61,6.16-13.76,13.76-13.76 M16.05,22.74v127.15 M29.02,140.85l-12.52,8.2 M28.47,141.01 h105.78 M134.25,141.01c7.61,0,13.76-6.16,13.76-13.76 M148.01,21.03v106.22 M29.81,8.97h106.2'/%3E%3Cpath style='stroke:%2302a7fd;stroke-width:2;stroke-miterlimit:10;' d='M148.01,18.02V-0.02 M138.99,9h18.04'/%3E%3C/svg%3E");
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__header-text {
|
||||
color: #02a7fd !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__header-title {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__tag {
|
||||
background-color: #02a7fd !important;
|
||||
border-bottom: 1px solid #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__tag-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__tag::before {
|
||||
border-color: transparent #3b7696 transparent transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__tag::after {
|
||||
border-color: transparent transparent transparent #3b7696 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__footer-link {
|
||||
color: #02a7fd !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__footer-title {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-blue .surly-badge__footer-text {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 164 150'%3E%3Cpath style='fill:none;stroke:%232e2e2e;stroke-width:2;stroke-miterlimit:10;' d='M16.05,22.74c0-7.61,6.16-13.76,13.76-13.76 M16.05,22.74v127.15 M29.02,140.85l-12.52,8.2 M28.47,141.01 h105.78 M134.25,141.01c7.61,0,13.76-6.16,13.76-13.76 M148.01,21.03v106.22 M29.81,8.97h106.2'/%3E%3Cpath style='stroke:%23ff5741;stroke-width:2;stroke-miterlimit:10;' d='M148.01,18.02V-0.02 M138.99,9h18.04'/%3E%3C/svg%3E");
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__header-text {
|
||||
color: #ff5741 !important;
|
||||
background: #ff715e !important;
|
||||
background-image: -webkit-gradient(
|
||||
linear,
|
||||
left top,
|
||||
right top,
|
||||
from(#ff715e),
|
||||
to(#00a8ff)
|
||||
) !important;
|
||||
background-image: -o-linear-gradient(
|
||||
left,
|
||||
#ff715e 0%,
|
||||
#00a8ff 100%
|
||||
) !important;
|
||||
background-image: linear-gradient(90deg, #ff715e 0%, #00a8ff 100%) !important;
|
||||
background-size: 100% !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-moz-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
-moz-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__header-title {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__tag {
|
||||
background: #ff715e !important;
|
||||
background: -webkit-gradient(
|
||||
linear,
|
||||
left top,
|
||||
right top,
|
||||
from(#ff715e),
|
||||
to(#00a8ff)
|
||||
) !important;
|
||||
background: -o-linear-gradient(left, #ff715e 0%, #00a8ff 100%) !important;
|
||||
background: linear-gradient(90deg, #ff715e 0%, #00a8ff 100%) !important;
|
||||
border-bottom: 1px solid #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__tag-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__tag::before {
|
||||
border-color: transparent #914339 transparent transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__tag::after {
|
||||
border-color: transparent transparent transparent #3b7696 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__footer-link {
|
||||
color: #ff5741 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__footer-title {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-gradient .surly-badge__footer-text {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 164 150'%3E%3Cpath style='fill:none;stroke:%232e2e2e;stroke-width:2;stroke-miterlimit:10;' d='M16.05,22.74c0-7.61,6.16-13.76,13.76-13.76 M16.05,22.74v127.15 M29.02,140.85l-12.52,8.2 M28.47,141.01 h105.78 M134.25,141.01c7.61,0,13.76-6.16,13.76-13.76 M148.01,21.03v106.22 M29.81,8.97h106.2'/%3E%3Cpath style='stroke:%23ff715e;stroke-width:2;stroke-miterlimit:10;' d='M148.01,18.02V-0.02 M138.99,9h18.04'/%3E%3C/svg%3E");
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__header-text {
|
||||
color: #ff715e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__header-title {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__tag {
|
||||
background-color: #ff715e !important;
|
||||
border-bottom: 1px solid #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__tag-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__tag::before {
|
||||
border-color: transparent #914339 transparent transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__tag::after {
|
||||
border-color: transparent transparent transparent #914339 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__footer-link {
|
||||
color: #ff715e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__footer-title {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-red .surly-badge__footer-text {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 164 150'%3E%3Cpath style='fill:none;stroke:%232e2e2e;stroke-width:2;stroke-miterlimit:10;' d='M16.05,22.74c0-7.61,6.16-13.76,13.76-13.76 M16.05,22.74v127.15 M29.02,140.85l-12.52,8.2 M28.47,141.01 h105.78 M134.25,141.01c7.61,0,13.76-6.16,13.76-13.76 M148.01,21.03v106.22 M29.81,8.97h106.2'/%3E%3Cpath style='stroke:%23707070;stroke-width:2;stroke-miterlimit:10;' d='M148.01,18.02V-0.02 M138.99,9h18.04'/%3E%3C/svg%3E");
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__header-text {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__header-title {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__tag {
|
||||
background-color: #2e2e2e !important;
|
||||
border-bottom: 1px solid #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__tag-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__tag::before {
|
||||
border-color: transparent #707070 transparent transparent !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__tag::after {
|
||||
border-color: transparent transparent transparent #707070 !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__footer-link {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__footer-title {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge.surly-badge_white-black .surly-badge__footer-text {
|
||||
color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__header {
|
||||
position: relative !important;
|
||||
z-index: 10 !important;
|
||||
padding: 12px 6px 0 !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__header-title {
|
||||
font-family: sans-serif !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
line-height: 1 !important;
|
||||
float: none !important;
|
||||
text-align: center !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__header-text {
|
||||
font-size: 40px !important;
|
||||
font-weight: 700 !important;
|
||||
text-transform: uppercase !important;
|
||||
line-height: 33px !important;
|
||||
float: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__tag {
|
||||
height: 18px !important;
|
||||
width: calc(100% + 26px) !important;
|
||||
display: -webkit-box !important;
|
||||
display: -ms-flexbox !important;
|
||||
display: flex !important;
|
||||
-webkit-box-align: center !important;
|
||||
-ms-flex-align: center !important;
|
||||
align-items: center !important;
|
||||
position: relative !important;
|
||||
z-index: 10 !important;
|
||||
-ms-flex-negative: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
padding: 0 2px !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__tag-text {
|
||||
font-size: 10px !important;
|
||||
font-weight: 500 !important;
|
||||
cursor: pointer !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
width: 100% !important;
|
||||
float: none !important;
|
||||
-o-text-overflow: ellipsis !important;
|
||||
text-overflow: ellipsis !important;
|
||||
line-height: initial !important;
|
||||
text-decoration: none !important;
|
||||
padding: 0 0 !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__tag::before,
|
||||
#surly-badge .surly-badge__tag::after {
|
||||
content: "" !important;
|
||||
display: block !important;
|
||||
position: absolute !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
border-style: solid !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__tag::before {
|
||||
border-width: 0 15px 15px 0 !important;
|
||||
left: 0 !important;
|
||||
bottom: -15px !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__tag::after {
|
||||
border-width: 15px 0 0 15px !important;
|
||||
right: 0 !important;
|
||||
top: -15px !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__footer {
|
||||
position: relative !important;
|
||||
z-index: 10 !important;
|
||||
white-space: nowrap !important;
|
||||
width: 100% !important;
|
||||
padding-top: 6px !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__footer-title {
|
||||
font-family: sans-serif !important;
|
||||
font-size: 15px !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
overflow: hidden !important;
|
||||
-o-text-overflow: ellipsis !important;
|
||||
text-overflow: ellipsis !important;
|
||||
letter-spacing: -0.5px !important;
|
||||
line-height: 1 !important;
|
||||
float: none !important;
|
||||
text-align: center !important;
|
||||
-webkit-box-sizing: border-box !important;
|
||||
box-sizing: border-box !important;
|
||||
padding: 0 12px !important;
|
||||
margin: 0 !important;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__footer-text {
|
||||
font-size: 13px !important;
|
||||
font-weight: 500 !important;
|
||||
line-height: 1 !important;
|
||||
float: none !important;
|
||||
text-align: center !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__footer-link {
|
||||
font-size: 13px !important;
|
||||
cursor: pointer !important;
|
||||
text-decoration: underline !important;
|
||||
line-height: initial !important;
|
||||
display: inline-block !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
#surly-badge .surly-badge__date {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
-webkit-box-flex: 1 !important;
|
||||
-ms-flex-positive: 1 !important;
|
||||
flex-grow: 1 !important;
|
||||
display: -webkit-box !important;
|
||||
display: -ms-flexbox !important;
|
||||
display: flex !important;
|
||||
-webkit-box-align: end !important;
|
||||
-ms-flex-align: end !important;
|
||||
align-items: flex-end !important;
|
||||
line-height: 1 !important;
|
||||
text-align: center !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
#surly-badge br {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.surly__id_56263329.surly-badge_white-blue {
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.surly__id_135641946#surly-badge {
|
||||
padding-top: 6px !important;
|
||||
}
|
||||
|
||||
.surly__id_135641946#surly-badge .surly-badge__footer {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.surly__id_135641946#surly-badge .surly-badge__footer-title {
|
||||
margin-bottom: 2px !important;
|
||||
}
|
||||
@@ -18,7 +18,6 @@ Identica: @fmarier
|
||||
Location: Wellington, New Zealand
|
||||
|
||||
Developer: Jonathan Harker
|
||||
Site: https://www.ohloh.net/accounts/jonathanharker
|
||||
Identica: @jonathanharker
|
||||
|
||||
Developer: Brett Wilkins
|
||||
@@ -33,7 +32,6 @@ Site: http://blogs.ijw.co.nz/chris
|
||||
Twitter: @ijw_chrisf
|
||||
|
||||
Library maintainer (PHP): Melissa Draper
|
||||
Site: http://www.meldraweb.com/
|
||||
Twitter: @elkbuntu
|
||||
Identica: @elkbuntu
|
||||
|
||||
@@ -69,7 +67,6 @@ Site: http://hendry.iki.fi/
|
||||
Twitter: @kaihendry
|
||||
|
||||
Name: Lars Wirzenius
|
||||
Site: http://braawi.com/
|
||||
Identica: @liw
|
||||
|
||||
Name: Olly Betts
|
||||
|
||||
BIN
ivatar/static/img/Gandi-Logo.wine.png
Executable file
|
After Width: | Height: | Size: 38 KiB |
BIN
ivatar/static/img/broken.tif
Normal file
BIN
ivatar/static/img/broken.webp
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
ivatar/static/img/gandi_logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
7210
ivatar/static/img/logo4hex/libravatar_org.ai
Executable file
BIN
ivatar/static/img/logo4hex/libravatar_org.eps
Executable file
143
ivatar/static/img/logo4hex/libravatar_org.svg
Executable file
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 841.9 967.3" style="enable-background:new 0 0 841.9 967.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;fill:url(#SVGID_1_);}
|
||||
.st2{fill:url(#SVGID_2_);stroke:#000000;stroke-miterlimit:10;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="_x36_eck" class="st0">
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="89.9133" y1="153.4554" x2="750.3038" y2="813.8458">
|
||||
<stop offset="8.207178e-03" style="stop-color:#3C5DA8"/>
|
||||
<stop offset="0.2203" style="stop-color:#3C62AC"/>
|
||||
<stop offset="0.5531" style="stop-color:#3C70B7"/>
|
||||
<stop offset="0.9629" style="stop-color:#3E88C8"/>
|
||||
<stop offset="1" style="stop-color:#3E8ACA"/>
|
||||
</linearGradient>
|
||||
<polygon class="st1" points="420.9,0 840.1,241.1 839.4,724.8 419.3,967.3 0.1,726.2 0.9,242.5 "/>
|
||||
</g>
|
||||
<g id="bg">
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-31.3529" y1="31.3529" x2="873.2426" y2="935.9484">
|
||||
<stop offset="8.207178e-03" style="stop-color:#3C5DA8"/>
|
||||
<stop offset="0.2203" style="stop-color:#3C62AC"/>
|
||||
<stop offset="0.5531" style="stop-color:#3C70B7"/>
|
||||
<stop offset="0.9629" style="stop-color:#3E88C8"/>
|
||||
<stop offset="1" style="stop-color:#3E8ACA"/>
|
||||
</linearGradient>
|
||||
<rect class="st2" width="841.9" height="967.3"/>
|
||||
</g>
|
||||
<g id="libravatar.org">
|
||||
<g>
|
||||
<path class="st3" d="M142.8,652.2v74.3h-12.4v-74.3H142.8z"/>
|
||||
<path class="st3" d="M169.7,660.3c0,1.1-0.2,2.1-0.6,3c-0.4,0.9-1,1.8-1.7,2.5c-0.7,0.7-1.6,1.3-2.5,1.7c-1,0.4-2,0.6-3.1,0.6
|
||||
c-1.1,0-2.1-0.2-3-0.6c-1-0.4-1.8-1-2.5-1.7c-0.7-0.7-1.3-1.5-1.7-2.5c-0.4-0.9-0.6-1.9-0.6-3c0-1.1,0.2-2.1,0.6-3.1
|
||||
c0.4-1,1-1.8,1.7-2.5c0.7-0.7,1.5-1.3,2.5-1.7c0.9-0.4,2-0.6,3-0.6c1.1,0,2.1,0.2,3.1,0.6c1,0.4,1.8,1,2.5,1.7
|
||||
c0.7,0.7,1.3,1.5,1.7,2.5C169.5,658.1,169.7,659.2,169.7,660.3z M168,675.2v51.3h-12.4v-51.3H168z"/>
|
||||
<path class="st3" d="M192.9,681.5c2.1-2.2,4.4-3.9,6.9-5.2c2.5-1.3,5.4-1.9,8.6-1.9c3,0,5.6,0.6,8,1.8c2.4,1.2,4.4,2.8,6.1,5
|
||||
c1.7,2.2,3,4.8,3.9,7.9c0.9,3.1,1.3,6.6,1.3,10.5c0,4.2-0.5,8-1.5,11.4c-1,3.4-2.5,6.3-4.4,8.7c-1.9,2.4-4.2,4.3-6.9,5.6
|
||||
c-2.7,1.3-5.7,2-9,2c-1.6,0-3-0.2-4.4-0.5c-1.3-0.3-2.5-0.8-3.6-1.3c-1.1-0.6-2.1-1.3-3-2.1c-0.9-0.8-1.8-1.7-2.6-2.7l-0.5,3.4
|
||||
c-0.2,0.9-0.5,1.5-1,1.9c-0.5,0.4-1.1,0.5-2,0.5h-8.1v-74.3h12.4V681.5z M192.9,713.2c1.4,1.7,2.9,2.9,4.6,3.6s3.4,1,5.2,1
|
||||
c1.9,0,3.6-0.3,5.1-1c1.5-0.7,2.8-1.8,3.9-3.2c1.1-1.5,1.9-3.3,2.5-5.5c0.6-2.2,0.8-4.9,0.8-8.1c0-5.6-0.9-9.7-2.8-12.2
|
||||
c-1.9-2.5-4.5-3.8-8-3.8c-2.4,0-4.4,0.5-6.2,1.6c-1.8,1.1-3.5,2.6-5,4.6V713.2z"/>
|
||||
<path class="st3" d="M249,684.1c1.6-3.1,3.5-5.5,5.7-7.2c2.2-1.8,4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2c-1-0.2-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7s-1.6,1.7-2.2,2.7c-0.6,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.1,0.2,2.6,0.7s0.8,1.3,1,2.4
|
||||
L249,684.1z"/>
|
||||
<path class="st3" d="M272.8,682.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1c1.6,1.8,2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9c-1.8-0.6-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7
|
||||
c-0.6-0.5-1.1-1-1.4-1.6L272.8,682.4z M301.1,704.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7c-1.5,0.7-2.5,1.5-3.1,2.5
|
||||
c-0.6,0.9-1,2-1,3c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V704.6z"/>
|
||||
<path class="st3" d="M369.8,675.2l-20.4,51.3h-11.2l-20.3-51.3h10.2c0.9,0,1.7,0.2,2.3,0.6c0.6,0.4,1,1,1.2,1.7l9.9,27.4
|
||||
c0.5,1.6,1,3.2,1.4,4.7c0.4,1.5,0.8,3.1,1,4.6c0.3-1.5,0.7-3.1,1.1-4.6c0.4-1.5,0.9-3.1,1.5-4.7l10.1-27.4
|
||||
c0.2-0.7,0.6-1.2,1.2-1.7c0.6-0.4,1.3-0.6,2.1-0.6H369.8z"/>
|
||||
<path class="st3" d="M373.9,682.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1c1.6,1.8,2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9c-1.8-0.6-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7
|
||||
c-0.6-0.5-1.1-1-1.4-1.6L373.9,682.4z M402.2,704.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7c-1.5,0.7-2.5,1.5-3.1,2.5
|
||||
c-0.6,0.9-1,2-1,3c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V704.6z"/>
|
||||
<path class="st3" d="M442.2,727.3c-4.5,0-7.9-1.3-10.3-3.8c-2.4-2.5-3.6-6-3.6-10.4v-28.6h-5.2c-0.7,0-1.2-0.2-1.7-0.7
|
||||
c-0.5-0.4-0.7-1.1-0.7-2V677l8.2-1.4l2.6-14c0.2-0.7,0.5-1.2,1-1.5c0.5-0.4,1.1-0.5,1.8-0.5h6.4v16.2h13.7v8.8h-13.7v27.8
|
||||
c0,1.6,0.4,2.8,1.2,3.8c0.8,0.9,1.9,1.3,3.2,1.3c0.8,0,1.4-0.1,1.9-0.3c0.5-0.2,1-0.4,1.4-0.6c0.4-0.2,0.7-0.4,1-0.6
|
||||
c0.3-0.2,0.6-0.3,0.9-0.3c0.4,0,0.7,0.1,0.9,0.3c0.2,0.2,0.5,0.5,0.8,0.8l3.7,6c-1.8,1.5-3.9,2.6-6.2,3.4
|
||||
C447.1,726.9,444.7,727.3,442.2,727.3z"/>
|
||||
<path class="st3" d="M461.1,682.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1s2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9s-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7s-1.1-1-1.5-1.6
|
||||
L461.1,682.4z M489.4,704.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7s-2.5,1.5-3.1,2.5c-0.6,0.9-1,2-1,3
|
||||
c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V704.6z"/>
|
||||
<path class="st3" d="M524.7,684.1c1.6-3.1,3.5-5.5,5.7-7.2s4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2s-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7c-0.9,0.8-1.6,1.7-2.2,2.7c-0.7,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.2,0.2,2.7,0.7
|
||||
s0.8,1.3,1,2.4L524.7,684.1z"/>
|
||||
<path class="st3" d="M544.6,719.7c0-1,0.2-2,0.6-3c0.4-0.9,0.9-1.7,1.6-2.4c0.7-0.7,1.5-1.2,2.4-1.6s1.9-0.6,3-0.6s2.1,0.2,3,0.6
|
||||
c0.9,0.4,1.7,0.9,2.4,1.6c0.7,0.7,1.2,1.5,1.6,2.4c0.4,0.9,0.6,1.9,0.6,3c0,1.1-0.2,2.1-0.6,3c-0.4,0.9-0.9,1.7-1.6,2.4
|
||||
c-0.7,0.7-1.5,1.2-2.4,1.6c-0.9,0.4-1.9,0.6-3,0.6s-2.1-0.2-3-0.6s-1.7-0.9-2.4-1.6c-0.7-0.7-1.2-1.5-1.6-2.4
|
||||
C544.8,721.8,544.6,720.8,544.6,719.7z"/>
|
||||
<path class="st3" d="M592.9,674.4c3.8,0,7.3,0.6,10.4,1.8c3.1,1.2,5.8,3,8,5.2c2.2,2.3,3.9,5,5.1,8.3c1.2,3.3,1.8,6.9,1.8,11
|
||||
c0,4.1-0.6,7.7-1.8,11c-1.2,3.3-2.9,6-5.1,8.3c-2.2,2.3-4.9,4.1-8,5.3c-3.1,1.2-6.6,1.8-10.4,1.8c-3.8,0-7.3-0.6-10.5-1.8
|
||||
c-3.1-1.2-5.8-3-8-5.3c-2.2-2.3-3.9-5.1-5.2-8.3c-1.2-3.3-1.8-6.9-1.8-11c0-4,0.6-7.7,1.8-11c1.2-3.3,2.9-6,5.2-8.3
|
||||
c2.2-2.3,4.9-4,8-5.2C585.6,675,589,674.4,592.9,674.4z M592.9,717.8c4.3,0,7.4-1.4,9.5-4.3c2-2.9,3.1-7.1,3.1-12.6
|
||||
c0-5.5-1-9.8-3.1-12.6c-2.1-2.9-5.2-4.4-9.5-4.4c-4.3,0-7.5,1.5-9.6,4.4c-2.1,2.9-3.1,7.1-3.1,12.6s1,9.7,3.1,12.6
|
||||
C585.3,716.3,588.5,717.8,592.9,717.8z"/>
|
||||
<path class="st3" d="M639.3,684.1c1.6-3.1,3.5-5.5,5.7-7.2s4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2s-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7c-0.9,0.8-1.6,1.7-2.2,2.7c-0.7,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.2,0.2,2.7,0.7
|
||||
s0.8,1.3,1,2.4L639.3,684.1z"/>
|
||||
<path class="st3" d="M709.4,677v4.6c0,1.5-0.9,2.4-2.6,2.7l-4.6,0.8c0.7,1.8,1.1,3.7,1.1,5.8c0,2.5-0.5,4.8-1.5,6.9
|
||||
c-1,2-2.4,3.8-4.2,5.2c-1.8,1.4-3.9,2.5-6.4,3.3c-2.5,0.8-5.1,1.2-7.9,1.2c-1,0-2,0-2.9-0.2c-0.9-0.1-1.9-0.2-2.8-0.4
|
||||
c-1.6,1-2.4,2-2.4,3.2c0,1,0.5,1.8,1.4,2.3c1,0.5,2.2,0.8,3.8,1c1.6,0.2,3.3,0.3,5.3,0.4s4.1,0.2,6.2,0.3s4.2,0.5,6.2,0.9
|
||||
s3.8,1.1,5.3,2c1.6,0.9,2.8,2.1,3.8,3.7c0.9,1.6,1.4,3.5,1.4,6c0,2.3-0.6,4.5-1.7,6.6c-1.1,2.1-2.7,4-4.9,5.7
|
||||
c-2.1,1.7-4.7,3-7.8,4c-3.1,1-6.6,1.5-10.5,1.5c-3.9,0-7.2-0.4-10.1-1.1c-2.9-0.8-5.2-1.8-7.2-3c-1.9-1.2-3.3-2.7-4.2-4.3
|
||||
s-1.4-3.3-1.4-5.1c0-2.4,0.7-4.4,2.2-6c1.5-1.6,3.4-2.9,6-3.9c-1.4-0.7-2.4-1.6-3.2-2.8c-0.8-1.2-1.2-2.7-1.2-4.6
|
||||
c0-0.8,0.1-1.6,0.4-2.4c0.3-0.8,0.7-1.6,1.2-2.4c0.5-0.8,1.2-1.6,2.1-2.3c0.8-0.7,1.8-1.4,2.9-1.9c-2.6-1.4-4.6-3.3-6.1-5.6
|
||||
c-1.5-2.3-2.2-5.1-2.2-8.2c0-2.5,0.5-4.8,1.5-6.9c1-2.1,2.4-3.8,4.2-5.2c1.8-1.5,4-2.6,6.4-3.3s5.2-1.2,8.1-1.2
|
||||
c2.2,0,4.3,0.2,6.2,0.7c1.9,0.5,3.7,1.1,5.3,2H709.4z M697.1,728.8c0-1-0.3-1.8-0.9-2.5c-0.6-0.6-1.4-1.1-2.5-1.5
|
||||
c-1-0.3-2.2-0.6-3.6-0.8c-1.4-0.2-2.9-0.3-4.4-0.4c-1.5-0.1-3.1-0.2-4.8-0.2c-1.7-0.1-3.3-0.2-4.8-0.4c-1.4,0.8-2.5,1.7-3.3,2.7
|
||||
c-0.8,1-1.3,2.2-1.3,3.6c0,0.9,0.2,1.7,0.7,2.5s1.2,1.5,2.2,2c1,0.6,2.3,1,3.8,1.3c1.6,0.3,3.5,0.5,5.8,0.5c2.3,0,4.3-0.2,6-0.5
|
||||
c1.7-0.3,3-0.8,4.1-1.4c1.1-0.6,1.9-1.4,2.4-2.2C696.8,730.7,697.1,729.8,697.1,728.8z M683.1,699.6c1.5,0,2.9-0.2,4-0.6
|
||||
c1.1-0.4,2.1-1,2.8-1.7c0.8-0.7,1.3-1.6,1.7-2.7c0.4-1,0.6-2.2,0.6-3.4c0-2.5-0.8-4.5-2.3-6c-1.5-1.5-3.8-2.2-6.8-2.2
|
||||
s-5.3,0.7-6.8,2.2c-1.5,1.5-2.3,3.5-2.3,6c0,1.2,0.2,2.3,0.6,3.3c0.4,1,1,1.9,1.7,2.7s1.7,1.3,2.8,1.8
|
||||
C680.3,699.4,681.6,699.6,683.1,699.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon">
|
||||
<path id="path2852" class="st3" d="M314.8,180.9c-12.6,0-24,2-34.1,5.8c-10.1,3.8-18.3,9.2-25.3,16.4c-7,7.2-12.3,15.7-15.8,25.9
|
||||
c-3.8,10.2-5.4,21.8-5.7,34.5c0,11.9,1.6,22.5,4.4,31.7c2.8,9.2,7,17.4,12,23.9c4.7,6.5,10.4,11.9,17.1,16c6.3,4.1,13,7.2,19.9,8.5
|
||||
v2.4c-9.8,2.4-19,5.5-26.9,9.9s-14.8,9.9-20.5,17.1c-5.7,6.8-10.1,15.4-13,25.2c-2.8,9.9-4.4,21.1-4.4,34.5c0,16,1.9,30,6,42.3
|
||||
c4.1,12.3,9.8,22.5,17.1,31c7.3,8.2,16.4,14.7,27.5,19.1c10.7,4.4,23.1,6.5,36.3,6.5s24.6-1.4,34.4-4.1c9.5-2.7,17.7-5.5,24.6-8.5
|
||||
v112.6h37V513.2h29.1v118.4h37V519c7,3.1,14.8,6.1,24.6,8.5c9.8,2.7,21.2,4.1,34.4,4.1c13.6,0,25.6-2,36.3-6.5
|
||||
c10.7-4.4,19.9-10.6,27.5-19.1c7.3-8.2,13-18.8,17.1-31s6-26.6,6-42.3c0-13.3-1.6-24.9-4.4-34.5c-2.8-9.9-7.3-18.4-13-25.2
|
||||
c-5.7-7.2-12.3-12.6-20.5-17.1c-7.9-4.4-17.1-7.5-26.9-9.9v-2.4c7-1.7,13.6-4.4,19.9-8.5c6.6-4.1,12.3-9.6,17.1-16
|
||||
c5.1-6.8,8.8-14.7,12-23.9c2.8-9.2,4.4-19.8,4.4-31.7c0-12.6-1.9-24.2-5.7-34.5c-3.5-10.2-8.8-18.8-15.8-25.9
|
||||
c-7-7.2-15.2-12.6-25.3-16.4c-9.8-3.8-21.2-5.8-34.1-5.8c-13.6,0-25.9,2-37.3,6.1c-11.1,4.1-20.5,10.6-28.8,20.1
|
||||
c-7.9,9.2-14.2,21.5-18.6,36.8c-3.5,12.3-5.7,26.6-6.3,43.3h-29.7c-0.6-16.7-2.8-31-6.3-43.3c-4.4-15.4-10.7-27.6-18.6-36.8
|
||||
c-7.9-9.6-17.4-16-28.8-20.1C340.7,183,328,180.9,314.8,180.9z M418.7,187.8c-3.2,0-6.3,0.7-9.5,2.4c-2.8,1.4-5.4,3.1-7.6,5.5
|
||||
c-2.2,2.4-4.1,5.1-5.4,8.5c-1.3,3.1-1.9,6.5-1.9,10.2c0,3.4,0.6,6.8,1.9,9.9c1.3,3.1,3.2,5.8,5.4,8.2c2.2,2.4,4.7,4.4,7.6,5.8
|
||||
c2.8,1.4,6,2,9.5,2c3.2,0,6.3-0.7,9.2-2c2.8-1.4,5.7-3.4,7.9-5.8c2.2-2.4,3.8-5.1,5.1-8.2c1.3-3.1,1.9-6.5,1.9-9.9
|
||||
c0-3.4-0.6-6.8-1.9-10.2c-1.3-3.1-2.8-6.1-5.1-8.5c-2.2-2.4-4.7-4.1-7.9-5.5C425,188.4,421.9,187.8,418.7,187.8z M315.1,200
|
||||
c8.2,0,15.8,1.7,22.4,4.8c6.6,3.1,12.3,8.5,17.1,16.4c4.7,7.8,8.2,18.1,10.7,31.4c2.5,13,3.5,29.7,3.5,49.5v199.2
|
||||
c-7.9,2.7-16.4,5.1-25.9,6.8c-9.5,1.7-19.6,2.4-30.3,2.4c-16.1,0-28.4-6.1-37.3-18.8c-8.8-12.6-13.3-32.1-13.3-58.3
|
||||
c0-11.9,1.3-22.5,3.8-31.7c2.5-9.6,6.3-17.4,11.7-23.9s12.3-11.6,20.9-15.4c8.5-3.8,19-5.8,31.3-6.1v-18.8
|
||||
c-10.4-0.3-19.3-2-26.5-5.1c-7.3-3.1-13.3-7.8-18-14c-4.7-6.1-7.9-13.6-10.1-22.5c-2.2-9.2-3.2-19.8-3.2-31.7
|
||||
c0-9.6,0.9-18.4,2.5-26.3c1.6-7.8,4.1-14.7,7.9-20.1c3.5-5.5,7.9-9.9,13.3-13C301.2,201.4,307.5,200,315.1,200z M525.5,200
|
||||
c7.6,0,14.2,1.7,19.6,4.4c5.4,3.1,9.8,7.2,13.3,13c3.5,5.5,6.3,12.3,7.9,20.1c1.9,7.8,2.5,16.7,2.5,26.3c0,11.9-0.9,22.5-3.2,31.7
|
||||
c-2.2,8.9-5.4,16.4-10.1,22.5c-4.7,6.1-10.4,10.9-18,14c-7.3,3.1-16.1,4.8-26.5,5.1v18.8c12.3,0.3,22.7,2.4,31.3,6.1
|
||||
c8.5,3.8,15.5,8.9,20.9,15.4s9.5,14.7,11.7,23.9c2.5,9.2,3.8,19.8,3.8,31.7c0,26.3-4.4,45.7-13.3,58.3
|
||||
c-8.8,12.6-21.2,18.8-37.3,18.8c-10.7,0-20.9-0.7-30.3-2.4c-7.6-1.4-14.8-3.1-21.5-5.1v-0.7l-3.5-0.7c-0.3,0-0.9-0.3-1.3-0.3V301.7
|
||||
c0-20.1,1.3-36.5,3.5-49.5c2.5-13,6-23.5,10.7-31.4c4.7-7.8,10.4-13.3,17.1-16.4C509.7,201.7,517.3,200,525.5,200z"/>
|
||||
</g>
|
||||
<g id="Ebene_5">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
7267
ivatar/static/img/logo4hex/libravatar_org_6.ai
Executable file
BIN
ivatar/static/img/logo4hex/libravatar_org_6.eps
Executable file
143
ivatar/static/img/logo4hex/libravatar_org_6.svg
Executable file
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 841.9 967.3" style="enable-background:new 0 0 841.9 967.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{display:none;}
|
||||
.st2{display:inline;fill:url(#SVGID_2_);stroke:#000000;stroke-miterlimit:10;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="_x36_eck">
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="89.9133" y1="153.4554" x2="750.3038" y2="813.8458">
|
||||
<stop offset="8.207178e-03" style="stop-color:#3C5DA8"/>
|
||||
<stop offset="0.2203" style="stop-color:#3C62AC"/>
|
||||
<stop offset="0.5531" style="stop-color:#3C70B7"/>
|
||||
<stop offset="0.9629" style="stop-color:#3E88C8"/>
|
||||
<stop offset="1" style="stop-color:#3E8ACA"/>
|
||||
</linearGradient>
|
||||
<polygon class="st0" points="420.9,0 840.1,241.1 839.4,724.8 419.3,967.3 0.1,726.2 0.9,242.5 "/>
|
||||
</g>
|
||||
<g id="bg" class="st1">
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-31.3529" y1="31.3529" x2="873.2426" y2="935.9484">
|
||||
<stop offset="8.207178e-03" style="stop-color:#3C5DA8"/>
|
||||
<stop offset="0.2203" style="stop-color:#3C62AC"/>
|
||||
<stop offset="0.5531" style="stop-color:#3C70B7"/>
|
||||
<stop offset="0.9629" style="stop-color:#3E88C8"/>
|
||||
<stop offset="1" style="stop-color:#3E8ACA"/>
|
||||
</linearGradient>
|
||||
<rect class="st2" width="841.9" height="967.3"/>
|
||||
</g>
|
||||
<g id="libravatar.org">
|
||||
<g>
|
||||
<path class="st3" d="M142.8,652.2v74.3h-12.4v-74.3H142.8z"/>
|
||||
<path class="st3" d="M169.7,660.3c0,1.1-0.2,2.1-0.6,3c-0.4,0.9-1,1.8-1.7,2.5c-0.7,0.7-1.6,1.3-2.5,1.7c-1,0.4-2,0.6-3.1,0.6
|
||||
c-1.1,0-2.1-0.2-3-0.6c-1-0.4-1.8-1-2.5-1.7c-0.7-0.7-1.3-1.5-1.7-2.5c-0.4-0.9-0.6-1.9-0.6-3c0-1.1,0.2-2.1,0.6-3.1
|
||||
c0.4-1,1-1.8,1.7-2.5c0.7-0.7,1.5-1.3,2.5-1.7c0.9-0.4,2-0.6,3-0.6c1.1,0,2.1,0.2,3.1,0.6c1,0.4,1.8,1,2.5,1.7
|
||||
c0.7,0.7,1.3,1.5,1.7,2.5C169.5,658.1,169.7,659.2,169.7,660.3z M168,675.2v51.3h-12.4v-51.3H168z"/>
|
||||
<path class="st3" d="M192.9,681.5c2.1-2.2,4.4-3.9,6.9-5.2c2.5-1.3,5.4-1.9,8.6-1.9c3,0,5.6,0.6,8,1.8c2.4,1.2,4.4,2.8,6.1,5
|
||||
c1.7,2.2,3,4.8,3.9,7.9c0.9,3.1,1.3,6.6,1.3,10.5c0,4.2-0.5,8-1.5,11.4c-1,3.4-2.5,6.3-4.4,8.7c-1.9,2.4-4.2,4.3-6.9,5.6
|
||||
c-2.7,1.3-5.7,2-9,2c-1.6,0-3-0.2-4.4-0.5c-1.3-0.3-2.5-0.8-3.6-1.3c-1.1-0.6-2.1-1.3-3-2.1c-0.9-0.8-1.8-1.7-2.6-2.7l-0.5,3.4
|
||||
c-0.2,0.9-0.5,1.5-1,1.9c-0.5,0.4-1.1,0.5-2,0.5h-8.1v-74.3h12.4V681.5z M192.9,713.2c1.4,1.7,2.9,2.9,4.6,3.6s3.4,1,5.2,1
|
||||
c1.9,0,3.6-0.3,5.1-1c1.5-0.7,2.8-1.8,3.9-3.2c1.1-1.5,1.9-3.3,2.5-5.5c0.6-2.2,0.8-4.9,0.8-8.1c0-5.6-0.9-9.7-2.8-12.2
|
||||
c-1.9-2.5-4.5-3.8-8-3.8c-2.4,0-4.4,0.5-6.2,1.6c-1.8,1.1-3.5,2.6-5,4.6V713.2z"/>
|
||||
<path class="st3" d="M249,684.1c1.6-3.1,3.5-5.5,5.7-7.2c2.2-1.8,4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2c-1-0.2-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7s-1.6,1.7-2.2,2.7c-0.6,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.1,0.2,2.6,0.7s0.8,1.3,1,2.4
|
||||
L249,684.1z"/>
|
||||
<path class="st3" d="M272.8,682.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1c1.6,1.8,2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9c-1.8-0.6-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7
|
||||
c-0.6-0.5-1.1-1-1.4-1.6L272.8,682.4z M301.1,704.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7c-1.5,0.7-2.5,1.5-3.1,2.5
|
||||
c-0.6,0.9-1,2-1,3c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V704.6z"/>
|
||||
<path class="st3" d="M369.8,675.2l-20.4,51.3h-11.2l-20.3-51.3h10.2c0.9,0,1.7,0.2,2.3,0.6c0.6,0.4,1,1,1.2,1.7l9.9,27.4
|
||||
c0.5,1.6,1,3.2,1.4,4.7c0.4,1.5,0.8,3.1,1,4.6c0.3-1.5,0.7-3.1,1.1-4.6c0.4-1.5,0.9-3.1,1.5-4.7l10.1-27.4
|
||||
c0.2-0.7,0.6-1.2,1.2-1.7c0.6-0.4,1.3-0.6,2.1-0.6H369.8z"/>
|
||||
<path class="st3" d="M373.9,682.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1c1.6,1.8,2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9c-1.8-0.6-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7
|
||||
c-0.6-0.5-1.1-1-1.4-1.6L373.9,682.4z M402.2,704.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7c-1.5,0.7-2.5,1.5-3.1,2.5
|
||||
c-0.6,0.9-1,2-1,3c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V704.6z"/>
|
||||
<path class="st3" d="M442.2,727.3c-4.5,0-7.9-1.3-10.3-3.8c-2.4-2.5-3.6-6-3.6-10.4v-28.6h-5.2c-0.7,0-1.2-0.2-1.7-0.7
|
||||
c-0.5-0.4-0.7-1.1-0.7-2V677l8.2-1.4l2.6-14c0.2-0.7,0.5-1.2,1-1.5c0.5-0.4,1.1-0.5,1.8-0.5h6.4v16.2h13.7v8.8h-13.7v27.8
|
||||
c0,1.6,0.4,2.8,1.2,3.8c0.8,0.9,1.9,1.3,3.2,1.3c0.8,0,1.4-0.1,1.9-0.3c0.5-0.2,1-0.4,1.4-0.6c0.4-0.2,0.7-0.4,1-0.6
|
||||
c0.3-0.2,0.6-0.3,0.9-0.3c0.4,0,0.7,0.1,0.9,0.3c0.2,0.2,0.5,0.5,0.8,0.8l3.7,6c-1.8,1.5-3.9,2.6-6.2,3.4
|
||||
C447.1,726.9,444.7,727.3,442.2,727.3z"/>
|
||||
<path class="st3" d="M461.1,682.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1s2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9s-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7s-1.1-1-1.5-1.6
|
||||
L461.1,682.4z M489.4,704.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7s-2.5,1.5-3.1,2.5c-0.6,0.9-1,2-1,3
|
||||
c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V704.6z"/>
|
||||
<path class="st3" d="M524.7,684.1c1.6-3.1,3.5-5.5,5.7-7.2s4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2s-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7c-0.9,0.8-1.6,1.7-2.2,2.7c-0.7,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.2,0.2,2.7,0.7
|
||||
s0.8,1.3,1,2.4L524.7,684.1z"/>
|
||||
<path class="st3" d="M544.6,719.7c0-1,0.2-2,0.6-3c0.4-0.9,0.9-1.7,1.6-2.4c0.7-0.7,1.5-1.2,2.4-1.6s1.9-0.6,3-0.6s2.1,0.2,3,0.6
|
||||
c0.9,0.4,1.7,0.9,2.4,1.6c0.7,0.7,1.2,1.5,1.6,2.4c0.4,0.9,0.6,1.9,0.6,3c0,1.1-0.2,2.1-0.6,3c-0.4,0.9-0.9,1.7-1.6,2.4
|
||||
c-0.7,0.7-1.5,1.2-2.4,1.6c-0.9,0.4-1.9,0.6-3,0.6s-2.1-0.2-3-0.6s-1.7-0.9-2.4-1.6c-0.7-0.7-1.2-1.5-1.6-2.4
|
||||
C544.8,721.8,544.6,720.8,544.6,719.7z"/>
|
||||
<path class="st3" d="M592.9,674.4c3.8,0,7.3,0.6,10.4,1.8c3.1,1.2,5.8,3,8,5.2c2.2,2.3,3.9,5,5.1,8.3c1.2,3.3,1.8,6.9,1.8,11
|
||||
c0,4.1-0.6,7.7-1.8,11c-1.2,3.3-2.9,6-5.1,8.3c-2.2,2.3-4.9,4.1-8,5.3c-3.1,1.2-6.6,1.8-10.4,1.8c-3.8,0-7.3-0.6-10.5-1.8
|
||||
c-3.1-1.2-5.8-3-8-5.3c-2.2-2.3-3.9-5.1-5.2-8.3c-1.2-3.3-1.8-6.9-1.8-11c0-4,0.6-7.7,1.8-11c1.2-3.3,2.9-6,5.2-8.3
|
||||
c2.2-2.3,4.9-4,8-5.2C585.6,675,589,674.4,592.9,674.4z M592.9,717.8c4.3,0,7.4-1.4,9.5-4.3c2-2.9,3.1-7.1,3.1-12.6
|
||||
c0-5.5-1-9.8-3.1-12.6c-2.1-2.9-5.2-4.4-9.5-4.4c-4.3,0-7.5,1.5-9.6,4.4c-2.1,2.9-3.1,7.1-3.1,12.6s1,9.7,3.1,12.6
|
||||
C585.3,716.3,588.5,717.8,592.9,717.8z"/>
|
||||
<path class="st3" d="M639.3,684.1c1.6-3.1,3.5-5.5,5.7-7.2s4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2s-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7c-0.9,0.8-1.6,1.7-2.2,2.7c-0.7,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.2,0.2,2.7,0.7
|
||||
s0.8,1.3,1,2.4L639.3,684.1z"/>
|
||||
<path class="st3" d="M709.4,677v4.6c0,1.5-0.9,2.4-2.6,2.7l-4.6,0.8c0.7,1.8,1.1,3.7,1.1,5.8c0,2.5-0.5,4.8-1.5,6.9
|
||||
c-1,2-2.4,3.8-4.2,5.2c-1.8,1.4-3.9,2.5-6.4,3.3c-2.5,0.8-5.1,1.2-7.9,1.2c-1,0-2,0-2.9-0.2c-0.9-0.1-1.9-0.2-2.8-0.4
|
||||
c-1.6,1-2.4,2-2.4,3.2c0,1,0.5,1.8,1.4,2.3c1,0.5,2.2,0.8,3.8,1c1.6,0.2,3.3,0.3,5.3,0.4s4.1,0.2,6.2,0.3s4.2,0.5,6.2,0.9
|
||||
s3.8,1.1,5.3,2c1.6,0.9,2.8,2.1,3.8,3.7c0.9,1.6,1.4,3.5,1.4,6c0,2.3-0.6,4.5-1.7,6.6c-1.1,2.1-2.7,4-4.9,5.7
|
||||
c-2.1,1.7-4.7,3-7.8,4c-3.1,1-6.6,1.5-10.5,1.5c-3.9,0-7.2-0.4-10.1-1.1c-2.9-0.8-5.2-1.8-7.2-3c-1.9-1.2-3.3-2.7-4.2-4.3
|
||||
s-1.4-3.3-1.4-5.1c0-2.4,0.7-4.4,2.2-6c1.5-1.6,3.4-2.9,6-3.9c-1.4-0.7-2.4-1.6-3.2-2.8c-0.8-1.2-1.2-2.7-1.2-4.6
|
||||
c0-0.8,0.1-1.6,0.4-2.4c0.3-0.8,0.7-1.6,1.2-2.4c0.5-0.8,1.2-1.6,2.1-2.3c0.8-0.7,1.8-1.4,2.9-1.9c-2.6-1.4-4.6-3.3-6.1-5.6
|
||||
c-1.5-2.3-2.2-5.1-2.2-8.2c0-2.5,0.5-4.8,1.5-6.9c1-2.1,2.4-3.8,4.2-5.2c1.8-1.5,4-2.6,6.4-3.3s5.2-1.2,8.1-1.2
|
||||
c2.2,0,4.3,0.2,6.2,0.7c1.9,0.5,3.7,1.1,5.3,2H709.4z M697.1,728.8c0-1-0.3-1.8-0.9-2.5c-0.6-0.6-1.4-1.1-2.5-1.5
|
||||
c-1-0.3-2.2-0.6-3.6-0.8c-1.4-0.2-2.9-0.3-4.4-0.4c-1.5-0.1-3.1-0.2-4.8-0.2c-1.7-0.1-3.3-0.2-4.8-0.4c-1.4,0.8-2.5,1.7-3.3,2.7
|
||||
c-0.8,1-1.3,2.2-1.3,3.6c0,0.9,0.2,1.7,0.7,2.5s1.2,1.5,2.2,2c1,0.6,2.3,1,3.8,1.3c1.6,0.3,3.5,0.5,5.8,0.5c2.3,0,4.3-0.2,6-0.5
|
||||
c1.7-0.3,3-0.8,4.1-1.4c1.1-0.6,1.9-1.4,2.4-2.2C696.8,730.7,697.1,729.8,697.1,728.8z M683.1,699.6c1.5,0,2.9-0.2,4-0.6
|
||||
c1.1-0.4,2.1-1,2.8-1.7c0.8-0.7,1.3-1.6,1.7-2.7c0.4-1,0.6-2.2,0.6-3.4c0-2.5-0.8-4.5-2.3-6c-1.5-1.5-3.8-2.2-6.8-2.2
|
||||
s-5.3,0.7-6.8,2.2c-1.5,1.5-2.3,3.5-2.3,6c0,1.2,0.2,2.3,0.6,3.3c0.4,1,1,1.9,1.7,2.7s1.7,1.3,2.8,1.8
|
||||
C680.3,699.4,681.6,699.6,683.1,699.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon">
|
||||
<path id="path2852" class="st3" d="M314.8,180.9c-12.6,0-24,2-34.1,5.8c-10.1,3.8-18.3,9.2-25.3,16.4c-7,7.2-12.3,15.7-15.8,25.9
|
||||
c-3.8,10.2-5.4,21.8-5.7,34.5c0,11.9,1.6,22.5,4.4,31.7c2.8,9.2,7,17.4,12,23.9c4.7,6.5,10.4,11.9,17.1,16c6.3,4.1,13,7.2,19.9,8.5
|
||||
v2.4c-9.8,2.4-19,5.5-26.9,9.9s-14.8,9.9-20.5,17.1c-5.7,6.8-10.1,15.4-13,25.2c-2.8,9.9-4.4,21.1-4.4,34.5c0,16,1.9,30,6,42.3
|
||||
c4.1,12.3,9.8,22.5,17.1,31c7.3,8.2,16.4,14.7,27.5,19.1c10.7,4.4,23.1,6.5,36.3,6.5s24.6-1.4,34.4-4.1c9.5-2.7,17.7-5.5,24.6-8.5
|
||||
v112.6h37V513.2h29.1v118.4h37V519c7,3.1,14.8,6.1,24.6,8.5c9.8,2.7,21.2,4.1,34.4,4.1c13.6,0,25.6-2,36.3-6.5
|
||||
c10.7-4.4,19.9-10.6,27.5-19.1c7.3-8.2,13-18.8,17.1-31s6-26.6,6-42.3c0-13.3-1.6-24.9-4.4-34.5c-2.8-9.9-7.3-18.4-13-25.2
|
||||
c-5.7-7.2-12.3-12.6-20.5-17.1c-7.9-4.4-17.1-7.5-26.9-9.9v-2.4c7-1.7,13.6-4.4,19.9-8.5c6.6-4.1,12.3-9.6,17.1-16
|
||||
c5.1-6.8,8.8-14.7,12-23.9c2.8-9.2,4.4-19.8,4.4-31.7c0-12.6-1.9-24.2-5.7-34.5c-3.5-10.2-8.8-18.8-15.8-25.9
|
||||
c-7-7.2-15.2-12.6-25.3-16.4c-9.8-3.8-21.2-5.8-34.1-5.8c-13.6,0-25.9,2-37.3,6.1c-11.1,4.1-20.5,10.6-28.8,20.1
|
||||
c-7.9,9.2-14.2,21.5-18.6,36.8c-3.5,12.3-5.7,26.6-6.3,43.3h-29.7c-0.6-16.7-2.8-31-6.3-43.3c-4.4-15.4-10.7-27.6-18.6-36.8
|
||||
c-7.9-9.6-17.4-16-28.8-20.1C340.7,183,328,180.9,314.8,180.9z M418.7,187.8c-3.2,0-6.3,0.7-9.5,2.4c-2.8,1.4-5.4,3.1-7.6,5.5
|
||||
c-2.2,2.4-4.1,5.1-5.4,8.5c-1.3,3.1-1.9,6.5-1.9,10.2c0,3.4,0.6,6.8,1.9,9.9c1.3,3.1,3.2,5.8,5.4,8.2c2.2,2.4,4.7,4.4,7.6,5.8
|
||||
c2.8,1.4,6,2,9.5,2c3.2,0,6.3-0.7,9.2-2c2.8-1.4,5.7-3.4,7.9-5.8c2.2-2.4,3.8-5.1,5.1-8.2c1.3-3.1,1.9-6.5,1.9-9.9
|
||||
c0-3.4-0.6-6.8-1.9-10.2c-1.3-3.1-2.8-6.1-5.1-8.5c-2.2-2.4-4.7-4.1-7.9-5.5C425,188.4,421.9,187.8,418.7,187.8z M315.1,200
|
||||
c8.2,0,15.8,1.7,22.4,4.8c6.6,3.1,12.3,8.5,17.1,16.4c4.7,7.8,8.2,18.1,10.7,31.4c2.5,13,3.5,29.7,3.5,49.5v199.2
|
||||
c-7.9,2.7-16.4,5.1-25.9,6.8c-9.5,1.7-19.6,2.4-30.3,2.4c-16.1,0-28.4-6.1-37.3-18.8c-8.8-12.6-13.3-32.1-13.3-58.3
|
||||
c0-11.9,1.3-22.5,3.8-31.7c2.5-9.6,6.3-17.4,11.7-23.9s12.3-11.6,20.9-15.4c8.5-3.8,19-5.8,31.3-6.1v-18.8
|
||||
c-10.4-0.3-19.3-2-26.5-5.1c-7.3-3.1-13.3-7.8-18-14c-4.7-6.1-7.9-13.6-10.1-22.5c-2.2-9.2-3.2-19.8-3.2-31.7
|
||||
c0-9.6,0.9-18.4,2.5-26.3c1.6-7.8,4.1-14.7,7.9-20.1c3.5-5.5,7.9-9.9,13.3-13C301.2,201.4,307.5,200,315.1,200z M525.5,200
|
||||
c7.6,0,14.2,1.7,19.6,4.4c5.4,3.1,9.8,7.2,13.3,13c3.5,5.5,6.3,12.3,7.9,20.1c1.9,7.8,2.5,16.7,2.5,26.3c0,11.9-0.9,22.5-3.2,31.7
|
||||
c-2.2,8.9-5.4,16.4-10.1,22.5c-4.7,6.1-10.4,10.9-18,14c-7.3,3.1-16.1,4.8-26.5,5.1v18.8c12.3,0.3,22.7,2.4,31.3,6.1
|
||||
c8.5,3.8,15.5,8.9,20.9,15.4s9.5,14.7,11.7,23.9c2.5,9.2,3.8,19.8,3.8,31.7c0,26.3-4.4,45.7-13.3,58.3
|
||||
c-8.8,12.6-21.2,18.8-37.3,18.8c-10.7,0-20.9-0.7-30.3-2.4c-7.6-1.4-14.8-3.1-21.5-5.1v-0.7l-3.5-0.7c-0.3,0-0.9-0.3-1.3-0.3V301.7
|
||||
c0-20.1,1.3-36.5,3.5-49.5c2.5-13,6-23.5,10.7-31.4c4.7-7.8,10.4-13.3,17.1-16.4C509.7,201.7,517.3,200,525.5,200z"/>
|
||||
</g>
|
||||
<g id="Ebene_5">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
7483
ivatar/static/img/logo4hex/libravatar_org_process_blue.ai
Executable file
BIN
ivatar/static/img/logo4hex/libravatar_org_process_blue.eps
Executable file
132
ivatar/static/img/logo4hex/libravatar_org_process_blue.svg
Executable file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 841.9 967.3" style="enable-background:new 0 0 841.9 967.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;fill:#0885C7;}
|
||||
.st2{fill:#0885C7;stroke:#000000;stroke-miterlimit:10;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{display:none;opacity:0.25;}
|
||||
</style>
|
||||
<g id="_x36_eck" class="st0">
|
||||
<polygon class="st1" points="421.9,0 841.1,241.1 840.4,724.8 420.3,967.3 1.1,726.2 1.9,242.5 "/>
|
||||
</g>
|
||||
<g id="bg">
|
||||
<rect class="st2" width="841.9" height="967.3"/>
|
||||
</g>
|
||||
<g id="libravatar.org">
|
||||
<g>
|
||||
<path class="st3" d="M142.8,666.2v74.3h-12.4v-74.3H142.8z"/>
|
||||
<path class="st3" d="M169.7,674.3c0,1.1-0.2,2.1-0.6,3c-0.4,0.9-1,1.8-1.7,2.5c-0.7,0.7-1.6,1.3-2.5,1.7c-1,0.4-2,0.6-3.1,0.6
|
||||
c-1.1,0-2.1-0.2-3-0.6c-1-0.4-1.8-1-2.5-1.7c-0.7-0.7-1.3-1.5-1.7-2.5c-0.4-0.9-0.6-1.9-0.6-3c0-1.1,0.2-2.1,0.6-3.1
|
||||
c0.4-1,1-1.8,1.7-2.5c0.7-0.7,1.5-1.3,2.5-1.7c0.9-0.4,2-0.6,3-0.6c1.1,0,2.1,0.2,3.1,0.6c1,0.4,1.8,1,2.5,1.7
|
||||
c0.7,0.7,1.3,1.5,1.7,2.5C169.5,672.1,169.7,673.2,169.7,674.3z M168,689.2v51.3h-12.4v-51.3H168z"/>
|
||||
<path class="st3" d="M192.9,695.5c2.1-2.2,4.4-3.9,6.9-5.2c2.5-1.3,5.4-1.9,8.6-1.9c3,0,5.6,0.6,8,1.8c2.4,1.2,4.4,2.8,6.1,5
|
||||
c1.7,2.2,3,4.8,3.9,7.9c0.9,3.1,1.3,6.6,1.3,10.5c0,4.2-0.5,8-1.5,11.4c-1,3.4-2.5,6.3-4.4,8.7c-1.9,2.4-4.2,4.3-6.9,5.6
|
||||
c-2.7,1.3-5.7,2-9,2c-1.6,0-3-0.2-4.4-0.5c-1.3-0.3-2.5-0.8-3.6-1.3c-1.1-0.6-2.1-1.3-3-2.1c-0.9-0.8-1.8-1.7-2.6-2.7l-0.5,3.4
|
||||
c-0.2,0.9-0.5,1.5-1,1.9c-0.5,0.4-1.1,0.5-2,0.5h-8.1v-74.3h12.4V695.5z M192.9,727.2c1.4,1.7,2.9,2.9,4.6,3.6s3.4,1,5.2,1
|
||||
c1.9,0,3.6-0.3,5.1-1c1.5-0.7,2.8-1.8,3.9-3.2c1.1-1.5,1.9-3.3,2.5-5.5c0.6-2.2,0.8-4.9,0.8-8.1c0-5.6-0.9-9.7-2.8-12.2
|
||||
c-1.9-2.5-4.5-3.8-8-3.8c-2.4,0-4.4,0.5-6.2,1.6c-1.8,1.1-3.5,2.6-5,4.6V727.2z"/>
|
||||
<path class="st3" d="M249,698.1c1.6-3.1,3.5-5.5,5.7-7.2c2.2-1.8,4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2c-1-0.2-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7s-1.6,1.7-2.2,2.7c-0.6,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.1,0.2,2.6,0.7s0.8,1.3,1,2.4
|
||||
L249,698.1z"/>
|
||||
<path class="st3" d="M272.8,696.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1c1.6,1.8,2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9c-1.8-0.6-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7
|
||||
c-0.6-0.5-1.1-1-1.4-1.6L272.8,696.4z M301.1,718.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7c-1.5,0.7-2.5,1.5-3.1,2.5
|
||||
c-0.6,0.9-1,2-1,3c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V718.6z"/>
|
||||
<path class="st3" d="M369.8,689.2l-20.4,51.3h-11.2l-20.3-51.3h10.2c0.9,0,1.7,0.2,2.3,0.6c0.6,0.4,1,1,1.2,1.7l9.9,27.4
|
||||
c0.5,1.6,1,3.2,1.4,4.7c0.4,1.5,0.8,3.1,1,4.6c0.3-1.5,0.7-3.1,1.1-4.6c0.4-1.5,0.9-3.1,1.5-4.7l10.1-27.4
|
||||
c0.2-0.7,0.6-1.2,1.2-1.7c0.6-0.4,1.3-0.6,2.1-0.6H369.8z"/>
|
||||
<path class="st3" d="M373.9,696.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1c1.6,1.8,2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9c-1.8-0.6-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7
|
||||
c-0.6-0.5-1.1-1-1.4-1.6L373.9,696.4z M402.2,718.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7c-1.5,0.7-2.5,1.5-3.1,2.5
|
||||
c-0.6,0.9-1,2-1,3c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V718.6z"/>
|
||||
<path class="st3" d="M442.2,741.3c-4.5,0-7.9-1.3-10.3-3.8c-2.4-2.5-3.6-6-3.6-10.4v-28.6h-5.2c-0.7,0-1.2-0.2-1.7-0.7
|
||||
c-0.5-0.4-0.7-1.1-0.7-2V691l8.2-1.4l2.6-14c0.2-0.7,0.5-1.2,1-1.5c0.5-0.4,1.1-0.5,1.8-0.5h6.4v16.2h13.7v8.8h-13.7v27.8
|
||||
c0,1.6,0.4,2.8,1.2,3.8c0.8,0.9,1.9,1.3,3.2,1.3c0.8,0,1.4-0.1,1.9-0.3c0.5-0.2,1-0.4,1.4-0.6c0.4-0.2,0.7-0.4,1-0.6
|
||||
c0.3-0.2,0.6-0.3,0.9-0.3c0.4,0,0.7,0.1,0.9,0.3c0.2,0.2,0.5,0.5,0.8,0.8l3.7,6c-1.8,1.5-3.9,2.6-6.2,3.4
|
||||
C447.1,740.9,444.7,741.3,442.2,741.3z"/>
|
||||
<path class="st3" d="M461.1,696.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1s2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9s-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7s-1.1-1-1.5-1.6
|
||||
L461.1,696.4z M489.4,718.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7s-2.5,1.5-3.1,2.5c-0.6,0.9-1,2-1,3
|
||||
c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V718.6z"/>
|
||||
<path class="st3" d="M524.7,698.1c1.6-3.1,3.5-5.5,5.7-7.2s4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2s-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7c-0.9,0.8-1.6,1.7-2.2,2.7c-0.7,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.2,0.2,2.7,0.7
|
||||
s0.8,1.3,1,2.4L524.7,698.1z"/>
|
||||
<path class="st3" d="M544.6,733.7c0-1,0.2-2,0.6-3c0.4-0.9,0.9-1.7,1.6-2.4c0.7-0.7,1.5-1.2,2.4-1.6s1.9-0.6,3-0.6s2.1,0.2,3,0.6
|
||||
c0.9,0.4,1.7,0.9,2.4,1.6c0.7,0.7,1.2,1.5,1.6,2.4c0.4,0.9,0.6,1.9,0.6,3c0,1.1-0.2,2.1-0.6,3c-0.4,0.9-0.9,1.7-1.6,2.4
|
||||
c-0.7,0.7-1.5,1.2-2.4,1.6c-0.9,0.4-1.9,0.6-3,0.6s-2.1-0.2-3-0.6s-1.7-0.9-2.4-1.6c-0.7-0.7-1.2-1.5-1.6-2.4
|
||||
C544.8,735.8,544.6,734.8,544.6,733.7z"/>
|
||||
<path class="st3" d="M592.9,688.4c3.8,0,7.3,0.6,10.4,1.8c3.1,1.2,5.8,3,8,5.2c2.2,2.3,3.9,5,5.1,8.3c1.2,3.3,1.8,6.9,1.8,11
|
||||
c0,4.1-0.6,7.7-1.8,11c-1.2,3.3-2.9,6-5.1,8.3c-2.2,2.3-4.9,4.1-8,5.3c-3.1,1.2-6.6,1.8-10.4,1.8c-3.8,0-7.3-0.6-10.5-1.8
|
||||
c-3.1-1.2-5.8-3-8-5.3c-2.2-2.3-3.9-5.1-5.2-8.3c-1.2-3.3-1.8-6.9-1.8-11c0-4,0.6-7.7,1.8-11c1.2-3.3,2.9-6,5.2-8.3
|
||||
c2.2-2.3,4.9-4,8-5.2C585.6,689,589,688.4,592.9,688.4z M592.9,731.8c4.3,0,7.4-1.4,9.5-4.3c2-2.9,3.1-7.1,3.1-12.6
|
||||
c0-5.5-1-9.8-3.1-12.6c-2.1-2.9-5.2-4.4-9.5-4.4c-4.3,0-7.5,1.5-9.6,4.4c-2.1,2.9-3.1,7.1-3.1,12.6s1,9.7,3.1,12.6
|
||||
C585.3,730.3,588.5,731.8,592.9,731.8z"/>
|
||||
<path class="st3" d="M639.3,698.1c1.6-3.1,3.5-5.5,5.7-7.2s4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2s-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7c-0.9,0.8-1.6,1.7-2.2,2.7c-0.7,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.2,0.2,2.7,0.7
|
||||
s0.8,1.3,1,2.4L639.3,698.1z"/>
|
||||
<path class="st3" d="M709.4,691v4.6c0,1.5-0.9,2.4-2.6,2.7l-4.6,0.8c0.7,1.8,1.1,3.7,1.1,5.8c0,2.5-0.5,4.8-1.5,6.9
|
||||
c-1,2-2.4,3.8-4.2,5.2c-1.8,1.4-3.9,2.5-6.4,3.3c-2.5,0.8-5.1,1.2-7.9,1.2c-1,0-2,0-2.9-0.2c-0.9-0.1-1.9-0.2-2.8-0.4
|
||||
c-1.6,1-2.4,2-2.4,3.2c0,1,0.5,1.8,1.4,2.3c1,0.5,2.2,0.8,3.8,1c1.6,0.2,3.3,0.3,5.3,0.4s4.1,0.2,6.2,0.3s4.2,0.5,6.2,0.9
|
||||
s3.8,1.1,5.3,2c1.6,0.9,2.8,2.1,3.8,3.7c0.9,1.6,1.4,3.5,1.4,6c0,2.3-0.6,4.5-1.7,6.6c-1.1,2.1-2.7,4-4.9,5.7
|
||||
c-2.1,1.7-4.7,3-7.8,4c-3.1,1-6.6,1.5-10.5,1.5c-3.9,0-7.2-0.4-10.1-1.1c-2.9-0.8-5.2-1.8-7.2-3c-1.9-1.2-3.3-2.7-4.2-4.3
|
||||
s-1.4-3.3-1.4-5.1c0-2.4,0.7-4.4,2.2-6c1.5-1.6,3.4-2.9,6-3.9c-1.4-0.7-2.4-1.6-3.2-2.8c-0.8-1.2-1.2-2.7-1.2-4.6
|
||||
c0-0.8,0.1-1.6,0.4-2.4c0.3-0.8,0.7-1.6,1.2-2.4c0.5-0.8,1.2-1.6,2.1-2.3c0.8-0.7,1.8-1.4,2.9-1.9c-2.6-1.4-4.6-3.3-6.1-5.6
|
||||
c-1.5-2.3-2.2-5.1-2.2-8.2c0-2.5,0.5-4.8,1.5-6.9c1-2.1,2.4-3.8,4.2-5.2c1.8-1.5,4-2.6,6.4-3.3s5.2-1.2,8.1-1.2
|
||||
c2.2,0,4.3,0.2,6.2,0.7c1.9,0.5,3.7,1.1,5.3,2H709.4z M697.1,742.8c0-1-0.3-1.8-0.9-2.5c-0.6-0.6-1.4-1.1-2.5-1.5
|
||||
c-1-0.3-2.2-0.6-3.6-0.8c-1.4-0.2-2.9-0.3-4.4-0.4c-1.5-0.1-3.1-0.2-4.8-0.2c-1.7-0.1-3.3-0.2-4.8-0.4c-1.4,0.8-2.5,1.7-3.3,2.7
|
||||
c-0.8,1-1.3,2.2-1.3,3.6c0,0.9,0.2,1.7,0.7,2.5s1.2,1.5,2.2,2c1,0.6,2.3,1,3.8,1.3c1.6,0.3,3.5,0.5,5.8,0.5c2.3,0,4.3-0.2,6-0.5
|
||||
c1.7-0.3,3-0.8,4.1-1.4c1.1-0.6,1.9-1.4,2.4-2.2C696.8,744.7,697.1,743.8,697.1,742.8z M683.1,713.6c1.5,0,2.9-0.2,4-0.6
|
||||
c1.1-0.4,2.1-1,2.8-1.7c0.8-0.7,1.3-1.6,1.7-2.7c0.4-1,0.6-2.2,0.6-3.4c0-2.5-0.8-4.5-2.3-6c-1.5-1.5-3.8-2.2-6.8-2.2
|
||||
s-5.3,0.7-6.8,2.2c-1.5,1.5-2.3,3.5-2.3,6c0,1.2,0.2,2.3,0.6,3.3c0.4,1,1,1.9,1.7,2.7s1.7,1.3,2.8,1.8
|
||||
C680.3,713.4,681.6,713.6,683.1,713.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon">
|
||||
<path id="path2852" class="st3" d="M312.8,190.9c-12.6,0-24,2-34.1,5.8c-10.1,3.8-18.3,9.2-25.3,16.4c-7,7.2-12.3,15.7-15.8,25.9
|
||||
c-3.8,10.2-5.4,21.8-5.7,34.5c0,11.9,1.6,22.5,4.4,31.7c2.8,9.2,7,17.4,12,23.9c4.7,6.5,10.4,11.9,17.1,16c6.3,4.1,13,7.2,19.9,8.5
|
||||
v2.4c-9.8,2.4-19,5.5-26.9,9.9s-14.8,9.9-20.5,17.1c-5.7,6.8-10.1,15.4-13,25.2c-2.8,9.9-4.4,21.1-4.4,34.5c0,16,1.9,30,6,42.3
|
||||
c4.1,12.3,9.8,22.5,17.1,31c7.3,8.2,16.4,14.7,27.5,19.1c10.7,4.4,23.1,6.5,36.3,6.5s24.6-1.4,34.4-4.1c9.5-2.7,17.7-5.5,24.6-8.5
|
||||
v112.6h37V523.2h29.1v118.4h37V529c7,3.1,14.8,6.1,24.6,8.5c9.8,2.7,21.2,4.1,34.4,4.1c13.6,0,25.6-2,36.3-6.5
|
||||
c10.7-4.4,19.9-10.6,27.5-19.1c7.3-8.2,13-18.8,17.1-31s6-26.6,6-42.3c0-13.3-1.6-24.9-4.4-34.5c-2.8-9.9-7.3-18.4-13-25.2
|
||||
c-5.7-7.2-12.3-12.6-20.5-17.1c-7.9-4.4-17.1-7.5-26.9-9.9v-2.4c7-1.7,13.6-4.4,19.9-8.5c6.6-4.1,12.3-9.6,17.1-16
|
||||
c5.1-6.8,8.8-14.7,12-23.9c2.8-9.2,4.4-19.8,4.4-31.7c0-12.6-1.9-24.2-5.7-34.5c-3.5-10.2-8.8-18.8-15.8-25.9
|
||||
c-7-7.2-15.2-12.6-25.3-16.4c-9.8-3.8-21.2-5.8-34.1-5.8c-13.6,0-25.9,2-37.3,6.1c-11.1,4.1-20.5,10.6-28.8,20.1
|
||||
c-7.9,9.2-14.2,21.5-18.6,36.8c-3.5,12.3-5.7,26.6-6.3,43.3h-29.7c-0.6-16.7-2.8-31-6.3-43.3c-4.4-15.4-10.7-27.6-18.6-36.8
|
||||
c-7.9-9.6-17.4-16-28.8-20.1C338.7,193,326,190.9,312.8,190.9z M416.7,197.8c-3.2,0-6.3,0.7-9.5,2.4c-2.8,1.4-5.4,3.1-7.6,5.5
|
||||
c-2.2,2.4-4.1,5.1-5.4,8.5c-1.3,3.1-1.9,6.5-1.9,10.2c0,3.4,0.6,6.8,1.9,9.9c1.3,3.1,3.2,5.8,5.4,8.2c2.2,2.4,4.7,4.4,7.6,5.8
|
||||
c2.8,1.4,6,2,9.5,2c3.2,0,6.3-0.7,9.2-2c2.8-1.4,5.7-3.4,7.9-5.8c2.2-2.4,3.8-5.1,5.1-8.2c1.3-3.1,1.9-6.5,1.9-9.9
|
||||
c0-3.4-0.6-6.8-1.9-10.2c-1.3-3.1-2.8-6.1-5.1-8.5c-2.2-2.4-4.7-4.1-7.9-5.5C423,198.4,419.9,197.8,416.7,197.8z M313.1,210
|
||||
c8.2,0,15.8,1.7,22.4,4.8c6.6,3.1,12.3,8.5,17.1,16.4c4.7,7.8,8.2,18.1,10.7,31.4c2.5,13,3.5,29.7,3.5,49.5v199.2
|
||||
c-7.9,2.7-16.4,5.1-25.9,6.8c-9.5,1.7-19.6,2.4-30.3,2.4c-16.1,0-28.4-6.1-37.3-18.8c-8.8-12.6-13.3-32.1-13.3-58.3
|
||||
c0-11.9,1.3-22.5,3.8-31.7c2.5-9.6,6.3-17.4,11.7-23.9s12.3-11.6,20.9-15.4c8.5-3.8,19-5.8,31.3-6.1v-18.8
|
||||
c-10.4-0.3-19.3-2-26.5-5.1c-7.3-3.1-13.3-7.8-18-14c-4.7-6.1-7.9-13.6-10.1-22.5c-2.2-9.2-3.2-19.8-3.2-31.7
|
||||
c0-9.6,0.9-18.4,2.5-26.3c1.6-7.8,4.1-14.7,7.9-20.1c3.5-5.5,7.9-9.9,13.3-13C299.2,211.4,305.5,210,313.1,210z M523.5,210
|
||||
c7.6,0,14.2,1.7,19.6,4.4c5.4,3.1,9.8,7.2,13.3,13c3.5,5.5,6.3,12.3,7.9,20.1c1.9,7.8,2.5,16.7,2.5,26.3c0,11.9-0.9,22.5-3.2,31.7
|
||||
c-2.2,8.9-5.4,16.4-10.1,22.5c-4.7,6.1-10.4,10.9-18,14c-7.3,3.1-16.1,4.8-26.5,5.1v18.8c12.3,0.3,22.7,2.4,31.3,6.1
|
||||
c8.5,3.8,15.5,8.9,20.9,15.4s9.5,14.7,11.7,23.9c2.5,9.2,3.8,19.8,3.8,31.7c0,26.3-4.4,45.7-13.3,58.3
|
||||
c-8.8,12.6-21.2,18.8-37.3,18.8c-10.7,0-20.9-0.7-30.3-2.4c-7.6-1.4-14.8-3.1-21.5-5.1v-0.7l-3.5-0.7c-0.3,0-0.9-0.3-1.3-0.3V311.7
|
||||
c0-20.1,1.3-36.5,3.5-49.5c2.5-13,6-23.5,10.7-31.4c4.7-7.8,10.4-13.3,17.1-16.4C507.7,211.7,515.3,210,523.5,210z"/>
|
||||
</g>
|
||||
<g id="Ebene_5">
|
||||
</g>
|
||||
<g id="Ebene_6" class="st4">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
7646
ivatar/static/img/logo4hex/libravatar_org_process_blue_6.ai
Executable file
BIN
ivatar/static/img/logo4hex/libravatar_org_process_blue_6.eps
Executable file
132
ivatar/static/img/logo4hex/libravatar_org_process_blue_6.svg
Executable file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 841.9 967.3" style="enable-background:new 0 0 841.9 967.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#0885C7;}
|
||||
.st1{display:none;}
|
||||
.st2{display:inline;fill:#0885C7;stroke:#000000;stroke-miterlimit:10;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{display:none;opacity:0.25;}
|
||||
</style>
|
||||
<g id="_x36_eck">
|
||||
<polygon class="st0" points="421.9,0 841.1,241.1 840.4,724.8 420.3,967.3 1.1,726.2 1.9,242.5 "/>
|
||||
</g>
|
||||
<g id="bg" class="st1">
|
||||
<rect class="st2" width="841.9" height="967.3"/>
|
||||
</g>
|
||||
<g id="libravatar.org">
|
||||
<g>
|
||||
<path class="st3" d="M142.8,666.2v74.3h-12.4v-74.3H142.8z"/>
|
||||
<path class="st3" d="M169.7,674.3c0,1.1-0.2,2.1-0.6,3c-0.4,0.9-1,1.8-1.7,2.5c-0.7,0.7-1.6,1.3-2.5,1.7c-1,0.4-2,0.6-3.1,0.6
|
||||
c-1.1,0-2.1-0.2-3-0.6c-1-0.4-1.8-1-2.5-1.7c-0.7-0.7-1.3-1.5-1.7-2.5c-0.4-0.9-0.6-1.9-0.6-3c0-1.1,0.2-2.1,0.6-3.1
|
||||
c0.4-1,1-1.8,1.7-2.5c0.7-0.7,1.5-1.3,2.5-1.7c0.9-0.4,2-0.6,3-0.6c1.1,0,2.1,0.2,3.1,0.6c1,0.4,1.8,1,2.5,1.7
|
||||
c0.7,0.7,1.3,1.5,1.7,2.5C169.5,672.1,169.7,673.2,169.7,674.3z M168,689.2v51.3h-12.4v-51.3H168z"/>
|
||||
<path class="st3" d="M192.9,695.5c2.1-2.2,4.4-3.9,6.9-5.2c2.5-1.3,5.4-1.9,8.6-1.9c3,0,5.6,0.6,8,1.8c2.4,1.2,4.4,2.8,6.1,5
|
||||
c1.7,2.2,3,4.8,3.9,7.9c0.9,3.1,1.3,6.6,1.3,10.5c0,4.2-0.5,8-1.5,11.4c-1,3.4-2.5,6.3-4.4,8.7c-1.9,2.4-4.2,4.3-6.9,5.6
|
||||
c-2.7,1.3-5.7,2-9,2c-1.6,0-3-0.2-4.4-0.5c-1.3-0.3-2.5-0.8-3.6-1.3c-1.1-0.6-2.1-1.3-3-2.1c-0.9-0.8-1.8-1.7-2.6-2.7l-0.5,3.4
|
||||
c-0.2,0.9-0.5,1.5-1,1.9c-0.5,0.4-1.1,0.5-2,0.5h-8.1v-74.3h12.4V695.5z M192.9,727.2c1.4,1.7,2.9,2.9,4.6,3.6s3.4,1,5.2,1
|
||||
c1.9,0,3.6-0.3,5.1-1c1.5-0.7,2.8-1.8,3.9-3.2c1.1-1.5,1.9-3.3,2.5-5.5c0.6-2.2,0.8-4.9,0.8-8.1c0-5.6-0.9-9.7-2.8-12.2
|
||||
c-1.9-2.5-4.5-3.8-8-3.8c-2.4,0-4.4,0.5-6.2,1.6c-1.8,1.1-3.5,2.6-5,4.6V727.2z"/>
|
||||
<path class="st3" d="M249,698.1c1.6-3.1,3.5-5.5,5.7-7.2c2.2-1.8,4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2c-1-0.2-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7s-1.6,1.7-2.2,2.7c-0.6,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.1,0.2,2.6,0.7s0.8,1.3,1,2.4
|
||||
L249,698.1z"/>
|
||||
<path class="st3" d="M272.8,696.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1c1.6,1.8,2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9c-1.8-0.6-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7
|
||||
c-0.6-0.5-1.1-1-1.4-1.6L272.8,696.4z M301.1,718.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7c-1.5,0.7-2.5,1.5-3.1,2.5
|
||||
c-0.6,0.9-1,2-1,3c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V718.6z"/>
|
||||
<path class="st3" d="M369.8,689.2l-20.4,51.3h-11.2l-20.3-51.3h10.2c0.9,0,1.7,0.2,2.3,0.6c0.6,0.4,1,1,1.2,1.7l9.9,27.4
|
||||
c0.5,1.6,1,3.2,1.4,4.7c0.4,1.5,0.8,3.1,1,4.6c0.3-1.5,0.7-3.1,1.1-4.6c0.4-1.5,0.9-3.1,1.5-4.7l10.1-27.4
|
||||
c0.2-0.7,0.6-1.2,1.2-1.7c0.6-0.4,1.3-0.6,2.1-0.6H369.8z"/>
|
||||
<path class="st3" d="M373.9,696.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1c1.6,1.8,2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9c-1.8-0.6-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7
|
||||
c-0.6-0.5-1.1-1-1.4-1.6L373.9,696.4z M402.2,718.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7c-1.5,0.7-2.5,1.5-3.1,2.5
|
||||
c-0.6,0.9-1,2-1,3c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V718.6z"/>
|
||||
<path class="st3" d="M442.2,741.3c-4.5,0-7.9-1.3-10.3-3.8c-2.4-2.5-3.6-6-3.6-10.4v-28.6h-5.2c-0.7,0-1.2-0.2-1.7-0.7
|
||||
c-0.5-0.4-0.7-1.1-0.7-2V691l8.2-1.4l2.6-14c0.2-0.7,0.5-1.2,1-1.5c0.5-0.4,1.1-0.5,1.8-0.5h6.4v16.2h13.7v8.8h-13.7v27.8
|
||||
c0,1.6,0.4,2.8,1.2,3.8c0.8,0.9,1.9,1.3,3.2,1.3c0.8,0,1.4-0.1,1.9-0.3c0.5-0.2,1-0.4,1.4-0.6c0.4-0.2,0.7-0.4,1-0.6
|
||||
c0.3-0.2,0.6-0.3,0.9-0.3c0.4,0,0.7,0.1,0.9,0.3c0.2,0.2,0.5,0.5,0.8,0.8l3.7,6c-1.8,1.5-3.9,2.6-6.2,3.4
|
||||
C447.1,740.9,444.7,741.3,442.2,741.3z"/>
|
||||
<path class="st3" d="M461.1,696.4c5.9-5.4,13-8.1,21.3-8.1c3,0,5.7,0.5,8,1.5c2.4,1,4.4,2.4,6,4.1s2.9,3.8,3.7,6.3
|
||||
c0.9,2.4,1.3,5.1,1.3,8v32.4h-5.6c-1.2,0-2.1-0.2-2.7-0.5c-0.6-0.3-1.1-1.1-1.5-2.1l-1.1-3.7c-1.3,1.2-2.6,2.2-3.8,3.1
|
||||
c-1.2,0.9-2.5,1.6-3.8,2.2c-1.3,0.6-2.8,1.1-4.3,1.4c-1.5,0.3-3.2,0.5-5,0.5c-2.2,0-4.2-0.3-6-0.9s-3.4-1.5-4.8-2.6
|
||||
c-1.3-1.2-2.4-2.6-3.1-4.3c-0.7-1.7-1.1-3.8-1.1-6c0-1.3,0.2-2.6,0.6-3.9c0.4-1.3,1.1-2.5,2.1-3.7c1-1.2,2.3-2.3,3.8-3.3
|
||||
c1.6-1,3.5-1.9,5.8-2.7c2.3-0.8,4.9-1.4,8-1.9c3-0.5,6.5-0.8,10.4-0.9v-3c0-3.4-0.7-6-2.2-7.6c-1.5-1.6-3.6-2.5-6.4-2.5
|
||||
c-2,0-3.7,0.2-5,0.7c-1.3,0.5-2.5,1-3.5,1.6s-1.9,1.1-2.7,1.6c-0.8,0.5-1.7,0.7-2.7,0.7c-0.9,0-1.6-0.2-2.2-0.7s-1.1-1-1.5-1.6
|
||||
L461.1,696.4z M489.4,718.6c-3.6,0.2-6.6,0.5-9,0.9c-2.4,0.4-4.4,1-5.9,1.7s-2.5,1.5-3.1,2.5c-0.6,0.9-1,2-1,3
|
||||
c0,2.2,0.6,3.7,1.9,4.7s3,1.4,5,1.4c2.5,0,4.7-0.5,6.6-1.4c1.9-0.9,3.7-2.3,5.4-4.2V718.6z"/>
|
||||
<path class="st3" d="M524.7,698.1c1.6-3.1,3.5-5.5,5.7-7.2s4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2s-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7c-0.9,0.8-1.6,1.7-2.2,2.7c-0.7,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.2,0.2,2.7,0.7
|
||||
s0.8,1.3,1,2.4L524.7,698.1z"/>
|
||||
<path class="st3" d="M544.6,733.7c0-1,0.2-2,0.6-3c0.4-0.9,0.9-1.7,1.6-2.4c0.7-0.7,1.5-1.2,2.4-1.6s1.9-0.6,3-0.6s2.1,0.2,3,0.6
|
||||
c0.9,0.4,1.7,0.9,2.4,1.6c0.7,0.7,1.2,1.5,1.6,2.4c0.4,0.9,0.6,1.9,0.6,3c0,1.1-0.2,2.1-0.6,3c-0.4,0.9-0.9,1.7-1.6,2.4
|
||||
c-0.7,0.7-1.5,1.2-2.4,1.6c-0.9,0.4-1.9,0.6-3,0.6s-2.1-0.2-3-0.6s-1.7-0.9-2.4-1.6c-0.7-0.7-1.2-1.5-1.6-2.4
|
||||
C544.8,735.8,544.6,734.8,544.6,733.7z"/>
|
||||
<path class="st3" d="M592.9,688.4c3.8,0,7.3,0.6,10.4,1.8c3.1,1.2,5.8,3,8,5.2c2.2,2.3,3.9,5,5.1,8.3c1.2,3.3,1.8,6.9,1.8,11
|
||||
c0,4.1-0.6,7.7-1.8,11c-1.2,3.3-2.9,6-5.1,8.3c-2.2,2.3-4.9,4.1-8,5.3c-3.1,1.2-6.6,1.8-10.4,1.8c-3.8,0-7.3-0.6-10.5-1.8
|
||||
c-3.1-1.2-5.8-3-8-5.3c-2.2-2.3-3.9-5.1-5.2-8.3c-1.2-3.3-1.8-6.9-1.8-11c0-4,0.6-7.7,1.8-11c1.2-3.3,2.9-6,5.2-8.3
|
||||
c2.2-2.3,4.9-4,8-5.2C585.6,689,589,688.4,592.9,688.4z M592.9,731.8c4.3,0,7.4-1.4,9.5-4.3c2-2.9,3.1-7.1,3.1-12.6
|
||||
c0-5.5-1-9.8-3.1-12.6c-2.1-2.9-5.2-4.4-9.5-4.4c-4.3,0-7.5,1.5-9.6,4.4c-2.1,2.9-3.1,7.1-3.1,12.6s1,9.7,3.1,12.6
|
||||
C585.3,730.3,588.5,731.8,592.9,731.8z"/>
|
||||
<path class="st3" d="M639.3,698.1c1.6-3.1,3.5-5.5,5.7-7.2s4.8-2.6,7.8-2.6c2.4,0,4.3,0.5,5.7,1.5l-0.8,9.2
|
||||
c-0.2,0.6-0.4,1-0.7,1.3c-0.3,0.2-0.7,0.4-1.3,0.4c-0.5,0-1.2-0.1-2.2-0.2s-1.9-0.2-2.9-0.2c-1.4,0-2.6,0.2-3.6,0.6
|
||||
c-1.1,0.4-2,1-2.9,1.7c-0.9,0.8-1.6,1.7-2.2,2.7c-0.7,1.1-1.3,2.3-1.8,3.7v31.6h-12.4v-51.3h7.2c1.3,0,2.2,0.2,2.7,0.7
|
||||
s0.8,1.3,1,2.4L639.3,698.1z"/>
|
||||
<path class="st3" d="M709.4,691v4.6c0,1.5-0.9,2.4-2.6,2.7l-4.6,0.8c0.7,1.8,1.1,3.7,1.1,5.8c0,2.5-0.5,4.8-1.5,6.9
|
||||
c-1,2-2.4,3.8-4.2,5.2c-1.8,1.4-3.9,2.5-6.4,3.3c-2.5,0.8-5.1,1.2-7.9,1.2c-1,0-2,0-2.9-0.2c-0.9-0.1-1.9-0.2-2.8-0.4
|
||||
c-1.6,1-2.4,2-2.4,3.2c0,1,0.5,1.8,1.4,2.3c1,0.5,2.2,0.8,3.8,1c1.6,0.2,3.3,0.3,5.3,0.4s4.1,0.2,6.2,0.3s4.2,0.5,6.2,0.9
|
||||
s3.8,1.1,5.3,2c1.6,0.9,2.8,2.1,3.8,3.7c0.9,1.6,1.4,3.5,1.4,6c0,2.3-0.6,4.5-1.7,6.6c-1.1,2.1-2.7,4-4.9,5.7
|
||||
c-2.1,1.7-4.7,3-7.8,4c-3.1,1-6.6,1.5-10.5,1.5c-3.9,0-7.2-0.4-10.1-1.1c-2.9-0.8-5.2-1.8-7.2-3c-1.9-1.2-3.3-2.7-4.2-4.3
|
||||
s-1.4-3.3-1.4-5.1c0-2.4,0.7-4.4,2.2-6c1.5-1.6,3.4-2.9,6-3.9c-1.4-0.7-2.4-1.6-3.2-2.8c-0.8-1.2-1.2-2.7-1.2-4.6
|
||||
c0-0.8,0.1-1.6,0.4-2.4c0.3-0.8,0.7-1.6,1.2-2.4c0.5-0.8,1.2-1.6,2.1-2.3c0.8-0.7,1.8-1.4,2.9-1.9c-2.6-1.4-4.6-3.3-6.1-5.6
|
||||
c-1.5-2.3-2.2-5.1-2.2-8.2c0-2.5,0.5-4.8,1.5-6.9c1-2.1,2.4-3.8,4.2-5.2c1.8-1.5,4-2.6,6.4-3.3s5.2-1.2,8.1-1.2
|
||||
c2.2,0,4.3,0.2,6.2,0.7c1.9,0.5,3.7,1.1,5.3,2H709.4z M697.1,742.8c0-1-0.3-1.8-0.9-2.5c-0.6-0.6-1.4-1.1-2.5-1.5
|
||||
c-1-0.3-2.2-0.6-3.6-0.8c-1.4-0.2-2.9-0.3-4.4-0.4c-1.5-0.1-3.1-0.2-4.8-0.2c-1.7-0.1-3.3-0.2-4.8-0.4c-1.4,0.8-2.5,1.7-3.3,2.7
|
||||
c-0.8,1-1.3,2.2-1.3,3.6c0,0.9,0.2,1.7,0.7,2.5s1.2,1.5,2.2,2c1,0.6,2.3,1,3.8,1.3c1.6,0.3,3.5,0.5,5.8,0.5c2.3,0,4.3-0.2,6-0.5
|
||||
c1.7-0.3,3-0.8,4.1-1.4c1.1-0.6,1.9-1.4,2.4-2.2C696.8,744.7,697.1,743.8,697.1,742.8z M683.1,713.6c1.5,0,2.9-0.2,4-0.6
|
||||
c1.1-0.4,2.1-1,2.8-1.7c0.8-0.7,1.3-1.6,1.7-2.7c0.4-1,0.6-2.2,0.6-3.4c0-2.5-0.8-4.5-2.3-6c-1.5-1.5-3.8-2.2-6.8-2.2
|
||||
s-5.3,0.7-6.8,2.2c-1.5,1.5-2.3,3.5-2.3,6c0,1.2,0.2,2.3,0.6,3.3c0.4,1,1,1.9,1.7,2.7s1.7,1.3,2.8,1.8
|
||||
C680.3,713.4,681.6,713.6,683.1,713.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon">
|
||||
<path id="path2852" class="st3" d="M312.8,190.9c-12.6,0-24,2-34.1,5.8c-10.1,3.8-18.3,9.2-25.3,16.4c-7,7.2-12.3,15.7-15.8,25.9
|
||||
c-3.8,10.2-5.4,21.8-5.7,34.5c0,11.9,1.6,22.5,4.4,31.7c2.8,9.2,7,17.4,12,23.9c4.7,6.5,10.4,11.9,17.1,16c6.3,4.1,13,7.2,19.9,8.5
|
||||
v2.4c-9.8,2.4-19,5.5-26.9,9.9s-14.8,9.9-20.5,17.1c-5.7,6.8-10.1,15.4-13,25.2c-2.8,9.9-4.4,21.1-4.4,34.5c0,16,1.9,30,6,42.3
|
||||
c4.1,12.3,9.8,22.5,17.1,31c7.3,8.2,16.4,14.7,27.5,19.1c10.7,4.4,23.1,6.5,36.3,6.5s24.6-1.4,34.4-4.1c9.5-2.7,17.7-5.5,24.6-8.5
|
||||
v112.6h37V523.2h29.1v118.4h37V529c7,3.1,14.8,6.1,24.6,8.5c9.8,2.7,21.2,4.1,34.4,4.1c13.6,0,25.6-2,36.3-6.5
|
||||
c10.7-4.4,19.9-10.6,27.5-19.1c7.3-8.2,13-18.8,17.1-31s6-26.6,6-42.3c0-13.3-1.6-24.9-4.4-34.5c-2.8-9.9-7.3-18.4-13-25.2
|
||||
c-5.7-7.2-12.3-12.6-20.5-17.1c-7.9-4.4-17.1-7.5-26.9-9.9v-2.4c7-1.7,13.6-4.4,19.9-8.5c6.6-4.1,12.3-9.6,17.1-16
|
||||
c5.1-6.8,8.8-14.7,12-23.9c2.8-9.2,4.4-19.8,4.4-31.7c0-12.6-1.9-24.2-5.7-34.5c-3.5-10.2-8.8-18.8-15.8-25.9
|
||||
c-7-7.2-15.2-12.6-25.3-16.4c-9.8-3.8-21.2-5.8-34.1-5.8c-13.6,0-25.9,2-37.3,6.1c-11.1,4.1-20.5,10.6-28.8,20.1
|
||||
c-7.9,9.2-14.2,21.5-18.6,36.8c-3.5,12.3-5.7,26.6-6.3,43.3h-29.7c-0.6-16.7-2.8-31-6.3-43.3c-4.4-15.4-10.7-27.6-18.6-36.8
|
||||
c-7.9-9.6-17.4-16-28.8-20.1C338.7,193,326,190.9,312.8,190.9z M416.7,197.8c-3.2,0-6.3,0.7-9.5,2.4c-2.8,1.4-5.4,3.1-7.6,5.5
|
||||
c-2.2,2.4-4.1,5.1-5.4,8.5c-1.3,3.1-1.9,6.5-1.9,10.2c0,3.4,0.6,6.8,1.9,9.9c1.3,3.1,3.2,5.8,5.4,8.2c2.2,2.4,4.7,4.4,7.6,5.8
|
||||
c2.8,1.4,6,2,9.5,2c3.2,0,6.3-0.7,9.2-2c2.8-1.4,5.7-3.4,7.9-5.8c2.2-2.4,3.8-5.1,5.1-8.2c1.3-3.1,1.9-6.5,1.9-9.9
|
||||
c0-3.4-0.6-6.8-1.9-10.2c-1.3-3.1-2.8-6.1-5.1-8.5c-2.2-2.4-4.7-4.1-7.9-5.5C423,198.4,419.9,197.8,416.7,197.8z M313.1,210
|
||||
c8.2,0,15.8,1.7,22.4,4.8c6.6,3.1,12.3,8.5,17.1,16.4c4.7,7.8,8.2,18.1,10.7,31.4c2.5,13,3.5,29.7,3.5,49.5v199.2
|
||||
c-7.9,2.7-16.4,5.1-25.9,6.8c-9.5,1.7-19.6,2.4-30.3,2.4c-16.1,0-28.4-6.1-37.3-18.8c-8.8-12.6-13.3-32.1-13.3-58.3
|
||||
c0-11.9,1.3-22.5,3.8-31.7c2.5-9.6,6.3-17.4,11.7-23.9s12.3-11.6,20.9-15.4c8.5-3.8,19-5.8,31.3-6.1v-18.8
|
||||
c-10.4-0.3-19.3-2-26.5-5.1c-7.3-3.1-13.3-7.8-18-14c-4.7-6.1-7.9-13.6-10.1-22.5c-2.2-9.2-3.2-19.8-3.2-31.7
|
||||
c0-9.6,0.9-18.4,2.5-26.3c1.6-7.8,4.1-14.7,7.9-20.1c3.5-5.5,7.9-9.9,13.3-13C299.2,211.4,305.5,210,313.1,210z M523.5,210
|
||||
c7.6,0,14.2,1.7,19.6,4.4c5.4,3.1,9.8,7.2,13.3,13c3.5,5.5,6.3,12.3,7.9,20.1c1.9,7.8,2.5,16.7,2.5,26.3c0,11.9-0.9,22.5-3.2,31.7
|
||||
c-2.2,8.9-5.4,16.4-10.1,22.5c-4.7,6.1-10.4,10.9-18,14c-7.3,3.1-16.1,4.8-26.5,5.1v18.8c12.3,0.3,22.7,2.4,31.3,6.1
|
||||
c8.5,3.8,15.5,8.9,20.9,15.4s9.5,14.7,11.7,23.9c2.5,9.2,3.8,19.8,3.8,31.7c0,26.3-4.4,45.7-13.3,58.3
|
||||
c-8.8,12.6-21.2,18.8-37.3,18.8c-10.7,0-20.9-0.7-30.3-2.4c-7.6-1.4-14.8-3.1-21.5-5.1v-0.7l-3.5-0.7c-0.3,0-0.9-0.3-1.3-0.3V311.7
|
||||
c0-20.1,1.3-36.5,3.5-49.5c2.5-13,6-23.5,10.7-31.4c4.7-7.8,10.4-13.3,17.1-16.4C507.7,211.7,515.3,210,523.5,210z"/>
|
||||
</g>
|
||||
<g id="Ebene_5">
|
||||
</g>
|
||||
<g id="Ebene_6" class="st4">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
182
ivatar/tasks.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Celery tasks for avatar generation
|
||||
"""
|
||||
|
||||
import logging
|
||||
from celery import shared_task
|
||||
from django.contrib.auth.models import User
|
||||
from io import BytesIO
|
||||
|
||||
from ivatar.ai_service import generate_avatar_image, AIServiceError
|
||||
from ivatar.ivataraccount.models import GenerationTask, Photo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_task(bind=True, name="ivatar.tasks.generate_avatar_task")
|
||||
def generate_avatar_task(self, task_id, user_id, prompt, model, quality, allow_nsfw):
|
||||
"""
|
||||
Background task to generate avatar images
|
||||
|
||||
Args:
|
||||
task_id: GenerationTask ID
|
||||
user_id: User ID
|
||||
prompt: Avatar description
|
||||
model: AI model to use
|
||||
quality: Generation quality
|
||||
allow_nsfw: Whether to allow NSFW content
|
||||
|
||||
Returns:
|
||||
dict: Task result with photo_id or error
|
||||
"""
|
||||
try:
|
||||
# Get the task object
|
||||
task = GenerationTask.objects.get(pk=task_id)
|
||||
|
||||
# Update task status
|
||||
task.status = "processing"
|
||||
task.task_id = self.request.id
|
||||
task.progress = 10
|
||||
task.save()
|
||||
|
||||
logger.info(f"Starting avatar generation task {task_id} for user {user_id}")
|
||||
|
||||
# Update progress
|
||||
self.update_state(
|
||||
state="PROGRESS",
|
||||
meta={"progress": 20, "status": "Initializing generation..."},
|
||||
)
|
||||
task.progress = 20
|
||||
task.save()
|
||||
|
||||
# Generate the avatar image
|
||||
self.update_state(
|
||||
state="PROGRESS", meta={"progress": 30, "status": "Generating image..."}
|
||||
)
|
||||
task.progress = 30
|
||||
task.save()
|
||||
|
||||
generated_image = generate_avatar_image(
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
size=(512, 512),
|
||||
quality=quality,
|
||||
allow_nsfw=allow_nsfw,
|
||||
)
|
||||
|
||||
# Update progress
|
||||
self.update_state(
|
||||
state="PROGRESS", meta={"progress": 70, "status": "Processing image..."}
|
||||
)
|
||||
task.progress = 70
|
||||
task.save()
|
||||
|
||||
# Convert PIL image to bytes
|
||||
img_buffer = BytesIO()
|
||||
generated_image.save(img_buffer, format="PNG")
|
||||
img_data = img_buffer.getvalue()
|
||||
|
||||
# Get user
|
||||
user = User.objects.get(pk=user_id)
|
||||
|
||||
# Create Photo object
|
||||
photo = Photo()
|
||||
photo.user = user
|
||||
photo.ip_address = "127.0.0.1" # Default IP for background tasks
|
||||
photo.data = img_data
|
||||
photo.format = "png"
|
||||
|
||||
# Store AI generation metadata
|
||||
photo.ai_generated = True
|
||||
photo.ai_prompt = prompt
|
||||
photo.ai_model = model
|
||||
photo.ai_quality = quality
|
||||
|
||||
photo.save()
|
||||
|
||||
# Update progress
|
||||
self.update_state(
|
||||
state="PROGRESS", meta={"progress": 90, "status": "Saving avatar..."}
|
||||
)
|
||||
task.progress = 90
|
||||
task.save()
|
||||
|
||||
# Update task with completed status
|
||||
task.status = "completed"
|
||||
task.progress = 100
|
||||
task.generated_photo = photo
|
||||
task.save()
|
||||
|
||||
logger.info(
|
||||
f"Completed avatar generation task {task_id}, created photo {photo.pk}"
|
||||
)
|
||||
|
||||
return {"status": "completed", "photo_id": photo.pk, "task_id": task_id}
|
||||
|
||||
except AIServiceError as e:
|
||||
logger.error(f"AI service error in task {task_id}: {e}")
|
||||
task.status = "failed"
|
||||
task.error_message = str(e)
|
||||
task.progress = 0
|
||||
task.save()
|
||||
|
||||
return {"status": "failed", "error": str(e), "task_id": task_id}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in task {task_id}: {e}")
|
||||
task.status = "failed"
|
||||
task.error_message = str(e)
|
||||
task.progress = 0
|
||||
task.save()
|
||||
|
||||
return {
|
||||
"status": "failed",
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"task_id": task_id,
|
||||
}
|
||||
|
||||
|
||||
@shared_task(name="ivatar.tasks.update_queue_positions")
|
||||
def update_queue_positions():
|
||||
"""
|
||||
Update queue positions for pending tasks
|
||||
"""
|
||||
try:
|
||||
pending_tasks = GenerationTask.objects.filter(status="pending").order_by(
|
||||
"add_date"
|
||||
)
|
||||
|
||||
for index, task in enumerate(pending_tasks):
|
||||
task.queue_position = index + 1
|
||||
task.save()
|
||||
|
||||
logger.info(f"Updated queue positions for {len(pending_tasks)} pending tasks")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating queue positions: {e}")
|
||||
|
||||
|
||||
@shared_task(name="ivatar.tasks.cleanup_old_tasks")
|
||||
def cleanup_old_tasks():
|
||||
"""
|
||||
Clean up old completed/failed tasks
|
||||
"""
|
||||
try:
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
# Delete tasks older than 7 days
|
||||
cutoff_date = timezone.now() - timedelta(days=7)
|
||||
|
||||
old_tasks = GenerationTask.objects.filter(
|
||||
add_date__lt=cutoff_date, status__in=["completed", "failed", "cancelled"]
|
||||
)
|
||||
|
||||
count = old_tasks.count()
|
||||
old_tasks.delete()
|
||||
|
||||
logger.info(f"Cleaned up {count} old tasks")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up old tasks: {e}")
|
||||
48
ivatar/test_auxiliary.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test various other parts of ivatar/libravatar in order
|
||||
to increase the overall test coverage. Test in here, didn't
|
||||
fit anywhere else.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from ivatar.utils import random_string
|
||||
from ivatar.ivataraccount.models import pil_format, UserPreference
|
||||
|
||||
|
||||
class Tester(TestCase):
|
||||
"""
|
||||
Main test class
|
||||
"""
|
||||
|
||||
user = None
|
||||
username = random_string()
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Prepare tests.
|
||||
- Create user
|
||||
"""
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
)
|
||||
|
||||
def test_pil_format(self):
|
||||
"""
|
||||
Test pil format function
|
||||
"""
|
||||
self.assertEqual(pil_format("jpg"), "JPEG")
|
||||
self.assertEqual(pil_format("jpeg"), "JPEG")
|
||||
self.assertEqual(pil_format("png"), "PNG")
|
||||
self.assertEqual(pil_format("gif"), "GIF")
|
||||
self.assertEqual(pil_format("webp"), "WEBP")
|
||||
self.assertEqual(pil_format("abc"), None)
|
||||
|
||||
def test_userprefs_str(self):
|
||||
"""
|
||||
Test if str representation of UserPreferences is as expected
|
||||
"""
|
||||
up = UserPreference(theme="default", user=self.user)
|
||||
print(up)
|
||||
@@ -1,57 +1,45 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||
'''
|
||||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
from urllib.parse import urlsplit
|
||||
from io import BytesIO
|
||||
import io
|
||||
import os
|
||||
import django
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
import hashlib
|
||||
|
||||
from libravatar import libravatar_url
|
||||
|
||||
from PIL import Image
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar import settings
|
||||
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
||||
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId
|
||||
from ivatar.utils import random_string
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
||||
django.setup()
|
||||
|
||||
|
||||
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))
|
||||
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')
|
||||
openid = "http://%s.%s.%s/" % (username, random_string(), "org")
|
||||
|
||||
def login(self):
|
||||
'''
|
||||
"""
|
||||
Login as user
|
||||
'''
|
||||
"""
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def setUp(self):
|
||||
'''
|
||||
"""
|
||||
Prepare for tests.
|
||||
- Create user
|
||||
'''
|
||||
"""
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
@@ -61,20 +49,19 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Test contact page
|
||||
"""
|
||||
response = self.client.get(reverse('contact'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
response = self.client.get(reverse("contact"))
|
||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
||||
|
||||
def test_description_page(self):
|
||||
"""
|
||||
Test description page
|
||||
"""
|
||||
response = self.client.get(reverse('description'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
response = self.client.get(reverse("description"))
|
||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
||||
|
||||
def test_security_page(self):
|
||||
"""
|
||||
Test security page
|
||||
"""
|
||||
response = self.client.get(reverse('security'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
|
||||
response = self.client.get(reverse("security"))
|
||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test our utils from ivatar.utils
|
||||
'''
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from ivatar.utils import openid_variations
|
||||
from ivatar.utils import is_trusted_url, openid_variations
|
||||
|
||||
|
||||
class Tester(TestCase):
|
||||
'''
|
||||
"""
|
||||
Main test class
|
||||
'''
|
||||
"""
|
||||
|
||||
def test_openid_variations(self):
|
||||
'''
|
||||
"""
|
||||
Test if the OpenID variation "generator" does the correct thing
|
||||
'''
|
||||
openid0 = 'http://user.url/'
|
||||
openid1 = 'http://user.url'
|
||||
openid2 = 'https://user.url/'
|
||||
openid3 = 'https://user.url'
|
||||
"""
|
||||
openid0 = "http://user.url/"
|
||||
openid1 = "http://user.url"
|
||||
openid2 = "https://user.url/"
|
||||
openid3 = "https://user.url"
|
||||
|
||||
# First variation
|
||||
self.assertEqual(openid_variations(openid0)[0], openid0)
|
||||
@@ -44,3 +45,73 @@ class Tester(TestCase):
|
||||
self.assertEqual(openid_variations(openid3)[1], openid1)
|
||||
self.assertEqual(openid_variations(openid3)[2], openid2)
|
||||
self.assertEqual(openid_variations(openid3)[3], openid3)
|
||||
|
||||
def test_is_trusted_url(self):
|
||||
test_gravatar_true = is_trusted_url("https://gravatar.com/avatar/63a75a80e6b1f4adfdb04c1ca02e596c", [
|
||||
{
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"host_equals": "gravatar.com",
|
||||
"path_prefix": "/avatar/"
|
||||
}
|
||||
])
|
||||
self.assertTrue(test_gravatar_true)
|
||||
|
||||
test_gravatar_false = is_trusted_url("https://gravatar.com.example.org/avatar/63a75a80e6b1f4adfdb04c1ca02e596c", [
|
||||
{
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"host_suffix": ".gravatar.com",
|
||||
"path_prefix": "/avatar/"
|
||||
}
|
||||
])
|
||||
self.assertFalse(test_gravatar_false)
|
||||
|
||||
test_open_redirect = is_trusted_url("https://github.com/SethFalco/?boop=https://secure.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50", [
|
||||
{
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"host_suffix": ".gravatar.com",
|
||||
"path_prefix": "/avatar/"
|
||||
}
|
||||
])
|
||||
self.assertFalse(test_open_redirect)
|
||||
|
||||
test_multiple_filters = is_trusted_url("https://ui-avatars.com/api/blah", [
|
||||
{
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"host_equals": "ui-avatars.com",
|
||||
"path_prefix": "/api/"
|
||||
},
|
||||
{
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"host_suffix": ".gravatar.com",
|
||||
"path_prefix": "/avatar/"
|
||||
}
|
||||
])
|
||||
self.assertTrue(test_multiple_filters)
|
||||
|
||||
test_url_prefix_true = is_trusted_url("https://ui-avatars.com/api/blah", [
|
||||
{
|
||||
"url_prefix": "https://ui-avatars.com/api/"
|
||||
}
|
||||
])
|
||||
self.assertTrue(test_url_prefix_true)
|
||||
|
||||
test_url_prefix_false = is_trusted_url("https://ui-avatars.com/api/blah", [
|
||||
{
|
||||
"url_prefix": "https://gravatar.com/avatar/"
|
||||
}
|
||||
])
|
||||
self.assertFalse(test_url_prefix_false)
|
||||
|
||||
@@ -1,57 +1,52 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
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
|
||||
import io
|
||||
import os
|
||||
import json
|
||||
import django
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
import hashlib
|
||||
from ivatar.utils import random_string, Bluesky
|
||||
|
||||
from libravatar import libravatar_url
|
||||
|
||||
from PIL import Image
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||
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()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar import settings
|
||||
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
||||
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId
|
||||
from ivatar.utils import random_string
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
|
||||
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
'''
|
||||
"""
|
||||
Main test class
|
||||
'''
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
user = None
|
||||
username = random_string()
|
||||
password = random_string()
|
||||
email = '%s@%s.%s' % (username, random_string(), random_string(2))
|
||||
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')
|
||||
openid = "http://%s.%s.%s/" % (username, random_string(), "org")
|
||||
|
||||
def login(self):
|
||||
'''
|
||||
"""
|
||||
Login as user
|
||||
'''
|
||||
"""
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def setUp(self):
|
||||
'''
|
||||
"""
|
||||
Prepare for tests.
|
||||
- Create user
|
||||
'''
|
||||
"""
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
@@ -61,8 +56,58 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Test incorrect digest
|
||||
"""
|
||||
response = self.client.get('/avatar/%s' % 'x'*65, follow=True)
|
||||
self.assertRedirects(
|
||||
response=response,
|
||||
expected_url='/static/img/deadbeef.png',
|
||||
msg_prefix='Why does an invalid hash not redirect to deadbeef?')
|
||||
response = self.client.get("/avatar/" + "x" * 65, follow=True)
|
||||
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 an invalid hash not redirect to deadbeef?",
|
||||
# )
|
||||
|
||||
def test_stats(self):
|
||||
"""
|
||||
Test incorrect digest
|
||||
"""
|
||||
response = self.client.get("/stats/", follow=True)
|
||||
self.assertEqual(response.status_code, 200, "unable to fetch stats!")
|
||||
j = json.loads(response.content)
|
||||
self.assertEqual(j["users"], 1, "user count incorrect")
|
||||
self.assertEqual(j["mails"], 0, "mails count incorrect")
|
||||
self.assertEqual(j["openids"], 0, "openids count incorrect")
|
||||
self.assertEqual(j["unconfirmed_mails"], 0, "unconfirmed mails count incorrect")
|
||||
self.assertEqual(
|
||||
j["unconfirmed_openids"], 0, "unconfirmed openids count incorrect"
|
||||
)
|
||||
self.assertEqual(j["avatars"], 0, "avatars count incorrect")
|
||||
|
||||
def test_logout(self):
|
||||
"""
|
||||
Test if logout works correctly
|
||||
"""
|
||||
self.login()
|
||||
response = self.client.get(reverse("logout"), follow=True)
|
||||
self.assertEqual(
|
||||
response.status_code, 405, "logout with get should lead to http error 405"
|
||||
)
|
||||
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("libravatar.org")
|
||||
self.assertEqual(profile["handle"], "libravata.org")
|
||||
# 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)
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Unit tests for WSGI
|
||||
'''
|
||||
"""
|
||||
import unittest
|
||||
|
||||
import os
|
||||
import django
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
||||
django.setup()
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
'''
|
||||
"""
|
||||
Simple testcase to see if WSGI loads correctly
|
||||
'''
|
||||
"""
|
||||
|
||||
def test_run_wsgi(self):
|
||||
'''
|
||||
"""
|
||||
Run wsgi import
|
||||
'''
|
||||
import ivatar.wsgi
|
||||
self.assertEqual(ivatar.wsgi.application.__class__,
|
||||
django.core.handlers.wsgi.WSGIHandler)
|
||||
"""
|
||||
import ivatar.wsgi # pylint: disable=import-outside-toplevel
|
||||
|
||||
self.assertEqual(
|
||||
ivatar.wsgi.application.__class__, django.core.handlers.wsgi.WSGIHandler
|
||||
)
|
||||
|
||||
0
ivatar/tools/__init__.py
Normal file
@@ -1,8 +1,9 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Classes for our ivatar.tools.forms
|
||||
'''
|
||||
"""
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms.utils import ErrorList
|
||||
|
||||
@@ -12,45 +13,40 @@ from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL
|
||||
|
||||
|
||||
class CheckDomainForm(forms.Form):
|
||||
'''
|
||||
"""
|
||||
Form handling domain check
|
||||
'''
|
||||
"""
|
||||
|
||||
domain = forms.CharField(
|
||||
label=_('Domain'),
|
||||
label=_("Domain"),
|
||||
required=True,
|
||||
error_messages={
|
||||
'required':
|
||||
_('Cannot check without a domain name.')
|
||||
}
|
||||
error_messages={"required": _("Cannot check without a domain name.")},
|
||||
)
|
||||
|
||||
|
||||
class CheckForm(forms.Form):
|
||||
'''
|
||||
"""
|
||||
Form handling check
|
||||
'''
|
||||
"""
|
||||
|
||||
mail = forms.EmailField(
|
||||
label=_('E-Mail'),
|
||||
label=_("E-Mail"),
|
||||
required=False,
|
||||
min_length=MIN_LENGTH_EMAIL,
|
||||
max_length=MAX_LENGTH_EMAIL,
|
||||
error_messages={
|
||||
'required':
|
||||
_('Cannot check without a domain name.')
|
||||
})
|
||||
error_messages={"required": _("Cannot check without a domain name.")},
|
||||
)
|
||||
|
||||
openid = forms.CharField(
|
||||
label=_('OpenID'),
|
||||
label=_("OpenID"),
|
||||
required=False,
|
||||
min_length=MIN_LENGTH_URL,
|
||||
max_length=MAX_LENGTH_URL,
|
||||
error_messages={
|
||||
'required':
|
||||
_('Cannot check without an openid name.')
|
||||
})
|
||||
error_messages={"required": _("Cannot check without an openid name.")},
|
||||
)
|
||||
|
||||
size = forms.IntegerField(
|
||||
label=_('Size'),
|
||||
label=_("Size"),
|
||||
initial=80,
|
||||
min_value=5,
|
||||
max_value=AVATAR_MAX_SIZE,
|
||||
@@ -58,23 +54,24 @@ class CheckForm(forms.Form):
|
||||
)
|
||||
|
||||
default_opt = forms.ChoiceField(
|
||||
label=_('Default'),
|
||||
label=_("Default"),
|
||||
required=False,
|
||||
widget=forms.RadioSelect,
|
||||
choices = [
|
||||
('retro', _('Retro style (similar to GitHub)')),
|
||||
('robohash', _('Roboter style')),
|
||||
('pagan', _('Retro adventure character')),
|
||||
('wavatar', _('Wavatar style')),
|
||||
('monsterid', _('Monster style')),
|
||||
('identicon', _('Identicon style')),
|
||||
('mm', _('Mystery man')),
|
||||
('none', _('None')),
|
||||
choices=[
|
||||
("retro", _("Retro style (similar to GitHub)")),
|
||||
("robohash", _("Roboter style")),
|
||||
("pagan", _("Retro adventure character")),
|
||||
("wavatar", _("Wavatar style")),
|
||||
("monsterid", _("Monster style")),
|
||||
("identicon", _("Identicon style")),
|
||||
("mm", _("Mystery man")),
|
||||
("mmng", _("Mystery man NextGen")),
|
||||
("none", _("None")),
|
||||
],
|
||||
)
|
||||
|
||||
default_url = forms.URLField(
|
||||
label=_('Default URL'),
|
||||
label=_("Default URL"),
|
||||
min_length=1,
|
||||
max_length=MAX_LENGTH_URL,
|
||||
required=False,
|
||||
@@ -82,19 +79,27 @@ class CheckForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
self.cleaned_data = super().clean()
|
||||
mail = self.cleaned_data.get('mail')
|
||||
openid = self.cleaned_data.get('openid')
|
||||
default_url = self.cleaned_data.get('default_url')
|
||||
default_opt = self.cleaned_data.get('default_opt')
|
||||
if default_url and default_opt and default_opt != 'none':
|
||||
if not 'default_url' in self._errors:
|
||||
self._errors['default_url'] = ErrorList()
|
||||
if not 'default_opt' in self._errors:
|
||||
self._errors['default_opt'] = ErrorList()
|
||||
mail = self.cleaned_data.get("mail")
|
||||
openid = self.cleaned_data.get("openid")
|
||||
default_url = self.cleaned_data.get("default_url")
|
||||
default_opt = self.cleaned_data.get("default_opt")
|
||||
if default_url and default_opt and default_opt != "none":
|
||||
if "default_url" not in self._errors:
|
||||
self._errors["default_url"] = ErrorList()
|
||||
if "default_opt" not in self._errors:
|
||||
self._errors["default_opt"] = ErrorList()
|
||||
|
||||
errstring = _('Only default URL OR default keyword may be specified')
|
||||
self._errors['default_url'].append(errstring)
|
||||
self._errors['default_opt'].append(errstring)
|
||||
errstring = _("Only default URL OR default keyword may be specified")
|
||||
self._errors["default_url"].append(errstring)
|
||||
self._errors["default_opt"].append(errstring)
|
||||
if not mail and not openid:
|
||||
raise ValidationError(_('Either OpenID or mail must be specified'))
|
||||
raise ValidationError(_("Either OpenID or mail must be specified"))
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_openid(self):
|
||||
data = self.cleaned_data["openid"]
|
||||
return data.lower()
|
||||
|
||||
def clean_mail(self):
|
||||
data = self.cleaned_data["mail"]
|
||||
return data.lower()
|
||||
|
||||
@@ -66,38 +66,37 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div style="max-width:640px">
|
||||
<div class="form-container">
|
||||
<form method="post" name="check">
|
||||
{% csrf_token %}
|
||||
<div class="form-group"><label for="id_mail">{% trans 'E-Mail' %}</label>
|
||||
<input type="email" name="mail" maxlength="254" minlength="6" class="form-control" placeholder="{% trans 'E-Mail' %}" {% if form.mail.value %} value="{{ form.mail.value }}" {% endif %} id="id_mail"></div>
|
||||
<div class="form-group"><label for="id_openid">{% trans 'OpenID' %}</label>
|
||||
<input type="text" name="openid" maxlength="255" minlength="11" class="form-control" placeholder="{% trans 'OpenID' %}" {% if form.openid.value %} value="{{ form.openid.value }}" {% endif %} id="id_openid"></div>
|
||||
<div class="form-group"><label for="id_size">{% trans 'Size' %}</label>
|
||||
<input type="number" name="size" min="5" max="512" class="form-control" placeholder="{% trans 'Size' %}" {% if form.size.value %} value="{{ form.size.value }}" {% else %} value="100" {% endif %} required id="id_size"></div>
|
||||
{% if form.default_url.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ form.default_url.errors }}</div>
|
||||
{% endif %}
|
||||
<div class="form-group"><label for="id_default_url">{% trans 'Default URL or special keyword' %}</label>
|
||||
<input type="text" name="default_url" class="form-control" placeholder="{% trans 'Default' %}" {% if form.default_url.value %} value="{{ form.default_url.value }}" {% endif %} id="id_default_url"></div>
|
||||
{% if form.default_opt.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ form.default_opt.errors }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group"><label for="id_default_opt">{% trans 'Default (special keyword)' %}</label>
|
||||
<div class="form-group">
|
||||
<label for="id_mail" class="form-label">{% trans 'E-Mail' %}</label>
|
||||
<input type="email" name="mail" maxlength="254" minlength="6" class="form-control" placeholder="{% trans 'E-Mail' %}" {% if form.mail.value %} value="{{ form.mail.value }}" {% endif %} id="id_mail">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_openid" class="form-label">{% trans 'OpenID' %}</label>
|
||||
<input type="text" name="openid" maxlength="255" minlength="11" class="form-control" placeholder="{% trans 'OpenID' %}" {% if form.openid.value %} value="{{ form.openid.value }}" {% endif %} id="id_openid">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_size" class="form-label">{% trans 'Size' %}</label>
|
||||
<input type="number" name="size" min="5" max="512" class="form-control" placeholder="{% trans 'Size' %}" {% if form.size.value %} value="{{ form.size.value }}" {% else %} value="100" {% endif %} required id="id_size">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_default_url" class="form-label">{% trans 'Default URL or special keyword' %}</label>
|
||||
<input type="text" name="default_url" class="form-control" placeholder="{% trans 'Default' %}" {% if form.default_url.value %} value="{{ form.default_url.value }}" {% endif %} id="id_default_url">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans 'Default (special keyword)' %}</label>
|
||||
{% for opt in form.default_opt.field.choices %}
|
||||
<div class="radio" {% if forloop.counter|divisibleby:2 %}even{% else %}odd{% endif %}>
|
||||
<input type="radio" name="default_opt" value="{{ opt.0 }}"
|
||||
id="default_opt-{{ opt.0 }}"
|
||||
{% if form.default_opt.value == opt.0 %}checked{% endif %}
|
||||
>
|
||||
<label for="default_opt-{{ opt.0 }}">{{ opt.1 }}</label>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="default_opt" value="{{ opt.0 }}" class="form-check-input" id="default_opt-{{ opt.0 }}" {% if form.default_opt.value == opt.0 %}checked{% endif %}>
|
||||
<label for="default_opt-{{ opt.0 }}" class="form-check-label">{{ opt.1 }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="button">{% trans 'Check' %}</button>
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Check' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,72 +1,156 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||
'''
|
||||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
from urllib.parse import urlsplit
|
||||
from io import BytesIO
|
||||
import io
|
||||
import os
|
||||
import django
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
import hashlib
|
||||
|
||||
from libravatar import libravatar_url
|
||||
|
||||
from PIL import Image
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar import settings
|
||||
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
||||
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId
|
||||
from ivatar.utils import random_string
|
||||
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
|
||||
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
'''
|
||||
"""
|
||||
Main test class
|
||||
'''
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
user = None
|
||||
username = random_string()
|
||||
password = random_string()
|
||||
email = '%s@%s.%s' % (username, random_string(), random_string(2))
|
||||
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')
|
||||
openid = "http://%s.%s.%s/" % (username, random_string(), "org")
|
||||
|
||||
def login(self):
|
||||
'''
|
||||
"""
|
||||
Login as user
|
||||
'''
|
||||
"""
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def setUp(self):
|
||||
'''
|
||||
"""
|
||||
Prepare for tests.
|
||||
- Create user
|
||||
'''
|
||||
"""
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
)
|
||||
|
||||
def test_check(self):
|
||||
def test_check_mail(self):
|
||||
"""
|
||||
Test check page
|
||||
"""
|
||||
response = self.client.get(reverse('tools_check'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
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={"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):
|
||||
"""
|
||||
Test check domain page
|
||||
"""
|
||||
response = self.client.get(reverse('tools_check_domain'))
|
||||
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||
self.login()
|
||||
response = self.client.get(reverse("tools_check_domain"))
|
||||
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!?",
|
||||
)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ivatar/tools URL configuration
|
||||
'''
|
||||
"""
|
||||
|
||||
from django.conf.urls import url
|
||||
from . views import CheckView, CheckDomainView
|
||||
from django.urls import path, re_path
|
||||
from .views import CheckView, CheckDomainView
|
||||
|
||||
urlpatterns = [ # pylint: disable=invalid-name
|
||||
url('check/', CheckView.as_view(), name='tools_check'),
|
||||
url('check_domain/', CheckDomainView.as_view(), name='tools_check_domain'),
|
||||
url('check_domain$', CheckDomainView.as_view(), name='tools_check_domain'),
|
||||
path("check/", CheckView.as_view(), name="tools_check"),
|
||||
path("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"),
|
||||
re_path("check_domain$", CheckDomainView.as_view(), name="tools_check_domain"),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
View classes for ivatar/tools/
|
||||
'''
|
||||
"""
|
||||
from socket import inet_ntop, AF_INET6
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
from django.views.generic.edit import FormView
|
||||
from django.urls import reverse_lazy as reverse
|
||||
@@ -12,44 +15,60 @@ import DNS
|
||||
from libravatar import libravatar_url, parse_user_identity
|
||||
from libravatar import SECURE_BASE_URL as LIBRAVATAR_SECURE_BASE_URL
|
||||
from libravatar import BASE_URL as LIBRAVATAR_BASE_URL
|
||||
import hashlib
|
||||
|
||||
from .forms import CheckDomainForm, CheckForm
|
||||
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,
|
||||
) # pylint: disable=relative-beyond-top-level
|
||||
|
||||
|
||||
class CheckDomainView(FormView):
|
||||
'''
|
||||
"""
|
||||
View class for checking a domain
|
||||
'''
|
||||
template_name = 'check_domain.html'
|
||||
"""
|
||||
|
||||
template_name = "check_domain.html"
|
||||
form_class = CheckDomainForm
|
||||
success_url = reverse('tools_check_domain')
|
||||
success_url = reverse("tools_check_domain")
|
||||
|
||||
def form_valid(self, form):
|
||||
result = {}
|
||||
super().form_valid(form)
|
||||
domain = form.cleaned_data['domain']
|
||||
result['avatar_server_http'] = lookup_avatar_server(domain, False)
|
||||
if result['avatar_server_http']:
|
||||
result['avatar_server_http_ipv4'] = lookup_ip_address(result['avatar_server_http'], False)
|
||||
result['avatar_server_http_ipv6'] = lookup_ip_address(result['avatar_server_http'], True)
|
||||
result['avatar_server_https'] = lookup_avatar_server(domain, True)
|
||||
if result['avatar_server_https']:
|
||||
result['avatar_server_https_ipv4'] = lookup_ip_address(result['avatar_server_https'], False)
|
||||
result['avatar_server_https_ipv6'] = lookup_ip_address(result['avatar_server_https'], True)
|
||||
return render(self.request, self.template_name, {
|
||||
'form': form,
|
||||
'result': result,
|
||||
})
|
||||
domain = form.cleaned_data["domain"]
|
||||
result = {"avatar_server_http": lookup_avatar_server(domain, False)}
|
||||
if result["avatar_server_http"]:
|
||||
result["avatar_server_http_ipv4"] = lookup_ip_address(
|
||||
result["avatar_server_http"], False
|
||||
)
|
||||
result["avatar_server_http_ipv6"] = lookup_ip_address(
|
||||
result["avatar_server_http"], True
|
||||
)
|
||||
result["avatar_server_https"] = lookup_avatar_server(domain, True)
|
||||
if result["avatar_server_https"]:
|
||||
result["avatar_server_https_ipv4"] = lookup_ip_address(
|
||||
result["avatar_server_https"], False
|
||||
)
|
||||
result["avatar_server_https_ipv6"] = lookup_ip_address(
|
||||
result["avatar_server_https"], True
|
||||
)
|
||||
return render(
|
||||
self.request,
|
||||
self.template_name,
|
||||
{
|
||||
"form": form,
|
||||
"result": result,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CheckView(FormView):
|
||||
'''
|
||||
"""
|
||||
View class for checking an e-mail or openid address
|
||||
'''
|
||||
template_name = 'check.html'
|
||||
"""
|
||||
|
||||
template_name = "check.html"
|
||||
form_class = CheckForm
|
||||
success_url = reverse('tools_check')
|
||||
success_url = reverse("tools_check")
|
||||
|
||||
def form_valid(self, form):
|
||||
mailurl = None
|
||||
@@ -60,166 +79,211 @@ class CheckView(FormView):
|
||||
mail_hash = None
|
||||
mail_hash256 = None
|
||||
openid_hash = None
|
||||
size = 80
|
||||
|
||||
super().form_valid(form)
|
||||
|
||||
if form.cleaned_data['default_url']:
|
||||
default_url = form.cleaned_data['default_url']
|
||||
elif form.cleaned_data['default_opt'] and form.cleaned_data['default_opt'] != 'none':
|
||||
default_url = form.cleaned_data['default_opt']
|
||||
if form.cleaned_data["default_url"]:
|
||||
default_url = form.cleaned_data["default_url"]
|
||||
elif (
|
||||
form.cleaned_data["default_opt"]
|
||||
and form.cleaned_data["default_opt"] != "none"
|
||||
):
|
||||
default_url = form.cleaned_data["default_opt"]
|
||||
else:
|
||||
default_url = None
|
||||
|
||||
if 'size' in form.cleaned_data:
|
||||
size = form.cleaned_data['size']
|
||||
if form.cleaned_data['mail']:
|
||||
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)
|
||||
email=form.cleaned_data["mail"], size=size, default=default_url
|
||||
)
|
||||
mailurl = mailurl.replace(LIBRAVATAR_BASE_URL, BASE_URL)
|
||||
mailurl_secure = libravatar_url(
|
||||
email=form.cleaned_data['mail'],
|
||||
email=form.cleaned_data["mail"],
|
||||
size=size,
|
||||
https=True,
|
||||
default=default_url)
|
||||
default=default_url,
|
||||
)
|
||||
mailurl_secure = mailurl_secure.replace(
|
||||
LIBRAVATAR_SECURE_BASE_URL,
|
||||
SECURE_BASE_URL)
|
||||
LIBRAVATAR_SECURE_BASE_URL, SECURE_BASE_URL
|
||||
)
|
||||
mail_hash = parse_user_identity(
|
||||
email=form.cleaned_data['mail'],
|
||||
openid=None)[0]
|
||||
hash_obj = hashlib.new('sha256')
|
||||
hash_obj.update(form.cleaned_data['mail'].encode('utf-8'))
|
||||
email=form.cleaned_data["mail"], openid=None
|
||||
)[0]
|
||||
hash_obj = hashlib.new("sha256")
|
||||
hash_obj.update(form.cleaned_data["mail"].encode("utf-8"))
|
||||
mail_hash256 = hash_obj.hexdigest()
|
||||
mailurl_secure_256 = mailurl_secure.replace(
|
||||
mail_hash,
|
||||
mail_hash256)
|
||||
if form.cleaned_data['openid']:
|
||||
if not form.cleaned_data['openid'].startswith('http://') and not form.cleaned_data['openid'].startswith('https://'):
|
||||
form.cleaned_data['openid'] = 'http://%s' % form.cleaned_data['openid']
|
||||
mailurl_secure_256 = mailurl_secure.replace(mail_hash, mail_hash256)
|
||||
if form.cleaned_data["openid"]:
|
||||
if not form.cleaned_data["openid"].startswith(
|
||||
"http://"
|
||||
) and not form.cleaned_data["openid"].startswith("https://"):
|
||||
form.cleaned_data["openid"] = f'http://{form.cleaned_data["openid"]}'
|
||||
openidurl = libravatar_url(
|
||||
openid=form.cleaned_data['openid'],
|
||||
size=size,
|
||||
default=default_url)
|
||||
openid=form.cleaned_data["openid"], size=size, default=default_url
|
||||
)
|
||||
openidurl = openidurl.replace(LIBRAVATAR_BASE_URL, BASE_URL)
|
||||
openidurl_secure = libravatar_url(
|
||||
openid=form.cleaned_data['openid'],
|
||||
openid=form.cleaned_data["openid"],
|
||||
size=size,
|
||||
https=True,
|
||||
default=default_url)
|
||||
default=default_url,
|
||||
)
|
||||
openidurl_secure = openidurl_secure.replace(
|
||||
LIBRAVATAR_SECURE_BASE_URL,
|
||||
SECURE_BASE_URL)
|
||||
LIBRAVATAR_SECURE_BASE_URL, SECURE_BASE_URL
|
||||
)
|
||||
openid_hash = parse_user_identity(
|
||||
openid=form.cleaned_data['openid'],
|
||||
email=None)[0]
|
||||
openid=form.cleaned_data["openid"], email=None
|
||||
)[0]
|
||||
|
||||
return render(self.request, self.template_name, {
|
||||
'form': form,
|
||||
'mailurl': mailurl,
|
||||
'openidurl': openidurl,
|
||||
'mailurl_secure': mailurl_secure,
|
||||
'mailurl_secure_256': mailurl_secure_256,
|
||||
'openidurl_secure': openidurl_secure,
|
||||
'mail_hash': mail_hash,
|
||||
'mail_hash256': mail_hash256,
|
||||
'openid_hash': openid_hash,
|
||||
'size': size,
|
||||
})
|
||||
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,
|
||||
{
|
||||
"form": form,
|
||||
"mailurl": mailurl,
|
||||
"openidurl": openidurl,
|
||||
"mailurl_secure": mailurl_secure,
|
||||
"mailurl_secure_256": mailurl_secure_256,
|
||||
"openidurl_secure": openidurl_secure,
|
||||
"mail_hash": mail_hash,
|
||||
"mail_hash256": mail_hash256,
|
||||
"openid_hash": openid_hash,
|
||||
"size": size,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def lookup_avatar_server(domain, https):
|
||||
'''
|
||||
"""
|
||||
Extract the avatar server from an SRV record in the DNS zone
|
||||
|
||||
The SRV records should look like this:
|
||||
|
||||
_avatars._tcp.example.com. IN SRV 0 0 80 avatars.example.com
|
||||
_avatars-sec._tcp.example.com. IN SRV 0 0 443 avatars.example.com
|
||||
'''
|
||||
"""
|
||||
|
||||
if domain and len(domain) > 60:
|
||||
domain = domain[:60]
|
||||
|
||||
service_name = None
|
||||
if https:
|
||||
service_name = "_avatars-sec._tcp.%s" % domain
|
||||
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()
|
||||
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':
|
||||
if dns_request.header["status"] == "NXDOMAIN":
|
||||
# Not an error, but no point in going any further
|
||||
return None
|
||||
|
||||
if dns_request.header['status'] != 'NOERROR':
|
||||
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], domain))
|
||||
if dns_request.header["status"] != "NOERROR":
|
||||
print(f'DNS Error: status={dns_request.header["status"]} ({domain})')
|
||||
return None
|
||||
|
||||
records = []
|
||||
for answer in dns_request.answers:
|
||||
if ('data' not in answer) or (not answer['data']) or (not answer['typename']) or (answer['typename'] != 'SRV'):
|
||||
if (
|
||||
("data" not in answer)
|
||||
or (not answer["data"])
|
||||
or (not answer["typename"])
|
||||
or (answer["typename"] != "SRV")
|
||||
):
|
||||
continue
|
||||
|
||||
record = {'priority': int(answer['data'][0]), 'weight': int(answer['data'][1]),
|
||||
'port': int(answer['data'][2]), 'target': answer['data'][3]}
|
||||
record = {
|
||||
"priority": int(answer["data"][0]),
|
||||
"weight": int(answer["data"][1]),
|
||||
"port": int(answer["data"][2]),
|
||||
"target": answer["data"][3],
|
||||
}
|
||||
|
||||
records.append(record)
|
||||
|
||||
target, port = srv_hostname(records)
|
||||
|
||||
if target and ((https and port != 443) or (not https and port != 80)):
|
||||
return "%s:%s" % (target, port)
|
||||
return f"{target}:{port}"
|
||||
|
||||
return target
|
||||
|
||||
|
||||
def srv_hostname(records):
|
||||
'''
|
||||
"""
|
||||
Return the right (target, port) pair from a list of SRV records.
|
||||
'''
|
||||
"""
|
||||
|
||||
if len(records) < 1:
|
||||
return (None, None)
|
||||
|
||||
if len(records) == 1:
|
||||
ret = records[0]
|
||||
return (ret['target'], ret['port'])
|
||||
return (ret["target"], ret["port"])
|
||||
|
||||
# Keep only the servers in the top priority
|
||||
priority_records = []
|
||||
total_weight = 0
|
||||
top_priority = records[0]['priority'] # highest priority = lowest number
|
||||
top_priority = records[0]["priority"] # highest priority = lowest number
|
||||
|
||||
for ret in records:
|
||||
if ret['priority'] > top_priority:
|
||||
if ret["priority"] > top_priority:
|
||||
# ignore the record (ret has lower priority)
|
||||
continue
|
||||
elif ret['priority'] < top_priority:
|
||||
# reset the aretay (ret has higher priority)
|
||||
top_priority = ret['priority']
|
||||
|
||||
# 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 priority (ret has higher priority)
|
||||
top_priority = ret["priority"]
|
||||
total_weight = 0
|
||||
priority_records = []
|
||||
|
||||
total_weight += ret['weight']
|
||||
total_weight += ret["weight"]
|
||||
|
||||
if ret['weight'] > 0:
|
||||
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:
|
||||
unused, ret = priority_records[0]
|
||||
return (ret['target'], ret['port'])
|
||||
unused, ret = priority_records[0] # pylint: disable=unused-variable
|
||||
return (ret["target"], ret["port"])
|
||||
|
||||
# Select first record according to RFC2782 weight ordering algorithm (page 3)
|
||||
random_number = random.randint(0, total_weight)
|
||||
@@ -228,9 +292,9 @@ def srv_hostname(records):
|
||||
weighted_index, ret = record
|
||||
|
||||
if weighted_index >= random_number:
|
||||
return (ret['target'], ret['port'])
|
||||
return (ret["target"], ret["port"])
|
||||
|
||||
print('There is something wrong with our SRV weight ordering algorithm')
|
||||
print("There is something wrong with our SRV weight ordering algorithm")
|
||||
return (None, None)
|
||||
|
||||
|
||||
@@ -246,22 +310,20 @@ 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))
|
||||
if dns_request.header["status"] != "NOERROR":
|
||||
print(f'DNS Error: status={dns_request.header["status"]} ({hostname})')
|
||||
return None
|
||||
|
||||
for answer in dns_request.answers:
|
||||
if ('data' not in answer) or (not answer['data']):
|
||||
if ("data" not in answer) or (not answer["data"]):
|
||||
continue
|
||||
if (ipv6 and answer['typename'] != 'AAAA') or (not ipv6 and answer['typename'] != 'A'):
|
||||
if (ipv6 and answer["typename"] != "AAAA") or (
|
||||
not ipv6 and answer["typename"] != "A"
|
||||
):
|
||||
continue # skip CNAME records
|
||||
|
||||
if ipv6:
|
||||
return inet_ntop(AF_INET6, answer['data'])
|
||||
else:
|
||||
return answer['data']
|
||||
|
||||
return inet_ntop(AF_INET6, answer["data"]) if ipv6 else answer["data"]
|
||||
return None
|
||||
|
||||
118
ivatar/urls.py
@@ -1,63 +1,89 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ivatar URL configuration
|
||||
'''
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf.urls import url
|
||||
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
|
||||
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')),
|
||||
url('openid/', include('django_openid_auth.urls')),
|
||||
url('tools/', include('ivatar.tools.urls')),
|
||||
# Encrypted digest
|
||||
url(
|
||||
r'avatar/(?P<digest>\w{264})',
|
||||
AvatarImageView.as_view(), name='avatar_view'),
|
||||
url(
|
||||
r'avatar/(?P<digest>\w{200})',
|
||||
AvatarImageView.as_view(), name='avatar_view'),
|
||||
# Unencrypted digest
|
||||
url(
|
||||
r'avatar/(?P<digest>\w{64})',
|
||||
AvatarImageView.as_view(), name='avatar_view'),
|
||||
url(
|
||||
r'avatar/(?P<digest>\w{32})',
|
||||
AvatarImageView.as_view(), name='avatar_view'),
|
||||
url(r'avatar/$', AvatarImageView.as_view(), name='avatar_view'),
|
||||
url(
|
||||
r'avatar/(?P<digest>\w*)',
|
||||
RedirectView.as_view(url='/static/img/deadbeef.png'), name='invalid_hash'),
|
||||
url(
|
||||
r'gravatarproxy/(?P<digest>\w*)',
|
||||
GravatarProxyView.as_view(), name='gravatarproxy'),
|
||||
url('description/', TemplateView.as_view(template_name='description.html'), name='description'),
|
||||
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"
|
||||
),
|
||||
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*)",
|
||||
RedirectView.as_view(url="/static/img/deadbeef.png"),
|
||||
name="invalid_hash",
|
||||
),
|
||||
re_path(
|
||||
r"gravatarproxy/(?P<digest>\w*)",
|
||||
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"),
|
||||
name="description",
|
||||
),
|
||||
# The following two are TODO TODO TODO TODO TODO
|
||||
url('run_your_own/', TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'),
|
||||
url('features/', TemplateView.as_view(template_name='features.html'), name='features'),
|
||||
url('security/', TemplateView.as_view(template_name='security.html'), name='security'),
|
||||
url('privacy/', TemplateView.as_view(template_name='privacy.html'), name='privacy'),
|
||||
url('contact/', TemplateView.as_view(template_name='contact.html'), name='contact'),
|
||||
path('talk_to_us/', RedirectView.as_view(url='/contact'), name='talk_to_us'),
|
||||
path(
|
||||
"run_your_own/",
|
||||
TemplateView.as_view(template_name="run_your_own.html"),
|
||||
name="run_your_own",
|
||||
),
|
||||
path(
|
||||
"features/",
|
||||
TemplateView.as_view(template_name="features.html"),
|
||||
name="features",
|
||||
),
|
||||
path(
|
||||
"security/",
|
||||
TemplateView.as_view(template_name="security.html"),
|
||||
name="security",
|
||||
),
|
||||
path(
|
||||
"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("stats/", StatsView.as_view(), name="stats"),
|
||||
]
|
||||
|
||||
MAINTENANCE = False
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
if settings.MAINTENANCE:
|
||||
MAINTENANCE = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if MAINTENANCE:
|
||||
urlpatterns.append(url('', TemplateView.as_view(template_name='maintenance.html'), name='home'))
|
||||
urlpatterns.insert(3, url('accounts/', RedirectView.as_view(url='/')))
|
||||
urlpatterns.append(
|
||||
path("", TemplateView.as_view(template_name="maintenance.html"), name="home")
|
||||
)
|
||||
urlpatterns.insert(3, path("accounts/", RedirectView.as_view(url="/")))
|
||||
else:
|
||||
urlpatterns.append(url('', TemplateView.as_view(template_name='home.html'), name='home'))
|
||||
urlpatterns.insert(3, url('accounts/', include('ivatar.ivataraccount.urls')))
|
||||
|
||||
urlpatterns.append(
|
||||
path("", TemplateView.as_view(template_name="home.html"), name="home")
|
||||
)
|
||||
urlpatterns.insert(3, path("accounts/", include("ivatar.ivataraccount.urls")))
|
||||
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
270
ivatar/utils.py
@@ -1,35 +1,275 @@
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
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):
|
||||
'''
|
||||
"""
|
||||
Return some random string with default length 10
|
||||
'''
|
||||
return ''.join(random.SystemRandom().choice(
|
||||
string.ascii_lowercase + string.digits) for _ in range(length))
|
||||
"""
|
||||
return "".join(
|
||||
random.SystemRandom().choice(string.ascii_lowercase + string.digits)
|
||||
for _ in range(length)
|
||||
)
|
||||
|
||||
|
||||
def openid_variations(openid):
|
||||
'''
|
||||
"""
|
||||
Return the various OpenID variations, ALWAYS in the same order:
|
||||
- http w/ trailing slash
|
||||
- http w/o trailing slash
|
||||
- https w/ trailing slash
|
||||
- https w/o trailing slash
|
||||
'''
|
||||
"""
|
||||
|
||||
# Make the 'base' version: http w/ trailing slash
|
||||
if openid.startswith('https://'):
|
||||
openid = openid.replace('https://', 'http://')
|
||||
if openid[-1] != '/':
|
||||
openid = openid + '/'
|
||||
if openid.startswith("https://"):
|
||||
openid = openid.replace("https://", "http://")
|
||||
if openid[-1] != "/":
|
||||
openid = f"{openid}/"
|
||||
|
||||
# http w/o trailing slash
|
||||
var1 = openid[0:-1]
|
||||
var2 = openid.replace('http://', 'https://')
|
||||
var3 = var2[0:-1]
|
||||
var1 = openid[:-1]
|
||||
var2 = openid.replace("http://", "https://")
|
||||
var3 = var2[:-1]
|
||||
return (openid, var1, var2, var3)
|
||||
|
||||
|
||||
def mm_ng(
|
||||
idhash, size=80, add_red=0, add_green=0, add_blue=0
|
||||
): # pylint: disable=too-many-locals
|
||||
"""
|
||||
Return an MM (mystery man) image, based on a given hash
|
||||
add some red, green or blue, if specified
|
||||
"""
|
||||
|
||||
# Make sure the lightest bg color we paint is e0, else
|
||||
# we do not see the MM any more
|
||||
if idhash[0] == "f":
|
||||
idhash = "e0"
|
||||
|
||||
# How large is the circle?
|
||||
circle_size = size * 0.6
|
||||
|
||||
# Coordinates for the circle
|
||||
start_x = int(size * 0.2)
|
||||
end_x = start_x + circle_size
|
||||
start_y = int(size * 0.05)
|
||||
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[: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 = 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 = 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 = f"0{blue}"
|
||||
|
||||
# Assemble the bg color "string" in web notation. Eg. '#d3d3d3'
|
||||
bg_color = f"#{red}{green}{blue}"
|
||||
|
||||
# Image
|
||||
image = Image.new("RGB", (size, size))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Draw background
|
||||
draw.rectangle(((0, 0), (size, size)), fill=bg_color)
|
||||
|
||||
# Draw MMs head
|
||||
draw.ellipse((start_x, start_y, end_x, end_y), fill="white")
|
||||
|
||||
# Draw MMs 'body'
|
||||
draw.polygon(
|
||||
(
|
||||
(start_x + circle_size / 2, size / 2.5),
|
||||
(size * 0.15, size),
|
||||
(size - size * 0.15, size),
|
||||
),
|
||||
fill="white",
|
||||
)
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def is_trusted_url(url, url_filters):
|
||||
"""
|
||||
Check if a URL is valid and considered a trusted URL.
|
||||
If the URL is malformed, returns False.
|
||||
|
||||
Based on: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/events/UrlFilter
|
||||
"""
|
||||
(scheme, netloc, path, params, query, fragment) = urlparse(url)
|
||||
|
||||
for ufilter in url_filters:
|
||||
if "schemes" in ufilter:
|
||||
schemes = ufilter["schemes"]
|
||||
|
||||
if scheme not in schemes:
|
||||
continue
|
||||
|
||||
if "host_equals" in ufilter:
|
||||
host_equals = ufilter["host_equals"]
|
||||
|
||||
if netloc != host_equals:
|
||||
continue
|
||||
|
||||
if "host_suffix" in ufilter:
|
||||
host_suffix = ufilter["host_suffix"]
|
||||
|
||||
if not netloc.endswith(host_suffix):
|
||||
continue
|
||||
|
||||
if "path_prefix" in ufilter:
|
||||
path_prefix = ufilter["path_prefix"]
|
||||
|
||||
if not path.startswith(path_prefix):
|
||||
continue
|
||||
|
||||
if "url_prefix" in ufilter:
|
||||
url_prefix = ufilter["url_prefix"]
|
||||
|
||||
if not url.startswith(url_prefix):
|
||||
continue
|
||||
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||