mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-16 04:58:01 +00:00
Merge branch 'master' into trust
This commit is contained in:
@@ -5,6 +5,11 @@ omit =
|
|||||||
node_modules/*
|
node_modules/*
|
||||||
.virtualenv/*
|
.virtualenv/*
|
||||||
import_libravatar.py
|
import_libravatar.py
|
||||||
|
requirements.txt
|
||||||
|
static/admin/*
|
||||||
|
static/humans.txt
|
||||||
|
static/img/robots.txt
|
||||||
|
|
||||||
|
|
||||||
[html]
|
[html]
|
||||||
extra_css = coverage_extra_style.css
|
extra_css = coverage_extra_style.css
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
image: docker.io/ofalk/fedora31-python3
|
image:
|
||||||
|
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
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ cd ivatar
|
|||||||
virtualenv -p python3 .virtualenv
|
virtualenv -p python3 .virtualenv
|
||||||
source .virtualenv/bin/activate
|
source .virtualenv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
pip install pillow
|
||||||
~~~~
|
~~~~
|
||||||
|
|
||||||
## (SQL) Migrations
|
## (SQL) Migrations
|
||||||
|
|||||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
20
config.py
20
config.py
@@ -52,7 +52,7 @@ OPENID_CREATE_USERS = True
|
|||||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||||
|
|
||||||
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
|
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
|
||||||
IVATAR_VERSION = '1.3'
|
IVATAR_VERSION = '1.4'
|
||||||
|
|
||||||
SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/')
|
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/')
|
BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/')
|
||||||
@@ -108,7 +108,7 @@ else:
|
|||||||
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
|
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
|
||||||
}
|
}
|
||||||
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
|
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
|
||||||
except Exception as exc:
|
except Exception as exc: # pragma: nocover
|
||||||
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')
|
||||||
@@ -143,9 +143,6 @@ if 'POSTGRESQL_DATABASE' in os.environ:
|
|||||||
'HOST': 'postgresql',
|
'HOST': 'postgresql',
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')):
|
|
||||||
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
|
|
||||||
|
|
||||||
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||||
|
|
||||||
USE_X_FORWARDED_HOST = True
|
USE_X_FORWARDED_HOST = True
|
||||||
@@ -185,9 +182,20 @@ CACHES = {
|
|||||||
'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
|
# so the sites don't hit ivatar so much - it's what's set in the HTTP header
|
||||||
CACHE_IMAGES_MAX_AGE = 5 * 60
|
CACHE_IMAGES_MAX_AGE = 5 * 60
|
||||||
|
|
||||||
|
CACHE_RESPONSE = True
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -62,12 +62,14 @@ class AddEmailForm(forms.Form):
|
|||||||
_('Address already added, currently unconfirmed'))
|
_('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)
|
||||||
if ConfirmedEmail.objects.filter(
|
check_mail = ConfirmedEmail.objects.filter(
|
||||||
email=self.cleaned_data['email']).exists():
|
email=self.cleaned_data['email'])
|
||||||
self.add_error(
|
if check_mail.exists():
|
||||||
'email',
|
msg = _('Address already confirmed (by someone else)')
|
||||||
_('Address already confirmed (by someone else)'))
|
if check_mail.first().user == request.user:
|
||||||
|
msg = _('Address already confirmed (by you)')
|
||||||
|
self.add_error('email', msg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
unconfirmed = UnconfirmedEmail()
|
unconfirmed = UnconfirmedEmail()
|
||||||
|
|||||||
23
ivatar/ivataraccount/migrations/0016_auto_20210413_0904.py
Normal file
23
ivatar/ivataraccount/migrations/0016_auto_20210413_0904.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-04-13 09:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ivataraccount', '0015_auto_20200225_0934'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='unconfirmedemail',
|
||||||
|
name='last_send_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='unconfirmedemail',
|
||||||
|
name='last_status',
|
||||||
|
field=models.TextField(blank=True, max_length=2047, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
48
ivatar/ivataraccount/migrations/0017_auto_20210528_1314.py
Normal file
48
ivatar/ivataraccount/migrations/0017_auto_20210528_1314.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 3.2.3 on 2021-05-28 13:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ivataraccount', '0016_auto_20210413_0904'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='confirmedemail',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='confirmedopenid',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='openidassociation',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='openidnonce',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='photo',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unconfirmedemail',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unconfirmedopenid',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -347,6 +347,8 @@ class UnconfirmedEmail(BaseAccountModel):
|
|||||||
'''
|
'''
|
||||||
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
|
||||||
'''
|
'''
|
||||||
@@ -357,11 +359,12 @@ class UnconfirmedEmail(BaseAccountModel):
|
|||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None,
|
def save(self, force_insert=False, force_update=False, using=None,
|
||||||
update_fields=None):
|
update_fields=None):
|
||||||
hash_object = hashlib.new('sha256')
|
if not self.verification_key:
|
||||||
hash_object.update(
|
hash_object = hashlib.new('sha256')
|
||||||
urandom(1024) + self.user.username.encode('utf-8') # pylint: disable=no-member
|
hash_object.update(
|
||||||
) # pylint: disable=no-member
|
urandom(1024) + self.user.username.encode('utf-8') # pylint: disable=no-member
|
||||||
self.verification_key = hash_object.hexdigest()
|
) # pylint: disable=no-member
|
||||||
|
self.verification_key = hash_object.hexdigest()
|
||||||
super(UnconfirmedEmail, self).save(
|
super(UnconfirmedEmail, self).save(
|
||||||
force_insert,
|
force_insert,
|
||||||
force_update,
|
force_update,
|
||||||
@@ -382,11 +385,17 @@ class UnconfirmedEmail(BaseAccountModel):
|
|||||||
'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)
|
||||||
send_mail(
|
try:
|
||||||
email_subject, email_body, DEFAULT_FROM_EMAIL,
|
send_mail(
|
||||||
[self.email])
|
email_subject, email_body, DEFAULT_FROM_EMAIL,
|
||||||
|
[self.email])
|
||||||
|
except Exception as e:
|
||||||
|
self.last_status = "%s" % e
|
||||||
|
self.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -7,36 +7,56 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans 'Account settings' %}</h1>
|
<h1>{% trans 'Account settings' %}</h1>
|
||||||
|
|
||||||
<div class="form-group">
|
<label for="id_username">{% trans 'Username' %}:</label>
|
||||||
<label for="id_email">{% trans 'Your email' %}:</label>
|
<input type="text" name="username" class="form-control" id="id_username" disabled value="{{ user.username }}" style="max-width:600px;">
|
||||||
<input type="text" name="email" disabled class="form-control" value="{{ user.email }}" id="id_email" style="max-width:600px;">
|
<form action="{% url 'user_preference' %}" method="post">{% csrf_token %}
|
||||||
</div>
|
<div class="form-group">
|
||||||
|
<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 }}" {% if language.code == LANGUAGE_CODE %}checked{% endif %}>
|
<input type="radio" name="language" value="{{ language.code }}" id="language-{{ language.code }}"
|
||||||
<label for="language-{{ language.code }}">{{ language.name_local }}</label>
|
{% if language.code == LANGUAGE_CODE %}checked{% endif %}>
|
||||||
</div>
|
<label for="language-{{ language.code }}">{{ language.name_local }}</label>
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
<br/>
|
</div>
|
||||||
<button type="submit" class="button">{% trans 'Save' %}</button>
|
<br/>
|
||||||
|
<button type="submit" class="button">{% trans 'Save' %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<div style="height:40px"></div>
|
<div style="height:100px"></div>
|
||||||
|
|
||||||
<!-- <p><a href="{% url 'export' %}" class="button">{% trans 'Export your data' %}</a></p> -->
|
<!-- <p><a href="{% url 'export' %}" class="button">{% trans 'Export your data' %}</a></p> -->
|
||||||
|
|
||||||
<p><a href="{% url 'delete' %}" class="button">{% trans 'Permanently delete your account' %}</a></p>
|
<!-- TODO: Better coloring of the button -->
|
||||||
|
<p><a href="{% url 'delete' %}" class="button" style="background:red; color:white;">{% trans 'Permanently delete your account' %}</a></p>
|
||||||
<div style="height:2rem"></div>
|
<div style="height:2rem"></div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -155,10 +155,13 @@ outline: inherit;
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not max_photos %}
|
{% if not max_photos %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'upload_photo' %}" class="button">{% trans 'Upload a new photo' %}</a>
|
<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>
|
<a href="{% url 'import_photo' %}" class="button">{% trans 'Import photo from other services' %}</a>
|
||||||
</p>
|
</p>
|
||||||
|
{% else %}
|
||||||
|
{% trans "You've reached the maximum number of allowed images!" %}<br/>
|
||||||
|
{% trans "No further images can be uploaded." %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div style="height:40px"></div>
|
<div style="height:40px"></div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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.core import mail
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -24,7 +25,7 @@ django.setup()
|
|||||||
# pylint: disable=wrong-import-position
|
# pylint: disable=wrong-import-position
|
||||||
from ivatar import settings
|
from ivatar import settings
|
||||||
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
|
||||||
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId
|
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId, ConfirmedEmail
|
||||||
from ivatar.utils import random_string
|
from ivatar.utils import random_string
|
||||||
# pylint: enable=wrong-import-position
|
# pylint: enable=wrong-import-position
|
||||||
|
|
||||||
@@ -451,7 +452,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
'email',
|
'email',
|
||||||
'Address already added, currently unconfirmed')
|
'Address already added, currently unconfirmed')
|
||||||
|
|
||||||
def test_add_already_confirmed_email(self): # pylint: disable=invalid-name
|
def test_add_already_confirmed_email_self(self): # pylint: disable=invalid-name
|
||||||
'''
|
'''
|
||||||
Request adding mail address that is already confirmed (by someone)
|
Request adding mail address that is already confirmed (by someone)
|
||||||
'''
|
'''
|
||||||
@@ -459,6 +460,33 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
# Should set EMAIL_BACKEND, so no need to do it here
|
# Should set EMAIL_BACKEND, so no need to do it here
|
||||||
self.test_confirm_email()
|
self.test_confirm_email()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('add_email'), {
|
||||||
|
'email': self.email,
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertFormError(
|
||||||
|
response,
|
||||||
|
'form',
|
||||||
|
'email',
|
||||||
|
'Address already confirmed (by you)')
|
||||||
|
|
||||||
|
def test_add_already_confirmed_email_other(self): # pylint: disable=invalid-name
|
||||||
|
'''
|
||||||
|
Request adding mail address that is already confirmed (by someone)
|
||||||
|
'''
|
||||||
|
# Create test mail and confirm it, reuse test code
|
||||||
|
# Should set EMAIL_BACKEND, so no need to do it here
|
||||||
|
self.test_confirm_email()
|
||||||
|
|
||||||
|
# Create another user and assign the mail address to that one
|
||||||
|
# in order to test the correct error message
|
||||||
|
otheruser = User.objects.create(username='otheruser')
|
||||||
|
confirmedemail = ConfirmedEmail.objects.last()
|
||||||
|
confirmedemail.user = otheruser
|
||||||
|
confirmedemail.save()
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('add_email'), {
|
reverse('add_email'), {
|
||||||
'email': self.email,
|
'email': self.email,
|
||||||
@@ -1513,3 +1541,79 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
200,
|
200,
|
||||||
'First and last name not correctly listed in profile page',
|
'First and last name not correctly listed in profile page',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_password_reset_page(self):
|
||||||
|
'''
|
||||||
|
Just test if the password reset page come up correctly
|
||||||
|
'''
|
||||||
|
response = self.client.get(reverse('password_reset'))
|
||||||
|
self.assertEqual(response.status_code, 200, 'no 200 ok?')
|
||||||
|
|
||||||
|
def test_password_reset_wo_mail(self):
|
||||||
|
'''
|
||||||
|
Test if the password reset doesn't error out
|
||||||
|
if the mail address doesn't exist
|
||||||
|
'''
|
||||||
|
# Avoid sending out mails
|
||||||
|
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
|
|
||||||
|
# Empty database / eliminate existing users
|
||||||
|
User.objects.all().delete()
|
||||||
|
url = reverse('password_reset')
|
||||||
|
response = self.client.post(
|
||||||
|
url, {
|
||||||
|
'email': 'asdf@asdf.local',
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200, 'password reset page not working?')
|
||||||
|
self.assertEqual(len(mail.outbox), 0, 'user does not exist, there should be no mail in the outbox!')
|
||||||
|
|
||||||
|
def test_password_reset_w_mail(self):
|
||||||
|
'''
|
||||||
|
Test if the password reset works correctly with email in
|
||||||
|
User object
|
||||||
|
'''
|
||||||
|
# Avoid sending out mails
|
||||||
|
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
|
|
||||||
|
url = reverse('password_reset')
|
||||||
|
# Our test user doesn't have an email address by default - but we need one set
|
||||||
|
self.user.email = 'asdf@asdf.local'
|
||||||
|
self.user.save()
|
||||||
|
response = self.client.post(
|
||||||
|
url, {
|
||||||
|
'email': self.user.email,
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200, 'password reset page not working?')
|
||||||
|
self.assertEqual(len(mail.outbox), 1, 'User exists, there should be a mail in the outbox!')
|
||||||
|
self.assertEqual(mail.outbox[0].to[0], self.user.email, 'Sending mails to the wrong \
|
||||||
|
mail address?')
|
||||||
|
|
||||||
|
def test_password_reset_w_confirmed_mail(self):
|
||||||
|
'''
|
||||||
|
Test if the password reset works correctly with confirmed
|
||||||
|
mail
|
||||||
|
'''
|
||||||
|
# Avoid sending out mails
|
||||||
|
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
|
|
||||||
|
url = reverse('password_reset')
|
||||||
|
# Our test user doesn't have a confirmed mail identity - add one
|
||||||
|
self.user.confirmedemail_set.create(email='asdf@asdf.local')
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
url, {
|
||||||
|
'email': self.user.confirmedemail_set.first().email,
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
# Since the object is touched in another process, we need to refresh it
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertEqual(response.status_code, 200, 'password reset page not working?')
|
||||||
|
self.assertEqual(self.user.email, self.user.confirmedemail_set.first().email, 'The password reset view, should have corrected this!')
|
||||||
|
self.assertEqual(len(mail.outbox), 1, 'user exists, there should be a mail in the outbox!')
|
||||||
|
self.assertEqual(mail.outbox[0].to[0], self.user.email, 'why are we sending mails to the wrong mail address?')
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ urlpatterns = [ # pylint: disable=invalid-name
|
|||||||
r'import_photo/$',
|
r'import_photo/$',
|
||||||
ImportPhotoView.as_view(), name='import_photo'),
|
ImportPhotoView.as_view(), name='import_photo'),
|
||||||
url(
|
url(
|
||||||
r'import_photo/(?P<email_addr>[\w.]+@[\w.]+.[\w.]+)',
|
r'import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)',
|
||||||
ImportPhotoView.as_view(), name='import_photo'),
|
ImportPhotoView.as_view(), name='import_photo'),
|
||||||
url(
|
url(
|
||||||
r'import_photo/(?P<email_id>\d+)',
|
r'import_photo/(?P<email_id>\d+)',
|
||||||
|
|||||||
@@ -731,6 +731,9 @@ class UserPreferenceView(FormView, UpdateView):
|
|||||||
success_url = reverse_lazy('user_preference')
|
success_url = reverse_lazy('user_preference')
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
||||||
|
'''
|
||||||
|
Process POST-ed data from this form
|
||||||
|
'''
|
||||||
userpref = None
|
userpref = None
|
||||||
try:
|
try:
|
||||||
userpref = self.request.user.userpreference
|
userpref = self.request.user.userpreference
|
||||||
@@ -738,6 +741,30 @@ class UserPreferenceView(FormView, UpdateView):
|
|||||||
userpref = UserPreference(user=self.request.user)
|
userpref = UserPreference(user=self.request.user)
|
||||||
userpref.theme = request.POST['theme']
|
userpref.theme = request.POST['theme']
|
||||||
userpref.save()
|
userpref.save()
|
||||||
|
try:
|
||||||
|
if request.POST['email'] != self.request.user.email:
|
||||||
|
addresses = list(self.request.user.confirmedemail_set.all().values_list('email', flat=True))
|
||||||
|
if request.POST['email'] not in addresses:
|
||||||
|
messages.error(self.request, _('Mail address not allowed: %s' % request.POST['email']))
|
||||||
|
else:
|
||||||
|
self.request.user.email = request.POST['email']
|
||||||
|
self.request.user.save()
|
||||||
|
messages.info(self.request, _('Mail address changed.'))
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
messages.error(self.request, _('Error setting new mail address: %s' % e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if request.POST['first_name'] or request.POST['last_name']:
|
||||||
|
if request.POST['first_name'] != self.request.user.first_name:
|
||||||
|
self.request.user.first_name = request.POST['first_name']
|
||||||
|
messages.info(self.request, _('First name changed.'))
|
||||||
|
if request.POST['last_name'] != self.request.user.last_name:
|
||||||
|
self.request.user.last_name = request.POST['last_name']
|
||||||
|
messages.info(self.request, _('Last name changed.'))
|
||||||
|
self.request.user.save()
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
messages.error(self.request, _('Error setting names: %s' % e))
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse_lazy('user_preference'))
|
return HttpResponseRedirect(reverse_lazy('user_preference'))
|
||||||
|
|
||||||
|
|
||||||
@@ -898,6 +925,18 @@ class ProfileView(TemplateView):
|
|||||||
self._confirm_claimed_openid()
|
self._confirm_claimed_openid()
|
||||||
return super().get(self, request, args, kwargs)
|
return super().get(self, request, args, kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
'''
|
||||||
|
Provide additional context data, like if max_photos is reached
|
||||||
|
already or not.
|
||||||
|
'''
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['max_photos'] = False
|
||||||
|
if self.request.user:
|
||||||
|
if self.request.user.photo_set.all().count() >= MAX_NUM_PHOTOS:
|
||||||
|
context['max_photos'] = True
|
||||||
|
return context
|
||||||
|
|
||||||
def _confirm_claimed_openid(self):
|
def _confirm_claimed_openid(self):
|
||||||
openids = self.request.user.useropenid_set.all()
|
openids = self.request.user.useropenid_set.all()
|
||||||
# If there is only one OpenID, we eventually need to add it to the user account
|
# If there is only one OpenID, we eventually need to add it to the user account
|
||||||
@@ -924,18 +963,47 @@ class PasswordResetView(PasswordResetViewOriginal):
|
|||||||
'''
|
'''
|
||||||
Since we have the mail addresses in ConfirmedEmail model,
|
Since we have the mail addresses in ConfirmedEmail model,
|
||||||
we need to set the email on the user object in order for the
|
we need to set the email on the user object in order for the
|
||||||
PasswordResetView class to pick up the correct user
|
PasswordResetView class to pick up the correct user.
|
||||||
|
In case we have the mail address in the User objecct, we still
|
||||||
|
need to assign a random password in order for PasswordResetView
|
||||||
|
class to pick up the user - else it will silently do nothing.
|
||||||
'''
|
'''
|
||||||
if 'email' in request.POST:
|
if 'email' in request.POST:
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# Try to find the user via the normal user class
|
||||||
try:
|
try:
|
||||||
confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email'])
|
user = User.objects.get(email=request.POST['email'])
|
||||||
confirmed_email.user.email = confirmed_email.email
|
except ObjectDoesNotExist as exc: # pylint: disable=unused-variable
|
||||||
if not confirmed_email.user.password or confirmed_email.user.password == '!':
|
# keep this for debugging only
|
||||||
random_pass = User.objects.make_random_password()
|
# print('Exception: %s' % exc)
|
||||||
confirmed_email.user.set_pasword(random_pass)
|
|
||||||
confirmed_email.user.save()
|
|
||||||
except Exception as exc:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# If we didn't find the user in the previous step,
|
||||||
|
# try the ConfirmedEmail class instead.
|
||||||
|
# If we find the user there, we need to set the mail
|
||||||
|
# attribute on the user object accordingly
|
||||||
|
if not user:
|
||||||
|
try:
|
||||||
|
confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email'])
|
||||||
|
user = confirmed_email.user
|
||||||
|
user.email = confirmed_email.email
|
||||||
|
user.save()
|
||||||
|
except ObjectDoesNotExist as exc: # pylint: disable=unused-variable
|
||||||
|
# keep this for debugging only
|
||||||
|
# print('Exception: %s' % exc)
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If we found the user, set a random password. Else, the
|
||||||
|
# ResetPasswordView class will silently ignore the password
|
||||||
|
# reset request
|
||||||
|
if user:
|
||||||
|
if not user.password or user.password.startswith('!'):
|
||||||
|
random_pass = User.objects.make_random_password()
|
||||||
|
user.set_password(random_pass)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Whatever happens above, let the original function handle the rest
|
||||||
return super().post(self, request, args, kwargs)
|
return super().post(self, request, args, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-publi
|
|||||||
with multiple proxies
|
with multiple proxies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request): # pylint: disable=no-self-use
|
||||||
"""
|
"""
|
||||||
Rewrites the proxy headers so that forwarded server is
|
Rewrites the proxy headers so that forwarded server is
|
||||||
used if available.
|
used if available.
|
||||||
|
|||||||
@@ -118,4 +118,6 @@ PROJECT_ROOT = os.path.abspath(
|
|||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ color:#335ECF;
|
|||||||
}
|
}
|
||||||
footer {
|
footer {
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
position: absolute;
|
position: relative;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
footer .container {
|
footer .container {
|
||||||
|
|||||||
46
ivatar/test_auxiliary.py
Normal file
46
ivatar/test_auxiliary.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
'''
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -2,32 +2,18 @@
|
|||||||
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 libravatar import libravatar_url
|
from ivatar.utils import random_string
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
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
|
||||||
'''
|
'''
|
||||||
@@ -77,4 +63,3 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
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?')
|
||||||
|
|
||||||
|
|||||||
@@ -2,32 +2,17 @@
|
|||||||
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.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth import authenticate
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from libravatar import libravatar_url
|
from ivatar.utils import random_string
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
|
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
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ class TestCase(unittest.TestCase):
|
|||||||
'''
|
'''
|
||||||
Run wsgi import
|
Run wsgi import
|
||||||
'''
|
'''
|
||||||
import ivatar.wsgi
|
import ivatar.wsgi # pylint: disable=import-outside-toplevel
|
||||||
self.assertEqual(ivatar.wsgi.application.__class__,
|
self.assertEqual(ivatar.wsgi.application.__class__,
|
||||||
django.core.handlers.wsgi.WSGIHandler)
|
django.core.handlers.wsgi.WSGIHandler)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class CheckForm(forms.Form):
|
|||||||
('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')),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -98,3 +99,12 @@ class CheckForm(forms.Form):
|
|||||||
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']
|
||||||
|
print(data)
|
||||||
|
return data.lower()
|
||||||
|
|||||||
@@ -2,6 +2,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
|
||||||
@@ -12,10 +14,9 @@ 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):
|
||||||
@@ -32,12 +33,20 @@ class CheckDomainView(FormView):
|
|||||||
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'], False)
|
result['avatar_server_http_ipv4'] = lookup_ip_address(
|
||||||
result['avatar_server_http_ipv6'] = lookup_ip_address(result['avatar_server_http'], True)
|
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_https'] = lookup_avatar_server(domain, True)
|
||||||
if result['avatar_server_https']:
|
if result['avatar_server_https']:
|
||||||
result['avatar_server_https_ipv4'] = lookup_ip_address(result['avatar_server_https'], False)
|
result['avatar_server_https_ipv4'] = lookup_ip_address(
|
||||||
result['avatar_server_https_ipv6'] = lookup_ip_address(result['avatar_server_https'], True)
|
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, {
|
return render(self.request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
'result': result,
|
'result': result,
|
||||||
@@ -75,21 +84,21 @@ class CheckView(FormView):
|
|||||||
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'],
|
email=form.cleaned_data['mail'],
|
||||||
size=size,
|
size=size,
|
||||||
default=default_url)
|
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,
|
LIBRAVATAR_SECURE_BASE_URL,
|
||||||
SECURE_BASE_URL)
|
SECURE_BASE_URL)
|
||||||
mail_hash = parse_user_identity(
|
mail_hash = parse_user_identity(
|
||||||
email=form.cleaned_data['mail'],
|
email=form.cleaned_data['mail'],
|
||||||
openid=None)[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()
|
||||||
@@ -97,24 +106,25 @@ class CheckView(FormView):
|
|||||||
mail_hash,
|
mail_hash,
|
||||||
mail_hash256)
|
mail_hash256)
|
||||||
if form.cleaned_data['openid']:
|
if form.cleaned_data['openid']:
|
||||||
if not form.cleaned_data['openid'].startswith('http://') 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'],
|
openid=form.cleaned_data['openid'],
|
||||||
size=size,
|
size=size,
|
||||||
default=default_url)
|
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,
|
LIBRAVATAR_SECURE_BASE_URL,
|
||||||
SECURE_BASE_URL)
|
SECURE_BASE_URL)
|
||||||
openid_hash = parse_user_identity(
|
openid_hash = parse_user_identity(
|
||||||
openid=form.cleaned_data['openid'],
|
openid=form.cleaned_data['openid'],
|
||||||
email=None)[0]
|
email=None)[0]
|
||||||
|
|
||||||
return render(self.request, self.template_name, {
|
return render(self.request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
@@ -166,7 +176,8 @@ def lookup_avatar_server(domain, https):
|
|||||||
|
|
||||||
records = []
|
records = []
|
||||||
for answer in dns_request.answers:
|
for answer in dns_request.answers:
|
||||||
if ('data' not in answer) or (not answer['data']) or (not answer['typename']) or (answer['typename'] != 'SRV'):
|
if ('data' not in answer) or (not answer['data']) or \
|
||||||
|
(not answer['typename']) or (answer['typename'] != 'SRV'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
record = {'priority': int(answer['data'][0]), 'weight': int(answer['data'][1]),
|
record = {'priority': int(answer['data'][0]), 'weight': int(answer['data'][1]),
|
||||||
@@ -203,7 +214,10 @@ def srv_hostname(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
|
||||||
@@ -218,7 +232,7 @@ def srv_hostname(records):
|
|||||||
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]
|
unused, ret = priority_records[0] # pylint: disable=unused-variable
|
||||||
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)
|
||||||
@@ -261,7 +275,7 @@ def lookup_ip_address(hostname, ipv6):
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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
|
from . views import AvatarImageView, GravatarProxyView, StatsView
|
||||||
|
|
||||||
urlpatterns = [ # pylint: disable=invalid-name
|
urlpatterns = [ # pylint: disable=invalid-name
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
@@ -29,20 +29,22 @@ urlpatterns = [ # pylint: disable=invalid-name
|
|||||||
GravatarProxyView.as_view(), name='gravatarproxy'),
|
GravatarProxyView.as_view(), name='gravatarproxy'),
|
||||||
url('description/', TemplateView.as_view(template_name='description.html'), name='description'),
|
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('run_your_own/', TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'),
|
url('run_your_own/',
|
||||||
|
TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'),
|
||||||
url('features/', TemplateView.as_view(template_name='features.html'), name='features'),
|
url('features/', TemplateView.as_view(template_name='features.html'), name='features'),
|
||||||
url('security/', TemplateView.as_view(template_name='security.html'), name='security'),
|
url('security/', TemplateView.as_view(template_name='security.html'), name='security'),
|
||||||
url('privacy/', TemplateView.as_view(template_name='privacy.html'), name='privacy'),
|
url('privacy/', TemplateView.as_view(template_name='privacy.html'), name='privacy'),
|
||||||
url('contact/', TemplateView.as_view(template_name='contact.html'), name='contact'),
|
url('contact/', TemplateView.as_view(template_name='contact.html'), name='contact'),
|
||||||
path('talk_to_us/', RedirectView.as_view(url='/contact'), name='talk_to_us'),
|
path('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:
|
except: # pylint: disable=bare-except
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if MAINTENANCE:
|
if MAINTENANCE:
|
||||||
urlpatterns.append(url('', TemplateView.as_view(template_name='maintenance.html'), name='home'))
|
urlpatterns.append(url('', TemplateView.as_view(template_name='maintenance.html'), name='home'))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Simple module providing reusable random_string function
|
|||||||
'''
|
'''
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
|
||||||
def random_string(length=10):
|
def random_string(length=10):
|
||||||
@@ -33,3 +34,72 @@ def openid_variations(openid):
|
|||||||
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
|
||||||
|
|||||||
112
ivatar/views.py
112
ivatar/views.py
@@ -8,11 +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, HttpResponseNotFound
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
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 ugettext_lazy as _
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
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
|
||||||
|
|
||||||
@@ -23,9 +26,11 @@ 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 . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
||||||
from . ivataraccount.models import pil_format, file_format
|
from . ivataraccount.models import pil_format, file_format
|
||||||
|
from . utils import mm_ng
|
||||||
|
|
||||||
URL_TIMEOUT = 5 # in seconds
|
URL_TIMEOUT = 5 # in seconds
|
||||||
|
|
||||||
@@ -54,13 +59,29 @@ 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): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
|
def options(self, request, *args, **kwargs):
|
||||||
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
|
||||||
@@ -77,9 +98,22 @@ class AvatarImageView(TemplateView):
|
|||||||
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 not 'digest' in kwargs:
|
if 'digest' not in kwargs:
|
||||||
return HttpResponseRedirect(reverse_lazy('home'))
|
return HttpResponseRedirect(reverse_lazy('home'))
|
||||||
|
|
||||||
if 'd' in request.GET:
|
if 'd' in request.GET:
|
||||||
@@ -110,7 +144,7 @@ class AvatarImageView(TemplateView):
|
|||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
model = ConfirmedOpenId
|
model = ConfirmedOpenId
|
||||||
try:
|
try:
|
||||||
d = kwargs['digest']
|
d = kwargs['digest'] # pylint: disable=invalid-name
|
||||||
# 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()
|
||||||
@@ -119,7 +153,7 @@ class AvatarImageView(TemplateView):
|
|||||||
Q(alt_digest1=d) |
|
Q(alt_digest1=d) |
|
||||||
Q(alt_digest2=d) |
|
Q(alt_digest2=d) |
|
||||||
Q(alt_digest3=d)).first()
|
Q(alt_digest3=d)).first()
|
||||||
except:
|
except: # pylint: disable=bare-except
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -147,8 +181,6 @@ class AvatarImageView(TemplateView):
|
|||||||
+ '?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):
|
if str(default) == str(404):
|
||||||
return HttpResponseNotFound(_('<h1>Image not found</h1>'))
|
return HttpResponseNotFound(_('<h1>Image not found</h1>'))
|
||||||
|
|
||||||
@@ -157,7 +189,8 @@ class AvatarImageView(TemplateView):
|
|||||||
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 = HttpResponse(
|
response = CachingHttpResponse(
|
||||||
|
uri,
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
@@ -172,7 +205,8 @@ class AvatarImageView(TemplateView):
|
|||||||
data = BytesIO()
|
data = BytesIO()
|
||||||
robohash.img.save(data, format='png')
|
robohash.img.save(data, format='png')
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
response = HttpResponse(
|
response = CachingHttpResponse(
|
||||||
|
uri,
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
@@ -185,7 +219,8 @@ class AvatarImageView(TemplateView):
|
|||||||
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 = HttpResponse(
|
response = CachingHttpResponse(
|
||||||
|
uri,
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
@@ -197,21 +232,35 @@ class AvatarImageView(TemplateView):
|
|||||||
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 = HttpResponse(
|
response = CachingHttpResponse(
|
||||||
|
uri,
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if str(default) == 'identicon':
|
if str(default) == 'identicon':
|
||||||
p = Pydenticon5()
|
p = Pydenticon5() # pylint: disable=invalid-name
|
||||||
# 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(bytes(kwargs['digest'], 'utf-8')).hexdigest()
|
newdigest = hashlib.md5(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 = HttpResponse(
|
response = CachingHttpResponse(
|
||||||
|
uri,
|
||||||
|
data,
|
||||||
|
content_type='image/png')
|
||||||
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
|
return response
|
||||||
|
|
||||||
|
if str(default) == 'mmng':
|
||||||
|
mmngimg = mm_ng(idhash=kwargs['digest'], size=size)
|
||||||
|
data = BytesIO()
|
||||||
|
mmngimg.save(data, 'PNG', quality=JPEG_QUALITY)
|
||||||
|
data.seek(0)
|
||||||
|
response = CachingHttpResponse(
|
||||||
|
uri,
|
||||||
data,
|
data,
|
||||||
content_type='image/png')
|
content_type='image/png')
|
||||||
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
@@ -251,7 +300,8 @@ class AvatarImageView(TemplateView):
|
|||||||
obj.save()
|
obj.save()
|
||||||
if imgformat == 'jpg':
|
if imgformat == 'jpg':
|
||||||
imgformat = 'jpeg'
|
imgformat = 'jpeg'
|
||||||
response = HttpResponse(
|
response = CachingHttpResponse(
|
||||||
|
uri,
|
||||||
data,
|
data,
|
||||||
content_type='image/%s' % imgformat)
|
content_type='image/%s' % imgformat)
|
||||||
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
|
||||||
@@ -263,7 +313,7 @@ class GravatarProxyView(View):
|
|||||||
'''
|
'''
|
||||||
# TODO: Do cache images!! Memcached?
|
# TODO: Do cache images!! Memcached?
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument
|
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
|
||||||
'''
|
'''
|
||||||
Override get from parent class
|
Override get from parent class
|
||||||
'''
|
'''
|
||||||
@@ -271,7 +321,7 @@ class GravatarProxyView(View):
|
|||||||
url = reverse_lazy(
|
url = reverse_lazy(
|
||||||
'avatar_view',
|
'avatar_view',
|
||||||
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
|
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
|
||||||
if default != None:
|
if default is not None:
|
||||||
url += '&default=%s' % default
|
url += '&default=%s' % default
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
@@ -282,7 +332,7 @@ class GravatarProxyView(View):
|
|||||||
try:
|
try:
|
||||||
if str(request.GET['default']) != 'None':
|
if str(request.GET['default']) != 'None':
|
||||||
default = request.GET['default']
|
default = request.GET['default']
|
||||||
except:
|
except: # pylint: disable=bare-except
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if str(default) != 'wavatar':
|
if str(default) != 'wavatar':
|
||||||
@@ -291,34 +341,46 @@ class GravatarProxyView(View):
|
|||||||
# redirect to our default instead.
|
# redirect to our default instead.
|
||||||
gravatar_test_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
gravatar_test_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
||||||
+ '?s=%i' % 50
|
+ '?s=%i' % 50
|
||||||
|
if cache.get(gravatar_test_url) == 'default':
|
||||||
|
# DEBUG only
|
||||||
|
# print("Cached Gravatar response: Default.")
|
||||||
|
return redir_default(default)
|
||||||
try:
|
try:
|
||||||
testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
|
testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
|
||||||
data = BytesIO(testdata.read())
|
data = BytesIO(testdata.read())
|
||||||
if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a':
|
if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a':
|
||||||
|
cache.set(gravatar_test_url, 'default', 60)
|
||||||
return redir_default(default)
|
return redir_default(default)
|
||||||
except Exception as exc:
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
print('Gravatar test url fetch failed: %s' % exc)
|
print('Gravatar test url fetch failed: %s' % exc)
|
||||||
|
|
||||||
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
|
||||||
+ '?s=%i' % size + '&d=%s' % default
|
+ '?s=%i' % size + '&d=%s' % default
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if cache.get(gravatar_url) == 'err':
|
||||||
|
print('Cached Gravatar fetch failed with URL error')
|
||||||
|
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' %
|
'Gravatar fetch failed with an unexpected %s HTTP error' %
|
||||||
exc.code)
|
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(
|
print(
|
||||||
'Gravatar fetch failed with URL error: %s' %
|
'Gravatar fetch failed with URL error: %s' %
|
||||||
exc.reason)
|
exc.reason)
|
||||||
|
cache.set(gravatar_url, 'err', 30)
|
||||||
return redir_default(default)
|
return redir_default(default)
|
||||||
except SSLError as exc:
|
except SSLError as exc:
|
||||||
print(
|
print(
|
||||||
'Gravatar fetch failed with SSL error: %s' %
|
'Gravatar fetch failed with SSL error: %s' %
|
||||||
exc.reason)
|
exc.reason)
|
||||||
|
cache.set(gravatar_url, 'err', 30)
|
||||||
return redir_default(default)
|
return redir_default(default)
|
||||||
try:
|
try:
|
||||||
data = BytesIO(gravatarimagedata.read())
|
data = BytesIO(gravatarimagedata.read())
|
||||||
@@ -336,3 +398,17 @@ class GravatarProxyView(View):
|
|||||||
|
|
||||||
# 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.all().count(),
|
||||||
|
'mails': ConfirmedEmail.objects.all().count(),
|
||||||
|
'openids': ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponse(retval)
|
||||||
|
|||||||
104
schemas/export/0.1/export.xsd
Normal file
104
schemas/export/0.1/export.xsd
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?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>
|
||||||
105
schemas/export/0.2/export.xsd
Normal file
105
schemas/export/0.2/export.xsd
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?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>
|
||||||
@@ -14,9 +14,7 @@
|
|||||||
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>
|
||||||
|
|
||||||
If you have an IRC client already, you can join #libravatar on chat.freenode.net.
|
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.
|
||||||
<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 :)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user