mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-14 12:08:04 +00:00
Compare commits
10 Commits
import_csv
...
apikeys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de954aad67 | ||
|
|
d308fc181f | ||
|
|
577596a8f2 | ||
|
|
466ffeaecb | ||
|
|
70f66a1974 | ||
|
|
61b0cf5973 | ||
|
|
bd8dfd18a0 | ||
|
|
751cfc157e | ||
|
|
6e5d91252e | ||
|
|
d01ddb5cc8 |
@@ -5,15 +5,6 @@ omit =
|
|||||||
node_modules/*
|
node_modules/*
|
||||||
.virtualenv/*
|
.virtualenv/*
|
||||||
import_libravatar.py
|
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]
|
[html]
|
||||||
extra_css = coverage_extra_style.css
|
extra_css = coverage_extra_style.css
|
||||||
|
|||||||
6
.flake8
6
.flake8
@@ -1,6 +0,0 @@
|
|||||||
[flake8]
|
|
||||||
ignore = E501, W503, E402, C901
|
|
||||||
max-line-length = 79
|
|
||||||
max-complexity = 18
|
|
||||||
select = B,C,E,F,W,T4,B9
|
|
||||||
exclude = .git,__pycache__,.virtualenv
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,4 +13,3 @@ db.sqlite3.SAVE
|
|||||||
node_modules/
|
node_modules/
|
||||||
config_local.py
|
config_local.py
|
||||||
locale/*/LC_MESSAGES/django.mo
|
locale/*/LC_MESSAGES/django.mo
|
||||||
.DS_Store
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
image:
|
image: docker.io/ofalk/fedora31-python3
|
||||||
name: quay.io/rhn_support_ofalk/fedora34-python3
|
|
||||||
entrypoint: [ '/bin/sh', '-c' ]
|
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- virtualenv -p python3 /tmp/.virtualenv
|
- virtualenv -p python3 /tmp/.virtualenv
|
||||||
@@ -14,7 +12,6 @@ before_script:
|
|||||||
|
|
||||||
test_and_coverage:
|
test_and_coverage:
|
||||||
stage: test
|
stage: test
|
||||||
coverage: '/^TOTAL.*\s+(\d+\%)$/'
|
|
||||||
script:
|
script:
|
||||||
- echo 'from ivatar.settings import TEMPLATES' > config_local.py
|
- echo 'from ivatar.settings import TEMPLATES' > config_local.py
|
||||||
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
|
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
fail_fast: true
|
|
||||||
repos:
|
|
||||||
- repo: meta
|
|
||||||
hooks:
|
|
||||||
- id: check-useless-excludes
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
|
||||||
rev: v2.6.2
|
|
||||||
hooks:
|
|
||||||
- id: prettier
|
|
||||||
files: \.(css|js|md|markdown|json)
|
|
||||||
- repo: https://github.com/python/black
|
|
||||||
rev: 22.3.0
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v4.2.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://gitlab.com/pycqa/flake8
|
|
||||||
rev: 3.9.2
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: shfmt
|
|
||||||
name: shfmt
|
|
||||||
minimum_pre_commit_version: 2.4.0
|
|
||||||
language: golang
|
|
||||||
additional_dependencies:
|
|
||||||
- mvdan.cc/sh/v3/cmd/shfmt@v3.1.1
|
|
||||||
entry: shfmt
|
|
||||||
args:
|
|
||||||
- -w
|
|
||||||
- -i
|
|
||||||
- '4'
|
|
||||||
types:
|
|
||||||
- shell
|
|
||||||
- repo: https://github.com/asottile/blacken-docs
|
|
||||||
rev: v1.12.1
|
|
||||||
hooks:
|
|
||||||
- id: blacken-docs
|
|
||||||
- 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"]
|
|
||||||
10
INSTALL.md
10
INSTALL.md
@@ -1,6 +1,6 @@
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
## Prerequisites
|
## Prequisits
|
||||||
|
|
||||||
Python 3.x + virtualenv
|
Python 3.x + virtualenv
|
||||||
|
|
||||||
@@ -10,13 +10,6 @@ Python 3.x + virtualenv
|
|||||||
yum install python34-virtualenv.noarch
|
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
|
## Checkout
|
||||||
|
|
||||||
~~~~bash
|
~~~~bash
|
||||||
@@ -29,7 +22,6 @@ cd ivatar
|
|||||||
~~~~bash
|
~~~~bash
|
||||||
virtualenv -p python3 .virtualenv
|
virtualenv -p python3 .virtualenv
|
||||||
source .virtualenv/bin/activate
|
source .virtualenv/bin/activate
|
||||||
pip install pillow
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
~~~~
|
~~~~
|
||||||
|
|
||||||
|
|||||||
661
LICENSE
661
LICENSE
@@ -1,661 +0,0 @@
|
|||||||
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/>.
|
|
||||||
305
config.py
305
config.py
@@ -1,12 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
''' yes
|
||||||
"""
|
|
||||||
Configuration overrides for settings.py
|
Configuration overrides for settings.py
|
||||||
"""
|
'''
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.contrib.messages import constants as message_constants
|
from django.contrib.messages import constants as message_constants
|
||||||
from ivatar.settings import BASE_DIR
|
from ivatar.settings import BASE_DIR
|
||||||
|
|
||||||
@@ -15,61 +14,50 @@ from ivatar.settings import INSTALLED_APPS
|
|||||||
from ivatar.settings import TEMPLATES
|
from ivatar.settings import TEMPLATES
|
||||||
|
|
||||||
ADMIN_USERS = []
|
ADMIN_USERS = []
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
INSTALLED_APPS.extend(
|
INSTALLED_APPS.extend([
|
||||||
[
|
'django_extensions',
|
||||||
"django_extensions",
|
'django_openid_auth',
|
||||||
"django_openid_auth",
|
'bootstrap4',
|
||||||
"bootstrap4",
|
'anymail',
|
||||||
"anymail",
|
'ivatar',
|
||||||
"ivatar",
|
'ivatar.ivataraccount',
|
||||||
"ivatar.ivataraccount",
|
'ivatar.tools',
|
||||||
"ivatar.tools",
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
MIDDLEWARE.extend(
|
MIDDLEWARE.extend([
|
||||||
[
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
MIDDLEWARE.insert(
|
MIDDLEWARE.insert(
|
||||||
0,
|
0, 'ivatar.middleware.MultipleProxyMiddleware',
|
||||||
"ivatar.middleware.MultipleProxyMiddleware",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
# Enable this to allow LDAP authentication.
|
# Enable this to allow LDAP authentication.
|
||||||
# See INSTALL for more information.
|
# See INSTALL for more information.
|
||||||
# 'django_auth_ldap.backend.LDAPBackend',
|
# 'django_auth_ldap.backend.LDAPBackend',
|
||||||
"django_openid_auth.auth.OpenIDBackend",
|
'django_openid_auth.auth.OpenIDBackend',
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
)
|
)
|
||||||
|
|
||||||
TEMPLATES[0]["DIRS"].extend(
|
TEMPLATES[0]['DIRS'].extend([
|
||||||
[
|
os.path.join(BASE_DIR, 'templates'),
|
||||||
os.path.join(BASE_DIR, "templates"),
|
])
|
||||||
]
|
TEMPLATES[0]['OPTIONS']['context_processors'].append(
|
||||||
)
|
'ivatar.context_processors.basepage',
|
||||||
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
|
|
||||||
"ivatar.context_processors.basepage",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
OPENID_CREATE_USERS = True
|
OPENID_CREATE_USERS = True
|
||||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||||
|
|
||||||
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
|
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
|
||||||
IVATAR_VERSION = "1.6"
|
IVATAR_VERSION = '1.3'
|
||||||
|
|
||||||
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
|
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/')
|
||||||
|
|
||||||
SECURE_BASE_URL = os.environ.get(
|
LOGIN_REDIRECT_URL = reverse_lazy('profile')
|
||||||
"SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/"
|
|
||||||
)
|
|
||||||
BASE_URL = os.environ.get("BASE_URL", "http://avatars.linux-kernel.at/avatar/")
|
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = reverse_lazy("profile")
|
|
||||||
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
||||||
|
|
||||||
MAX_NUM_PHOTOS = 5
|
MAX_NUM_PHOTOS = 5
|
||||||
@@ -87,198 +75,119 @@ MIN_LENGTH_EMAIL = 6 # eg. x@x.xx
|
|||||||
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
|
||||||
|
|
||||||
BOOTSTRAP4 = {
|
BOOTSTRAP4 = {
|
||||||
"include_jquery": False,
|
'include_jquery': False,
|
||||||
"javascript_in_head": False,
|
'javascript_in_head': False,
|
||||||
"css_url": {
|
'css_url': {
|
||||||
"href": "/static/css/bootstrap.min.css",
|
'href': '/static/css/bootstrap.min.css',
|
||||||
"integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB",
|
'integrity':
|
||||||
"crossorigin": "anonymous",
|
'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB',
|
||||||
|
'crossorigin': 'anonymous',
|
||||||
},
|
},
|
||||||
"javascript_url": {
|
'javascript_url': {
|
||||||
"url": "/static/js/bootstrap.min.js",
|
'url': '/static/js/bootstrap.min.js',
|
||||||
"integrity": "",
|
'integrity': '',
|
||||||
"crossorigin": "anonymous",
|
'crossorigin': 'anonymous',
|
||||||
},
|
},
|
||||||
"popper_url": {
|
'popper_url': {
|
||||||
"url": "/static/js/popper.min.js",
|
'url': '/static/js/popper.min.js',
|
||||||
"integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49",
|
'integrity':
|
||||||
"crossorigin": "anonymous",
|
'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49',
|
||||||
|
'crossorigin': 'anonymous',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if "EMAIL_BACKEND" in os.environ:
|
if 'EMAIL_BACKEND' in os.environ:
|
||||||
EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover
|
EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover
|
||||||
else:
|
else:
|
||||||
if "test" in sys.argv or "collectstatic" in sys.argv:
|
if 'test' in sys.argv or 'collectstatic' in sys.argv:
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
ANYMAIL = { # pragma: no cover
|
ANYMAIL = { # pragma: no cover
|
||||||
"MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"],
|
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
|
||||||
"MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"],
|
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
|
||||||
}
|
}
|
||||||
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover
|
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
|
||||||
except Exception: # pragma: nocover # pylint: disable=broad-except
|
except Exception as exc:
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
|
||||||
SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "ivatar@mg.linux-kernel.at")
|
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'ivatar@mg.linux-kernel.at')
|
||||||
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at")
|
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'ivatar@mg.linux-kernel.at')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ivatar.settings import DATABASES
|
from ivatar.settings import DATABASES
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
DATABASES = [] # pragma: no cover
|
DATABASES = [] # pragma: no cover
|
||||||
|
|
||||||
if "default" not in DATABASES:
|
if 'default' not in DATABASES:
|
||||||
DATABASES["default"] = { # pragma: no cover
|
DATABASES['default'] = { # pragma: no cover
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
}
|
}
|
||||||
|
|
||||||
if "MYSQL_DATABASE" in os.environ:
|
if 'MYSQL_DATABASE' in os.environ:
|
||||||
DATABASES["default"] = { # pragma: no cover
|
DATABASES['default'] = { # pragma: no cover
|
||||||
"ENGINE": "django.db.backends.mysql",
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
"NAME": os.environ["MYSQL_DATABASE"],
|
'NAME': os.environ['MYSQL_DATABASE'],
|
||||||
"USER": os.environ["MYSQL_USER"],
|
'USER': os.environ['MYSQL_USER'],
|
||||||
"PASSWORD": os.environ["MYSQL_PASSWORD"],
|
'PASSWORD': os.environ['MYSQL_PASSWORD'],
|
||||||
"HOST": "mysql",
|
'HOST': 'mysql',
|
||||||
}
|
}
|
||||||
|
|
||||||
if "POSTGRESQL_DATABASE" in os.environ:
|
if 'POSTGRESQL_DATABASE' in os.environ:
|
||||||
DATABASES["default"] = { # pragma: no cover
|
DATABASES['default'] = { # pragma: no cover
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
"NAME": os.environ["POSTGRESQL_DATABASE"],
|
'NAME': os.environ['POSTGRESQL_DATABASE'],
|
||||||
"USER": os.environ["POSTGRESQL_USER"],
|
'USER': os.environ['POSTGRESQL_USER'],
|
||||||
"PASSWORD": os.environ["POSTGRESQL_PASSWORD"],
|
'PASSWORD': os.environ['POSTGRESQL_PASSWORD'],
|
||||||
"HOST": "postgresql",
|
'HOST': 'postgresql',
|
||||||
}
|
}
|
||||||
|
|
||||||
SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer"
|
if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')):
|
||||||
|
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
|
||||||
|
|
||||||
|
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||||
|
|
||||||
USE_X_FORWARDED_HOST = True
|
USE_X_FORWARDED_HOST = True
|
||||||
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [
|
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',]
|
||||||
"avatars.linux-kernel.at",
|
|
||||||
"localhost",
|
|
||||||
]
|
|
||||||
|
|
||||||
DEFAULT_AVATAR_SIZE = 80
|
DEFAULT_AVATAR_SIZE = 80
|
||||||
|
|
||||||
LANGUAGES = (
|
LANGUAGES = (
|
||||||
("de", _("Deutsch")),
|
('de', _('Deutsch')),
|
||||||
("en", _("English")),
|
('en', _('English')),
|
||||||
("ca", _("Català")),
|
('ca', _('Català')),
|
||||||
("cs", _("Česky")),
|
('cs', _('Česky')),
|
||||||
("es", _("Español")),
|
('es', _('Español')),
|
||||||
("eu", _("Basque")),
|
('eu', _('Basque')),
|
||||||
("fr", _("Français")),
|
('fr', _('Français')),
|
||||||
("it", _("Italiano")),
|
('it', _('Italiano')),
|
||||||
("ja", _("日本語")),
|
('ja', _('日本語')),
|
||||||
("nl", _("Nederlands")),
|
('nl', _('Nederlands')),
|
||||||
("pt", _("Português")),
|
('pt', _('Português')),
|
||||||
("ru", _("Русский")),
|
('ru', _('Русский')),
|
||||||
("sq", _("Shqip")),
|
('sq', _('Shqip')),
|
||||||
("tr", _("Türkçe")),
|
('tr', _('Türkçe')),
|
||||||
("uk", _("Українська")),
|
('uk', _('Українська')),
|
||||||
)
|
)
|
||||||
|
|
||||||
MESSAGE_TAGS = {
|
MESSAGE_TAGS = {
|
||||||
message_constants.DEBUG: "debug",
|
message_constants.DEBUG: 'debug',
|
||||||
message_constants.INFO: "info",
|
message_constants.INFO: 'info',
|
||||||
message_constants.SUCCESS: "success",
|
message_constants.SUCCESS: 'success',
|
||||||
message_constants.WARNING: "warning",
|
message_constants.WARNING: 'warning',
|
||||||
message_constants.ERROR: "danger",
|
message_constants.ERROR: 'danger',
|
||||||
}
|
}
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
'default': {
|
||||||
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
|
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||||
"LOCATION": [
|
'LOCATION': [
|
||||||
"127.0.0.1:11211",
|
'127.0.0.1:11211',
|
||||||
],
|
],
|
||||||
},
|
}
|
||||||
"filesystem": {
|
|
||||||
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
|
|
||||||
"LOCATION": "/var/tmp/ivatar_cache",
|
|
||||||
"TIMEOUT": 900, # 15 minutes
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# This is 5 minutes caching for generated/resized images,
|
# This is 5 minutes caching for generated/resized images,
|
||||||
# so the sites don't hit ivatar so much - it's what's set in the HTTP header
|
# so the sites don't hit ivatar so much
|
||||||
CACHE_IMAGES_MAX_AGE = 5 * 60
|
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": "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/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Import a CSV - Format as follows:
|
|
||||||
<mailaddr>,<path_to_image>
|
|
||||||
|
|
||||||
Example:
|
|
||||||
myuser@mydomain.tld,myphoto.jpeg
|
|
||||||
|
|
||||||
This will create or update an existing user and assign the image
|
|
||||||
to the given address.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from os.path import isfile
|
|
||||||
import sys
|
|
||||||
from io import BytesIO
|
|
||||||
import csv
|
|
||||||
import django
|
|
||||||
|
|
||||||
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 ivatar.settings import JPEG_QUALITY
|
|
||||||
from ivatar.ivataraccount.models import ConfirmedEmail
|
|
||||||
from ivatar.ivataraccount.models import Photo
|
|
||||||
from ivatar.ivataraccount.models import file_format
|
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("First argument to '%s' must be the path to the CSV" % sys.argv[0])
|
|
||||||
exit(-255)
|
|
||||||
|
|
||||||
if not isfile(sys.argv[1]):
|
|
||||||
print("First argument to '%s' must be a path to the CSV" % sys.argv[0])
|
|
||||||
exit(-255)
|
|
||||||
|
|
||||||
PATH = sys.argv[1]
|
|
||||||
with open(PATH, newline="") as csvfile:
|
|
||||||
contactreader = csv.reader(csvfile, delimiter=",")
|
|
||||||
for row in contactreader:
|
|
||||||
mailaddr = row[0]
|
|
||||||
image = row[1]
|
|
||||||
|
|
||||||
if not isfile(image):
|
|
||||||
print("File '%s' doesn't exist - cannot add" % image)
|
|
||||||
continue
|
|
||||||
|
|
||||||
print("Adding: %s" % mailaddr)
|
|
||||||
|
|
||||||
(user, created) = User.objects.get_or_create(username=mailaddr)
|
|
||||||
if not user.confirmedemail_set.count() < 1:
|
|
||||||
ConfirmedEmail.objects.get_or_create(
|
|
||||||
email=mailaddr,
|
|
||||||
user=user,
|
|
||||||
)
|
|
||||||
user.save()
|
|
||||||
with open(image, "rb") as avatar:
|
|
||||||
pilobj = Image.open(avatar)
|
|
||||||
out = BytesIO()
|
|
||||||
pilobj.save(out, pilobj.format, quality=JPEG_QUALITY)
|
|
||||||
out.seek(0)
|
|
||||||
photo = None
|
|
||||||
if user.photo_set.count() < 1:
|
|
||||||
photo = Photo()
|
|
||||||
photo.user = user
|
|
||||||
else:
|
|
||||||
photo = user.photo_set.first()
|
|
||||||
photo.ip_address = "0.0.0.0"
|
|
||||||
photo.format = file_format(pilobj.format)
|
|
||||||
photo.data = out.read()
|
|
||||||
photo.save()
|
|
||||||
print("xxx: %s" % user.confirmedemail_set.first())
|
|
||||||
confirmed_email = user.confirmedemail_set.first()
|
|
||||||
confirmed_email.photo_id = photo.id
|
|
||||||
confirmed_email.save()
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Import the whole libravatar export
|
Import the whole libravatar export
|
||||||
"""
|
'''
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from os.path import isfile, isdir, join
|
from os.path import isfile, isdir, join
|
||||||
@@ -10,18 +9,13 @@ import sys
|
|||||||
import base64
|
import base64
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import django
|
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
|
django.setup() # pylint: disable=wrong-import-position
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from django_openid_auth.models import UserOpenID
|
from django_openid_auth.models import UserOpenID
|
||||||
from ivatar.settings import JPEG_QUALITY
|
from ivatar.settings import JPEG_QUALITY
|
||||||
from ivatar.ivataraccount.read_libravatar_export import (
|
from ivatar.ivataraccount.read_libravatar_export import read_gzdata as libravatar_read_gzdata
|
||||||
read_gzdata as libravatar_read_gzdata,
|
|
||||||
)
|
|
||||||
from ivatar.ivataraccount.models import ConfirmedEmail
|
from ivatar.ivataraccount.models import ConfirmedEmail
|
||||||
from ivatar.ivataraccount.models import ConfirmedOpenId
|
from ivatar.ivataraccount.models import ConfirmedOpenId
|
||||||
from ivatar.ivataraccount.models import Photo
|
from ivatar.ivataraccount.models import Photo
|
||||||
@@ -32,63 +26,54 @@ if len(sys.argv) < 2:
|
|||||||
exit(-255)
|
exit(-255)
|
||||||
|
|
||||||
if not isdir(sys.argv[1]):
|
if not isdir(sys.argv[1]):
|
||||||
print(
|
print("First argument to '%s' must be a directory containing the exports" % sys.argv[0])
|
||||||
"First argument to '%s' must be a directory containing the exports"
|
|
||||||
% sys.argv[0]
|
|
||||||
)
|
|
||||||
exit(-255)
|
exit(-255)
|
||||||
|
|
||||||
PATH = sys.argv[1]
|
PATH = sys.argv[1]
|
||||||
for file in os.listdir(PATH):
|
for file in os.listdir(PATH):
|
||||||
if not file.endswith(".xml.gz"):
|
if not file.endswith('.xml.gz'):
|
||||||
continue
|
continue
|
||||||
if isfile(join(PATH, file)):
|
if isfile(join(PATH, file)):
|
||||||
fh = open(join(PATH, file), "rb")
|
fh = open(join(PATH, file), 'rb')
|
||||||
items = libravatar_read_gzdata(fh.read())
|
items = libravatar_read_gzdata(fh.read())
|
||||||
print('Adding user "%s"' % items["username"])
|
print('Adding user "%s"' % items['username'])
|
||||||
(user, created) = User.objects.get_or_create(username=items["username"])
|
(user, created) = User.objects.get_or_create(username=items['username'])
|
||||||
user.password = items["password"]
|
user.password = items['password']
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
saved_photos = {}
|
saved_photos = {}
|
||||||
for photo in items["photos"]:
|
for photo in items['photos']:
|
||||||
photo_id = photo["id"]
|
photo_id = photo['id']
|
||||||
data = base64.decodebytes(bytes(photo["data"], "utf-8"))
|
data = base64.decodebytes(bytes(photo['data'], 'utf-8'))
|
||||||
pilobj = Image.open(BytesIO(data))
|
pilobj = Image.open(BytesIO(data))
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
pilobj.save(out, pilobj.format, quality=JPEG_QUALITY)
|
pilobj.save(out, pilobj.format, quality=JPEG_QUALITY)
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
photo = Photo()
|
photo = Photo()
|
||||||
photo.user = user
|
photo.user = user
|
||||||
photo.ip_address = "0.0.0.0"
|
photo.ip_address = '0.0.0.0'
|
||||||
photo.format = file_format(pilobj.format)
|
photo.format = file_format(pilobj.format)
|
||||||
photo.data = out.read()
|
photo.data = out.read()
|
||||||
photo.save()
|
photo.save()
|
||||||
saved_photos[photo_id] = photo
|
saved_photos[photo_id] = photo
|
||||||
|
|
||||||
for email in items["emails"]:
|
for email in items['emails']:
|
||||||
try:
|
try:
|
||||||
ConfirmedEmail.objects.get_or_create(
|
ConfirmedEmail.objects.get_or_create(email=email['email'], user=user,
|
||||||
email=email["email"],
|
photo=saved_photos.get(email['photo_id']))
|
||||||
user=user,
|
|
||||||
photo=saved_photos.get(email["photo_id"]),
|
|
||||||
)
|
|
||||||
except django.db.utils.IntegrityError:
|
except django.db.utils.IntegrityError:
|
||||||
print("%s not unique?" % email["email"])
|
print('%s not unique?' % email['email'])
|
||||||
|
|
||||||
for openid in items["openids"]:
|
for openid in items['openids']:
|
||||||
try:
|
try:
|
||||||
ConfirmedOpenId.objects.get_or_create(
|
ConfirmedOpenId.objects.get_or_create(openid=openid['openid'], user=user,
|
||||||
openid=openid["openid"],
|
photo=saved_photos.get(openid['photo_id'])) # pylint: disable=no-member
|
||||||
user=user,
|
|
||||||
photo=saved_photos.get(openid["photo_id"]),
|
|
||||||
) # pylint: disable=no-member
|
|
||||||
UserOpenID.objects.get_or_create(
|
UserOpenID.objects.get_or_create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
claimed_id=openid["openid"],
|
claimed_id=openid['openid'],
|
||||||
display_id=openid["openid"],
|
display_id=openid['openid'],
|
||||||
)
|
)
|
||||||
except django.db.utils.IntegrityError:
|
except django.db.utils.IntegrityError:
|
||||||
print("%s not unique?" % openid["openid"])
|
print('%s not unique?' % openid['openid'])
|
||||||
|
|
||||||
fh.close()
|
fh.close()
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Module init
|
Module init
|
||||||
"""
|
'''
|
||||||
|
|
||||||
app_label = __name__ # pylint: disable=invalid-name
|
app_label = __name__ # pylint: disable=invalid-name
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Default: useful variables for the base page templates.
|
Default: useful variables for the base page templates.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
from ipware import get_client_ip
|
from ipware import get_client_ip
|
||||||
from ivatar.settings import IVATAR_VERSION, SITE_NAME, MAX_PHOTO_SIZE
|
from ivatar.settings import IVATAR_VERSION, SITE_NAME, MAX_PHOTO_SIZE
|
||||||
@@ -10,28 +9,27 @@ from ivatar.settings import MAX_NUM_UNCONFIRMED_EMAILS
|
|||||||
|
|
||||||
|
|
||||||
def basepage(request):
|
def basepage(request):
|
||||||
"""
|
'''
|
||||||
Our contextprocessor adds additional context variables
|
Our contextprocessor adds additional context variables
|
||||||
in order to be used in the templates
|
in order to be used in the templates
|
||||||
"""
|
'''
|
||||||
context = {}
|
context = {}
|
||||||
if "openid_identifier" in request.GET:
|
if 'openid_identifier' in request.GET:
|
||||||
context["openid_identifier"] = request.GET[
|
context['openid_identifier'] = \
|
||||||
"openid_identifier"
|
request.GET['openid_identifier'] # pragma: no cover
|
||||||
] # pragma: no cover
|
|
||||||
client_ip = get_client_ip(request)[0]
|
client_ip = get_client_ip(request)[0]
|
||||||
context["client_ip"] = client_ip
|
context['client_ip'] = client_ip
|
||||||
context["ivatar_version"] = IVATAR_VERSION
|
context['ivatar_version'] = IVATAR_VERSION
|
||||||
context["site_name"] = SITE_NAME
|
context['site_name'] = SITE_NAME
|
||||||
context["site_url"] = request.build_absolute_uri("/")[:-1]
|
context['site_url'] = request.build_absolute_uri('/')[:-1]
|
||||||
context["max_file_size"] = MAX_PHOTO_SIZE
|
context['max_file_size'] = MAX_PHOTO_SIZE
|
||||||
context["BASE_URL"] = BASE_URL
|
context['BASE_URL'] = BASE_URL
|
||||||
context["SECURE_BASE_URL"] = SECURE_BASE_URL
|
context['SECURE_BASE_URL'] = SECURE_BASE_URL
|
||||||
context["max_emails"] = False
|
context['max_emails'] = False
|
||||||
if request.user:
|
if request.user:
|
||||||
if not request.user.is_anonymous:
|
if not request.user.is_anonymous:
|
||||||
unconfirmed = request.user.unconfirmedemail_set.count()
|
unconfirmed = request.user.unconfirmedemail_set.count()
|
||||||
if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS:
|
if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS:
|
||||||
context["max_emails"] = True
|
context['max_emails'] = True
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Module init
|
Module init
|
||||||
"""
|
'''
|
||||||
app_label = __name__ # pylint: disable=invalid-name
|
app_label = __name__ # pylint: disable=invalid-name
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Register models in admin
|
Register models in admin
|
||||||
"""
|
'''
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Photo, ConfirmedEmail, UnconfirmedEmail
|
from . models import Photo, ConfirmedEmail, UnconfirmedEmail
|
||||||
from .models import ConfirmedOpenId, UnconfirmedOpenId
|
from . models import ConfirmedOpenId, UnconfirmedOpenId
|
||||||
from .models import OpenIDNonce, OpenIDAssociation
|
from . models import OpenIDNonce, OpenIDAssociation
|
||||||
from .models import UserPreference
|
from . models import UserPreference
|
||||||
|
|
||||||
# Register models in admin
|
# Register models in admin
|
||||||
admin.site.register(Photo)
|
admin.site.register(Photo)
|
||||||
|
|||||||
@@ -1,117 +1,110 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Classes for our ivatar.ivataraccount.forms
|
Classes for our ivatar.ivataraccount.forms
|
||||||
"""
|
'''
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from ipware import get_client_ip
|
from ipware import get_client_ip
|
||||||
|
|
||||||
from ivatar import settings
|
from ivatar import settings
|
||||||
from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL
|
from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL
|
||||||
from ivatar.settings import MIN_LENGTH_URL, MAX_LENGTH_URL
|
from ivatar.settings import MIN_LENGTH_URL, MAX_LENGTH_URL
|
||||||
from .models import UnconfirmedEmail, ConfirmedEmail, Photo
|
from . models import UnconfirmedEmail, ConfirmedEmail, Photo
|
||||||
from .models import UnconfirmedOpenId, ConfirmedOpenId
|
from . models import UnconfirmedOpenId, ConfirmedOpenId
|
||||||
from .models import UserPreference
|
from . models import UserPreference
|
||||||
|
|
||||||
|
|
||||||
MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5
|
MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5
|
||||||
|
|
||||||
|
|
||||||
class AddEmailForm(forms.Form):
|
class AddEmailForm(forms.Form):
|
||||||
"""
|
'''
|
||||||
Form to handle adding email addresses
|
Form to handle adding email addresses
|
||||||
"""
|
'''
|
||||||
|
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label=_("Email"),
|
label=_('Email'),
|
||||||
min_length=MIN_LENGTH_EMAIL,
|
min_length=MIN_LENGTH_EMAIL,
|
||||||
max_length=MAX_LENGTH_EMAIL,
|
max_length=MAX_LENGTH_EMAIL,
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_email(self):
|
def clean_email(self):
|
||||||
"""
|
'''
|
||||||
Enforce lowercase email
|
Enforce lowercase email
|
||||||
"""
|
'''
|
||||||
# TODO: Domain restriction as in libravatar?
|
# TODO: Domain restriction as in libravatar?
|
||||||
return self.cleaned_data["email"].lower()
|
return self.cleaned_data['email'].lower()
|
||||||
|
|
||||||
def save(self, request):
|
def save(self, request):
|
||||||
"""
|
'''
|
||||||
Save the model, ensuring some safety
|
Save the model, ensuring some safety
|
||||||
"""
|
'''
|
||||||
user = request.user
|
user = request.user
|
||||||
# Enforce the maximum number of unconfirmed emails a user can have
|
# Enforce the maximum number of unconfirmed emails a user can have
|
||||||
num_unconfirmed = user.unconfirmedemail_set.count()
|
num_unconfirmed = user.unconfirmedemail_set.count()
|
||||||
|
|
||||||
max_num_unconfirmed_emails = getattr(
|
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:
|
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
|
return False
|
||||||
|
|
||||||
# Check whether or not a confirmation email has been
|
# Check whether or not a confirmation email has been
|
||||||
# sent by this user already
|
# sent by this user already
|
||||||
if UnconfirmedEmail.objects.filter( # pylint: disable=no-member
|
if UnconfirmedEmail.objects.filter( # pylint: disable=no-member
|
||||||
user=user, email=self.cleaned_data["email"]
|
user=user, email=self.cleaned_data['email']).exists():
|
||||||
).exists():
|
self.add_error(
|
||||||
self.add_error("email", _("Address already added, currently unconfirmed"))
|
'email',
|
||||||
|
_('Address already added, currently unconfirmed'))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check whether or not the email is already confirmed (by someone)
|
# Check whether or not the email is already confirmed by someone
|
||||||
check_mail = ConfirmedEmail.objects.filter(email=self.cleaned_data["email"])
|
if ConfirmedEmail.objects.filter(
|
||||||
if check_mail.exists():
|
email=self.cleaned_data['email']).exists():
|
||||||
msg = _("Address already confirmed (by someone else)")
|
self.add_error(
|
||||||
if check_mail.first().user == request.user:
|
'email',
|
||||||
msg = _("Address already confirmed (by you)")
|
_('Address already confirmed (by someone else)'))
|
||||||
self.add_error("email", msg)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
unconfirmed = UnconfirmedEmail()
|
unconfirmed = UnconfirmedEmail()
|
||||||
unconfirmed.email = self.cleaned_data["email"]
|
unconfirmed.email = self.cleaned_data['email']
|
||||||
unconfirmed.user = user
|
unconfirmed.user = user
|
||||||
unconfirmed.save()
|
unconfirmed.save()
|
||||||
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1])
|
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri('/')[:-1])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class UploadPhotoForm(forms.Form):
|
class UploadPhotoForm(forms.Form):
|
||||||
"""
|
'''
|
||||||
Form handling photo upload
|
Form handling photo upload
|
||||||
"""
|
'''
|
||||||
|
|
||||||
photo = forms.FileField(
|
photo = forms.FileField(
|
||||||
label=_("Photo"),
|
label=_('Photo'),
|
||||||
error_messages={"required": _("You must choose an image to upload.")},
|
error_messages={'required': _('You must choose an image to upload.')})
|
||||||
)
|
|
||||||
not_porn = forms.BooleanField(
|
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,
|
required=True,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": _(
|
'required':
|
||||||
'We only host "G-rated" images and so this field must be checked.'
|
_('We only host "G-rated" images and so this field must be checked.')
|
||||||
)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
can_distribute = forms.BooleanField(
|
can_distribute = forms.BooleanField(
|
||||||
label=_("can be freely copied"),
|
label=_('can be freely copied'),
|
||||||
required=True,
|
required=True,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": _(
|
'required':
|
||||||
"This field must be checked since we need to be able to distribute photos to third parties."
|
_('This field must be checked since we need to be able to distribute photos to third parties.')
|
||||||
)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save(request, data):
|
def save(request, data):
|
||||||
"""
|
'''
|
||||||
Save the model and assign it to the current user
|
Save the model and assign it to the current user
|
||||||
"""
|
'''
|
||||||
# Link this file to the user's profile
|
# Link this file to the user's profile
|
||||||
photo = Photo()
|
photo = Photo()
|
||||||
photo.user = request.user
|
photo.user = request.user
|
||||||
@@ -124,48 +117,47 @@ class UploadPhotoForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class AddOpenIDForm(forms.Form):
|
class AddOpenIDForm(forms.Form):
|
||||||
"""
|
'''
|
||||||
Form to handle adding OpenID
|
Form to handle adding OpenID
|
||||||
"""
|
'''
|
||||||
|
|
||||||
openid = forms.URLField(
|
openid = forms.URLField(
|
||||||
label=_("OpenID"),
|
label=_('OpenID'),
|
||||||
min_length=MIN_LENGTH_URL,
|
min_length=MIN_LENGTH_URL,
|
||||||
max_length=MAX_LENGTH_URL,
|
max_length=MAX_LENGTH_URL,
|
||||||
initial="http://",
|
initial='http://'
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_openid(self):
|
def clean_openid(self):
|
||||||
"""
|
'''
|
||||||
Enforce restrictions
|
Enforce restrictions
|
||||||
"""
|
'''
|
||||||
# Lowercase hostname port of the URL
|
# Lowercase hostname port of the URL
|
||||||
url = urlsplit(self.cleaned_data["openid"])
|
url = urlsplit(self.cleaned_data['openid'])
|
||||||
data = urlunsplit(
|
data = urlunsplit(
|
||||||
(url.scheme.lower(), url.netloc.lower(), url.path, url.query, url.fragment)
|
(url.scheme.lower(), url.netloc.lower(), url.path,
|
||||||
)
|
url.query, url.fragment))
|
||||||
|
|
||||||
# TODO: Domain restriction as in libravatar?
|
# TODO: Domain restriction as in libravatar?
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self, user):
|
def save(self, user):
|
||||||
"""
|
'''
|
||||||
Save the model, ensuring some safety
|
Save the model, ensuring some safety
|
||||||
"""
|
'''
|
||||||
if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
|
if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
|
||||||
openid=self.cleaned_data["openid"]
|
openid=self.cleaned_data['openid']).exists():
|
||||||
).exists():
|
self.add_error('openid', _('OpenID already added and confirmed!'))
|
||||||
self.add_error("openid", _("OpenID already added and confirmed!"))
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member
|
if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member
|
||||||
openid=self.cleaned_data["openid"]
|
openid=self.cleaned_data['openid']).exists():
|
||||||
).exists():
|
self.add_error(
|
||||||
self.add_error("openid", _("OpenID already added, but not confirmed yet!"))
|
'openid',
|
||||||
|
_('OpenID already added, but not confirmed yet!'))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
unconfirmed = UnconfirmedOpenId()
|
unconfirmed = UnconfirmedOpenId()
|
||||||
unconfirmed.openid = self.cleaned_data["openid"]
|
unconfirmed.openid = self.cleaned_data['openid']
|
||||||
unconfirmed.user = user
|
unconfirmed.user = user
|
||||||
unconfirmed.save()
|
unconfirmed.save()
|
||||||
|
|
||||||
@@ -173,50 +165,40 @@ class AddOpenIDForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class UpdatePreferenceForm(forms.ModelForm):
|
class UpdatePreferenceForm(forms.ModelForm):
|
||||||
"""
|
'''
|
||||||
Form for updating user preferences
|
Form for updating user preferences
|
||||||
"""
|
'''
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
"""
|
'''
|
||||||
Meta class for UpdatePreferenceForm
|
Meta class for UpdatePreferenceForm
|
||||||
"""
|
'''
|
||||||
|
|
||||||
model = UserPreference
|
model = UserPreference
|
||||||
fields = ["theme"]
|
fields = ['theme']
|
||||||
|
|
||||||
|
|
||||||
class UploadLibravatarExportForm(forms.Form):
|
class UploadLibravatarExportForm(forms.Form):
|
||||||
"""
|
'''
|
||||||
Form handling libravatar user export upload
|
Form handling libravatar user export upload
|
||||||
"""
|
'''
|
||||||
|
|
||||||
export_file = forms.FileField(
|
export_file = forms.FileField(
|
||||||
label=_("Export file"),
|
label=_('Export file'),
|
||||||
error_messages={"required": _("You must choose an export file to upload.")},
|
error_messages={'required': _('You must choose an export file to upload.')})
|
||||||
)
|
|
||||||
not_porn = forms.BooleanField(
|
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,
|
required=True,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": _(
|
'required':
|
||||||
'We only host "G-rated" images and so this field must be checked.'
|
_('We only host "G-rated" images and so this field must be checked.')
|
||||||
)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
can_distribute = forms.BooleanField(
|
can_distribute = forms.BooleanField(
|
||||||
label=_("can be freely copied"),
|
label=_('can be freely copied'),
|
||||||
required=True,
|
required=True,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": _(
|
'required':
|
||||||
"This field must be checked since we need to be able to\
|
_('This field must be checked since we need to be able to\
|
||||||
distribute photos to third parties."
|
distribute photos to third parties.')
|
||||||
)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteAccountForm(forms.Form):
|
class DeleteAccountForm(forms.Form):
|
||||||
password = forms.CharField(
|
password = forms.CharField(label=_('Password'), required=False, widget=forms.PasswordInput())
|
||||||
label=_("Password"), required=False, widget=forms.PasswordInput()
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,58 +1,53 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Helper method to fetch Gravatar image
|
Helper method to fetch Gravatar image
|
||||||
"""
|
'''
|
||||||
from ssl import SSLError
|
from ssl import SSLError
|
||||||
from urllib.request import urlopen, HTTPError, URLError
|
from urllib.request import urlopen, HTTPError, URLError
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from ..settings import AVATAR_MAX_SIZE
|
from .. settings import AVATAR_MAX_SIZE
|
||||||
|
|
||||||
URL_TIMEOUT = 5 # in seconds
|
URL_TIMEOUT = 5 # in seconds
|
||||||
|
|
||||||
|
|
||||||
def get_photo(email):
|
def get_photo(email):
|
||||||
"""
|
'''
|
||||||
Fetch photo from Gravatar, given an email address
|
Fetch photo from Gravatar, given an email address
|
||||||
"""
|
'''
|
||||||
hash_object = hashlib.new("md5")
|
hash_object = hashlib.new('md5')
|
||||||
hash_object.update(email.lower().encode("utf-8"))
|
hash_object.update(email.lower().encode('utf-8'))
|
||||||
thumbnail_url = (
|
thumbnail_url = 'https://secure.gravatar.com/avatar/' + \
|
||||||
"https://secure.gravatar.com/avatar/"
|
hash_object.hexdigest() + '?s=%i&d=404' % AVATAR_MAX_SIZE
|
||||||
+ hash_object.hexdigest()
|
image_url = 'https://secure.gravatar.com/avatar/' + hash_object.hexdigest(
|
||||||
+ "?s=%i&d=404" % AVATAR_MAX_SIZE
|
) + '?s=512&d=404'
|
||||||
)
|
|
||||||
image_url = (
|
|
||||||
"https://secure.gravatar.com/avatar/" + hash_object.hexdigest() + "?s=512&d=404"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Will redirect to the public profile URL if it exists
|
# Will redirect to the public profile URL if it exists
|
||||||
service_url = "http://www.gravatar.com/" + hash_object.hexdigest()
|
service_url = 'http://www.gravatar.com/' + hash_object.hexdigest()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
urlopen(image_url, timeout=URL_TIMEOUT)
|
urlopen(image_url, timeout=URL_TIMEOUT)
|
||||||
except HTTPError as exc:
|
except HTTPError as exc:
|
||||||
if exc.code != 404 and exc.code != 503:
|
if exc.code != 404 and exc.code != 503:
|
||||||
print( # pragma: no cover
|
print( # pragma: no cover
|
||||||
"Gravatar fetch failed with an unexpected %s HTTP error" % exc.code
|
'Gravatar fetch failed with an unexpected %s HTTP error' %
|
||||||
)
|
exc.code)
|
||||||
return False
|
return False
|
||||||
except URLError as exc: # pragma: no cover
|
except URLError as exc: # pragma: no cover
|
||||||
print(
|
print(
|
||||||
"Gravatar fetch failed with URL error: %s" % exc.reason
|
'Gravatar fetch failed with URL error: %s' %
|
||||||
) # pragma: no cover
|
exc.reason) # pragma: no cover
|
||||||
return False # pragma: no cover
|
return False # pragma: no cover
|
||||||
except SSLError as exc: # pragma: no cover
|
except SSLError as exc: # pragma: no cover
|
||||||
print(
|
print(
|
||||||
"Gravatar fetch failed with SSL error: %s" % exc.reason
|
'Gravatar fetch failed with SSL error: %s' %
|
||||||
) # pragma: no cover
|
exc.reason) # pragma: no cover
|
||||||
return False # pragma: no cover
|
return False # pragma: no cover
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"thumbnail_url": thumbnail_url,
|
'thumbnail_url': thumbnail_url,
|
||||||
"image_url": image_url,
|
'image_url': image_url,
|
||||||
"width": AVATAR_MAX_SIZE,
|
'width': AVATAR_MAX_SIZE,
|
||||||
"height": AVATAR_MAX_SIZE,
|
'height': AVATAR_MAX_SIZE,
|
||||||
"service_url": service_url,
|
'service_url': service_url,
|
||||||
"service_name": "Gravatar",
|
'service_name': 'Gravatar'
|
||||||
}
|
}
|
||||||
|
|||||||
25
ivatar/ivataraccount/migrations/0015_apikey.py
Normal file
25
ivatar/ivataraccount/migrations/0015_apikey.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
34
ivatar/ivataraccount/migrations/0016_auto_20190311_1016.py
Normal file
34
ivatar/ivataraccount/migrations/0016_auto_20190311_1016.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 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)
|
||||||
|
]
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
30
ivatar/ivataraccount/migrations/0017_auto_20190311_1103.py
Normal file
30
ivatar/ivataraccount/migrations/0017_auto_20190311_1103.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 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)
|
||||||
|
]
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Our models for ivatar.ivataraccount
|
Our models for ivatar.ivataraccount
|
||||||
"""
|
'''
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -19,7 +18,7 @@ from django.db import models
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
@@ -27,6 +26,9 @@ from openid.association import Association as OIDAssociation
|
|||||||
from openid.store import nonce as oidnonce
|
from openid.store import nonce as oidnonce
|
||||||
from openid.store.interface import OpenIDStore
|
from openid.store.interface import OpenIDStore
|
||||||
|
|
||||||
|
from simplecrypt import encrypt
|
||||||
|
from binascii import hexlify
|
||||||
|
|
||||||
from libravatar import libravatar_url
|
from libravatar import libravatar_url
|
||||||
|
|
||||||
from ivatar.settings import MAX_LENGTH_EMAIL, logger
|
from ivatar.settings import MAX_LENGTH_EMAIL, logger
|
||||||
@@ -35,52 +37,87 @@ from ivatar.settings import MAX_LENGTH_URL
|
|||||||
from ivatar.settings import SECURE_BASE_URL, SITE_NAME, DEFAULT_FROM_EMAIL
|
from ivatar.settings import SECURE_BASE_URL, SITE_NAME, DEFAULT_FROM_EMAIL
|
||||||
from ivatar.utils import openid_variations
|
from ivatar.utils import openid_variations
|
||||||
from .gravatar import get_photo as get_gravatar_photo
|
from .gravatar import get_photo as get_gravatar_photo
|
||||||
|
from ivatar.utils import random_string
|
||||||
|
|
||||||
|
|
||||||
def file_format(image_type):
|
def file_format(image_type):
|
||||||
"""
|
'''
|
||||||
Helper method returning a short image type
|
Helper method returning a short image type
|
||||||
"""
|
'''
|
||||||
if image_type == "JPEG":
|
if image_type == 'JPEG':
|
||||||
return "jpg"
|
return 'jpg'
|
||||||
elif image_type == "PNG":
|
elif image_type == 'PNG':
|
||||||
return "png"
|
return 'png'
|
||||||
elif image_type == "GIF":
|
elif image_type == 'GIF':
|
||||||
return "gif"
|
return 'gif'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def random_api_key():
|
||||||
|
return random_string(32)
|
||||||
|
|
||||||
|
|
||||||
def pil_format(image_type):
|
def pil_format(image_type):
|
||||||
"""
|
'''
|
||||||
Helper method returning the 'encoder name' for PIL
|
Helper method returning the 'encoder name' for PIL
|
||||||
"""
|
'''
|
||||||
if image_type == "jpg" or image_type == "jpeg":
|
if image_type == 'jpg' or image_type == 'jpeg':
|
||||||
return "JPEG"
|
return 'JPEG'
|
||||||
elif image_type == "png":
|
elif image_type == 'png':
|
||||||
return "PNG"
|
return 'PNG'
|
||||||
elif image_type == "gif":
|
elif image_type == 'gif':
|
||||||
return "GIF"
|
return 'GIF'
|
||||||
|
|
||||||
logger.info("Unsupported file format: %s", image_type)
|
logger.info('Unsupported file format: %s', image_type)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_key_from_args(public_key=None, secret_key=None):
|
||||||
|
encryption_key = None
|
||||||
|
if type(public_key) is str:
|
||||||
|
try:
|
||||||
|
key = APIKey.objects.get(public_key=public_key)
|
||||||
|
encryption_key = key.secret_key
|
||||||
|
except APIKey.DoesNotExist:
|
||||||
|
raise Exception(_('Key given, does not exist'))
|
||||||
|
if type(public_key) is APIKey:
|
||||||
|
encryption_key = public_key.secret_key
|
||||||
|
if type(secret_key) is str:
|
||||||
|
encryption_key = secret_key
|
||||||
|
if type(secret_key) is APIKey:
|
||||||
|
encryption_key = secret_key.secret_key
|
||||||
|
if not encryption_key:
|
||||||
|
raise Exception(_('No key given for encryption'))
|
||||||
|
return encryption_key
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class APIKey(models.Model):
|
||||||
|
'''
|
||||||
|
Holds the users API keys (public and private/secret)
|
||||||
|
'''
|
||||||
|
public_key = models.CharField(max_length=32, default=random_api_key, unique=True, blank=False, null=False)
|
||||||
|
secret_key = models.CharField(max_length=32, default=random_api_key, unique=False, blank=False, null=False)
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
on_delete=models.deletion.CASCADE,
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserPreference(models.Model):
|
class UserPreference(models.Model):
|
||||||
"""
|
'''
|
||||||
Holds the user users preferences
|
Holds the user users preferences
|
||||||
"""
|
'''
|
||||||
|
|
||||||
THEMES = (
|
THEMES = (
|
||||||
("default", "Default theme"),
|
('default', 'Default theme'),
|
||||||
("clime", "climes theme"),
|
('clime', 'climes theme'),
|
||||||
("green", "green theme"),
|
('green', 'green theme'),
|
||||||
("red", "red theme"),
|
('red', 'red theme'),
|
||||||
)
|
)
|
||||||
|
|
||||||
theme = models.CharField(
|
theme = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=THEMES,
|
choices=THEMES,
|
||||||
default="default",
|
default='default',
|
||||||
)
|
)
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
@@ -90,14 +127,13 @@ class UserPreference(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Preference (%i) for %s" % (self.pk, self.user)
|
return 'Preference (%i) for %s' % (self.pk, self.user)
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountModel(models.Model):
|
class BaseAccountModel(models.Model):
|
||||||
"""
|
'''
|
||||||
Base, abstract model, holding fields we use in all cases
|
Base, abstract model, holding fields we use in all cases
|
||||||
"""
|
'''
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.deletion.CASCADE,
|
on_delete=models.deletion.CASCADE,
|
||||||
@@ -106,43 +142,40 @@ class BaseAccountModel(models.Model):
|
|||||||
add_date = models.DateTimeField(default=timezone.now)
|
add_date = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
"""
|
'''
|
||||||
Class attributes
|
Class attributes
|
||||||
"""
|
'''
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Photo(BaseAccountModel):
|
class Photo(BaseAccountModel):
|
||||||
"""
|
'''
|
||||||
Model holding the photos and information about them
|
Model holding the photos and information about them
|
||||||
"""
|
'''
|
||||||
|
|
||||||
ip_address = models.GenericIPAddressField(unpack_ipv4=True)
|
ip_address = models.GenericIPAddressField(unpack_ipv4=True)
|
||||||
data = models.BinaryField()
|
data = models.BinaryField()
|
||||||
format = models.CharField(max_length=3)
|
format = models.CharField(max_length=3)
|
||||||
access_count = models.BigIntegerField(default=0, editable=False)
|
access_count = models.BigIntegerField(default=0, editable=False)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
"""
|
'''
|
||||||
Class attributes
|
Class attributes
|
||||||
"""
|
'''
|
||||||
|
verbose_name = _('photo')
|
||||||
verbose_name = _("photo")
|
verbose_name_plural = _('photos')
|
||||||
verbose_name_plural = _("photos")
|
|
||||||
|
|
||||||
def import_image(self, service_name, email_address):
|
def import_image(self, service_name, email_address):
|
||||||
"""
|
'''
|
||||||
Allow to import image from other (eg. Gravatar) service
|
Allow to import image from other (eg. Gravatar) service
|
||||||
"""
|
'''
|
||||||
image_url = False
|
image_url = False
|
||||||
|
|
||||||
if service_name == "Gravatar":
|
if service_name == 'Gravatar':
|
||||||
gravatar = get_gravatar_photo(email_address)
|
gravatar = get_gravatar_photo(email_address)
|
||||||
if gravatar:
|
if gravatar:
|
||||||
image_url = gravatar["image_url"]
|
image_url = gravatar['image_url']
|
||||||
|
|
||||||
if service_name == "Libravatar":
|
if service_name == 'Libravatar':
|
||||||
image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE)
|
image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE)
|
||||||
|
|
||||||
if not image_url:
|
if not image_url:
|
||||||
@@ -152,12 +185,13 @@ class Photo(BaseAccountModel):
|
|||||||
# No idea how to test this
|
# No idea how to test this
|
||||||
# pragma: no cover
|
# pragma: no cover
|
||||||
except HTTPError as exc:
|
except HTTPError as exc:
|
||||||
print("%s import failed with an HTTP error: %s" % (service_name, exc.code))
|
print('%s import failed with an HTTP error: %s' %
|
||||||
|
(service_name, exc.code))
|
||||||
return False
|
return False
|
||||||
# No idea how to test this
|
# No idea how to test this
|
||||||
# pragma: no cover
|
# pragma: no cover
|
||||||
except URLError as exc:
|
except URLError as exc:
|
||||||
print("%s import failed: %s" % (service_name, exc.reason))
|
print('%s import failed: %s' % (service_name, exc.reason))
|
||||||
return False
|
return False
|
||||||
data = image.read()
|
data = image.read()
|
||||||
|
|
||||||
@@ -169,36 +203,35 @@ class Photo(BaseAccountModel):
|
|||||||
|
|
||||||
self.format = file_format(img.format)
|
self.format = file_format(img.format)
|
||||||
if not self.format:
|
if not self.format:
|
||||||
print("Unable to determine format: %s" % img) # pragma: no cover
|
print('Unable to determine format: %s' % img) # pragma: no cover
|
||||||
return False # pragma: no cover
|
return False # pragma: no cover
|
||||||
self.data = data
|
self.data = data
|
||||||
super().save()
|
super().save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def save(
|
def save(self, force_insert=False, force_update=False, using=None,
|
||||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
update_fields=None):
|
||||||
):
|
'''
|
||||||
"""
|
|
||||||
Override save from parent, taking care about the image
|
Override save from parent, taking care about the image
|
||||||
"""
|
'''
|
||||||
# Use PIL to read the file format
|
# Use PIL to read the file format
|
||||||
try:
|
try:
|
||||||
img = Image.open(BytesIO(self.data))
|
img = Image.open(BytesIO(self.data))
|
||||||
# Testing? Ideas anyone?
|
# Testing? Ideas anyone?
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
# For debugging only
|
# For debugging only
|
||||||
print("Exception caught in Photo.save(): %s" % exc)
|
print('Exception caught in Photo.save(): %s' % exc)
|
||||||
return False
|
return False
|
||||||
self.format = file_format(img.format)
|
self.format = file_format(img.format)
|
||||||
if not self.format:
|
if not self.format:
|
||||||
print("Format not recognized")
|
print('Format not recognized')
|
||||||
return False
|
return False
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def perform_crop(self, request, dimensions, email, openid):
|
def perform_crop(self, request, dimensions, email, openid):
|
||||||
"""
|
'''
|
||||||
Helper to crop the image
|
Helper to crop the image
|
||||||
"""
|
'''
|
||||||
if request.user.photo_set.count() == 1:
|
if request.user.photo_set.count() == 1:
|
||||||
# This is the first photo, assign to all confirmed addresses
|
# This is the first photo, assign to all confirmed addresses
|
||||||
for addr in request.user.confirmedemail_set.all():
|
for addr in request.user.confirmedemail_set.all():
|
||||||
@@ -223,40 +256,34 @@ class Photo(BaseAccountModel):
|
|||||||
img = Image.open(BytesIO(self.data))
|
img = Image.open(BytesIO(self.data))
|
||||||
|
|
||||||
# This should be anyway checked during save...
|
# This should be anyway checked during save...
|
||||||
dimensions["a"], dimensions["b"] = img.size # pylint: disable=invalid-name
|
dimensions['a'], \
|
||||||
if dimensions["a"] > MAX_PIXELS or dimensions["b"] > MAX_PIXELS:
|
dimensions['b'] = img.size # pylint: disable=invalid-name
|
||||||
|
if dimensions['a'] > MAX_PIXELS or dimensions['b'] > MAX_PIXELS:
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
_(
|
_('Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s' % {
|
||||||
"Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s"
|
max_pixels: MAX_PIXELS,
|
||||||
% {
|
}))
|
||||||
"max_pixels": MAX_PIXELS,
|
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
|
||||||
|
|
||||||
if dimensions["w"] == 0 and dimensions["h"] == 0:
|
if dimensions['w'] == 0 and dimensions['h'] == 0:
|
||||||
dimensions["w"], dimensions["h"] = dimensions["a"], dimensions["b"]
|
dimensions['w'], dimensions['h'] = dimensions['a'], dimensions['b']
|
||||||
min_from_w_h = min(dimensions["w"], dimensions["h"])
|
min_from_w_h = min(dimensions['w'], dimensions['h'])
|
||||||
dimensions["w"], dimensions["h"] = min_from_w_h, min_from_w_h
|
dimensions['w'], dimensions['h'] = min_from_w_h, min_from_w_h
|
||||||
elif (
|
elif ((dimensions['w'] < 0)
|
||||||
(dimensions["w"] < 0)
|
or ((dimensions['x'] + dimensions['w']) > dimensions['a'])
|
||||||
or ((dimensions["x"] + dimensions["w"]) > dimensions["a"])
|
or (dimensions['h'] < 0)
|
||||||
or (dimensions["h"] < 0)
|
or ((dimensions['y'] + dimensions['h']) > dimensions['b'])):
|
||||||
or ((dimensions["y"] + dimensions["h"]) > dimensions["b"])
|
messages.error(
|
||||||
):
|
request,
|
||||||
messages.error(request, _("Crop outside of original image bounding box"))
|
_('Crop outside of original image bounding box'))
|
||||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||||
|
|
||||||
cropped = img.crop(
|
cropped = img.crop((
|
||||||
(
|
dimensions['x'],
|
||||||
dimensions["x"],
|
dimensions['y'],
|
||||||
dimensions["y"],
|
dimensions['x'] + dimensions['w'],
|
||||||
dimensions["x"] + dimensions["w"],
|
dimensions['y'] + dimensions['h']))
|
||||||
dimensions["y"] + dimensions["h"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# cropped.load()
|
# cropped.load()
|
||||||
# Resize the image only if it's larger than the specified max width.
|
# Resize the image only if it's larger than the specified max width.
|
||||||
cropped_w, cropped_h = cropped.size
|
cropped_w, cropped_h = cropped.size
|
||||||
@@ -272,26 +299,26 @@ class Photo(BaseAccountModel):
|
|||||||
self.data = data.read()
|
self.data = data.read()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
return HttpResponseRedirect(reverse_lazy('profile'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s (%i) from %s" % (self.format, self.pk or 0, self.user)
|
return '%s (%i) from %s' % (self.format, self.pk or 0, self.user)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class ConfirmedEmailManager(models.Manager):
|
class ConfirmedEmailManager(models.Manager):
|
||||||
"""
|
'''
|
||||||
Manager for our confirmed email addresses model
|
Manager for our confirmed email addresses model
|
||||||
"""
|
'''
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_confirmed_email(user, email_address, is_logged_in):
|
def create_confirmed_email(user, email_address, is_logged_in):
|
||||||
"""
|
'''
|
||||||
Helper method to create confirmed email address
|
Helper method to create confirmed email address
|
||||||
"""
|
'''
|
||||||
confirmed = ConfirmedEmail()
|
confirmed = ConfirmedEmail()
|
||||||
confirmed.user = user
|
confirmed.user = user
|
||||||
confirmed.ip_address = "0.0.0.0"
|
confirmed.ip_address = '0.0.0.0'
|
||||||
confirmed.email = email_address
|
confirmed.email = email_address
|
||||||
confirmed.save()
|
confirmed.save()
|
||||||
|
|
||||||
@@ -305,15 +332,14 @@ class ConfirmedEmailManager(models.Manager):
|
|||||||
|
|
||||||
|
|
||||||
class ConfirmedEmail(BaseAccountModel):
|
class ConfirmedEmail(BaseAccountModel):
|
||||||
"""
|
'''
|
||||||
Model holding our confirmed email addresses, as well as the relation
|
Model holding our confirmed email addresses, as well as the relation
|
||||||
to the assigned photo
|
to the assigned photo
|
||||||
"""
|
'''
|
||||||
|
|
||||||
email = models.EmailField(unique=True, max_length=MAX_LENGTH_EMAIL)
|
email = models.EmailField(unique=True, max_length=MAX_LENGTH_EMAIL)
|
||||||
photo = models.ForeignKey(
|
photo = models.ForeignKey(
|
||||||
Photo,
|
Photo,
|
||||||
related_name="emails",
|
related_name='emails',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.deletion.SET_NULL,
|
on_delete=models.deletion.SET_NULL,
|
||||||
@@ -324,129 +350,122 @@ class ConfirmedEmail(BaseAccountModel):
|
|||||||
access_count = models.BigIntegerField(default=0, editable=False)
|
access_count = models.BigIntegerField(default=0, editable=False)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
"""
|
'''
|
||||||
Class attributes
|
Class attributes
|
||||||
"""
|
'''
|
||||||
|
verbose_name = _('confirmed email')
|
||||||
verbose_name = _("confirmed email")
|
verbose_name_plural = _('confirmed emails')
|
||||||
verbose_name_plural = _("confirmed emails")
|
|
||||||
|
|
||||||
def set_photo(self, photo):
|
def set_photo(self, photo):
|
||||||
"""
|
'''
|
||||||
Helper method to set photo
|
Helper method to set photo
|
||||||
"""
|
'''
|
||||||
self.photo = photo
|
self.photo = photo
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(
|
def save(self, force_insert=False, force_update=False, using=None,
|
||||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
update_fields=None):
|
||||||
):
|
'''
|
||||||
"""
|
|
||||||
Override save from parent, add digest
|
Override save from parent, add digest
|
||||||
"""
|
'''
|
||||||
self.digest = hashlib.md5(
|
self.digest = hashlib.md5(
|
||||||
self.email.strip().lower().encode("utf-8")
|
self.email.strip().lower().encode('utf-8')
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
self.digest_sha256 = hashlib.sha256(
|
self.digest_sha256 = hashlib.sha256(
|
||||||
self.email.strip().lower().encode("utf-8")
|
self.email.strip().lower().encode('utf-8')
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s (%i) from %s" % (self.email, self.pk, self.user)
|
return '%s (%i) from %s' % (self.email, self.pk, self.user)
|
||||||
|
|
||||||
|
def encrypted_digest_sha256(self, public_key=None, secret_key=None):
|
||||||
|
encryption_key = get_key_from_args(public_key=public_key, secret_key=secret_key)
|
||||||
|
return hexlify(encrypt(encryption_key, self.digest_sha256))
|
||||||
|
|
||||||
|
def encrypted_digest(self, public_key=None, secret_key=None):
|
||||||
|
encryption_key = get_key_from_args(public_key=public_key, secret_key=secret_key)
|
||||||
|
return hexlify(encrypt(encryption_key, self.digest))
|
||||||
|
|
||||||
|
|
||||||
class UnconfirmedEmail(BaseAccountModel):
|
class UnconfirmedEmail(BaseAccountModel):
|
||||||
"""
|
'''
|
||||||
Model holding unconfirmed email addresses as well as the verification key
|
Model holding unconfirmed email addresses as well as the verification key
|
||||||
"""
|
'''
|
||||||
|
|
||||||
email = models.EmailField(max_length=MAX_LENGTH_EMAIL)
|
email = models.EmailField(max_length=MAX_LENGTH_EMAIL)
|
||||||
verification_key = models.CharField(max_length=64)
|
verification_key = models.CharField(max_length=64)
|
||||||
last_send_date = models.DateTimeField(null=True, blank=True)
|
|
||||||
last_status = models.TextField(max_length=2047, null=True, blank=True)
|
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
"""
|
'''
|
||||||
Class attributes
|
Class attributes
|
||||||
"""
|
'''
|
||||||
|
verbose_name = _('unconfirmed email')
|
||||||
|
verbose_name_plural = _('unconfirmed emails')
|
||||||
|
|
||||||
verbose_name = _("unconfirmed email")
|
def save(self, force_insert=False, force_update=False, using=None,
|
||||||
verbose_name_plural = _("unconfirmed emails")
|
update_fields=None):
|
||||||
|
hash_object = hashlib.new('sha256')
|
||||||
def save(
|
hash_object.update(
|
||||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
urandom(1024) + self.user.username.encode('utf-8') # pylint: disable=no-member
|
||||||
):
|
) # pylint: disable=no-member
|
||||||
if not self.verification_key:
|
self.verification_key = hash_object.hexdigest()
|
||||||
hash_object = hashlib.new("sha256")
|
|
||||||
hash_object.update(
|
|
||||||
urandom(1024)
|
|
||||||
+ self.user.username.encode("utf-8") # pylint: disable=no-member
|
|
||||||
) # pylint: disable=no-member
|
|
||||||
self.verification_key = hash_object.hexdigest()
|
|
||||||
super(UnconfirmedEmail, self).save(
|
super(UnconfirmedEmail, self).save(
|
||||||
force_insert, force_update, using, update_fields
|
force_insert,
|
||||||
)
|
force_update,
|
||||||
|
using,
|
||||||
|
update_fields)
|
||||||
|
|
||||||
def send_confirmation_mail(self, url=SECURE_BASE_URL):
|
def send_confirmation_mail(self, url=SECURE_BASE_URL):
|
||||||
"""
|
'''
|
||||||
Send confirmation mail to that mail address
|
Send confirmation mail to that mail address
|
||||||
"""
|
'''
|
||||||
link = url + reverse(
|
link = url + \
|
||||||
"confirm_email", kwargs={"verification_key": self.verification_key}
|
reverse(
|
||||||
)
|
'confirm_email',
|
||||||
email_subject = _("Confirm your email address on %s") % SITE_NAME
|
kwargs={'verification_key': self.verification_key})
|
||||||
email_body = render_to_string(
|
email_subject = _('Confirm your email address on %s') % \
|
||||||
"email_confirmation.txt",
|
SITE_NAME
|
||||||
{
|
email_body = render_to_string('email_confirmation.txt', {
|
||||||
"verification_link": link,
|
'verification_link': link,
|
||||||
"site_name": SITE_NAME,
|
'site_name': SITE_NAME,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
self.last_send_date = timezone.now()
|
|
||||||
self.last_status = "OK"
|
|
||||||
# if settings.DEBUG:
|
# if settings.DEBUG:
|
||||||
# print('DEBUG: %s' % link)
|
# print('DEBUG: %s' % link)
|
||||||
try:
|
send_mail(
|
||||||
send_mail(email_subject, email_body, DEFAULT_FROM_EMAIL, [self.email])
|
email_subject, email_body, DEFAULT_FROM_EMAIL,
|
||||||
except Exception as e:
|
[self.email])
|
||||||
self.last_status = "%s" % e
|
|
||||||
self.save()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s (%i) from %s" % (self.email, self.pk, self.user)
|
return '%s (%i) from %s' % (self.email, self.pk, self.user)
|
||||||
|
|
||||||
|
|
||||||
class UnconfirmedOpenId(BaseAccountModel):
|
class UnconfirmedOpenId(BaseAccountModel):
|
||||||
"""
|
'''
|
||||||
Model holding unconfirmed OpenIDs
|
Model holding unconfirmed OpenIDs
|
||||||
"""
|
'''
|
||||||
|
|
||||||
openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL)
|
openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
"""
|
'''
|
||||||
Meta class
|
Meta class
|
||||||
"""
|
'''
|
||||||
|
verbose_name = _('unconfirmed OpenID')
|
||||||
verbose_name = _("unconfirmed OpenID")
|
verbose_name_plural = ('unconfirmed_OpenIDs')
|
||||||
verbose_name_plural = "unconfirmed_OpenIDs"
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s (%i) from %s" % (self.openid, self.pk, self.user)
|
return '%s (%i) from %s' % (self.openid, self.pk, self.user)
|
||||||
|
|
||||||
|
|
||||||
class ConfirmedOpenId(BaseAccountModel):
|
class ConfirmedOpenId(BaseAccountModel):
|
||||||
"""
|
'''
|
||||||
Model holding confirmed OpenIDs, as well as the relation to
|
Model holding confirmed OpenIDs, as well as the relation to
|
||||||
the assigned photo
|
the assigned photo
|
||||||
"""
|
'''
|
||||||
|
|
||||||
openid = models.URLField(unique=True, max_length=MAX_LENGTH_URL)
|
openid = models.URLField(unique=True, max_length=MAX_LENGTH_URL)
|
||||||
photo = models.ForeignKey(
|
photo = models.ForeignKey(
|
||||||
Photo,
|
Photo,
|
||||||
related_name="openids",
|
related_name='openids',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.deletion.SET_NULL,
|
on_delete=models.deletion.SET_NULL,
|
||||||
@@ -463,27 +482,25 @@ class ConfirmedOpenId(BaseAccountModel):
|
|||||||
access_count = models.BigIntegerField(default=0, editable=False)
|
access_count = models.BigIntegerField(default=0, editable=False)
|
||||||
|
|
||||||
class Meta: # pylint: disable=too-few-public-methods
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
"""
|
'''
|
||||||
Meta class
|
Meta class
|
||||||
"""
|
'''
|
||||||
|
verbose_name = _('confirmed OpenID')
|
||||||
verbose_name = _("confirmed OpenID")
|
verbose_name_plural = _('confirmed OpenIDs')
|
||||||
verbose_name_plural = _("confirmed OpenIDs")
|
|
||||||
|
|
||||||
def set_photo(self, photo):
|
def set_photo(self, photo):
|
||||||
"""
|
'''
|
||||||
Helper method to save photo
|
Helper method to save photo
|
||||||
"""
|
'''
|
||||||
self.photo = photo
|
self.photo = photo
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(
|
def save(self, force_insert=False, force_update=False, using=None,
|
||||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
update_fields=None):
|
||||||
):
|
|
||||||
url = urlsplit(self.openid)
|
url = urlsplit(self.openid)
|
||||||
if url.username: # pragma: no cover
|
if url.username: # pragma: no cover
|
||||||
password = url.password or ""
|
password = url.password or ''
|
||||||
netloc = url.username + ":" + password + "@" + url.hostname
|
netloc = url.username + ':' + password + '@' + url.hostname
|
||||||
else:
|
else:
|
||||||
netloc = url.hostname
|
netloc = url.hostname
|
||||||
lowercase_url = urlunsplit(
|
lowercase_url = urlunsplit(
|
||||||
@@ -491,44 +508,37 @@ class ConfirmedOpenId(BaseAccountModel):
|
|||||||
)
|
)
|
||||||
self.openid = lowercase_url
|
self.openid = lowercase_url
|
||||||
|
|
||||||
self.digest = hashlib.sha256(
|
self.digest = hashlib.sha256(openid_variations(lowercase_url)[0].encode('utf-8')).hexdigest()
|
||||||
openid_variations(lowercase_url)[0].encode("utf-8")
|
self.alt_digest1 = hashlib.sha256(openid_variations(lowercase_url)[1].encode('utf-8')).hexdigest()
|
||||||
).hexdigest()
|
self.alt_digest2 = hashlib.sha256(openid_variations(lowercase_url)[2].encode('utf-8')).hexdigest()
|
||||||
self.alt_digest1 = hashlib.sha256(
|
self.alt_digest3 = hashlib.sha256(openid_variations(lowercase_url)[3].encode('utf-8')).hexdigest()
|
||||||
openid_variations(lowercase_url)[1].encode("utf-8")
|
|
||||||
).hexdigest()
|
|
||||||
self.alt_digest2 = hashlib.sha256(
|
|
||||||
openid_variations(lowercase_url)[2].encode("utf-8")
|
|
||||||
).hexdigest()
|
|
||||||
self.alt_digest3 = hashlib.sha256(
|
|
||||||
openid_variations(lowercase_url)[3].encode("utf-8")
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s (%i) (%s)" % (self.openid, self.pk, self.user)
|
return '%s (%i) (%s)' % (self.openid, self.pk, self.user)
|
||||||
|
|
||||||
|
|
||||||
class OpenIDNonce(models.Model):
|
class OpenIDNonce(models.Model):
|
||||||
"""
|
'''
|
||||||
Model holding OpenID Nonces
|
Model holding OpenID Nonces
|
||||||
See also: https://github.com/edx/django-openid-auth/
|
See also: https://github.com/edx/django-openid-auth/
|
||||||
"""
|
'''
|
||||||
|
|
||||||
server_url = models.CharField(max_length=255)
|
server_url = models.CharField(max_length=255)
|
||||||
timestamp = models.IntegerField()
|
timestamp = models.IntegerField()
|
||||||
salt = models.CharField(max_length=128)
|
salt = models.CharField(max_length=128)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s (%i) (timestamp: %i)" % (self.server_url, self.pk, self.timestamp)
|
return '%s (%i) (timestamp: %i)' % (
|
||||||
|
self.server_url,
|
||||||
|
self.pk,
|
||||||
|
self.timestamp)
|
||||||
|
|
||||||
|
|
||||||
class OpenIDAssociation(models.Model):
|
class OpenIDAssociation(models.Model):
|
||||||
"""
|
'''
|
||||||
Model holding the relation/association about OpenIDs
|
Model holding the relation/association about OpenIDs
|
||||||
"""
|
'''
|
||||||
|
|
||||||
server_url = models.TextField(max_length=2047)
|
server_url = models.TextField(max_length=2047)
|
||||||
handle = models.CharField(max_length=255)
|
handle = models.CharField(max_length=255)
|
||||||
secret = models.TextField(max_length=255) # stored base64 encoded
|
secret = models.TextField(max_length=255) # stored base64 encoded
|
||||||
@@ -537,62 +547,56 @@ class OpenIDAssociation(models.Model):
|
|||||||
assoc_type = models.TextField(max_length=64)
|
assoc_type = models.TextField(max_length=64)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s (%i) (%s, lifetime: %i)" % (
|
return '%s (%i) (%s, lifetime: %i)' % (
|
||||||
self.server_url,
|
self.server_url,
|
||||||
self.pk,
|
self.pk,
|
||||||
self.assoc_type,
|
self.assoc_type,
|
||||||
self.lifetime,
|
self.lifetime)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoOpenIDStore(OpenIDStore):
|
class DjangoOpenIDStore(OpenIDStore):
|
||||||
"""
|
'''
|
||||||
The Python openid library needs an OpenIDStore subclass to persist data
|
The Python openid library needs an OpenIDStore subclass to persist data
|
||||||
related to OpenID authentications. This one uses our Django models.
|
related to OpenID authentications. This one uses our Django models.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def storeAssociation(server_url, association): # pragma: no cover
|
def storeAssociation(server_url, association): # pragma: no cover
|
||||||
"""
|
'''
|
||||||
Helper method to store associations
|
Helper method to store associations
|
||||||
"""
|
'''
|
||||||
assoc = OpenIDAssociation(
|
assoc = OpenIDAssociation(
|
||||||
server_url=server_url,
|
server_url=server_url,
|
||||||
handle=association.handle,
|
handle=association.handle,
|
||||||
secret=base64.encodebytes(association.secret),
|
secret=base64.encodebytes(association.secret),
|
||||||
issued=association.issued,
|
issued=association.issued,
|
||||||
lifetime=association.issued,
|
lifetime=association.issued,
|
||||||
assoc_type=association.assoc_type,
|
assoc_type=association.assoc_type)
|
||||||
)
|
|
||||||
assoc.save()
|
assoc.save()
|
||||||
|
|
||||||
def getAssociation(self, server_url, handle=None): # pragma: no cover
|
def getAssociation(self, server_url, handle=None): # pragma: no cover
|
||||||
"""
|
'''
|
||||||
Helper method to get associations
|
Helper method to get associations
|
||||||
"""
|
'''
|
||||||
assocs = []
|
assocs = []
|
||||||
if handle is not None:
|
if handle is not None:
|
||||||
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
||||||
server_url=server_url, handle=handle
|
server_url=server_url,
|
||||||
)
|
handle=handle)
|
||||||
else:
|
else:
|
||||||
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
||||||
server_url=server_url
|
server_url=server_url)
|
||||||
)
|
|
||||||
if not assocs:
|
if not assocs:
|
||||||
return None
|
return None
|
||||||
associations = []
|
associations = []
|
||||||
for assoc in assocs:
|
for assoc in assocs:
|
||||||
if isinstance(assoc.secret, str):
|
if isinstance(assoc.secret, str):
|
||||||
assoc.secret = assoc.secret.split("b'")[1].split("'")[0]
|
assoc.secret = assoc.secret.split("b'")[1].split("'")[0]
|
||||||
assoc.secret = bytes(assoc.secret, "utf-8")
|
assoc.secret = bytes(assoc.secret, 'utf-8')
|
||||||
association = OIDAssociation(
|
association = OIDAssociation(assoc.handle,
|
||||||
assoc.handle,
|
base64.decodebytes(assoc.secret),
|
||||||
base64.decodebytes(assoc.secret),
|
assoc.issued, assoc.lifetime,
|
||||||
assoc.issued,
|
assoc.assoc_type)
|
||||||
assoc.lifetime,
|
|
||||||
assoc.assoc_type,
|
|
||||||
)
|
|
||||||
expires = 0
|
expires = 0
|
||||||
try:
|
try:
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
@@ -609,14 +613,12 @@ class DjangoOpenIDStore(OpenIDStore):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def removeAssociation(server_url, handle): # pragma: no cover
|
def removeAssociation(server_url, handle): # pragma: no cover
|
||||||
"""
|
'''
|
||||||
Helper method to remove associations
|
Helper method to remove associations
|
||||||
"""
|
'''
|
||||||
assocs = list(
|
assocs = list(
|
||||||
OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
OpenIDAssociation.objects.filter( # pylint: disable=no-member
|
||||||
server_url=server_url, handle=handle
|
server_url=server_url, handle=handle))
|
||||||
)
|
|
||||||
)
|
|
||||||
assocs_exist = len(assocs) > 0
|
assocs_exist = len(assocs) > 0
|
||||||
for assoc in assocs:
|
for assoc in assocs:
|
||||||
assoc.delete()
|
assoc.delete()
|
||||||
@@ -624,9 +626,9 @@ class DjangoOpenIDStore(OpenIDStore):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def useNonce(server_url, timestamp, salt): # pragma: no cover
|
def useNonce(server_url, timestamp, salt): # pragma: no cover
|
||||||
"""
|
'''
|
||||||
Helper method to 'use' nonces
|
Helper method to 'use' nonces
|
||||||
"""
|
'''
|
||||||
# Has nonce expired?
|
# Has nonce expired?
|
||||||
if abs(timestamp - time.time()) > oidnonce.SKEW:
|
if abs(timestamp - time.time()) > oidnonce.SKEW:
|
||||||
return False
|
return False
|
||||||
@@ -634,30 +636,27 @@ class DjangoOpenIDStore(OpenIDStore):
|
|||||||
nonce = OpenIDNonce.objects.get( # pylint: disable=no-member
|
nonce = OpenIDNonce.objects.get( # pylint: disable=no-member
|
||||||
server_url__exact=server_url,
|
server_url__exact=server_url,
|
||||||
timestamp__exact=timestamp,
|
timestamp__exact=timestamp,
|
||||||
salt__exact=salt,
|
salt__exact=salt)
|
||||||
)
|
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
nonce = OpenIDNonce.objects.create( # pylint: disable=no-member
|
nonce = OpenIDNonce.objects.create( # pylint: disable=no-member
|
||||||
server_url=server_url, timestamp=timestamp, salt=salt
|
server_url=server_url, timestamp=timestamp, salt=salt)
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
nonce.delete()
|
nonce.delete()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cleanupNonces(): # pragma: no cover
|
def cleanupNonces(): # pragma: no cover
|
||||||
"""
|
'''
|
||||||
Helper method to cleanup nonces
|
Helper method to cleanup nonces
|
||||||
"""
|
'''
|
||||||
timestamp = int(time.time()) - oidnonce.SKEW
|
timestamp = int(time.time()) - oidnonce.SKEW
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete()
|
OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cleanupAssociations(): # pragma: no cover
|
def cleanupAssociations(): # pragma: no cover
|
||||||
"""
|
'''
|
||||||
Helper method to cleanup associations
|
Helper method to cleanup associations
|
||||||
"""
|
'''
|
||||||
OpenIDAssociation.objects.extra( # pylint: disable=no-member
|
OpenIDAssociation.objects.extra( # pylint: disable=no-member
|
||||||
where=["issued + lifetimeint < (%s)" % time.time()]
|
where=['issued + lifetimeint < (%s)' % time.time()]).delete()
|
||||||
).delete()
|
|
||||||
|
|||||||
@@ -1,122 +1,84 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Reading libravatar export
|
Reading libravatar export
|
||||||
"""
|
'''
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import os
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import gzip
|
import gzip
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
import base64
|
import base64
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import django
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.append(
|
|
||||||
os.path.join(
|
|
||||||
os.path.dirname(__file__),
|
|
||||||
"..",
|
|
||||||
"..",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
SCHEMAROOT = 'https://www.libravatar.org/schemas/export/0.2'
|
||||||
django.setup()
|
|
||||||
|
|
||||||
# pylint: disable=wrong-import-position
|
|
||||||
from ivatar.settings import SCHEMAROOT
|
|
||||||
|
|
||||||
|
|
||||||
def read_gzdata(gzdata=None):
|
def read_gzdata(gzdata=None):
|
||||||
"""
|
'''
|
||||||
Read gzipped data file
|
Read gzipped data file
|
||||||
"""
|
'''
|
||||||
emails = [] # pylint: disable=invalid-name
|
emails = [] # pylint: disable=invalid-name
|
||||||
openids = [] # pylint: disable=invalid-name
|
openids = [] # pylint: disable=invalid-name
|
||||||
photos = [] # pylint: disable=invalid-name
|
photos = [] # pylint: disable=invalid-name
|
||||||
username = None # pylint: disable=invalid-name
|
username = None # pylint: disable=invalid-name
|
||||||
password = None # pylint: disable=invalid-name
|
password = None # pylint: disable=invalid-name
|
||||||
|
|
||||||
if not gzdata:
|
if not gzdata:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
fh = gzip.open(BytesIO(gzdata), "rb") # pylint: disable=invalid-name
|
fh = gzip.open(BytesIO(gzdata), 'rb') # pylint: disable=invalid-name
|
||||||
content = fh.read()
|
content = fh.read()
|
||||||
fh.close()
|
fh.close()
|
||||||
root = xml.etree.ElementTree.fromstring(content)
|
root = xml.etree.ElementTree.fromstring(content)
|
||||||
if not root.tag == "{%s}user" % SCHEMAROOT:
|
if not root.tag == '{%s}user' % SCHEMAROOT:
|
||||||
print("Unknown export format: %s" % root.tag)
|
print('Unknown export format: %s' % root.tag)
|
||||||
exit(-1)
|
exit(-1)
|
||||||
|
|
||||||
# Username
|
# Username
|
||||||
for item in root.findall("{%s}account" % SCHEMAROOT)[0].items():
|
for item in root.findall('{%s}account' % SCHEMAROOT)[0].items():
|
||||||
if item[0] == "username":
|
if item[0] == 'username':
|
||||||
username = item[1]
|
username = item[1]
|
||||||
if item[0] == "password":
|
if item[0] == 'password':
|
||||||
password = item[1]
|
password = item[1]
|
||||||
|
|
||||||
# Emails
|
# Emails
|
||||||
for email in root.findall("{%s}emails" % SCHEMAROOT)[0]:
|
for email in root.findall('{%s}emails' % SCHEMAROOT)[0]:
|
||||||
if email.tag == "{%s}email" % SCHEMAROOT:
|
if email.tag == '{%s}email' % SCHEMAROOT:
|
||||||
emails.append({"email": email.text, "photo_id": email.attrib["photo_id"]})
|
emails.append({'email': email.text, 'photo_id': email.attrib['photo_id']})
|
||||||
|
|
||||||
# OpenIDs
|
# OpenIDs
|
||||||
for openid in root.findall("{%s}openids" % SCHEMAROOT)[0]:
|
for openid in root.findall('{%s}openids' % SCHEMAROOT)[0]:
|
||||||
if openid.tag == "{%s}openid" % SCHEMAROOT:
|
if openid.tag == '{%s}openid' % SCHEMAROOT:
|
||||||
openids.append(
|
openids.append({'openid': openid.text, 'photo_id': openid.attrib['photo_id']})
|
||||||
{"openid": openid.text, "photo_id": openid.attrib["photo_id"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Photos
|
# Photos
|
||||||
for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]:
|
for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]:
|
||||||
if photo.tag == "{%s}photo" % SCHEMAROOT:
|
if photo.tag == '{%s}photo' % SCHEMAROOT:
|
||||||
try:
|
try:
|
||||||
# Safty measures to make sure we do not try to parse
|
data = base64.decodebytes(bytes(photo.text, 'utf-8'))
|
||||||
# a binary encoded string
|
|
||||||
photo.text = photo.text.strip("'")
|
|
||||||
photo.text = photo.text.strip("\\n")
|
|
||||||
photo.text = photo.text.lstrip("b'")
|
|
||||||
data = base64.decodebytes(bytes(photo.text, "utf-8"))
|
|
||||||
except binascii.Error as exc:
|
except binascii.Error as exc:
|
||||||
print(
|
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
|
||||||
"Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s"
|
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
|
||||||
% (
|
|
||||||
photo.attrib["encoding"],
|
|
||||||
photo.attrib["format"],
|
|
||||||
photo.attrib["id"],
|
|
||||||
exc,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
Image.open(BytesIO(data))
|
Image.open(BytesIO(data))
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
print(
|
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
|
||||||
"Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s"
|
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
|
||||||
% (
|
|
||||||
photo.attrib["encoding"],
|
|
||||||
photo.attrib["format"],
|
|
||||||
photo.attrib["id"],
|
|
||||||
exc,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
# If it is a working image, we can use it
|
# If it is a working image, we can use it
|
||||||
photo.text.replace("\n", "")
|
photo.text.replace('\n', '')
|
||||||
photos.append(
|
photos.append({
|
||||||
{
|
'data': photo.text,
|
||||||
"data": photo.text,
|
'format': photo.attrib['format'],
|
||||||
"format": photo.attrib["format"],
|
'id': photo.attrib['id'],
|
||||||
"id": photo.attrib["id"],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"emails": emails,
|
'emails': emails,
|
||||||
"openids": openids,
|
'openids': openids,
|
||||||
"photos": photos,
|
'photos': photos,
|
||||||
"username": username,
|
'username': username,
|
||||||
"password": password,
|
'password': password,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}
|
|||||||
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
|
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
|
||||||
{% for email in emails %}
|
{% for email in emails %}
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email.email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email.email }}</label>
|
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email }}</label>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -7,56 +7,74 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans 'Account settings' %}</h1>
|
<h1>{% trans 'Account settings' %}</h1>
|
||||||
|
|
||||||
<label for="id_username">{% trans 'Username' %}:</label>
|
<div class="form-group">
|
||||||
<input type="text" name="username" class="form-control" id="id_username" disabled value="{{ user.username }}" style="max-width:600px;">
|
<label for="id_email">{% trans 'Your email' %}:</label>
|
||||||
<form action="{% url 'user_preference' %}" method="post">{% csrf_token %}
|
<input type="text" name="email" disabled class="form-control" value="{{ user.email }}" id="id_email" style="max-width:600px;">
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="id_first_name">{% trans 'Firstname' %}:</label>
|
|
||||||
<input type="text" name="first_name" class="form-control" id="id_first_name" value="{{ user.first_name }}" style="max-width:600px;">
|
|
||||||
<label for="id_last_name">{% trans 'Lastname' %}:</label>
|
|
||||||
<input type="text" name="last_name" class="form-control" id="id_last_name" value="{{ user.last_name }}" style="max-width:600px;">
|
|
||||||
|
|
||||||
<label for="id_email">{% trans 'E-mail address' %}:</label>
|
|
||||||
<select name="email" class="form-control" id="id_email" style="max-width:600px;">
|
|
||||||
<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 }}"/>
|
|
||||||
<button type="submit" class="button">{% trans 'Save' %}</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- TODO: Language stuff not yet fully implemented; Esp. translations are only half-way there
|
<!-- TODO: Language stuff not yet fully implemented; Esp. translations are only half-way there
|
||||||
|
|
||||||
<h2>{% trans 'Language' %}</h2>
|
<h2>{% trans 'Language' %}</h2>
|
||||||
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
|
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
{% get_available_languages as LANGUAGES %}
|
{% get_available_languages as LANGUAGES %}
|
||||||
{% get_language_info_list for LANGUAGES as languages %}
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
{% for language in languages %}
|
{% for language in languages %}
|
||||||
<div class="radio">
|
<div class="radio">
|
||||||
<input type="radio" name="language" value="{{ language.code }}" id="language-{{ language.code }}"
|
<input type="radio" name="language" value="{{ language.code }}" id="language-{{ language.code }}" {% if language.code == LANGUAGE_CODE %}checked{% endif %}>
|
||||||
{% if language.code == LANGUAGE_CODE %}checked{% endif %}>
|
<label for="language-{{ language.code }}">{{ language.name_local }}</label>
|
||||||
<label for="language-{{ language.code }}">{{ language.name_local }}</label>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
<br/>
|
||||||
<br/>
|
<button type="submit" class="button">{% trans 'Save' %}</button>
|
||||||
<button type="submit" class="button">{% trans 'Save' %}</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<div style="height:100px"></div>
|
{% 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>
|
||||||
|
|
||||||
<!-- <p><a href="{% url 'export' %}" class="button">{% trans 'Export your data' %}</a></p> -->
|
<!-- <p><a href="{% url 'export' %}" class="button">{% trans 'Export your data' %}</a></p> -->
|
||||||
|
|
||||||
<!-- TODO: Better coloring of the button -->
|
<p><a href="{% url 'delete' %}" class="button">{% trans 'Permanently delete your account' %}</a></p>
|
||||||
<p><a href="{% url 'delete' %}" class="button" style="background:red; color:white;">{% trans 'Permanently delete your account' %}</a></p>
|
|
||||||
<div style="height:2rem"></div>
|
<div style="height:2rem"></div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -6,247 +6,159 @@
|
|||||||
{% block title %}{% trans 'Your Profile' %}{% endblock title %}
|
{% block title %}{% trans 'Your Profile' %}{% endblock title %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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>
|
<h1>
|
||||||
{% trans 'Your Profile' %} -
|
{% trans 'Your Profile' %} -
|
||||||
{% if user.first_name and user.last_name %}
|
{% if user.first_name and user.last_name %}
|
||||||
{{ user.first_name }} {{ user.last_name }}
|
{{ user.first_name }} {{ user.last_name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.action-item:hover span {
|
.action-item:hover span {
|
||||||
display: inline !important;
|
display: inline !important;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 320px) {
|
||||||
|
.action-item, .btn {
|
||||||
|
padding-left: 0.3em;
|
||||||
|
padding-right: 0.3em;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 320px) {
|
}
|
||||||
.action-item, .btn {
|
.thumbnail {
|
||||||
padding-left: 0.3em;
|
max-width:80px;
|
||||||
padding-right: 0.3em;
|
max-height:80px;
|
||||||
}
|
}
|
||||||
}
|
.nobutton {
|
||||||
.thumbnail {
|
background: none;
|
||||||
max-width:80px;
|
color: inherit;
|
||||||
max-height:80px;
|
border: none;
|
||||||
}
|
padding: 0;
|
||||||
.nobutton {
|
font: inherit;
|
||||||
background: none;
|
cursor: pointer;
|
||||||
color: inherit;
|
outline: inherit;
|
||||||
border: none;
|
}
|
||||||
padding: 0;
|
.button {
|
||||||
font: inherit;
|
margin-bottom: 1.5rem;
|
||||||
cursor: pointer;
|
margin-right: 1rem;
|
||||||
outline: inherit;
|
}
|
||||||
}
|
.container{
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.btn-group{
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
.input-group-addon{
|
||||||
|
width: auto;
|
||||||
|
height: 3rem;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 470px) {
|
||||||
.button {
|
.button {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
.container{
|
.unconfirmed-mail-form{
|
||||||
display: grid;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
.btn-group{
|
.btn-group{
|
||||||
display: inline-flex;
|
display: contents;
|
||||||
}
|
|
||||||
.input-group-addon{
|
|
||||||
width: auto;
|
|
||||||
height: 3rem;
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 470px) {
|
|
||||||
.button {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
.unconfirmed-mail-form{
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.btn-group{
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 470px) {
|
|
||||||
p {
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
h3{
|
|
||||||
line-height: 3.4rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<noscript>
|
|
||||||
<style type="text/css">
|
|
||||||
.profile-container > ul{
|
|
||||||
display:block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
{% if user.confirmedemail_set.count or user.confirmedopenid_set.count %}
|
{% if user.confirmedemail_set.count or user.confirmedopenid_set.count %}
|
||||||
<h3>{% trans 'You have the following confirmed identities:' %}</h3>
|
<h3>{% trans 'You have the following confirmed identities:' %}</h3>
|
||||||
<div class="row profileid">
|
<div class="row">
|
||||||
{% for email in user.confirmedemail_set.all %}
|
{% for email in user.confirmedemail_set.all %}
|
||||||
{% if user.confirmedemail_set.all|length == 1%}
|
<form action="{% url 'remove_confirmed_email' email.id %}" method="post">
|
||||||
<form action="{% url 'remove_confirmed_email' email.id %}" method="post">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<div class="panel" style="width:172px;margin-left:20px;float:left">
|
||||||
<div id="email-conf-{{ forloop.counter }}" class="profile-container active">
|
<div class="panel-heading" style="padding-right:0">
|
||||||
<img title="{% trans 'Access count' %}: {{ email.access_count }}" src="{% if email.photo %}{% url 'raw_image' email.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
|
<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>
|
||||||
<h3 class="panel-title email-profile" title="{{ email.email }}">
|
<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 }}
|
{{ email.email|truncatechars:12 }}</h3>
|
||||||
</h3>
|
</div>
|
||||||
<ul>
|
<div class="panel-body" style="height:130px">
|
||||||
<li>
|
<center>
|
||||||
<a href="{% url 'assign_photo_email' email.id %}">
|
<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 %}">
|
||||||
Change Profile Picture
|
</center>
|
||||||
</a>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<li class="email-delete">
|
</form>
|
||||||
<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 %}{% 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>
|
|
||||||
<div id="id-conf-{{ forloop.counter }}" class="profile-container active">
|
|
||||||
<img title="{% trans 'Access count' %}: {{ openid.access_count }}" src="{% if openid.photo %}{% url 'raw_image' openid.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
|
|
||||||
<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 %}
|
|
||||||
|
|
||||||
{% 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>
|
|
||||||
{# 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>
|
|
||||||
</form>
|
|
||||||
{# TODO: (expires in xx hours) #}
|
|
||||||
{% endfor %}
|
|
||||||
{% 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-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 class="panel-body" style="height:130px">
|
|
||||||
<img title="{% trans 'Access count' %}: {{ photo.access_count }}" style="max-height:100px;max-width:100px" src="{% url 'raw_image' photo.id %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
{% for openid in user.confirmedopenid_set.all %}
|
||||||
{% endif %}
|
<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>
|
||||||
|
</form>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if not max_photos %}
|
{% if user.unconfirmedemail_set.count or user.unconfirmedopenid_set.count %}
|
||||||
<p>
|
<h3>{% trans 'You have the following unconfirmed email addresses and OpenIDs:' %}</h3>
|
||||||
<a href="{% url 'upload_photo' %}" class="button">{% trans 'Upload a new photo' %}</a>
|
{% for email in user.unconfirmedemail_set.all %}
|
||||||
<a href="{% url 'import_photo' %}" class="button">{% trans 'Import photo from other services' %}</a>
|
<form class="unconfirmed-mail-form" action="{% url 'remove_unconfirmed_email' email.id %}" method="post">
|
||||||
</p>
|
{% csrf_token %}
|
||||||
{% else %}
|
<div class="btn-group form-group" role="group">
|
||||||
{% trans "You've reached the maximum number of allowed images!" %}<br/>
|
<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>
|
||||||
{% trans "No further images can be uploaded." %}
|
<a href="{% url 'resend_confirmation_mail' email.pk %}" class="button"><i class="fa fa-envelope"></i></a>
|
||||||
{% endif %}
|
<span class="input-group-addon" style="width: auto;">{{ email.email }}</span>
|
||||||
<div style="height:40px"></div>
|
</div>
|
||||||
{% endblock content %}
|
</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>
|
||||||
|
</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">
|
||||||
|
{% for photo in user.photo_set.all %}
|
||||||
|
<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 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 %}
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,155 +1,121 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
URLs for ivatar.ivataraccount
|
URLs for ivatar.ivataraccount
|
||||||
"""
|
'''
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from django.views.generic import TemplateView
|
||||||
from django.contrib.auth.views import LogoutView
|
from django.contrib.auth.views import LogoutView
|
||||||
from django.contrib.auth.views import (
|
from django.contrib.auth.views import PasswordResetDoneView,\
|
||||||
PasswordResetDoneView,
|
PasswordResetConfirmView, PasswordResetCompleteView
|
||||||
PasswordResetConfirmView,
|
|
||||||
PasswordResetCompleteView,
|
|
||||||
)
|
|
||||||
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
|
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
from .views import ProfileView, PasswordResetView
|
from . views import ProfileView, PasswordResetView
|
||||||
from .views import CreateView, PasswordSetView, AddEmailView
|
from . views import CreateView, PasswordSetView, AddEmailView
|
||||||
from .views import RemoveUnconfirmedEmailView, ConfirmEmailView
|
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView
|
||||||
from .views import RemoveConfirmedEmailView, AssignPhotoEmailView
|
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
|
||||||
from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
|
from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
|
||||||
from .views import ImportPhotoView, RawImageView, DeletePhotoView
|
from . views import ImportPhotoView, RawImageView, DeletePhotoView
|
||||||
from .views import UploadPhotoView, AssignPhotoOpenIDView
|
from . views import UploadPhotoView, AssignPhotoOpenIDView
|
||||||
from .views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
|
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
|
||||||
from .views import CropPhotoView
|
from . views import CropPhotoView
|
||||||
from .views import UserPreferenceView, UploadLibravatarExportView
|
from . views import UserPreferenceView, UploadLibravatarExportView
|
||||||
from .views import ResendConfirmationMailView
|
from . views import ResendConfirmationMailView
|
||||||
from .views import IvatarLoginView
|
from . views import IvatarLoginView
|
||||||
from .views import DeleteAccountView
|
from . views import DeleteAccountView
|
||||||
from .views import ExportView
|
from . views import GenerateAPIKey
|
||||||
|
|
||||||
# Define URL patterns, self documenting
|
# Define URL patterns, self documenting
|
||||||
# To see the fancy, colorful evaluation of these use:
|
# To see the fancy, colorful evaluation of these use:
|
||||||
# ./manager show_urls
|
# ./manager show_urls
|
||||||
urlpatterns = [ # pylint: disable=invalid-name
|
urlpatterns = [ # pylint: disable=invalid-name
|
||||||
path("new/", CreateView.as_view(), name="new_account"),
|
path('new/', CreateView.as_view(), name='new_account'),
|
||||||
path("login/", IvatarLoginView.as_view(), name="login"),
|
path('login/', IvatarLoginView.as_view(), name='login'),
|
||||||
path("logout/", LogoutView.as_view(next_page="/"), name="logout"),
|
|
||||||
path(
|
path(
|
||||||
"password_change/",
|
'logout/', LogoutView.as_view(next_page='/'),
|
||||||
PasswordChangeView.as_view(template_name="password_change.html"),
|
name='logout'),
|
||||||
name="password_change",
|
|
||||||
),
|
path('password_change/',
|
||||||
path(
|
PasswordChangeView.as_view(template_name='password_change.html'),
|
||||||
"password_change/done/",
|
name='password_change'),
|
||||||
PasswordChangeDoneView.as_view(template_name="password_change_done.html"),
|
path('password_change/done/',
|
||||||
name="password_change_done",
|
PasswordChangeDoneView.as_view(template_name='password_change_done.html'),
|
||||||
),
|
name='password_change_done'),
|
||||||
path(
|
|
||||||
"password_reset/",
|
path('password_reset/',
|
||||||
PasswordResetView.as_view(template_name="password_reset.html"),
|
PasswordResetView.as_view(template_name='password_reset.html'),
|
||||||
name="password_reset",
|
name='password_reset'),
|
||||||
),
|
path('password_reset/done/',
|
||||||
path(
|
PasswordResetDoneView.as_view(
|
||||||
"password_reset/done/",
|
template_name='password_reset_submitted.html'),
|
||||||
PasswordResetDoneView.as_view(template_name="password_reset_submitted.html"),
|
name='password_reset_done'),
|
||||||
name="password_reset_done",
|
path('reset/<uidb64>/<token>/',
|
||||||
),
|
PasswordResetConfirmView.as_view(
|
||||||
path(
|
template_name='password_change.html'),
|
||||||
"reset/<uidb64>/<token>/",
|
name='password_reset_confirm'),
|
||||||
PasswordResetConfirmView.as_view(template_name="password_change.html"),
|
path('reset/done/',
|
||||||
name="password_reset_confirm",
|
PasswordResetCompleteView.as_view(
|
||||||
),
|
template_name='password_change_done.html'),
|
||||||
path(
|
name='password_reset_complete'),
|
||||||
"reset/done/",
|
|
||||||
PasswordResetCompleteView.as_view(template_name="password_change_done.html"),
|
path('export/', login_required(
|
||||||
name="password_reset_complete",
|
TemplateView.as_view(template_name='export.html')
|
||||||
),
|
), name='export'),
|
||||||
path(
|
path('delete/', DeleteAccountView.as_view(), name='delete'),
|
||||||
"export/",
|
path('profile/', ProfileView.as_view(), name='profile'),
|
||||||
ExportView.as_view(),
|
url('profile/(?P<profile_username>.+)', ProfileView.as_view(), name='profile_with_profile_username'),
|
||||||
name="export",
|
path('add_email/', AddEmailView.as_view(), name='add_email'),
|
||||||
),
|
path('add_openid/', AddOpenIDView.as_view(), name='add_openid'),
|
||||||
path("delete/", DeleteAccountView.as_view(), name="delete"),
|
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'),
|
||||||
path("profile/", ProfileView.as_view(), name="profile"),
|
path('password_set/', PasswordSetView.as_view(), name='password_set'),
|
||||||
url(
|
url(
|
||||||
"profile/(?P<profile_username>.+)",
|
r'remove_unconfirmed_openid/(?P<openid_id>\d+)',
|
||||||
ProfileView.as_view(),
|
|
||||||
name="profile_with_profile_username",
|
|
||||||
),
|
|
||||||
path("add_email/", AddEmailView.as_view(), name="add_email"),
|
|
||||||
path("add_openid/", AddOpenIDView.as_view(), name="add_openid"),
|
|
||||||
path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"),
|
|
||||||
path("password_set/", PasswordSetView.as_view(), name="password_set"),
|
|
||||||
url(
|
|
||||||
r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
|
|
||||||
RemoveUnconfirmedOpenIDView.as_view(),
|
RemoveUnconfirmedOpenIDView.as_view(),
|
||||||
name="remove_unconfirmed_openid",
|
name='remove_unconfirmed_openid'),
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"remove_confirmed_openid/(?P<openid_id>\d+)",
|
r'remove_confirmed_openid/(?P<openid_id>\d+)',
|
||||||
RemoveConfirmedOpenIDView.as_view(),
|
RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'),
|
||||||
name="remove_confirmed_openid",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"openid_redirection/(?P<openid_id>\d+)",
|
r'openid_redirection/(?P<openid_id>\d+)',
|
||||||
RedirectOpenIDView.as_view(),
|
RedirectOpenIDView.as_view(), name='openid_redirection'),
|
||||||
name="openid_redirection",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"confirm_openid/(?P<openid_id>\w+)",
|
r'confirm_openid/(?P<openid_id>\w+)',
|
||||||
ConfirmOpenIDView.as_view(),
|
ConfirmOpenIDView.as_view(), name='confirm_openid'),
|
||||||
name="confirm_openid",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"confirm_email/(?P<verification_key>\w+)",
|
r'confirm_email/(?P<verification_key>\w+)',
|
||||||
ConfirmEmailView.as_view(),
|
ConfirmEmailView.as_view(), name='confirm_email'),
|
||||||
name="confirm_email",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"remove_unconfirmed_email/(?P<email_id>\d+)",
|
r'remove_unconfirmed_email/(?P<email_id>\d+)',
|
||||||
RemoveUnconfirmedEmailView.as_view(),
|
RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'),
|
||||||
name="remove_unconfirmed_email",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"remove_confirmed_email/(?P<email_id>\d+)",
|
r'remove_confirmed_email/(?P<email_id>\d+)',
|
||||||
RemoveConfirmedEmailView.as_view(),
|
RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'),
|
||||||
name="remove_confirmed_email",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"assign_photo_email/(?P<email_id>\d+)",
|
r'assign_photo_email/(?P<email_id>\d+)',
|
||||||
AssignPhotoEmailView.as_view(),
|
AssignPhotoEmailView.as_view(), name='assign_photo_email'),
|
||||||
name="assign_photo_email",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"assign_photo_openid/(?P<openid_id>\d+)",
|
r'assign_photo_openid/(?P<openid_id>\d+)',
|
||||||
AssignPhotoOpenIDView.as_view(),
|
AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'),
|
||||||
name="assign_photo_openid",
|
|
||||||
),
|
|
||||||
url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
|
|
||||||
url(
|
url(
|
||||||
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
|
r'import_photo/$',
|
||||||
ImportPhotoView.as_view(),
|
ImportPhotoView.as_view(), name='import_photo'),
|
||||||
name="import_photo",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"import_photo/(?P<email_id>\d+)",
|
r'import_photo/(?P<email_addr>[\w.]+@[\w.]+.[\w.]+)',
|
||||||
ImportPhotoView.as_view(),
|
ImportPhotoView.as_view(), name='import_photo'),
|
||||||
name="import_photo",
|
|
||||||
),
|
|
||||||
url(r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"),
|
|
||||||
url(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
|
|
||||||
url(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
|
|
||||||
url(r"pref/$", UserPreferenceView.as_view(), name="user_preference"),
|
|
||||||
url(r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"),
|
|
||||||
url(
|
url(
|
||||||
r"upload_export/(?P<save>save)$",
|
r'import_photo/(?P<email_id>\d+)',
|
||||||
UploadLibravatarExportView.as_view(),
|
ImportPhotoView.as_view(), name='import_photo'),
|
||||||
name="upload_export",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"resend_confirmation_mail/(?P<email_id>\d+)",
|
r'delete_photo/(?P<pk>\d+)',
|
||||||
ResendConfirmationMailView.as_view(),
|
DeletePhotoView.as_view(), name='delete_photo'),
|
||||||
name="resend_confirmation_mail",
|
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'),
|
||||||
]
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,18 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
"""
|
||||||
Middleware classes
|
Middleware classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
||||||
|
class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods
|
||||||
class MultipleProxyMiddleware(
|
|
||||||
MiddlewareMixin
|
|
||||||
): # pylint: disable=too-few-public-methods
|
|
||||||
"""
|
"""
|
||||||
Middleware to rewrite proxy headers for deployments
|
Middleware to rewrite proxy headers for deployments
|
||||||
with multiple proxies
|
with multiple proxies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def process_request(self, request): # pylint: disable=no-self-use
|
def process_request(self, request):
|
||||||
"""
|
"""
|
||||||
Rewrites the proxy headers so that forwarded server is
|
Rewrites the proxy headers so that forwarded server is
|
||||||
used if available.
|
used if available.
|
||||||
"""
|
"""
|
||||||
if "HTTP_X_FORWARDED_SERVER" in request.META:
|
if 'HTTP_X_FORWARDED_SERVER' in request.META:
|
||||||
request.META["HTTP_X_FORWARDED_HOST"] = request.META[
|
request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER']
|
||||||
"HTTP_X_FORWARDED_SERVER"
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
"""
|
||||||
Django settings for ivatar project.
|
Django settings for ivatar project.
|
||||||
"""
|
"""
|
||||||
@@ -7,7 +6,7 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
log_level = logging.DEBUG # pylint: disable=invalid-name
|
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)
|
logger.setLevel(log_level)
|
||||||
|
|
||||||
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__))
|
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||||
@@ -15,7 +14,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||||||
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# 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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
@@ -26,52 +25,52 @@ ALLOWED_HOSTS = []
|
|||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
'django.contrib.admin',
|
||||||
"django.contrib.auth",
|
'django.contrib.auth',
|
||||||
"django.contrib.contenttypes",
|
'django.contrib.contenttypes',
|
||||||
"django.contrib.sessions",
|
'django.contrib.sessions',
|
||||||
"django.contrib.messages",
|
'django.contrib.messages',
|
||||||
"django.contrib.staticfiles",
|
'django.contrib.staticfiles',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
'django.middleware.security.SecurityMiddleware',
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
"django.middleware.common.CommonMiddleware",
|
'django.middleware.common.CommonMiddleware',
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "ivatar.urls"
|
ROOT_URLCONF = 'ivatar.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
"DIRS": [],
|
'DIRS': [],
|
||||||
"APP_DIRS": True,
|
'APP_DIRS': True,
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"context_processors": [
|
'context_processors': [
|
||||||
"django.template.context_processors.debug",
|
'django.template.context_processors.debug',
|
||||||
"django.template.context_processors.request",
|
'django.template.context_processors.request',
|
||||||
"django.contrib.auth.context_processors.auth",
|
'django.contrib.auth.context_processors.auth',
|
||||||
"django.contrib.messages.context_processors.messages",
|
'django.contrib.messages.context_processors.messages',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "ivatar.wsgi.application"
|
WSGI_APPLICATION = 'ivatar.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
'default': {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,16 +80,16 @@ DATABASES = {
|
|||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
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
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -98,9 +97,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.0/topics/i18n/
|
# 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
|
USE_I18N = True
|
||||||
|
|
||||||
@@ -110,10 +109,13 @@ USE_TZ = True
|
|||||||
|
|
||||||
|
|
||||||
# Static files configuration (esp. req. during dev.)
|
# Static files configuration (esp. req. during dev.)
|
||||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
PROJECT_ROOT = os.path.abspath(
|
||||||
STATIC_URL = "/static/"
|
os.path.join(
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
os.path.dirname(__file__),
|
||||||
|
os.pardir
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
)
|
||||||
|
)
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
|
|
||||||
from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import
|
from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import
|
||||||
|
|||||||
@@ -82,17 +82,14 @@ h2 {
|
|||||||
letter-spacing: 0.05rem;
|
letter-spacing: 0.05rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-family: 'Lato', sans-serif;
|
font-family: 'Lato', sans-serif;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
color: #545454;
|
color: #545454;
|
||||||
}
|
}
|
||||||
@media only screen and (max-width: 470px) {
|
|
||||||
h3{
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h4 {
|
h4 {
|
||||||
font-family: 'Lato', sans-serif;
|
font-family: 'Lato', sans-serif;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
@@ -407,6 +404,7 @@ transition: all 0.3s;
|
|||||||
top: 26rem;
|
top: 26rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 620px) {
|
@media only screen and (max-width: 620px) {
|
||||||
#page .container #home-form {
|
#page .container #home-form {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
@@ -672,7 +670,7 @@ color:#335ECF;
|
|||||||
}
|
}
|
||||||
footer {
|
footer {
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
position: relative;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
footer .container {
|
footer .container {
|
||||||
@@ -730,138 +728,3 @@ color:#335ECF;
|
|||||||
margin-top: 11rem !important;
|
margin-top: 11rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.profile-container{
|
|
||||||
border-top: solid 5px #2F95EDB3;
|
|
||||||
display: grid;
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.profile-container img{
|
|
||||||
margin: 0.5em;
|
|
||||||
}
|
|
||||||
.panel-body.profile > div, .panel-body.profile > img {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.panel-heading.profile{
|
|
||||||
background: none;
|
|
||||||
border-top-left-radius: unset;
|
|
||||||
border-top-right-radius: unset;
|
|
||||||
}
|
|
||||||
.profile-container > h3{
|
|
||||||
color: #353535;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.profile-container > ul > li > a, .profile-container button{
|
|
||||||
color: #353535;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.profile-container.active{
|
|
||||||
border-top: solid 5px #335ECF;
|
|
||||||
}
|
|
||||||
.profile-container ul > li > button:hover, .profile-container ul > li > a:hover{
|
|
||||||
color: #335ECF;
|
|
||||||
}
|
|
||||||
.email-profile { grid-area: email; }
|
|
||||||
.profile-container{
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
.profile-container > img {
|
|
||||||
grid-area: img;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.profile-container > ul {
|
|
||||||
grid-area: list;
|
|
||||||
list-style-type: none;
|
|
||||||
padding:0;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.profile-container > ul > li{
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 420px) {
|
|
||||||
.profile-container > ul > li {
|
|
||||||
padding-top: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.profile-container > ul > li > a:hover{
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.profile-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas:
|
|
||||||
'img email email email email email'
|
|
||||||
'img list list list list list';
|
|
||||||
grid-gap: 0;
|
|
||||||
grid-template-columns: 20% 80%;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 700px) {
|
|
||||||
.profile-container {
|
|
||||||
grid-template-columns: 40% 60%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.profile-container > div, profile-container > img {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.profile-container.active > img{
|
|
||||||
max-height:120px;
|
|
||||||
max-width:120px;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 420px) {
|
|
||||||
.profile-container.active > img {
|
|
||||||
max-height:80px;
|
|
||||||
max-width:80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.profile-container > ul{
|
|
||||||
display:none
|
|
||||||
}
|
|
||||||
.profile-container.active > ul{
|
|
||||||
display:block;
|
|
||||||
}
|
|
||||||
.profile-container > img{
|
|
||||||
max-height:80px;
|
|
||||||
max-width:80px;
|
|
||||||
}
|
|
||||||
h3.panel-title{
|
|
||||||
margin-top: unset;
|
|
||||||
}
|
|
||||||
.profile-container > h3{
|
|
||||||
padding-top: 26px;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 470px) {
|
|
||||||
.profile-container > h3{
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.profile-container:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.profile-container.active > h3{
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
.profile-container.active{
|
|
||||||
cursor: pointer;
|
|
||||||
background: #dcdcdcb5;
|
|
||||||
}
|
|
||||||
.profile-container.active:hover{
|
|
||||||
cursor:auto;
|
|
||||||
}
|
|
||||||
@media only screen and (min-width: 768px) {
|
|
||||||
.profile-container:hover ul {
|
|
||||||
display:block !important;
|
|
||||||
}
|
|
||||||
.profile-container:hover {
|
|
||||||
background: #dcdcdcb5;
|
|
||||||
}
|
|
||||||
.profile-container:hover img{
|
|
||||||
max-height:120px;
|
|
||||||
max-width:120px;
|
|
||||||
}
|
|
||||||
.profile-container:hover h3{
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert-success {
|
|
||||||
color: #353535;
|
|
||||||
background-color: #3582d71f;
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
@@ -1,47 +0,0 @@
|
|||||||
# -*- 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("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,45 +1,57 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||||
"""
|
'''
|
||||||
# pylint: disable=too-many-lines
|
# pylint: disable=too-many-lines
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
from io import BytesIO
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import django
|
import django
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from ivatar.utils import random_string
|
from libravatar import libravatar_url
|
||||||
|
|
||||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
from PIL import Image
|
||||||
|
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||||
django.setup()
|
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
|
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||||
"""
|
'''
|
||||||
Main test class
|
Main test class
|
||||||
"""
|
'''
|
||||||
|
|
||||||
client = Client()
|
client = Client()
|
||||||
user = None
|
user = None
|
||||||
username = random_string()
|
username = random_string()
|
||||||
password = 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
|
# 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):
|
def login(self):
|
||||||
"""
|
'''
|
||||||
Login as user
|
Login as user
|
||||||
"""
|
'''
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
'''
|
||||||
Prepare for tests.
|
Prepare for tests.
|
||||||
- Create user
|
- Create user
|
||||||
"""
|
'''
|
||||||
self.user = User.objects.create_user(
|
self.user = User.objects.create_user(
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
@@ -49,19 +61,20 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
Test contact page
|
Test contact page
|
||||||
"""
|
"""
|
||||||
response = self.client.get(reverse("contact"))
|
response = self.client.get(reverse('contact'))
|
||||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||||
|
|
||||||
def test_description_page(self):
|
def test_description_page(self):
|
||||||
"""
|
"""
|
||||||
Test description page
|
Test description page
|
||||||
"""
|
"""
|
||||||
response = self.client.get(reverse("description"))
|
response = self.client.get(reverse('description'))
|
||||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||||
|
|
||||||
def test_security_page(self):
|
def test_security_page(self):
|
||||||
"""
|
"""
|
||||||
Test security page
|
Test security page
|
||||||
"""
|
"""
|
||||||
response = self.client.get(reverse("security"))
|
response = self.client.get(reverse('security'))
|
||||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Test our utils from ivatar.utils
|
Test our utils from ivatar.utils
|
||||||
"""
|
'''
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from ivatar.utils import is_trusted_url, openid_variations
|
from ivatar.utils import openid_variations
|
||||||
|
|
||||||
|
|
||||||
class Tester(TestCase):
|
class Tester(TestCase):
|
||||||
"""
|
'''
|
||||||
Main test class
|
Main test class
|
||||||
"""
|
'''
|
||||||
|
|
||||||
def test_openid_variations(self):
|
def test_openid_variations(self):
|
||||||
"""
|
'''
|
||||||
Test if the OpenID variation "generator" does the correct thing
|
Test if the OpenID variation "generator" does the correct thing
|
||||||
"""
|
'''
|
||||||
openid0 = "http://user.url/"
|
openid0 = 'http://user.url/'
|
||||||
openid1 = "http://user.url"
|
openid1 = 'http://user.url'
|
||||||
openid2 = "https://user.url/"
|
openid2 = 'https://user.url/'
|
||||||
openid3 = "https://user.url"
|
openid3 = 'https://user.url'
|
||||||
|
|
||||||
# First variation
|
# First variation
|
||||||
self.assertEqual(openid_variations(openid0)[0], openid0)
|
self.assertEqual(openid_variations(openid0)[0], openid0)
|
||||||
@@ -45,60 +44,3 @@ class Tester(TestCase):
|
|||||||
self.assertEqual(openid_variations(openid3)[1], openid1)
|
self.assertEqual(openid_variations(openid3)[1], openid1)
|
||||||
self.assertEqual(openid_variations(openid3)[2], openid2)
|
self.assertEqual(openid_variations(openid3)[2], openid2)
|
||||||
self.assertEqual(openid_variations(openid3)[3], openid3)
|
self.assertEqual(openid_variations(openid3)[3], openid3)
|
||||||
|
|
||||||
def test_is_trusted_url(self):
|
|
||||||
test1 = is_trusted_url("https://gravatar.com/avatar/63a75a80e6b1f4adfdb04c1ca02e596c", [
|
|
||||||
{
|
|
||||||
"schemes": [
|
|
||||||
"http",
|
|
||||||
"https"
|
|
||||||
],
|
|
||||||
"host_equals": "gravatar.com",
|
|
||||||
"path_prefix": "/avatar/"
|
|
||||||
}
|
|
||||||
])
|
|
||||||
self.assertTrue(test1)
|
|
||||||
|
|
||||||
test2 = is_trusted_url("https://gravatar.com.example.org/avatar/63a75a80e6b1f4adfdb04c1ca02e596c", [
|
|
||||||
{
|
|
||||||
"schemes": [
|
|
||||||
"http",
|
|
||||||
"https"
|
|
||||||
],
|
|
||||||
"host_suffix": ".gravatar.com",
|
|
||||||
"path_prefix": "/avatar/"
|
|
||||||
}
|
|
||||||
])
|
|
||||||
self.assertFalse(test2)
|
|
||||||
|
|
||||||
# Test against open redirect with valid URL in query params
|
|
||||||
test3 = 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(test3)
|
|
||||||
|
|
||||||
test4 = 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(test4)
|
|
||||||
|
|||||||
@@ -1,45 +1,57 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||||
"""
|
'''
|
||||||
# pylint: disable=too-many-lines
|
# pylint: disable=too-many-lines
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
from io import BytesIO
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
import django
|
import django
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
from django.urls import reverse
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from ivatar.utils import random_string
|
from libravatar import libravatar_url
|
||||||
|
|
||||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
from PIL import Image
|
||||||
|
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||||
django.setup()
|
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
|
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||||
"""
|
'''
|
||||||
Main test class
|
Main test class
|
||||||
"""
|
'''
|
||||||
|
|
||||||
client = Client()
|
client = Client()
|
||||||
user = None
|
user = None
|
||||||
username = random_string()
|
username = random_string()
|
||||||
password = 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
|
# 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):
|
def login(self):
|
||||||
"""
|
'''
|
||||||
Login as user
|
Login as user
|
||||||
"""
|
'''
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
'''
|
||||||
Prepare for tests.
|
Prepare for tests.
|
||||||
- Create user
|
- Create user
|
||||||
"""
|
'''
|
||||||
self.user = User.objects.create_user(
|
self.user = User.objects.create_user(
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
@@ -49,25 +61,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
Test incorrect digest
|
Test incorrect digest
|
||||||
"""
|
"""
|
||||||
response = self.client.get("/avatar/%s" % "x" * 65, follow=True)
|
response = self.client.get('/avatar/%s' % 'x'*65, follow=True)
|
||||||
self.assertRedirects(
|
self.assertRedirects(
|
||||||
response=response,
|
response=response,
|
||||||
expected_url="/static/img/deadbeef.png",
|
expected_url='/static/img/deadbeef.png',
|
||||||
msg_prefix="Why does an invalid hash not redirect to deadbeef?",
|
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")
|
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Unit tests for WSGI
|
Unit tests for WSGI
|
||||||
"""
|
'''
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import django
|
import django
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
|
|
||||||
class TestCase(unittest.TestCase):
|
class TestCase(unittest.TestCase):
|
||||||
"""
|
'''
|
||||||
Simple testcase to see if WSGI loads correctly
|
Simple testcase to see if WSGI loads correctly
|
||||||
"""
|
'''
|
||||||
|
|
||||||
def test_run_wsgi(self):
|
def test_run_wsgi(self):
|
||||||
"""
|
'''
|
||||||
Run wsgi import
|
Run wsgi import
|
||||||
"""
|
'''
|
||||||
import ivatar.wsgi # pylint: disable=import-outside-toplevel
|
import ivatar.wsgi
|
||||||
|
self.assertEqual(ivatar.wsgi.application.__class__,
|
||||||
self.assertEqual(
|
django.core.handlers.wsgi.WSGIHandler)
|
||||||
ivatar.wsgi.application.__class__, django.core.handlers.wsgi.WSGIHandler
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Classes for our ivatar.tools.forms
|
Classes for our ivatar.tools.forms
|
||||||
"""
|
'''
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms.utils import ErrorList
|
from django.forms.utils import ErrorList
|
||||||
|
|
||||||
@@ -13,40 +12,45 @@ from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL
|
|||||||
|
|
||||||
|
|
||||||
class CheckDomainForm(forms.Form):
|
class CheckDomainForm(forms.Form):
|
||||||
"""
|
'''
|
||||||
Form handling domain check
|
Form handling domain check
|
||||||
"""
|
'''
|
||||||
|
|
||||||
domain = forms.CharField(
|
domain = forms.CharField(
|
||||||
label=_("Domain"),
|
label=_('Domain'),
|
||||||
required=True,
|
required=True,
|
||||||
error_messages={"required": _("Cannot check without a domain name.")},
|
error_messages={
|
||||||
|
'required':
|
||||||
|
_('Cannot check without a domain name.')
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CheckForm(forms.Form):
|
class CheckForm(forms.Form):
|
||||||
"""
|
'''
|
||||||
Form handling check
|
Form handling check
|
||||||
"""
|
'''
|
||||||
|
|
||||||
mail = forms.EmailField(
|
mail = forms.EmailField(
|
||||||
label=_("E-Mail"),
|
label=_('E-Mail'),
|
||||||
required=False,
|
required=False,
|
||||||
min_length=MIN_LENGTH_EMAIL,
|
min_length=MIN_LENGTH_EMAIL,
|
||||||
max_length=MAX_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(
|
openid = forms.CharField(
|
||||||
label=_("OpenID"),
|
label=_('OpenID'),
|
||||||
required=False,
|
required=False,
|
||||||
min_length=MIN_LENGTH_URL,
|
min_length=MIN_LENGTH_URL,
|
||||||
max_length=MAX_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(
|
size = forms.IntegerField(
|
||||||
label=_("Size"),
|
label=_('Size'),
|
||||||
initial=80,
|
initial=80,
|
||||||
min_value=5,
|
min_value=5,
|
||||||
max_value=AVATAR_MAX_SIZE,
|
max_value=AVATAR_MAX_SIZE,
|
||||||
@@ -54,24 +58,23 @@ class CheckForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
default_opt = forms.ChoiceField(
|
default_opt = forms.ChoiceField(
|
||||||
label=_("Default"),
|
label=_('Default'),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.RadioSelect,
|
widget=forms.RadioSelect,
|
||||||
choices=[
|
choices = [
|
||||||
("retro", _("Retro style (similar to GitHub)")),
|
('retro', _('Retro style (similar to GitHub)')),
|
||||||
("robohash", _("Roboter style")),
|
('robohash', _('Roboter style')),
|
||||||
("pagan", _("Retro adventure character")),
|
('pagan', _('Retro adventure character')),
|
||||||
("wavatar", _("Wavatar style")),
|
('wavatar', _('Wavatar style')),
|
||||||
("monsterid", _("Monster style")),
|
('monsterid', _('Monster style')),
|
||||||
("identicon", _("Identicon style")),
|
('identicon', _('Identicon style')),
|
||||||
("mm", _("Mystery man")),
|
('mm', _('Mystery man')),
|
||||||
("mmng", _("Mystery man NextGen")),
|
('none', _('None')),
|
||||||
("none", _("None")),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
default_url = forms.URLField(
|
default_url = forms.URLField(
|
||||||
label=_("Default URL"),
|
label=_('Default URL'),
|
||||||
min_length=1,
|
min_length=1,
|
||||||
max_length=MAX_LENGTH_URL,
|
max_length=MAX_LENGTH_URL,
|
||||||
required=False,
|
required=False,
|
||||||
@@ -79,27 +82,19 @@ class CheckForm(forms.Form):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.cleaned_data = super().clean()
|
self.cleaned_data = super().clean()
|
||||||
mail = self.cleaned_data.get("mail")
|
mail = self.cleaned_data.get('mail')
|
||||||
openid = self.cleaned_data.get("openid")
|
openid = self.cleaned_data.get('openid')
|
||||||
default_url = self.cleaned_data.get("default_url")
|
default_url = self.cleaned_data.get('default_url')
|
||||||
default_opt = self.cleaned_data.get("default_opt")
|
default_opt = self.cleaned_data.get('default_opt')
|
||||||
if default_url and default_opt and default_opt != "none":
|
if default_url and default_opt and default_opt != 'none':
|
||||||
if "default_url" not in self._errors:
|
if not 'default_url' in self._errors:
|
||||||
self._errors["default_url"] = ErrorList()
|
self._errors['default_url'] = ErrorList()
|
||||||
if "default_opt" not in self._errors:
|
if not 'default_opt' in self._errors:
|
||||||
self._errors["default_opt"] = ErrorList()
|
self._errors['default_opt'] = ErrorList()
|
||||||
|
|
||||||
errstring = _("Only default URL OR default keyword may be specified")
|
errstring = _('Only default URL OR default keyword may be specified')
|
||||||
self._errors["default_url"].append(errstring)
|
self._errors['default_url'].append(errstring)
|
||||||
self._errors["default_opt"].append(errstring)
|
self._errors['default_opt'].append(errstring)
|
||||||
if not mail and not openid:
|
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
|
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()
|
|
||||||
|
|||||||
@@ -1,48 +1,57 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Test our views in ivatar.ivataraccount.views and ivatar.views
|
Test our views in ivatar.ivataraccount.views and ivatar.views
|
||||||
"""
|
'''
|
||||||
# pylint: disable=too-many-lines
|
# pylint: disable=too-many-lines
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
from io import BytesIO
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import django
|
import django
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
import hashlib
|
||||||
|
|
||||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
|
from libravatar import libravatar_url
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
# pylint: disable=wrong-import-position
|
# 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
|
from ivatar.utils import random_string
|
||||||
|
|
||||||
# pylint: enable=wrong-import-position
|
# pylint: enable=wrong-import-position
|
||||||
|
|
||||||
|
|
||||||
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||||
"""
|
'''
|
||||||
Main test class
|
Main test class
|
||||||
"""
|
'''
|
||||||
|
|
||||||
client = Client()
|
client = Client()
|
||||||
user = None
|
user = None
|
||||||
username = random_string()
|
username = random_string()
|
||||||
password = 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
|
# 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):
|
def login(self):
|
||||||
"""
|
'''
|
||||||
Login as user
|
Login as user
|
||||||
"""
|
'''
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
'''
|
||||||
Prepare for tests.
|
Prepare for tests.
|
||||||
- Create user
|
- Create user
|
||||||
"""
|
'''
|
||||||
self.user = User.objects.create_user(
|
self.user = User.objects.create_user(
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
@@ -52,12 +61,12 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
Test check page
|
Test check page
|
||||||
"""
|
"""
|
||||||
response = self.client.get(reverse("tools_check"))
|
response = self.client.get(reverse('tools_check'))
|
||||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||||
|
|
||||||
def test_check_domain(self):
|
def test_check_domain(self):
|
||||||
"""
|
"""
|
||||||
Test check domain page
|
Test check domain page
|
||||||
"""
|
"""
|
||||||
response = self.client.get(reverse("tools_check_domain"))
|
response = self.client.get(reverse('tools_check_domain'))
|
||||||
self.assertEqual(response.status_code, 200, "no 200 ok?")
|
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
ivatar/tools URL configuration
|
ivatar/tools URL configuration
|
||||||
"""
|
'''
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from .views import CheckView, CheckDomainView
|
from . views import CheckView, CheckDomainView
|
||||||
|
|
||||||
urlpatterns = [ # pylint: disable=invalid-name
|
urlpatterns = [ # pylint: disable=invalid-name
|
||||||
url("check/", CheckView.as_view(), name="tools_check"),
|
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'),
|
||||||
url("check_domain$", CheckDomainView.as_view(), name="tools_check_domain"),
|
url('check_domain$', CheckDomainView.as_view(), name='tools_check_domain'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
View classes for ivatar/tools/
|
View classes for ivatar/tools/
|
||||||
"""
|
'''
|
||||||
from socket import inet_ntop, AF_INET6
|
from socket import inet_ntop, AF_INET6
|
||||||
import hashlib
|
|
||||||
import random
|
|
||||||
|
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
from django.urls import reverse_lazy as reverse
|
from django.urls import reverse_lazy as reverse
|
||||||
@@ -15,61 +12,44 @@ import DNS
|
|||||||
from libravatar import libravatar_url, parse_user_identity
|
from libravatar import libravatar_url, parse_user_identity
|
||||||
from libravatar import SECURE_BASE_URL as LIBRAVATAR_SECURE_BASE_URL
|
from libravatar import SECURE_BASE_URL as LIBRAVATAR_SECURE_BASE_URL
|
||||||
from libravatar import BASE_URL as LIBRAVATAR_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
|
||||||
from .forms import (
|
|
||||||
CheckDomainForm,
|
|
||||||
CheckForm,
|
|
||||||
) # pylint: disable=relative-beyond-top-level
|
|
||||||
|
|
||||||
|
|
||||||
class CheckDomainView(FormView):
|
class CheckDomainView(FormView):
|
||||||
"""
|
'''
|
||||||
View class for checking a domain
|
View class for checking a domain
|
||||||
"""
|
'''
|
||||||
|
template_name = 'check_domain.html'
|
||||||
template_name = "check_domain.html"
|
|
||||||
form_class = CheckDomainForm
|
form_class = CheckDomainForm
|
||||||
success_url = reverse("tools_check_domain")
|
success_url = reverse('tools_check_domain')
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
result = {}
|
result = {}
|
||||||
super().form_valid(form)
|
super().form_valid(form)
|
||||||
domain = form.cleaned_data["domain"]
|
domain = form.cleaned_data['domain']
|
||||||
result["avatar_server_http"] = lookup_avatar_server(domain, False)
|
result['avatar_server_http'] = lookup_avatar_server(domain, False)
|
||||||
if result["avatar_server_http"]:
|
if result['avatar_server_http']:
|
||||||
result["avatar_server_http_ipv4"] = lookup_ip_address(
|
result['avatar_server_http_ipv4'] = lookup_ip_address(result['avatar_server_http'], False)
|
||||||
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)
|
||||||
result["avatar_server_http_ipv6"] = lookup_ip_address(
|
if result['avatar_server_https']:
|
||||||
result["avatar_server_http"], True
|
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)
|
||||||
result["avatar_server_https"] = lookup_avatar_server(domain, True)
|
return render(self.request, self.template_name, {
|
||||||
if result["avatar_server_https"]:
|
'form': form,
|
||||||
result["avatar_server_https_ipv4"] = lookup_ip_address(
|
'result': result,
|
||||||
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):
|
class CheckView(FormView):
|
||||||
"""
|
'''
|
||||||
View class for checking an e-mail or openid address
|
View class for checking an e-mail or openid address
|
||||||
"""
|
'''
|
||||||
|
template_name = 'check.html'
|
||||||
template_name = "check.html"
|
|
||||||
form_class = CheckForm
|
form_class = CheckForm
|
||||||
success_url = reverse("tools_check")
|
success_url = reverse('tools_check')
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
mailurl = None
|
mailurl = None
|
||||||
@@ -84,88 +64,81 @@ class CheckView(FormView):
|
|||||||
|
|
||||||
super().form_valid(form)
|
super().form_valid(form)
|
||||||
|
|
||||||
if form.cleaned_data["default_url"]:
|
if form.cleaned_data['default_url']:
|
||||||
default_url = form.cleaned_data["default_url"]
|
default_url = form.cleaned_data['default_url']
|
||||||
elif (
|
elif form.cleaned_data['default_opt'] and form.cleaned_data['default_opt'] != 'none':
|
||||||
form.cleaned_data["default_opt"]
|
default_url = form.cleaned_data['default_opt']
|
||||||
and form.cleaned_data["default_opt"] != "none"
|
|
||||||
):
|
|
||||||
default_url = form.cleaned_data["default_opt"]
|
|
||||||
else:
|
else:
|
||||||
default_url = None
|
default_url = None
|
||||||
|
|
||||||
if "size" in form.cleaned_data:
|
if 'size' in form.cleaned_data:
|
||||||
size = form.cleaned_data["size"]
|
size = form.cleaned_data['size']
|
||||||
if form.cleaned_data["mail"]:
|
if form.cleaned_data['mail']:
|
||||||
mailurl = libravatar_url(
|
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 = mailurl.replace(LIBRAVATAR_BASE_URL, BASE_URL)
|
||||||
mailurl_secure = libravatar_url(
|
mailurl_secure = libravatar_url(
|
||||||
email=form.cleaned_data["mail"],
|
email=form.cleaned_data['mail'],
|
||||||
size=size,
|
size=size,
|
||||||
https=True,
|
https=True,
|
||||||
default=default_url,
|
default=default_url)
|
||||||
)
|
|
||||||
mailurl_secure = mailurl_secure.replace(
|
mailurl_secure = mailurl_secure.replace(
|
||||||
LIBRAVATAR_SECURE_BASE_URL, SECURE_BASE_URL
|
LIBRAVATAR_SECURE_BASE_URL,
|
||||||
)
|
SECURE_BASE_URL)
|
||||||
mail_hash = parse_user_identity(
|
mail_hash = parse_user_identity(
|
||||||
email=form.cleaned_data["mail"], openid=None
|
email=form.cleaned_data['mail'],
|
||||||
)[0]
|
openid=None)[0]
|
||||||
hash_obj = hashlib.new("sha256")
|
hash_obj = hashlib.new('sha256')
|
||||||
hash_obj.update(form.cleaned_data["mail"].encode("utf-8"))
|
hash_obj.update(form.cleaned_data['mail'].encode('utf-8'))
|
||||||
mail_hash256 = hash_obj.hexdigest()
|
mail_hash256 = hash_obj.hexdigest()
|
||||||
mailurl_secure_256 = mailurl_secure.replace(mail_hash, mail_hash256)
|
mailurl_secure_256 = mailurl_secure.replace(
|
||||||
if form.cleaned_data["openid"]:
|
mail_hash,
|
||||||
if not form.cleaned_data["openid"].startswith(
|
mail_hash256)
|
||||||
"http://"
|
if form.cleaned_data['openid']:
|
||||||
) and not form.cleaned_data["openid"].startswith("https://"):
|
if not form.cleaned_data['openid'].startswith('http://') and not form.cleaned_data['openid'].startswith('https://'):
|
||||||
form.cleaned_data["openid"] = "http://%s" % form.cleaned_data["openid"]
|
form.cleaned_data['openid'] = 'http://%s' % form.cleaned_data['openid']
|
||||||
openidurl = libravatar_url(
|
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 = openidurl.replace(LIBRAVATAR_BASE_URL, BASE_URL)
|
||||||
openidurl_secure = libravatar_url(
|
openidurl_secure = libravatar_url(
|
||||||
openid=form.cleaned_data["openid"],
|
openid=form.cleaned_data['openid'],
|
||||||
size=size,
|
size=size,
|
||||||
https=True,
|
https=True,
|
||||||
default=default_url,
|
default=default_url)
|
||||||
)
|
|
||||||
openidurl_secure = openidurl_secure.replace(
|
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_hash = parse_user_identity(
|
||||||
openid=form.cleaned_data["openid"], email=None
|
openid=form.cleaned_data['openid'],
|
||||||
)[0]
|
email=None)[0]
|
||||||
|
|
||||||
return render(
|
return render(self.request, self.template_name, {
|
||||||
self.request,
|
'form': form,
|
||||||
self.template_name,
|
'mailurl': mailurl,
|
||||||
{
|
'openidurl': openidurl,
|
||||||
"form": form,
|
'mailurl_secure': mailurl_secure,
|
||||||
"mailurl": mailurl,
|
'mailurl_secure_256': mailurl_secure_256,
|
||||||
"openidurl": openidurl,
|
'openidurl_secure': openidurl_secure,
|
||||||
"mailurl_secure": mailurl_secure,
|
'mail_hash': mail_hash,
|
||||||
"mailurl_secure_256": mailurl_secure_256,
|
'mail_hash256': mail_hash256,
|
||||||
"openidurl_secure": openidurl_secure,
|
'openid_hash': openid_hash,
|
||||||
"mail_hash": mail_hash,
|
'size': size,
|
||||||
"mail_hash256": mail_hash256,
|
})
|
||||||
"openid_hash": openid_hash,
|
|
||||||
"size": size,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def lookup_avatar_server(domain, https):
|
def lookup_avatar_server(domain, https):
|
||||||
"""
|
'''
|
||||||
Extract the avatar server from an SRV record in the DNS zone
|
Extract the avatar server from an SRV record in the DNS zone
|
||||||
|
|
||||||
The SRV records should look like this:
|
The SRV records should look like this:
|
||||||
|
|
||||||
_avatars._tcp.example.com. IN SRV 0 0 80 avatars.example.com
|
_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
|
_avatars-sec._tcp.example.com. IN SRV 0 0 443 avatars.example.com
|
||||||
"""
|
'''
|
||||||
|
|
||||||
if domain and len(domain) > 60:
|
if domain and len(domain) > 60:
|
||||||
domain = domain[:60]
|
domain = domain[:60]
|
||||||
@@ -178,35 +151,26 @@ def lookup_avatar_server(domain, https):
|
|||||||
|
|
||||||
DNS.DiscoverNameServers()
|
DNS.DiscoverNameServers()
|
||||||
try:
|
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:
|
except DNS.DNSError as message:
|
||||||
print("DNS Error: %s (%s)" % (message, domain))
|
print("DNS Error: %s (%s)" % (message, domain))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if dns_request.header["status"] == "NXDOMAIN":
|
if dns_request.header['status'] == 'NXDOMAIN':
|
||||||
# Not an error, but no point in going any further
|
# Not an error, but no point in going any further
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if dns_request.header["status"] != "NOERROR":
|
if dns_request.header['status'] != 'NOERROR':
|
||||||
print("DNS Error: status=%s (%s)" % (dns_request.header["status"], domain))
|
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], domain))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
for answer in dns_request.answers:
|
for answer in dns_request.answers:
|
||||||
if (
|
if ('data' not in answer) or (not answer['data']) or (not answer['typename']) or (answer['typename'] != 'SRV'):
|
||||||
("data" not in answer)
|
|
||||||
or (not answer["data"])
|
|
||||||
or (not answer["typename"])
|
|
||||||
or (answer["typename"] != "SRV")
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
record = {
|
record = {'priority': int(answer['data'][0]), 'weight': int(answer['data'][1]),
|
||||||
"priority": int(answer["data"][0]),
|
'port': int(answer['data'][2]), 'target': answer['data'][3]}
|
||||||
"weight": int(answer["data"][1]),
|
|
||||||
"port": int(answer["data"][2]),
|
|
||||||
"target": answer["data"][3],
|
|
||||||
}
|
|
||||||
|
|
||||||
records.append(record)
|
records.append(record)
|
||||||
|
|
||||||
@@ -219,46 +183,43 @@ def lookup_avatar_server(domain, https):
|
|||||||
|
|
||||||
|
|
||||||
def srv_hostname(records):
|
def srv_hostname(records):
|
||||||
"""
|
'''
|
||||||
Return the right (target, port) pair from a list of SRV records.
|
Return the right (target, port) pair from a list of SRV records.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
if len(records) < 1:
|
if len(records) < 1:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
if len(records) == 1:
|
if len(records) == 1:
|
||||||
ret = records[0]
|
ret = records[0]
|
||||||
return (ret["target"], ret["port"])
|
return (ret['target'], ret['port'])
|
||||||
|
|
||||||
# Keep only the servers in the top priority
|
# Keep only the servers in the top priority
|
||||||
priority_records = []
|
priority_records = []
|
||||||
total_weight = 0
|
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:
|
for ret in records:
|
||||||
if ret["priority"] > top_priority:
|
if ret['priority'] > top_priority:
|
||||||
# ignore the record (ret has lower priority)
|
# ignore the record (ret has lower priority)
|
||||||
continue
|
continue
|
||||||
|
elif ret['priority'] < top_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 aretay (ret has higher priority)
|
# reset the aretay (ret has higher priority)
|
||||||
top_priority = ret["priority"]
|
top_priority = ret['priority']
|
||||||
total_weight = 0
|
total_weight = 0
|
||||||
priority_records = []
|
priority_records = []
|
||||||
|
|
||||||
total_weight += ret["weight"]
|
total_weight += ret['weight']
|
||||||
|
|
||||||
if ret["weight"] > 0:
|
if ret['weight'] > 0:
|
||||||
priority_records.append((total_weight, ret))
|
priority_records.append((total_weight, ret))
|
||||||
else:
|
else:
|
||||||
# zero-weigth elements must come first
|
# zero-weigth elements must come first
|
||||||
priority_records.insert(0, (0, ret))
|
priority_records.insert(0, (0, ret))
|
||||||
|
|
||||||
if len(priority_records) == 1:
|
if len(priority_records) == 1:
|
||||||
unused, ret = priority_records[0] # pylint: disable=unused-variable
|
unused, ret = priority_records[0]
|
||||||
return (ret["target"], ret["port"])
|
return (ret['target'], ret['port'])
|
||||||
|
|
||||||
# Select first record according to RFC2782 weight ordering algorithm (page 3)
|
# Select first record according to RFC2782 weight ordering algorithm (page 3)
|
||||||
random_number = random.randint(0, total_weight)
|
random_number = random.randint(0, total_weight)
|
||||||
@@ -267,9 +228,9 @@ def srv_hostname(records):
|
|||||||
weighted_index, ret = record
|
weighted_index, ret = record
|
||||||
|
|
||||||
if weighted_index >= random_number:
|
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)
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
@@ -288,21 +249,19 @@ def lookup_ip_address(hostname, ipv6):
|
|||||||
print("DNS Error: %s (%s)" % (message, hostname))
|
print("DNS Error: %s (%s)" % (message, hostname))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if dns_request.header["status"] != "NOERROR":
|
if dns_request.header['status'] != 'NOERROR':
|
||||||
print("DNS Error: status=%s (%s)" % (dns_request.header["status"], hostname))
|
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], hostname))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for answer in dns_request.answers:
|
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
|
continue
|
||||||
if (ipv6 and answer["typename"] != "AAAA") or (
|
if (ipv6 and answer['typename'] != 'AAAA') or (not ipv6 and answer['typename'] != 'A'):
|
||||||
not ipv6 and answer["typename"] != "A"
|
|
||||||
):
|
|
||||||
continue # skip CNAME records
|
continue # skip CNAME records
|
||||||
|
|
||||||
if ipv6:
|
if ipv6:
|
||||||
return inet_ntop(AF_INET6, answer["data"])
|
return inet_ntop(AF_INET6, answer['data'])
|
||||||
|
else:
|
||||||
return answer["data"]
|
return answer['data']
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,77 +1,63 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
ivatar URL configuration
|
ivatar URL configuration
|
||||||
"""
|
'''
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.views.generic import TemplateView, RedirectView
|
from django.views.generic import TemplateView, RedirectView
|
||||||
from ivatar import settings
|
from ivatar import settings
|
||||||
from .views import AvatarImageView, GravatarProxyView, StatsView
|
from . views import AvatarImageView, GravatarProxyView
|
||||||
|
|
||||||
urlpatterns = [ # pylint: disable=invalid-name
|
urlpatterns = [ # pylint: disable=invalid-name
|
||||||
path("admin/", admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path("i18n/", include("django.conf.urls.i18n")),
|
path('i18n/', include('django.conf.urls.i18n')),
|
||||||
url("openid/", include("django_openid_auth.urls")),
|
url('openid/', include('django_openid_auth.urls')),
|
||||||
url("tools/", include("ivatar.tools.urls")),
|
url('tools/', include('ivatar.tools.urls')),
|
||||||
url(r"avatar/(?P<digest>\w{64})", AvatarImageView.as_view(), name="avatar_view"),
|
# Encrypted digest
|
||||||
url(r"avatar/(?P<digest>\w{32})", AvatarImageView.as_view(), name="avatar_view"),
|
|
||||||
url(r"avatar/$", AvatarImageView.as_view(), name="avatar_view"),
|
|
||||||
url(
|
url(
|
||||||
r"avatar/(?P<digest>\w*)",
|
r'avatar/(?P<digest>\w{264})',
|
||||||
RedirectView.as_view(url="/static/img/deadbeef.png"),
|
AvatarImageView.as_view(), name='avatar_view'),
|
||||||
name="invalid_hash",
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"gravatarproxy/(?P<digest>\w*)",
|
r'avatar/(?P<digest>\w{200})',
|
||||||
GravatarProxyView.as_view(),
|
AvatarImageView.as_view(), name='avatar_view'),
|
||||||
name="gravatarproxy",
|
# Unencrypted digest
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
"description/",
|
r'avatar/(?P<digest>\w{64})',
|
||||||
TemplateView.as_view(template_name="description.html"),
|
AvatarImageView.as_view(), name='avatar_view'),
|
||||||
name="description",
|
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'),
|
||||||
# The following two are TODO TODO TODO TODO TODO
|
# The following two are TODO TODO TODO TODO TODO
|
||||||
url(
|
url('run_your_own/', TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'),
|
||||||
"run_your_own/",
|
url('features/', TemplateView.as_view(template_name='features.html'), name='features'),
|
||||||
TemplateView.as_view(template_name="run_your_own.html"),
|
url('security/', TemplateView.as_view(template_name='security.html'), name='security'),
|
||||||
name="run_your_own",
|
url('privacy/', TemplateView.as_view(template_name='privacy.html'), name='privacy'),
|
||||||
),
|
url('contact/', TemplateView.as_view(template_name='contact.html'), name='contact'),
|
||||||
url(
|
path('talk_to_us/', RedirectView.as_view(url='/contact'), name='talk_to_us'),
|
||||||
"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"),
|
|
||||||
url("stats/", StatsView.as_view(), name="stats"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MAINTENANCE = False
|
MAINTENANCE = False
|
||||||
try:
|
try:
|
||||||
if settings.MAINTENANCE:
|
if settings.MAINTENANCE:
|
||||||
MAINTENANCE = True
|
MAINTENANCE = True
|
||||||
except: # pylint: disable=bare-except
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if MAINTENANCE:
|
if MAINTENANCE:
|
||||||
urlpatterns.append(
|
urlpatterns.append(url('', TemplateView.as_view(template_name='maintenance.html'), name='home'))
|
||||||
url("", TemplateView.as_view(template_name="maintenance.html"), name="home")
|
urlpatterns.insert(3, url('accounts/', RedirectView.as_view(url='/')))
|
||||||
)
|
|
||||||
urlpatterns.insert(3, url("accounts/", RedirectView.as_view(url="/")))
|
|
||||||
else:
|
else:
|
||||||
urlpatterns.append(
|
urlpatterns.append(url('', TemplateView.as_view(template_name='home.html'), name='home'))
|
||||||
url("", TemplateView.as_view(template_name="home.html"), name="home")
|
urlpatterns.insert(3, url('accounts/', include('ivatar.ivataraccount.urls')))
|
||||||
)
|
|
||||||
urlpatterns.insert(3, url("accounts/", include("ivatar.ivataraccount.urls")))
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|||||||
145
ivatar/utils.py
145
ivatar/utils.py
@@ -1,154 +1,35 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
Simple module providing reusable random_string function
|
Simple module providing reusable random_string function
|
||||||
"""
|
'''
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
|
|
||||||
def random_string(length=10):
|
def random_string(length=10):
|
||||||
"""
|
'''
|
||||||
Return some random string with default length 10
|
Return some random string with default length 10
|
||||||
"""
|
'''
|
||||||
return "".join(
|
return ''.join(random.SystemRandom().choice(
|
||||||
random.SystemRandom().choice(string.ascii_lowercase + string.digits)
|
string.ascii_lowercase + string.digits) for _ in range(length))
|
||||||
for _ in range(length)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def openid_variations(openid):
|
def openid_variations(openid):
|
||||||
"""
|
'''
|
||||||
Return the various OpenID variations, ALWAYS in the same order:
|
Return the various OpenID variations, ALWAYS in the same order:
|
||||||
- http w/ trailing slash
|
- http w/ trailing slash
|
||||||
- http w/o trailing slash
|
- http w/o trailing slash
|
||||||
- https w/ trailing slash
|
- https w/ trailing slash
|
||||||
- https w/o trailing slash
|
- https w/o trailing slash
|
||||||
"""
|
'''
|
||||||
|
|
||||||
# Make the 'base' version: http w/ trailing slash
|
# Make the 'base' version: http w/ trailing slash
|
||||||
if openid.startswith("https://"):
|
if openid.startswith('https://'):
|
||||||
openid = openid.replace("https://", "http://")
|
openid = openid.replace('https://', 'http://')
|
||||||
if openid[-1] != "/":
|
if openid[-1] != '/':
|
||||||
openid = openid + "/"
|
openid = openid + '/'
|
||||||
|
|
||||||
# http w/o trailing slash
|
# http w/o trailing slash
|
||||||
var1 = openid[0:-1]
|
var1 = openid[0:-1]
|
||||||
var2 = openid.replace("http://", "https://")
|
var2 = openid.replace('http://', 'https://')
|
||||||
var3 = var2[0:-1]
|
var3 = var2[0:-1]
|
||||||
return (openid, var1, var2, var3)
|
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?
|
|
||||||
circlesize = size * 0.6
|
|
||||||
|
|
||||||
# Coordinates for the circle
|
|
||||||
start_x = int(size * 0.2)
|
|
||||||
end_x = start_x + circlesize
|
|
||||||
start_y = int(size * 0.05)
|
|
||||||
end_y = start_y + circlesize
|
|
||||||
|
|
||||||
# All are the same, based on the input hash
|
|
||||||
# this should always result in a "gray-ish" background
|
|
||||||
red = idhash[0:2]
|
|
||||||
green = idhash[0:2]
|
|
||||||
blue = idhash[0:2]
|
|
||||||
|
|
||||||
# Add some red (i/a) and make sure it's not over 255
|
|
||||||
red = hex(int(red, 16) + add_red).replace("0x", "")
|
|
||||||
if int(red, 16) > 255:
|
|
||||||
red = "ff"
|
|
||||||
if len(red) == 1:
|
|
||||||
red = "0%s" % red
|
|
||||||
|
|
||||||
# Add some green (i/a) and make sure it's not over 255
|
|
||||||
green = hex(int(green, 16) + add_green).replace("0x", "")
|
|
||||||
if int(green, 16) > 255:
|
|
||||||
green = "ff"
|
|
||||||
if len(green) == 1:
|
|
||||||
green = "0%s" % green
|
|
||||||
|
|
||||||
# Add some blue (i/a) and make sure it's not over 255
|
|
||||||
blue = hex(int(blue, 16) + add_blue).replace("0x", "")
|
|
||||||
if int(blue, 16) > 255:
|
|
||||||
blue = "ff"
|
|
||||||
if len(blue) == 1:
|
|
||||||
blue = "0%s" % blue
|
|
||||||
|
|
||||||
# Assemable the bg color "string" in webnotation. Eg. '#d3d3d3'
|
|
||||||
bg_color = "#" + 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 + circlesize / 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 filter in url_filters:
|
|
||||||
if "schemes" in filter:
|
|
||||||
schemes = filter["schemes"]
|
|
||||||
|
|
||||||
if scheme not in schemes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "host_equals" in filter:
|
|
||||||
host_equals = filter["host_equals"]
|
|
||||||
|
|
||||||
if netloc != host_equals:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "host_suffix" in filter:
|
|
||||||
host_suffix = filter["host_suffix"]
|
|
||||||
|
|
||||||
if not netloc.endswith(host_suffix):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "path_prefix" in filter:
|
|
||||||
path_prefix = filter["path_prefix"]
|
|
||||||
|
|
||||||
if not path.startswith(path_prefix):
|
|
||||||
continue
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|||||||
426
ivatar/views.py
426
ivatar/views.py
@@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
'''
|
||||||
"""
|
|
||||||
views under /
|
views under /
|
||||||
"""
|
'''
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from os import path
|
from os import path
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -9,14 +8,14 @@ from urllib.request import urlopen
|
|||||||
from urllib.error import HTTPError, URLError
|
from urllib.error import HTTPError, URLError
|
||||||
from ssl import SSLError
|
from ssl import SSLError
|
||||||
from django.views.generic.base import TemplateView, View
|
from django.views.generic.base import TemplateView, View
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
|
||||||
from django.http import HttpResponseNotFound, JsonResponse
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.cache import cache, caches
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.contrib import messages
|
||||||
|
from simplecrypt import decrypt
|
||||||
|
from binascii import unhexlify
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -27,29 +26,25 @@ import pagan
|
|||||||
from robohash import Robohash
|
from robohash import Robohash
|
||||||
|
|
||||||
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
|
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
|
||||||
from ivatar.settings import CACHE_RESPONSE
|
|
||||||
from ivatar.settings import CACHE_IMAGES_MAX_AGE
|
from ivatar.settings import CACHE_IMAGES_MAX_AGE
|
||||||
from ivatar.settings import TRUSTED_DEFAULT_URLS
|
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
||||||
from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
from . ivataraccount.models import pil_format, file_format
|
||||||
from .ivataraccount.models import UnconfirmedEmail, UnconfirmedOpenId
|
from . ivataraccount.models import APIKey
|
||||||
from .ivataraccount.models import Photo
|
|
||||||
from .ivataraccount.models import pil_format, file_format
|
|
||||||
from .utils import is_trusted_url, mm_ng
|
|
||||||
|
|
||||||
URL_TIMEOUT = 5 # in seconds
|
URL_TIMEOUT = 5 # in seconds
|
||||||
|
|
||||||
|
|
||||||
def get_size(request, size=DEFAULT_AVATAR_SIZE):
|
def get_size(request, size=DEFAULT_AVATAR_SIZE):
|
||||||
"""
|
'''
|
||||||
Get size from the URL arguments
|
Get size from the URL arguments
|
||||||
"""
|
'''
|
||||||
sizetemp = None
|
sizetemp = None
|
||||||
if "s" in request.GET:
|
if 's' in request.GET:
|
||||||
sizetemp = request.GET["s"]
|
sizetemp = request.GET['s']
|
||||||
if "size" in request.GET:
|
if 'size' in request.GET:
|
||||||
sizetemp = request.GET["size"]
|
sizetemp = request.GET['size']
|
||||||
if sizetemp:
|
if sizetemp:
|
||||||
if sizetemp != "" and sizetemp is not None and sizetemp != "0":
|
if sizetemp != '' and sizetemp is not None and sizetemp != '0':
|
||||||
try:
|
try:
|
||||||
if int(sizetemp) > 0:
|
if int(sizetemp) > 0:
|
||||||
size = int(sizetemp)
|
size = int(sizetemp)
|
||||||
@@ -63,140 +58,94 @@ def get_size(request, size=DEFAULT_AVATAR_SIZE):
|
|||||||
return size
|
return size
|
||||||
|
|
||||||
|
|
||||||
class CachingHttpResponse(HttpResponse):
|
|
||||||
"""
|
|
||||||
Handle caching of response
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
uri,
|
|
||||||
content=b"",
|
|
||||||
content_type=None,
|
|
||||||
status=200, # pylint: disable=too-many-arguments
|
|
||||||
reason=None,
|
|
||||||
charset=None,
|
|
||||||
):
|
|
||||||
if CACHE_RESPONSE:
|
|
||||||
caches["filesystem"].set(
|
|
||||||
uri,
|
|
||||||
{
|
|
||||||
"content": content,
|
|
||||||
"content_type": content_type,
|
|
||||||
"status": status,
|
|
||||||
"reason": reason,
|
|
||||||
"charset": charset,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
super().__init__(content, content_type, status, reason, charset)
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarImageView(TemplateView):
|
class AvatarImageView(TemplateView):
|
||||||
"""
|
'''
|
||||||
View to return (binary) image, based on OpenID/Email (both by digest)
|
View to return (binary) image, based on OpenID/Email (both by digest)
|
||||||
"""
|
'''
|
||||||
|
|
||||||
# TODO: Do cache resize images!! Memcached?
|
# TODO: Do cache resize images!! Memcached?
|
||||||
|
|
||||||
def options(self, request, *args, **kwargs):
|
def options(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
|
||||||
response = HttpResponse("", content_type="text/plain")
|
response = HttpResponse("", content_type='text/plain')
|
||||||
response["Allow"] = "404 mm mp retro pagan wavatar monsterid robohash identicon"
|
response['Allow'] = "404 mm mp retro pagan wavatar monsterid robohash identicon"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get(
|
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
|
||||||
self, request, *args, **kwargs
|
'''
|
||||||
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
|
|
||||||
"""
|
|
||||||
Override get from parent class
|
Override get from parent class
|
||||||
"""
|
'''
|
||||||
model = ConfirmedEmail
|
model = ConfirmedEmail
|
||||||
size = get_size(request)
|
size = get_size(request)
|
||||||
imgformat = "png"
|
imgformat = 'png'
|
||||||
obj = None
|
obj = None
|
||||||
default = None
|
default = None
|
||||||
forcedefault = False
|
forcedefault = False
|
||||||
gravatarredirect = False
|
gravatarredirect = False
|
||||||
gravatarproxy = True
|
gravatarproxy = True
|
||||||
uri = request.build_absolute_uri()
|
|
||||||
|
|
||||||
# Check the cache first
|
|
||||||
if CACHE_RESPONSE:
|
|
||||||
centry = caches["filesystem"].get(uri)
|
|
||||||
if centry:
|
|
||||||
# For DEBUG purpose only print('Cached entry for %s' % uri)
|
|
||||||
return HttpResponse(
|
|
||||||
centry["content"],
|
|
||||||
content_type=centry["content_type"],
|
|
||||||
status=centry["status"],
|
|
||||||
reason=centry["reason"],
|
|
||||||
charset=centry["charset"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# In case no digest at all is provided, return to home page
|
# In case no digest at all is provided, return to home page
|
||||||
if "digest" not in kwargs:
|
if not 'digest' in kwargs:
|
||||||
return HttpResponseRedirect(reverse_lazy("home"))
|
return HttpResponseRedirect(reverse_lazy('home'))
|
||||||
|
|
||||||
if "d" in request.GET:
|
# Encrypted digest
|
||||||
default = request.GET["d"]
|
if len(kwargs['digest']) >= 200:
|
||||||
if "default" in request.GET:
|
# If this is an encrypted digest, we need the key, without the key, return home
|
||||||
default = request.GET["default"]
|
if not 'key' in request.GET:
|
||||||
|
print('No key provided. If digest >=200, we expect it to be encrypted and it needs a key to decrypt')
|
||||||
|
return HttpResponseRedirect(reverse_lazy('home'))
|
||||||
|
try:
|
||||||
|
keypair = APIKey.objects.get(public_key=request.GET['key'])
|
||||||
|
if not keypair:
|
||||||
|
print("Key %s doesn't exist!" % request.GET['key'])
|
||||||
|
messages.error(request, _("This key doesn't exist"))
|
||||||
|
kwargs['digest'] = decrypt(keypair.secret_key, unhexlify(kwargs['digest'])).decode('utf-8')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, _("Error decrypting: %s" % e))
|
||||||
|
|
||||||
# Check if default starts with an URL scheme and if it does,
|
if 'd' in request.GET:
|
||||||
# check if it's trusted
|
default = request.GET['d']
|
||||||
# Check for :// (schema)
|
if 'default' in request.GET:
|
||||||
if default is not None and default.find("://") > 0:
|
default = request.GET['default']
|
||||||
# Check if it's trusted, if not, reset to None
|
|
||||||
trusted_url = is_trusted_url(default, TRUSTED_DEFAULT_URLS)
|
|
||||||
|
|
||||||
if not trusted_url:
|
if 'f' in request.GET:
|
||||||
print(
|
if request.GET['f'] == 'y':
|
||||||
"Default URL is not in trusted URLs: '%s' ; Kicking it!" % default
|
|
||||||
)
|
|
||||||
default = None
|
|
||||||
|
|
||||||
if "f" in request.GET:
|
|
||||||
if request.GET["f"] == "y":
|
|
||||||
forcedefault = True
|
forcedefault = True
|
||||||
if "forcedefault" in request.GET:
|
if 'forcedefault' in request.GET:
|
||||||
if request.GET["forcedefault"] == "y":
|
if request.GET['forcedefault'] == 'y':
|
||||||
forcedefault = True
|
forcedefault = True
|
||||||
|
|
||||||
if "gravatarredirect" in request.GET:
|
if 'gravatarredirect' in request.GET:
|
||||||
if request.GET["gravatarredirect"] == "y":
|
if request.GET['gravatarredirect'] == 'y':
|
||||||
gravatarredirect = True
|
gravatarredirect = True
|
||||||
|
|
||||||
if "gravatarproxy" in request.GET:
|
if 'gravatarproxy' in request.GET:
|
||||||
if request.GET["gravatarproxy"] == "n":
|
if request.GET['gravatarproxy'] == 'n':
|
||||||
gravatarproxy = False
|
gravatarproxy = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = model.objects.get(digest=kwargs["digest"])
|
obj = model.objects.get(digest=kwargs['digest'])
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
try:
|
try:
|
||||||
obj = model.objects.get(digest_sha256=kwargs["digest"])
|
obj = model.objects.get(digest_sha256=kwargs['digest'])
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
model = ConfirmedOpenId
|
model = ConfirmedOpenId
|
||||||
try:
|
try:
|
||||||
d = kwargs["digest"] # pylint: disable=invalid-name
|
d = kwargs['digest']
|
||||||
# OpenID is tricky. http vs. https, versus trailing slash or not
|
# OpenID is tricky. http vs. https, versus trailing slash or not
|
||||||
# However, some users eventually have added their variations already
|
# However, some users eventually have added their variations already
|
||||||
# and therfore we need to use filter() and first()
|
# and therfore we need to use filter() and first()
|
||||||
obj = model.objects.filter(
|
obj = model.objects.filter(
|
||||||
Q(digest=d)
|
Q(digest=d) |
|
||||||
| Q(alt_digest1=d)
|
Q(alt_digest1=d) |
|
||||||
| Q(alt_digest2=d)
|
Q(alt_digest2=d) |
|
||||||
| Q(alt_digest3=d)
|
Q(alt_digest3=d)).first()
|
||||||
).first()
|
except:
|
||||||
except Exception: # pylint: disable=bare-except
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# If that mail/openid doesn't exist, or has no photo linked to it
|
# If that mail/openid doesn't exist, or has no photo linked to it
|
||||||
if not obj or not obj.photo or forcedefault:
|
if not obj or not obj.photo or forcedefault:
|
||||||
gravatar_url = (
|
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
||||||
"https://secure.gravatar.com/avatar/"
|
+ '?s=%i' % size
|
||||||
+ kwargs["digest"]
|
|
||||||
+ "?s=%i" % size
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we have redirection to Gravatar enabled, this overrides all
|
# If we have redirection to Gravatar enabled, this overrides all
|
||||||
# default= settings, except forcedefault!
|
# default= settings, except forcedefault!
|
||||||
@@ -205,115 +154,104 @@ class AvatarImageView(TemplateView):
|
|||||||
|
|
||||||
# Request to proxy Gravatar image - only if not forcedefault
|
# Request to proxy Gravatar image - only if not forcedefault
|
||||||
if gravatarproxy and not forcedefault:
|
if gravatarproxy and not forcedefault:
|
||||||
url = (
|
url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
|
||||||
reverse_lazy("gravatarproxy", args=[kwargs["digest"]])
|
+ '?s=%i' % size + '&default=%s' % default
|
||||||
+ "?s=%i" % size
|
|
||||||
)
|
|
||||||
# Ensure we do not convert None to string 'None'
|
|
||||||
if default:
|
|
||||||
url += "&default=%s" % default
|
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
# Return the default URL, as specified, or 404 Not Found, if default=404
|
# Return the default URL, as specified, or 404 Not Found, if default=404
|
||||||
if default:
|
if default:
|
||||||
# Proxy to gravatar to generate wavatar - lazy me
|
# Proxy to gravatar to generate wavatar - lazy me
|
||||||
if str(default) == "wavatar":
|
if str(default) == 'wavatar':
|
||||||
url = (
|
url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
|
||||||
reverse_lazy("gravatarproxy", args=[kwargs["digest"]])
|
+ '?s=%i' % size + '&default=%s&f=y' % default
|
||||||
+ "?s=%i" % size
|
|
||||||
+ "&default=%s&f=y" % default
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
if str(default) == str(404):
|
|
||||||
return HttpResponseNotFound(_("<h1>Image not found</h1>"))
|
|
||||||
|
|
||||||
if str(default) == "monsterid":
|
|
||||||
monsterdata = BuildMonster(seed=kwargs["digest"], size=(size, size))
|
if str(default) == str(404):
|
||||||
|
return HttpResponseNotFound(_('<h1>Image not found</h1>'))
|
||||||
|
|
||||||
|
if str(default) == 'monsterid':
|
||||||
|
monsterdata = BuildMonster(seed=kwargs['digest'], size=(size, size))
|
||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
monsterdata.save(data, "PNG", quality=JPEG_QUALITY)
|
monsterdata.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
response = CachingHttpResponse(uri, data, content_type="image/png")
|
response = HttpResponse(
|
||||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
data,
|
||||||
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if str(default) == "robohash":
|
if str(default) == 'robohash':
|
||||||
roboset = "any"
|
roboset = 'any'
|
||||||
if request.GET.get("robohash"):
|
if request.GET.get('robohash'):
|
||||||
roboset = request.GET.get("robohash")
|
roboset = request.GET.get('robohash')
|
||||||
robohash = Robohash(kwargs["digest"])
|
robohash = Robohash(kwargs['digest'])
|
||||||
robohash.assemble(roboset=roboset, sizex=size, sizey=size)
|
robohash.assemble(roboset=roboset, sizex=size, sizey=size)
|
||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
robohash.img.save(data, format="png")
|
robohash.img.save(data, format='png')
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
response = CachingHttpResponse(uri, data, content_type="image/png")
|
response = HttpResponse(
|
||||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
data,
|
||||||
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if str(default) == "retro":
|
if str(default) == 'retro':
|
||||||
identicon = Identicon.render(kwargs["digest"])
|
identicon = Identicon.render(kwargs['digest'])
|
||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
img = Image.open(BytesIO(identicon))
|
img = Image.open(BytesIO(identicon))
|
||||||
img = img.resize((size, size), Image.ANTIALIAS)
|
img = img.resize((size, size), Image.ANTIALIAS)
|
||||||
img.save(data, "PNG", quality=JPEG_QUALITY)
|
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
response = CachingHttpResponse(uri, data, content_type="image/png")
|
response = HttpResponse(
|
||||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
data,
|
||||||
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if str(default) == "pagan":
|
if str(default) == 'pagan':
|
||||||
paganobj = pagan.Avatar(kwargs["digest"])
|
paganobj = pagan.Avatar(kwargs['digest'])
|
||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
img = paganobj.img.resize((size, size), Image.ANTIALIAS)
|
img = paganobj.img.resize((size, size), Image.ANTIALIAS)
|
||||||
img.save(data, "PNG", quality=JPEG_QUALITY)
|
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
response = CachingHttpResponse(uri, data, content_type="image/png")
|
response = HttpResponse(
|
||||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
data,
|
||||||
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if str(default) == "identicon":
|
if str(default) == 'identicon':
|
||||||
p = Pydenticon5() # pylint: disable=invalid-name
|
p = Pydenticon5()
|
||||||
# In order to make use of the whole 32 bytes digest, we need to redigest them.
|
# In order to make use of the whole 32 bytes digest, we need to redigest them.
|
||||||
newdigest = hashlib.md5(
|
newdigest = hashlib.md5(bytes(kwargs['digest'], 'utf-8')).hexdigest()
|
||||||
bytes(kwargs["digest"], "utf-8")
|
|
||||||
).hexdigest()
|
|
||||||
img = p.draw(newdigest, size, 0)
|
img = p.draw(newdigest, size, 0)
|
||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
img.save(data, "PNG", quality=JPEG_QUALITY)
|
img.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
response = CachingHttpResponse(uri, data, content_type="image/png")
|
response = HttpResponse(
|
||||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
data,
|
||||||
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if str(default) == "mmng":
|
if str(default) == 'mm' or str(default) == 'mp':
|
||||||
mmngimg = mm_ng(idhash=kwargs["digest"], size=size)
|
|
||||||
data = BytesIO()
|
|
||||||
mmngimg.save(data, "PNG", quality=JPEG_QUALITY)
|
|
||||||
data.seek(0)
|
|
||||||
response = CachingHttpResponse(uri, data, content_type="image/png")
|
|
||||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
|
||||||
return response
|
|
||||||
|
|
||||||
if str(default) == "mm" or str(default) == "mp":
|
|
||||||
# If mm is explicitly given, we need to catch that
|
# If mm is explicitly given, we need to catch that
|
||||||
static_img = path.join(
|
static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png'))
|
||||||
"static", "img", "mm", "%s%s" % (str(size), ".png")
|
|
||||||
)
|
|
||||||
if not path.isfile(static_img):
|
if not path.isfile(static_img):
|
||||||
# We trust this exists!!!
|
# We trust this exists!!!
|
||||||
static_img = path.join("static", "img", "mm", "512.png")
|
static_img = path.join('static', 'img', 'mm', '512.png')
|
||||||
# We trust static/ is mapped to /static/
|
# We trust static/ is mapped to /static/
|
||||||
return HttpResponseRedirect("/" + static_img)
|
return HttpResponseRedirect('/' + static_img)
|
||||||
return HttpResponseRedirect(default)
|
return HttpResponseRedirect(default)
|
||||||
|
|
||||||
static_img = path.join(
|
static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png'))
|
||||||
"static", "img", "nobody", "%s%s" % (str(size), ".png")
|
|
||||||
)
|
|
||||||
if not path.isfile(static_img):
|
if not path.isfile(static_img):
|
||||||
# We trust this exists!!!
|
# We trust this exists!!!
|
||||||
static_img = path.join("static", "img", "nobody", "512.png")
|
static_img = path.join('static', 'img', 'nobody', '512.png')
|
||||||
# We trust static/ is mapped to /static/
|
# We trust static/ is mapped to /static/
|
||||||
return HttpResponseRedirect("/" + static_img)
|
return HttpResponseRedirect('/' + static_img)
|
||||||
|
|
||||||
imgformat = obj.photo.format
|
imgformat = obj.photo.format
|
||||||
photodata = Image.open(BytesIO(obj.photo.data))
|
photodata = Image.open(BytesIO(obj.photo.data))
|
||||||
@@ -330,35 +268,30 @@ class AvatarImageView(TemplateView):
|
|||||||
obj.photo.save()
|
obj.photo.save()
|
||||||
obj.access_count += 1
|
obj.access_count += 1
|
||||||
obj.save()
|
obj.save()
|
||||||
if imgformat == "jpg":
|
if imgformat == 'jpg':
|
||||||
imgformat = "jpeg"
|
imgformat = 'jpeg'
|
||||||
response = CachingHttpResponse(uri, data, content_type="image/%s" % imgformat)
|
response = HttpResponse(
|
||||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
data,
|
||||||
|
content_type='image/%s' % imgformat)
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class GravatarProxyView(View):
|
class GravatarProxyView(View):
|
||||||
"""
|
'''
|
||||||
Proxy request to Gravatar and return the image from there
|
Proxy request to Gravatar and return the image from there
|
||||||
"""
|
'''
|
||||||
|
|
||||||
# TODO: Do cache images!! Memcached?
|
# TODO: Do cache images!! Memcached?
|
||||||
|
|
||||||
def get(
|
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument
|
||||||
self, request, *args, **kwargs
|
'''
|
||||||
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
|
|
||||||
"""
|
|
||||||
Override get from parent class
|
Override get from parent class
|
||||||
"""
|
'''
|
||||||
|
|
||||||
def redir_default(default=None):
|
def redir_default(default=None):
|
||||||
url = (
|
url = reverse_lazy(
|
||||||
reverse_lazy("avatar_view", args=[kwargs["digest"]])
|
'avatar_view',
|
||||||
+ "?s=%i" % size
|
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
|
||||||
+ "&forcedefault=y"
|
if default != None:
|
||||||
)
|
url += '&default=%s' % default
|
||||||
if default is not None:
|
|
||||||
url += "&default=%s" % default
|
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
size = get_size(request)
|
size = get_size(request)
|
||||||
@@ -366,94 +299,59 @@ class GravatarProxyView(View):
|
|||||||
default = None
|
default = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if str(request.GET["default"]) != "None":
|
if str(request.GET['default']) != 'None':
|
||||||
default = request.GET["default"]
|
default = request.GET['default']
|
||||||
except Exception: # pylint: disable=bare-except
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if str(default) != "wavatar":
|
if str(default) != 'wavatar':
|
||||||
# This part is special/hackish
|
# This part is special/hackish
|
||||||
# Check if the image returned by Gravatar is their default image, if so,
|
# Check if the image returned by Gravatar is their default image, if so,
|
||||||
# redirect to our default instead.
|
# redirect to our default instead.
|
||||||
gravatar_test_url = (
|
gravatar_test_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
||||||
"https://secure.gravatar.com/avatar/"
|
+ '?s=%i' % 50
|
||||||
+ kwargs["digest"]
|
|
||||||
+ "?s=%i&d=%i" % (50, 404)
|
|
||||||
)
|
|
||||||
if cache.get(gravatar_test_url) == "default":
|
|
||||||
# DEBUG only
|
|
||||||
# print("Cached Gravatar response: Default.")
|
|
||||||
return redir_default(default)
|
|
||||||
try:
|
try:
|
||||||
urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
|
testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
|
||||||
except HTTPError as exc:
|
data = BytesIO(testdata.read())
|
||||||
if exc.code == 404:
|
if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a':
|
||||||
cache.set(gravatar_test_url, "default", 60)
|
return redir_default(default)
|
||||||
else:
|
except Exception as exc:
|
||||||
print("Gravatar test url fetch failed: %s" % exc)
|
print('Gravatar test url fetch failed: %s' % exc)
|
||||||
return redir_default(default)
|
|
||||||
|
|
||||||
gravatar_url = (
|
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
||||||
"https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % size
|
+ '?s=%i' % size + '&d=%s' % default
|
||||||
)
|
|
||||||
if default:
|
|
||||||
gravatar_url += "&d=%s" % default
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if cache.get(gravatar_url) == "err":
|
|
||||||
print("Cached Gravatar fetch failed with URL error: %s" % gravatar_url)
|
|
||||||
return redir_default(default)
|
|
||||||
|
|
||||||
gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)
|
gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)
|
||||||
except HTTPError as exc:
|
except HTTPError as exc:
|
||||||
if exc.code != 404 and exc.code != 503:
|
if exc.code != 404 and exc.code != 503:
|
||||||
print(
|
print(
|
||||||
"Gravatar fetch failed with an unexpected %s HTTP error: %s"
|
'Gravatar fetch failed with an unexpected %s HTTP error' %
|
||||||
% (exc.code, gravatar_url)
|
exc.code)
|
||||||
)
|
|
||||||
cache.set(gravatar_url, "err", 30)
|
|
||||||
return redir_default(default)
|
return redir_default(default)
|
||||||
except URLError as exc:
|
except URLError as exc:
|
||||||
print("Gravatar fetch failed with URL error: %s" % exc.reason)
|
print(
|
||||||
cache.set(gravatar_url, "err", 30)
|
'Gravatar fetch failed with URL error: %s' %
|
||||||
|
exc.reason)
|
||||||
return redir_default(default)
|
return redir_default(default)
|
||||||
except SSLError as exc:
|
except SSLError as exc:
|
||||||
print("Gravatar fetch failed with SSL error: %s" % exc.reason)
|
print(
|
||||||
cache.set(gravatar_url, "err", 30)
|
'Gravatar fetch failed with SSL error: %s' %
|
||||||
|
exc.reason)
|
||||||
return redir_default(default)
|
return redir_default(default)
|
||||||
try:
|
try:
|
||||||
data = BytesIO(gravatarimagedata.read())
|
data = BytesIO(gravatarimagedata.read())
|
||||||
img = Image.open(data)
|
img = Image.open(data)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
response = HttpResponse(
|
response = HttpResponse(
|
||||||
data.read(), content_type="image/%s" % file_format(img.format)
|
data.read(),
|
||||||
)
|
content_type='image/%s' % file_format(img.format))
|
||||||
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
print("Value error: %s" % exc)
|
print('Value error: %s' % exc)
|
||||||
return redir_default(default)
|
return redir_default(default)
|
||||||
|
|
||||||
# We shouldn't reach this point... But make sure we do something
|
# We shouldn't reach this point... But make sure we do something
|
||||||
return redir_default(default)
|
return redir_default(default)
|
||||||
|
|
||||||
|
|
||||||
class StatsView(TemplateView, JsonResponse):
|
|
||||||
"""
|
|
||||||
Return stats
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get(
|
|
||||||
self, request, *args, **kwargs
|
|
||||||
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
|
|
||||||
retval = {
|
|
||||||
"users": User.objects.count(),
|
|
||||||
"mails": ConfirmedEmail.objects.count(),
|
|
||||||
"openids": ConfirmedOpenId.objects.count(), # pylint: disable=no-member
|
|
||||||
"unconfirmed_mails": UnconfirmedEmail.objects.count(), # pylint: disable=no-member
|
|
||||||
"unconfirmed_openids": UnconfirmedOpenId.objects.count(), # pylint: disable=no-member
|
|
||||||
"avatars": Photo.objects.count(), # pylint: disable=no-member
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonResponse(retval)
|
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
sys.stderr.buffer.write(
|
sys.stderr.buffer.write(b'%s' % bytes(os.environ.get("QUERY_STRING", "No Query String in url"), 'utf-8'))
|
||||||
b"%s" % bytes(os.environ.get("QUERY_STRING", "No Query String in url"), "utf-8")
|
|
||||||
)
|
|
||||||
|
|
||||||
link = "https://www.libravatar.org/avatar/%s" % os.environ.get("QUERY_STRING", "x" * 32)
|
link = 'https://www.libravatar.org/avatar/%s' % os.environ.get("QUERY_STRING", 'x'*32)
|
||||||
sys.stderr.buffer.write(b"%s" % bytes(link, "utf-8"))
|
sys.stderr.buffer.write(b'%s' % bytes(link, 'utf-8'))
|
||||||
|
|
||||||
data = None
|
data = None
|
||||||
with urllib.request.urlopen(link) as f:
|
with urllib.request.urlopen(link) as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
|
||||||
for header in f.headers._headers:
|
for header in f.headers._headers:
|
||||||
if header[0] == "Content-Type":
|
if header[0] == 'Content-Type':
|
||||||
sys.stdout.buffer.write(
|
sys.stdout.buffer.write(b"%s: %s\n\n" % (bytes(header[0], 'utf-8'), bytes(header[1], 'utf-8')))
|
||||||
b"%s: %s\n\n" % (bytes(header[0], "utf-8"), bytes(header[1], "utf-8"))
|
|
||||||
)
|
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,26 @@
|
|||||||
autopep8
|
autopep8
|
||||||
bcrypt
|
bcrypt
|
||||||
defusedxml
|
defusedxml
|
||||||
Django < 4.0
|
Django
|
||||||
django-anymail[mailgun]
|
|
||||||
django-auth-ldap
|
django-auth-ldap
|
||||||
django-bootstrap4
|
django-bootstrap4
|
||||||
django-coverage-plugin
|
django-coverage-plugin
|
||||||
django-extensions
|
django-extensions
|
||||||
django-ipware
|
django-ipware
|
||||||
django-user-accounts
|
django-user-accounts
|
||||||
email-validator
|
|
||||||
fabric
|
fabric
|
||||||
flake8-respect-noqa
|
flake8-respect-noqa
|
||||||
git+https://github.com/ercpe/pydenticon5.git
|
|
||||||
git+https://github.com/flavono123/identicon.git
|
|
||||||
git+https://github.com/ofalk/django-openid-auth
|
git+https://github.com/ofalk/django-openid-auth
|
||||||
git+https://github.com/ofalk/monsterid.git
|
|
||||||
git+https://github.com/ofalk/Robohash.git@devel
|
|
||||||
mysqlclient
|
|
||||||
notsetuptools
|
|
||||||
pagan
|
|
||||||
Pillow
|
Pillow
|
||||||
pip
|
pip
|
||||||
psycopg2-binary
|
|
||||||
py3dns
|
py3dns
|
||||||
pydocstyle
|
pydocstyle
|
||||||
pyLibravatar
|
pyLibravatar
|
||||||
pylint
|
pylint
|
||||||
PyMySQL
|
PyMySQL
|
||||||
|
python3-openid
|
||||||
python-coveralls
|
python-coveralls
|
||||||
python-language-server
|
python-language-server
|
||||||
python-memcached
|
|
||||||
python3-openid
|
|
||||||
pytz
|
pytz
|
||||||
rope
|
rope
|
||||||
setuptools
|
setuptools
|
||||||
@@ -39,3 +28,14 @@ six
|
|||||||
social-auth-app-django
|
social-auth-app-django
|
||||||
wheel
|
wheel
|
||||||
yapf
|
yapf
|
||||||
|
django-anymail[mailgun]
|
||||||
|
mysqlclient
|
||||||
|
psycopg2-binary
|
||||||
|
notsetuptools
|
||||||
|
git+https://github.com/ofalk/monsterid.git
|
||||||
|
git+https://github.com/ofalk/Robohash.git@devel
|
||||||
|
python-memcached
|
||||||
|
git+https://github.com/ercpe/pydenticon5.git
|
||||||
|
git+https://github.com/flavono123/identicon.git
|
||||||
|
pagan
|
||||||
|
simple-crypt
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
||||||
targetNamespace="https://www.libravatar.org/schemas/export/0.1"
|
|
||||||
xmlns="https://www.libravatar.org/schemas/export/0.1"
|
|
||||||
elementFormDefault="qualified">
|
|
||||||
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
XML Schema for Libravatar user account exports.
|
|
||||||
Last Modifed 2011-04-09
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
|
|
||||||
<xsd:element name="user">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for emails, openids and photos elements.
|
|
||||||
This is the root element of the XML file.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="account" type="accountType"/>
|
|
||||||
<xsd:element name="emails" type="emailList"/>
|
|
||||||
<xsd:element name="openids" type="openidList"/>
|
|
||||||
<xsd:element name="photos" type="photoList"/>
|
|
||||||
</xsd:sequence>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
|
|
||||||
<xsd:complexType name="accountType">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Empty element holding user account-related information.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:complexContent>
|
|
||||||
<xsd:restriction base="xsd:anyType">
|
|
||||||
<xsd:attribute name="username" type="xsd:string"/>
|
|
||||||
<xsd:attribute name="site" type="xsd:string"/>
|
|
||||||
</xsd:restriction>
|
|
||||||
</xsd:complexContent>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:complexType name="emailList">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for confirmed email addresses.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="email" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
</xsd:sequence>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:complexType name="openidList">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for confirmed OpenID URLs.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="openid" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
</xsd:sequence>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:complexType name="photoList">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for uploaded/imported photos.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="photo" type="photoType" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
</xsd:sequence>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:complexType name="photoType">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for a base64 encoded photo.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:simpleContent>
|
|
||||||
<xsd:extension base="xsd:base64Binary">
|
|
||||||
<xsd:attribute name="encoding" type="xsd:string" fixed="base64"/>
|
|
||||||
<xsd:attribute name="format" type="photoFormat"/>
|
|
||||||
</xsd:extension>
|
|
||||||
</xsd:simpleContent>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:simpleType name="photoFormat">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
File type for the given photo.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:restriction base="xsd:string">
|
|
||||||
<xsd:enumeration value="jpg"/>
|
|
||||||
<xsd:enumeration value="png"/>
|
|
||||||
</xsd:restriction>
|
|
||||||
</xsd:simpleType>
|
|
||||||
|
|
||||||
</xsd:schema>
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
||||||
targetNamespace="https://www.libravatar.org/schemas/export/0.2"
|
|
||||||
xmlns="https://www.libravatar.org/schemas/export/0.2"
|
|
||||||
elementFormDefault="qualified">
|
|
||||||
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
XML Schema for Libravatar user account exports.
|
|
||||||
Last Modifed 2013-01-12
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
|
|
||||||
<xsd:element name="user">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for emails, openids and photos elements.
|
|
||||||
This is the root element of the XML file.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="account" type="accountType"/>
|
|
||||||
<xsd:element name="emails" type="emailList"/>
|
|
||||||
<xsd:element name="openids" type="openidList"/>
|
|
||||||
<xsd:element name="photos" type="photoList"/>
|
|
||||||
</xsd:sequence>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
|
|
||||||
<xsd:complexType name="accountType">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Empty element holding user account-related information.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:complexContent>
|
|
||||||
<xsd:restriction base="xsd:anyType">
|
|
||||||
<xsd:attribute name="username" type="xsd:string"/>
|
|
||||||
<xsd:attribute name="site" type="xsd:string"/>
|
|
||||||
</xsd:restriction>
|
|
||||||
</xsd:complexContent>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:complexType name="emailList">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for confirmed email addresses.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="email" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
</xsd:sequence>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:complexType name="openidList">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for confirmed OpenID URLs.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="openid" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
</xsd:sequence>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:complexType name="photoList">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for uploaded/imported photos.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="photo" type="photoType" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
</xsd:sequence>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:complexType name="photoType">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
Container for a base64 encoded photo.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:simpleContent>
|
|
||||||
<xsd:extension base="xsd:base64Binary">
|
|
||||||
<xsd:attribute name="encoding" type="xsd:string" fixed="base64"/>
|
|
||||||
<xsd:attribute name="format" type="photoFormat"/>
|
|
||||||
</xsd:extension>
|
|
||||||
</xsd:simpleContent>
|
|
||||||
</xsd:complexType>
|
|
||||||
|
|
||||||
<xsd:simpleType name="photoFormat">
|
|
||||||
<xsd:annotation>
|
|
||||||
<xsd:documentation>
|
|
||||||
File type for the given photo.
|
|
||||||
</xsd:documentation>
|
|
||||||
</xsd:annotation>
|
|
||||||
<xsd:restriction base="xsd:string">
|
|
||||||
<xsd:enumeration value="jpg"/>
|
|
||||||
<xsd:enumeration value="png"/>
|
|
||||||
<xsd:enumeration value="gif"/>
|
|
||||||
</xsd:restriction>
|
|
||||||
</xsd:simpleType>
|
|
||||||
|
|
||||||
</xsd:schema>
|
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
<li><a href="{% url 'user_preference' %}"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> {% trans 'Preferences' %}</a></li>
|
<li><a href="{% url 'user_preference' %}"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> {% trans 'Preferences' %}</a></li>
|
||||||
<li><a href="{% url 'import_photo' %}"><i class="fa fa-fw fa-envelope-square" aria-hidden="true"></i> {% trans 'Import photo via mail address' %}</a></li>
|
<li><a href="{% url 'import_photo' %}"><i class="fa fa-fw fa-envelope-square" aria-hidden="true"></i> {% trans 'Import photo via mail address' %}</a></li>
|
||||||
<li><a href="{% url 'upload_export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Import libravatar XML export' %}</a></li>
|
<li><a href="{% url 'upload_export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Import libravatar XML export' %}</a></li>
|
||||||
<li><a href="{% url 'export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Download your libravatar data' %}</a></li>
|
|
||||||
<li><a href="{% url 'password_change' %}"><i class="fa fa-fw fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
|
<li><a href="{% url 'password_change' %}"><i class="fa fa-fw fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
|
||||||
<li><a href="{% url 'password_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
|
<li><a href="{% url 'password_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
|
||||||
<li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>
|
<li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
There are a few ways to get in touch with the ivatar/libravatar developers:
|
There are a few ways to get in touch with the ivatar/libravatar developers:
|
||||||
<h4 style="margin-top: 2rem;margin-bottom: 1rem;">IRC</h4>
|
<h4 style="margin-top: 2rem;margin-bottom: 1rem;">IRC</h4>
|
||||||
|
|
||||||
You can join the Libravatar community chat at <a href="https://matrix.to/#/#libravatar:matrix.org?via=shivering-isles.com&via=matrix.org&via=foad.me.uk" title="Libravatar on Matrix">#libravatar:matrix.org</a>. It is also bridged to #libravatar on irc.<a href="https://libera.chat/">libera.chat</a> for those prefering IRC.
|
If you have an IRC client already, you can join #libravatar on chat.freenode.net.
|
||||||
|
<br/>
|
||||||
|
Otherwise, you can use this <a href="http://webchat.freenode.net/?channels=libravatar" title="http://webchat.freenode.net/?channels=libravatar">simple web interface</a>.
|
||||||
<br/>
|
<br/>
|
||||||
Please keep in mind that you may live in a different timezone than most of the developers. So if you do not get a response, it's not because we're ignoring you, it's probably because we're sleeping :)
|
Please keep in mind that you may live in a different timezone than most of the developers. So if you do not get a response, it's not because we're ignoring you, it's probably because we're sleeping :)
|
||||||
|
|
||||||
|
|||||||
@@ -82,11 +82,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding-bottom:10px;" class="text-center">
|
<div class="text-center">
|
||||||
<h3>{% trans 'Big thanks to our sponsors without whom none of this would be possible!' %}</h3>
|
<h3>{% trans 'Big thanks to our sponsors without whom none of this would be possible!' %}</h3>
|
||||||
<a href="https://fedoraproject.org/" target="_new" ><img src="{% static '/img/Fedora_logo.png' %}" title="Fedora Project" alt="{% trans 'Fedora Logo' %}"></a>
|
<a href="https://fedoraproject.org/" target="_new" ><img src="{% static '/img/Fedora_logo.png' %}" title="Fedora Project" alt="{% trans 'Fedora Logo' %}"></a>
|
||||||
<br/>
|
|
||||||
<a href="https://gandi.net/" target="_new" ><img src="{% static '/img/gandi_logo.png' %}" height="60" title="Gandi" alt="{% trans 'Gandi Logo' %}"></a>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -60,12 +60,6 @@ ivatar/Libravatar more secure by reporting security issues to us.
|
|||||||
title="https://www.linkedin.com/in/naharronak/" target="_new">
|
title="https://www.linkedin.com/in/naharronak/" target="_new">
|
||||||
Ronak Nahar</a>:
|
Ronak Nahar</a>:
|
||||||
Spotted and reported open server status from Apache HTTPD.</li>
|
Spotted and reported open server status from Apache HTTPD.</li>
|
||||||
<li>
|
|
||||||
<a href="https://daniel.priv.no/"
|
|
||||||
title="https://daniel.priv.no/" target="_new">
|
|
||||||
Daniel Aleksandersen</a>:
|
|
||||||
Spotted and reported an open redirect vulnerability, as described in <a href="https://cwe.mitre.org/data/definitions/601.html" taget="_new">CWE-601</a>.</li>
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div style="height:40px"></div>
|
<div style="height:40px"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user