initialization of project zoo
This commit is contained in:
commit
52c0b4a188
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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
|
||||
<http://www.gnu.org/licenses/>.
|
|
@ -0,0 +1,4 @@
|
|||
include COPYING
|
||||
include MANIFEST.in
|
||||
include VERSION
|
||||
include manage.py
|
|
@ -0,0 +1,6 @@
|
|||
.PHONY: init
|
||||
|
||||
init:
|
||||
dropdb zoo || true
|
||||
createdb zoo
|
||||
. ~/.virtualenvs/zoo/bin/activate; ./manage.py migrate && ./manage.py loaddata fixtures/admin.json rsu && ./manage.py runserver
|
|
@ -0,0 +1,9 @@
|
|||
Zoo
|
||||
===
|
||||
|
||||
Zoo is a simple data management system. You can store JSON documents and set
|
||||
relations between. Acceptable documents are defined using JSON schema [1]_. You
|
||||
can also set relations between documents which can also contain a JSON document
|
||||
to qualify the relation.
|
||||
|
||||
.. [1] http://json-schema.org/
|
|
@ -0,0 +1,21 @@
|
|||
Passerelle server for Debian
|
||||
============================
|
||||
|
||||
Create a tenant
|
||||
---------------
|
||||
|
||||
$ zoo-manage create_tenant foo.zoo.example.org
|
||||
|
||||
Configure nginx
|
||||
---------------
|
||||
|
||||
1. Copy /usr/share/doc/zoo/nginx-example.conf to /etc/nginx/sites-available/zoo.conf:
|
||||
# cp /usr/share/doc/zoo/nginx-example.conf /etc/nginx/sites-available/zoo.conf
|
||||
|
||||
2. Edit /etc/nginx/sites-available/zoo.conf
|
||||
|
||||
3. Enable nginx zoo site:
|
||||
# ln -s ../sites-available/zoo.conf /etc/nginx/sites-enabled/
|
||||
|
||||
4. Reload nginx:
|
||||
# service nginx restart
|
|
@ -0,0 +1,5 @@
|
|||
publik-zoo (0.0-1) unstable; urgency=low
|
||||
|
||||
* Initial release
|
||||
|
||||
-- Benjamin Dauvergne <bdauvergne@entrouvert.com> Tue, 31 Jan 2017 12:29:42 +0200
|
|
@ -0,0 +1 @@
|
|||
8
|
|
@ -0,0 +1,24 @@
|
|||
Source: publik-zoo
|
||||
Section: python
|
||||
Priority: optional
|
||||
Maintainer: Benjamin Dauvergne <bdauvergne@entrouvert.com>
|
||||
Build-Depends: debhelper (>= 8.0.0),
|
||||
python-django,
|
||||
python-setuptools (>= 0.6b3),
|
||||
python-all (>= 2.6.6-3)
|
||||
Standards-Version: 3.9.6
|
||||
Homepage: https://dev.entrouvert.org/projects/zoo
|
||||
X-Python-Version: >= 2.7
|
||||
|
||||
Package: publik-zoo
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, adduser,
|
||||
python-django (>= 1.10),
|
||||
python-hobo,
|
||||
python-django-tenant-schemas,
|
||||
python-psycopg2,
|
||||
python-memcache,
|
||||
python-django-mellon,
|
||||
gunicorn
|
||||
Recommends: nginx, postgresql, memcached
|
||||
Description: Maintain a graph of objects
|
|
@ -0,0 +1,18 @@
|
|||
Files: debian/*
|
||||
Copyright: 201i7 Benjamin Dauvergne <bdauvergne@entrouvert.com>
|
||||
License: GPL-2+
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package 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 General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
|
|
@ -0,0 +1,43 @@
|
|||
# This file is sourced by "execfile" from passerelle.settings
|
||||
|
||||
# Debian defaults
|
||||
DEBUG = False
|
||||
|
||||
PROJECT_NAME = 'zoo'
|
||||
|
||||
# SAML2 authentication
|
||||
INSTALLED_APPS += ('mellon',)
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'mellon.backends.SAMLBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGOUT_URL = '/logout/'
|
||||
|
||||
MELLON_ATTRIBUTE_MAPPING = {
|
||||
'email': '{attributes[email][0]}',
|
||||
'first_name': '{attributes[first_name][0]}',
|
||||
'last_name': '{attributes[last_name][0]}',
|
||||
}
|
||||
MELLON_SUPERUSER_MAPPING = {
|
||||
'is_superuser': 'true',
|
||||
}
|
||||
MELLON_USERNAME_TEMPLATE = '{attributes[name_id_content]}'
|
||||
MELLON_IDENTITY_PROVIDERS = []
|
||||
|
||||
#
|
||||
# hobotization (multitenant)
|
||||
#
|
||||
execfile('/usr/lib/hobo/debian_config_common.py')
|
||||
|
||||
# suds logs are buggy
|
||||
LOGGING['loggers']['suds'] = {
|
||||
'level': 'ERROR',
|
||||
'handlers': ['mail_admins', 'sentry'],
|
||||
'propagate': True,
|
||||
}
|
||||
|
||||
execfile('/etc/%s/settings.py' % PROJECT_NAME)
|
|
@ -0,0 +1,39 @@
|
|||
server {
|
||||
listen 443;
|
||||
server_name *.zoo.example.org;
|
||||
|
||||
ssl on;
|
||||
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
||||
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
||||
|
||||
access_log /var/log/nginx/zoo.example.org-access.log combined;
|
||||
error_log /var/log/nginx/zoo.example.org-error.log;
|
||||
|
||||
location ~ ^/static/(.+)$ {
|
||||
root /;
|
||||
try_files /var/lib/zoo/tenants/$host/static/$1
|
||||
/var/lib/zoo/collectstatic/$1
|
||||
=404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://unix:/run/zoo/zoo.sock;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-SSL on;
|
||||
proxy_set_header X-Forwarded-Protocol ssl;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name *.zoo.example.org;
|
||||
|
||||
access_log /var/log/nginx/zoo.example.org-access.log combined;
|
||||
error_log /var/log/nginx/zoo.example.org-error.log;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
MAILTO=root
|
||||
|
||||
@hourly zoo /usr/bin/zoo-manage tenant_command clearsessions --all-tenants
|
|
@ -0,0 +1,5 @@
|
|||
/etc/zoo
|
||||
/usr/lib/zoo
|
||||
/var/lib/zoo/collectstatic
|
||||
/var/lib/zoo/tenants
|
||||
/var/log/zoo
|
|
@ -0,0 +1,2 @@
|
|||
debian/nginx-example.conf
|
||||
debian/README.Debian
|
|
@ -0,0 +1,194 @@
|
|||
#!/bin/sh -e
|
||||
### BEGIN INIT INFO
|
||||
# Provides: zoo
|
||||
# Required-Start: $network $local_fs $remote_fs $syslog
|
||||
# Required-Stop: $network $local_fs $remote_fs $syslog
|
||||
# Should-Start: postgresql
|
||||
# Should-Stop: postgresql
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Passerelle server
|
||||
# Description: Passerelle provides an uniform access to multiple data sources and services.
|
||||
### END INIT INFO
|
||||
|
||||
# Author: Jérôme Schneider <jschneider@entrouvert.com>
|
||||
|
||||
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||
DESC=Passerelle
|
||||
NAME=zoo
|
||||
DAEMON=/usr/bin/gunicorn
|
||||
RUN_DIR=/run/$NAME
|
||||
PIDFILE=$RUN_DIR/$NAME.pid
|
||||
LOG_DIR=/var/log/$NAME
|
||||
SCRIPTNAME=/etc/init.d/$NAME
|
||||
BIND=unix:$RUN_DIR/$NAME.sock
|
||||
WORKERS=5
|
||||
TIMEOUT=30
|
||||
|
||||
PASSERELLE_SETTINGS_FILE=/usr/lib/$NAME/debian_config.py
|
||||
MANAGE_SCRIPT="/usr/bin/$NAME-manage"
|
||||
|
||||
USER=$NAME
|
||||
GROUP=$NAME
|
||||
|
||||
# Exit if the package is not installed
|
||||
[ -x $DAEMON ] || exit 0
|
||||
|
||||
# Read configuration variable file if it is present
|
||||
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
|
||||
|
||||
DAEMON_ARGS=${DAEMON_ARGS:-"--pid $PIDFILE \
|
||||
--user $USER --group $GROUP \
|
||||
--daemon \
|
||||
--access-logfile $LOG_DIR/gunicorn-access.log \
|
||||
--log-file $LOG_DIR/gunicorn-error.log \
|
||||
--bind=$BIND \
|
||||
--workers=$WORKERS \
|
||||
--worker-class=sync \
|
||||
--timeout=$TIMEOUT \
|
||||
--name $NAME \
|
||||
$NAME.wsgi:application"}
|
||||
|
||||
# Load the VERBOSE setting and other rcS variables
|
||||
. /lib/init/vars.sh
|
||||
|
||||
# Define LSB log_* functions.
|
||||
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
# Create /run directory
|
||||
if [ ! -d $RUN_DIR ]; then
|
||||
install -d -m 755 -o $USER -g $GROUP $RUN_DIR
|
||||
fi
|
||||
|
||||
# environment for wsgi
|
||||
export PASSERELLE_SETTINGS_FILE
|
||||
|
||||
#
|
||||
# Function that starts the daemon/service
|
||||
#
|
||||
do_start()
|
||||
{
|
||||
# Return
|
||||
# 0 if daemon has been started
|
||||
# 1 if daemon was already running
|
||||
# 2 if daemon could not be started
|
||||
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|
||||
|| return 1
|
||||
start-stop-daemon --start --quiet --exec $DAEMON -- \
|
||||
$DAEMON_ARGS \
|
||||
|| return 2
|
||||
}
|
||||
|
||||
#
|
||||
# Function that stops the daemon/service
|
||||
#
|
||||
do_stop()
|
||||
{
|
||||
# Return
|
||||
# 0 if daemon has been stopped
|
||||
# 1 if daemon was already stopped
|
||||
# 2 if daemon could not be stopped
|
||||
# other if a failure occurred
|
||||
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
|
||||
RETVAL="$?"
|
||||
[ "$RETVAL" = 2 ] && return 2
|
||||
# Wait for children to finish too if this is a daemon that forks
|
||||
# and if the daemon is only ever run from this initscript.
|
||||
# If the above conditions are not satisfied then add some other code
|
||||
# that waits for the process to drop all resources that could be
|
||||
# needed by services started subsequently. A last resort is to
|
||||
# sleep for some time.
|
||||
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
|
||||
[ "$?" = 2 ] && return 2
|
||||
# Many daemons don't delete their pidfiles when they exit.
|
||||
rm -f $PIDFILE
|
||||
return "$RETVAL"
|
||||
}
|
||||
|
||||
#
|
||||
# Function that sends a SIGHUP to the daemon/service
|
||||
#
|
||||
do_reload() {
|
||||
#
|
||||
# If the daemon can reload its configuration without
|
||||
# restarting (for example, when it is sent a SIGHUP),
|
||||
# then implement that here.
|
||||
#
|
||||
start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name `basename $DAEMON`
|
||||
return 0
|
||||
}
|
||||
|
||||
do_migrate() {
|
||||
log_action_msg "Applying new migrations .."
|
||||
su $USER -s /bin/sh -p -c "$MANAGE_SCRIPT migrate_schemas --noinput"
|
||||
log_action_msg ".. done"
|
||||
}
|
||||
|
||||
do_collectstatic() {
|
||||
log_action_msg "Collect static files.."
|
||||
su $USER -s /bin/sh -p -c "$MANAGE_SCRIPT collectstatic --noinput"
|
||||
log_action_msg ".. done"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
log_daemon_msg "Starting $DESC " "$NAME"
|
||||
do_migrate
|
||||
do_collectstatic
|
||||
do_start
|
||||
case "$?" in
|
||||
0|1) log_end_msg 0 ;;
|
||||
2) log_end_msg 1 ;;
|
||||
esac
|
||||
;;
|
||||
stop)
|
||||
log_daemon_msg "Stopping $DESC" "$NAME"
|
||||
do_stop
|
||||
case "$?" in
|
||||
0|1) log_end_msg 0 ;;
|
||||
2) log_end_msg 1 ;;
|
||||
esac
|
||||
;;
|
||||
status)
|
||||
status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
|
||||
;;
|
||||
reload|force-reload)
|
||||
#
|
||||
# If do_reload() is not implemented then leave this commented out
|
||||
# and leave 'force-reload' as an alias for 'restart'.
|
||||
#
|
||||
log_daemon_msg "Reloading $DESC" "$NAME"
|
||||
do_reload
|
||||
log_end_msg $?
|
||||
;;
|
||||
restart|force-reload)
|
||||
#
|
||||
# If the "reload" option is implemented then remove the
|
||||
# 'force-reload' alias
|
||||
#
|
||||
log_daemon_msg "Restarting $DESC" "$NAME"
|
||||
do_stop
|
||||
case "$?" in
|
||||
0|1)
|
||||
do_migrate
|
||||
do_collectstatic
|
||||
do_start
|
||||
case "$?" in
|
||||
0) log_end_msg 0 ;;
|
||||
1) log_end_msg 1 ;; # Old process is still running
|
||||
*) log_end_msg 1 ;; # Failed to start
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
# Failed to stop
|
||||
log_end_msg 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $SCRIPTNAME {start|stop|status|restart|reload|force-reload}" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
debian/zoo-manage /usr/bin
|
||||
debian/debian_config.py /usr/lib/zoo
|
||||
debian/settings.py /etc/zoo
|
|
@ -0,0 +1,50 @@
|
|||
#! /bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
NAME="zoo"
|
||||
USER=$NAME
|
||||
GROUP=$NAME
|
||||
CONFIG_DIR="/etc/$NAME"
|
||||
MANAGE_SCRIPT="/usr/bin/$NAME-manage"
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
|
||||
# make sure the administrative user exists
|
||||
if ! getent passwd $USER >/dev/null; then
|
||||
adduser --disabled-password --quiet --system \
|
||||
--no-create-home --home /var/lib/$NAME \
|
||||
--gecos "Passerelle user" --group $USER
|
||||
fi
|
||||
# ensure dirs ownership
|
||||
chown $USER:$GROUP /var/log/$NAME
|
||||
chown $USER:$GROUP /var/lib/$NAME/collectstatic
|
||||
chown $USER:$GROUP /var/lib/$NAME/tenants
|
||||
# create a secret file
|
||||
SECRET_FILE=$CONFIG_DIR/secret
|
||||
if [ ! -f $SECRET_FILE ]; then
|
||||
echo -n "Generating Django secret..." >&2
|
||||
cat /dev/urandom | tr -dc [:alnum:]-_\!\%\^:\; | head -c70 > $SECRET_FILE
|
||||
chown root:$GROUP $SECRET_FILE
|
||||
chmod 0440 $SECRET_FILE
|
||||
echo "done" >&2
|
||||
fi
|
||||
;;
|
||||
|
||||
triggered)
|
||||
su -s /bin/sh -c "$MANAGE_SCRIPT hobo_deploy --redeploy" $USER
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1 @@
|
|||
interest-noawait hobo-redeploy
|
|
@ -0,0 +1 @@
|
|||
/usr/lib/zoo
|
|
@ -0,0 +1,2 @@
|
|||
README
|
||||
LICENSE
|
|
@ -0,0 +1,2 @@
|
|||
usr/bin/manage.py /usr/lib/zoo
|
||||
usr/lib/python2*/*-packages
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
%:
|
||||
dh $@ --with python2
|
|
@ -0,0 +1,55 @@
|
|||
# Configuration for zoo.
|
||||
# You can override Passerelle default settings here
|
||||
|
||||
# Passerelle is a Django application: for the full list of settings and their
|
||||
# values, see https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||
# For more information on settings see
|
||||
# https://docs.djangoproject.com/en/1.7/topics/settings/
|
||||
|
||||
# WARNING! Quick-start development settings unsuitable for production!
|
||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||
|
||||
# This file is sourced by "execfile" from /usr/lib/zoo/debian_config.py
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
#DEBUG = False
|
||||
#TEMPLATE_DEBUG = False
|
||||
|
||||
#ADMINS = (
|
||||
# ('User 1', 'poulpe@example.org'),
|
||||
# ('User 2', 'janitor@example.net'),
|
||||
#)
|
||||
|
||||
# ALLOWED_HOSTS must be correct in production!
|
||||
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# If a tenant doesn't exist, the tenant middleware raise a 404 error. If you
|
||||
# prefer to redirect to a specific site, use:
|
||||
# TENANT_NOT_FOUND_REDIRECT_URL = 'http://www.example.net/'
|
||||
|
||||
# Database
|
||||
# Warning: don't change ENGINE, it must be 'tenant_schemas.postgresql_backend'
|
||||
#DATABASES['default']['NAME'] = 'zoo'
|
||||
#DATABASES['default']['USER'] = 'zoo'
|
||||
#DATABASES['default']['PASSWORD'] = '******'
|
||||
#DATABASES['default']['HOST'] = 'localhost'
|
||||
#DATABASES['default']['PORT'] = '5432'
|
||||
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
TIME_ZONE = 'Europe/Paris'
|
||||
|
||||
# Email configuration
|
||||
#EMAIL_SUBJECT_PREFIX = '[zoo] '
|
||||
#SERVER_EMAIL = 'root@zoo.example.org'
|
||||
#DEFAULT_FROM_EMAIL = 'webmaster@zoo.example.org'
|
||||
|
||||
# SMTP configuration
|
||||
#EMAIL_HOST = 'localhost'
|
||||
#EMAIL_HOST_USER = ''
|
||||
#EMAIL_HOST_PASSWORD = ''
|
||||
#EMAIL_PORT = 25
|
||||
|
||||
# HTTPS
|
||||
#CSRF_COOKIE_SECURE = True
|
||||
#SESSION_COOKIE_SECURE = True
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/sh
|
||||
|
||||
NAME=zoo
|
||||
MANAGE="/usr/lib/zoo/manage.py"
|
||||
|
||||
# load Debian default configuration
|
||||
export PASSERELLE_SETTINGS_FILE=/usr/lib/$NAME/debian_config.py
|
||||
|
||||
# check user
|
||||
if test x$1 = x"--forceuser"
|
||||
then
|
||||
shift
|
||||
elif test $(id -un) != "$NAME"
|
||||
then
|
||||
echo "error: must use $0 with user ${NAME}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if test $# -eq 0
|
||||
then
|
||||
python ${MANAGE} help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python ${MANAGE} "$@"
|
||||
|
|
@ -0,0 +1 @@
|
|||
[{"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$30000$49OTKb1GSj6M$igG19ps767DTiyTJE/6sllxxzZ0Z0aHp6I4SrAhduiU=", "last_login": "2016-12-06T18:58:58.703Z", "is_superuser": true, "username": "admin", "first_name": "", "last_name": "", "email": "admin@example.com", "is_staff": true, "is_active": true, "date_joined": "2016-12-06T18:58:48.850Z", "groups": [], "user_permissions": []}}]
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zoo.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
raise
|
||||
execute_from_command_line(sys.argv)
|
|
@ -0,0 +1,114 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from setuptools.command.install_lib import install_lib as _install_lib
|
||||
from distutils.command.build import build as _build
|
||||
from distutils.command.sdist import sdist
|
||||
from distutils.cmd import Command
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
class eo_sdist(sdist):
|
||||
def run(self):
|
||||
if os.path.exists('VERSION'):
|
||||
os.remove('VERSION')
|
||||
version = get_version()
|
||||
version_file = open('VERSION', 'w')
|
||||
version_file.write(version)
|
||||
version_file.close()
|
||||
sdist.run(self)
|
||||
if os.path.exists('VERSION'):
|
||||
os.remove('VERSION')
|
||||
|
||||
|
||||
def get_version():
|
||||
if os.path.exists('VERSION'):
|
||||
version_file = open('VERSION', 'r')
|
||||
version = version_file.read()
|
||||
version_file.close()
|
||||
return version
|
||||
if os.path.exists('.git'):
|
||||
p = subprocess.Popen(['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE)
|
||||
result = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
version = result.split()[0][1:]
|
||||
version = version.replace('-', '.')
|
||||
return version
|
||||
return '0'
|
||||
|
||||
|
||||
class compile_translations(Command):
|
||||
description = 'compile message catalogs to MO files via django compilemessages'
|
||||
user_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
from django.core.management import call_command
|
||||
for path, dirs, files in os.walk('combo'):
|
||||
if 'locale' not in dirs:
|
||||
continue
|
||||
curdir = os.getcwd()
|
||||
os.chdir(os.path.realpath(path))
|
||||
call_command('compilemessages')
|
||||
os.chdir(curdir)
|
||||
except ImportError:
|
||||
sys.stderr.write('!!! Please install Django >= 1.4 to build translations\n')
|
||||
|
||||
|
||||
class build(_build):
|
||||
sub_commands = [('compile_translations', None)] + _build.sub_commands
|
||||
|
||||
|
||||
class install_lib(_install_lib):
|
||||
def run(self):
|
||||
self.run_command('compile_translations')
|
||||
_install_lib.run(self)
|
||||
|
||||
|
||||
setup(
|
||||
name='zoo',
|
||||
version=get_version(),
|
||||
description='Manage datas and their relations',
|
||||
long_description=file('README').read(),
|
||||
author='Benjamin Dauvergne',
|
||||
author_email='bdauvergne@entrouvert.com',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
scripts=('manage.py',),
|
||||
url='https://dev.entrouvert.org/projects/zoo/',
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
],
|
||||
install_requires=[
|
||||
'django>=1.10',
|
||||
'isodate',
|
||||
'psycopg2',
|
||||
'jsonschema',
|
||||
'gadjo',
|
||||
'djangorestframework<3.4',
|
||||
],
|
||||
zip_safe=False,
|
||||
cmdclass={
|
||||
'build': build,
|
||||
'compile_translations': compile_translations,
|
||||
'install_lib': install_lib,
|
||||
'sdist': eo_sdist,
|
||||
},
|
||||
)
|
|
@ -0,0 +1,54 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
import random
|
||||
|
||||
import faker
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
from zoo.zoo_meta.models import EntitySchema
|
||||
from zoo.zoo_data.models import Entity, Transaction
|
||||
from zoo.zoo_nanterre.utils import age_in_years_and_months
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rsu(db):
|
||||
call_command('loaddata', 'rsu')
|
||||
schema = EntitySchema.objects.get(slug='individu')
|
||||
assert schema
|
||||
|
||||
# populate individus
|
||||
fake = faker.Factory.create('fr_FR')
|
||||
|
||||
sexes = ['femme', 'homme', 'autre']
|
||||
|
||||
tr = Transaction.objects.create(meta="initial import")
|
||||
entities = []
|
||||
for i in range(1000):
|
||||
individu = {}
|
||||
individu['genre'] = genre = sexes[random.randint(0, 2)]
|
||||
individu['email'] = fake.email()
|
||||
if genre == 'femme':
|
||||
individu['prenoms'] = fake.first_name_female()
|
||||
individu['nom_de_naissance'] = fake.last_name_female()
|
||||
if random.randint(0, 10) == 0:
|
||||
individu['nom_d_usage'] = fake.last_name_male()
|
||||
else:
|
||||
individu['prenoms'] = fake.first_name_male()
|
||||
individu['nom_de_naissance'] = fake.last_name_male()
|
||||
date_de_naissance = fake.date_time_between_dates(
|
||||
datetime_start=datetime.datetime(1930, 1, 1),
|
||||
datetime_end=datetime.datetime.now() + datetime.timedelta(days=120),
|
||||
tzinfo=None).date()
|
||||
individu['date_de_naissance'] = date_de_naissance.isoformat()
|
||||
individu['statut_legal'] = ('majeur' if age_in_years_and_months(date_de_naissance) >= (18,
|
||||
0)
|
||||
else 'mineur')
|
||||
individu['email'] = fake.email()
|
||||
entities.append(
|
||||
Entity(created=tr, schema=schema, content=individu))
|
||||
|
||||
Entity.objects.bulk_create(entities)
|
||||
return entities
|
|
@ -0,0 +1,27 @@
|
|||
import datetime
|
||||
|
||||
from zoo.zoo_nanterre.utils import PersonSearch
|
||||
|
||||
|
||||
def test_person_search(db, rsu):
|
||||
search = PersonSearch()
|
||||
|
||||
found = list(search.search_name(rsu[0].content['prenoms'], rsu[0].content['nom_de_naissance']))
|
||||
assert rsu[0].id == found[0].id
|
||||
assert found[0].similarity == 1.0
|
||||
|
||||
found = list(search.search_query(rsu[0].content['prenoms'] + ' ' +
|
||||
rsu[0].content['nom_de_naissance']))
|
||||
assert rsu[0].id == found[0].id
|
||||
assert found[0].similarity == 1.0
|
||||
|
||||
birthdate = datetime.datetime.strptime(rsu[0].content['date_de_naissance'], '%Y-%m-%d').date()
|
||||
found = list(search.search_birthdate(birthdate))
|
||||
|
||||
assert any(x for x in found if x.id == rsu[0].id)
|
||||
assert len(found) == 1
|
||||
|
||||
found = list(search.search_email(rsu[0].content['email']))
|
||||
assert found[0].id == rsu[0].id
|
||||
assert len(found) == 1
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from zoo.models import EntitySchema, Entity
|
||||
from zoo.zoo_data.search import JSONTextRef
|
||||
|
||||
from django.contrib.postgres.search import TrigramDistance
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def test_new_schema(db):
|
||||
|
||||
schema = EntitySchema.objects.create(
|
||||
name='person',
|
||||
schema={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'first_name': {
|
||||
'type': 'string',
|
||||
},
|
||||
'last_name': {
|
||||
'type': 'string',
|
||||
},
|
||||
'address': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'street': {
|
||||
'type': 'string',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Entity.objects.create(
|
||||
schema=schema,
|
||||
meta={},
|
||||
content={
|
||||
'first_name': 'Leon',
|
||||
'last_name': 'Blum',
|
||||
'address': {
|
||||
'street': 'Rue du Château',
|
||||
}
|
||||
})
|
||||
qs = Entity.objects.content_search(schema, address__street='chateau')
|
||||
print qs.query
|
||||
assert qs.count() == 1
|
||||
for e in qs:
|
||||
print e.similarity
|
|
@ -0,0 +1,39 @@
|
|||
# Tox (http://tox.testrun.org/) is a tool for running tests
|
||||
# in multiple virtualenvs. This configuration file will run the
|
||||
# test suite on all supported python versions. To use it, "pip install tox"
|
||||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/zoo/
|
||||
|
||||
[testenv]
|
||||
# django.contrib.auth is not tested it does not work with our templates
|
||||
whitelist_externals =
|
||||
/bin/mv
|
||||
setenv =
|
||||
DJANGO_SETTINGS_MODULE=zoo.settings
|
||||
coverage: COVERAGE=--junitxml=junit-{envname}.xml --cov-report xml --cov=zoo/
|
||||
fast: FAST=--nomigrations
|
||||
usedevelop =
|
||||
coverage: True
|
||||
nocoverage: False
|
||||
deps =
|
||||
pg: psycopg2
|
||||
coverage
|
||||
pytest-cov
|
||||
pytest-django
|
||||
mock
|
||||
pytest
|
||||
lxml
|
||||
cssselect
|
||||
pylint
|
||||
pylint-django
|
||||
django-webtest
|
||||
WebTest
|
||||
pyquery
|
||||
httmock
|
||||
pytest-capturelog
|
||||
pytz
|
||||
faker
|
||||
commands =
|
||||
py.test {env:FAST:} {env:COVERAGE:} {posargs:tests/}
|
|
@ -0,0 +1,139 @@
|
|||
# French translation for zoo
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
# This file is distributed under the same license as the zoo package.
|
||||
# Benjamin Dauvergne <bdauvergne@entrouvert.com>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-01-31 13:25+0000\n"
|
||||
"PO-Revision-Date: 2017-01-31 14:27+01:00\n"
|
||||
"Last-Translator: Benjamin Dauvergne <bdauvergne@entrouvert.com>\n"
|
||||
"Language-Team: french<fr@li.org>\n"
|
||||
"Language: french\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: zoo_data/apps.py:7
|
||||
msgid "datas"
|
||||
msgstr "données"
|
||||
|
||||
#: zoo_data/models.py:19 zoo_data/models.py:98 zoo_data/models.py:145
|
||||
msgid "created"
|
||||
msgstr "créé"
|
||||
|
||||
#: zoo_data/models.py:21 zoo_data/models.py:115 zoo_data/models.py:162
|
||||
msgid "meta"
|
||||
msgstr "méta-données"
|
||||
|
||||
#: zoo_data/models.py:25 zoo_data/models.py:119 zoo_data/models.py:166
|
||||
#: zoo_data/models.py:188
|
||||
msgid "content"
|
||||
msgstr "contenu"
|
||||
|
||||
#: zoo_data/models.py:34 zoo_data/models.py:180
|
||||
msgid "transaction"
|
||||
msgstr ""
|
||||
|
||||
#: zoo_data/models.py:35
|
||||
msgid "transactions"
|
||||
msgstr ""
|
||||
|
||||
#: zoo_data/models.py:93 zoo_data/models.py:132 zoo_meta/models.py:26
|
||||
msgid "schema"
|
||||
msgstr "schéma"
|
||||
|
||||
#: zoo_data/models.py:104 zoo_data/models.py:151
|
||||
msgid "modified"
|
||||
msgstr "modifié"
|
||||
|
||||
#: zoo_data/models.py:108 zoo_data/models.py:155
|
||||
msgid "deleted"
|
||||
msgstr "supprimé"
|
||||
|
||||
#: zoo_data/models.py:125 zoo_data/models.py:177
|
||||
msgid "entity"
|
||||
msgstr "entité"
|
||||
|
||||
#: zoo_data/models.py:126
|
||||
msgid "entities"
|
||||
msgstr "entités"
|
||||
|
||||
#: zoo_data/models.py:135
|
||||
msgid "left"
|
||||
msgstr "gauche"
|
||||
|
||||
#: zoo_data/models.py:139
|
||||
msgid "right"
|
||||
msgstr "droit"
|
||||
|
||||
#: zoo_data/models.py:170
|
||||
msgid "relation"
|
||||
msgstr ""
|
||||
|
||||
#: zoo_data/models.py:171
|
||||
msgid "relations"
|
||||
msgstr ""
|
||||
|
||||
#: zoo_data/models.py:184
|
||||
msgid "timestamp"
|
||||
msgstr "horodatage"
|
||||
|
||||
#: zoo_data/models.py:192
|
||||
msgid "url"
|
||||
msgstr ""
|
||||
|
||||
#: zoo_data/models.py:196
|
||||
msgid "log"
|
||||
msgstr "journal"
|
||||
|
||||
#: zoo_data/models.py:197
|
||||
msgid "logs"
|
||||
msgstr "journaux"
|
||||
|
||||
#: zoo_meta/apps.py:7
|
||||
msgid "metadatas"
|
||||
msgstr "métadonnées"
|
||||
|
||||
#: zoo_meta/models.py:20
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: zoo_meta/models.py:24
|
||||
msgid "slug"
|
||||
msgstr "nom court"
|
||||
|
||||
#: zoo_meta/models.py:29
|
||||
msgid "caption template"
|
||||
msgstr "canevas pour le libellé"
|
||||
|
||||
#: zoo_meta/models.py:129
|
||||
msgid "entity schema"
|
||||
msgstr "schéma d'entité"
|
||||
|
||||
#: zoo_meta/models.py:130
|
||||
msgid "entity schemas"
|
||||
msgstr "schéma des entités"
|
||||
|
||||
#: zoo_meta/models.py:136
|
||||
msgid "left schema"
|
||||
msgstr "schéma gauche"
|
||||
|
||||
#: zoo_meta/models.py:140
|
||||
msgid "right schema"
|
||||
msgstr "schéma droit"
|
||||
|
||||
#: zoo_meta/models.py:145
|
||||
msgid "is symmetric"
|
||||
msgstr "est symétrique"
|
||||
|
||||
#: zoo_meta/models.py:149
|
||||
msgid "relation schema"
|
||||
msgstr "schéma de relation"
|
||||
|
||||
#: zoo_meta/models.py:150
|
||||
msgid "relation schemas"
|
||||
msgstr "schémas des relations"
|
|
@ -0,0 +1,2 @@
|
|||
from .zoo_meta.models import *
|
||||
from .zoo_data.models import *
|
|
@ -0,0 +1,159 @@
|
|||
"""
|
||||
Django settings for zoo project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.10.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.10/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.10/ref/settings/
|
||||
"""
|
||||
|
||||
from django.conf import global_settings
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'wfi0)*286ko8f-zl6+p^!g&(u2$*m#w$3j*2)f$qccoj!f7mz1'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'gadjo',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.postgres',
|
||||
'rest_framework',
|
||||
'zoo.zoo_meta',
|
||||
'zoo.zoo_data',
|
||||
'zoo.zoo_nanterre',
|
||||
'zoo.zoo_demo',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'zoo.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'zoo.wsgi.application'
|
||||
|
||||
STATICFILES_FINDERS = global_settings.STATICFILES_FINDERS + ['gadjo.finders.XStaticFinder']
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'zoo',
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django.db': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['console'],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Rest Framework
|
||||
REST_FRAMEWORK = {
|
||||
'EXCEPTION_HANDLER': 'zoo.utils.rest_exception_handler',
|
||||
}
|
||||
|
||||
ZOO_NANTERRE_APPLICATIONS = {
|
||||
'info': 'Info',
|
||||
'technocarte': 'Technocarte',
|
||||
'implicit': 'Implicit',
|
||||
}
|
||||
|
||||
local_settings_file = os.environ.get('ZOO_SETTINGS_FILE')
|
||||
if local_settings_file and os.path.exists(local_settings_file):
|
||||
execfile(local_settings_file)
|
|
@ -0,0 +1,27 @@
|
|||
"""zoo URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/1.10/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf.urls import url, include
|
||||
from django.contrib import admin
|
||||
|
||||
from .views import login, logout
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^demo/', include('zoo.zoo_demo.urls')),
|
||||
url(r'^rsu/', include('zoo.zoo_nanterre.urls')),
|
||||
url(r'^logout/$', logout, name='logout'),
|
||||
url(r'^login/$', login, name='auth_login'),
|
||||
]
|
|
@ -0,0 +1,10 @@
|
|||
from rest_framework.views import exception_handler
|
||||
|
||||
|
||||
def rest_exception_handler(exc, context):
|
||||
response = exception_handler(exc, context)
|
||||
response.data = {
|
||||
'err': 1,
|
||||
'data': response.data,
|
||||
}
|
||||
return response
|
|
@ -0,0 +1,31 @@
|
|||
from django.conf import settings
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import resolve_url
|
||||
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
if 'mellon' in settings.INSTALLED_APPS:
|
||||
from mellon.utils import get_idps
|
||||
else:
|
||||
get_idps = lambda: []
|
||||
|
||||
|
||||
def login(request, *args, **kwargs):
|
||||
if any(get_idps()):
|
||||
if not 'next' in request.GET:
|
||||
return HttpResponseRedirect(resolve_url('mellon_login'))
|
||||
return HttpResponseRedirect(
|
||||
resolve_url('mellon_login') + '?next=' + request.GET.get('next'))
|
||||
return auth_views.login(request, *args, **kwargs)
|
||||
|
||||
|
||||
def logout(request, next_page=None):
|
||||
if any(get_idps()):
|
||||
return HttpResponseRedirect(resolve_url('mellon_logout'))
|
||||
auth.logout(request)
|
||||
if next_page is not None:
|
||||
next_page = resolve_url(next_page)
|
||||
else:
|
||||
next_page = '/'
|
||||
return HttpResponseRedirect(next_page)
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for zoo project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zoo.settings")
|
||||
|
||||
application = get_wsgi_application()
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'zoo.zoo_data.apps.ZooDataConfig'
|
|
@ -0,0 +1,72 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Transaction, Entity, Relation, Log
|
||||
from .widgets import JSONEditor
|
||||
|
||||
|
||||
class TransactionAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = 'created'
|
||||
list_display = ['id', 'created']
|
||||
|
||||
|
||||
class LeftRelationInlineAdmin(admin.TabularInline):
|
||||
fk_name = 'left'
|
||||
fields = ['right', 'schema', 'meta', 'content']
|
||||
raw_id_fields = ['left', 'right']
|
||||
model = Relation
|
||||
extra = 0
|
||||
|
||||
|
||||
class RightRelationInlineAdmin(admin.TabularInline):
|
||||
fk_name = 'right'
|
||||
fields = ['left', 'schema', 'meta', 'content']
|
||||
raw_id_fields = ['left', 'right']
|
||||
model = Relation
|
||||
extra = 0
|
||||
|
||||
|
||||
class LogInlineAdmin(admin.TabularInline):
|
||||
model = Log
|
||||
fields = ['timestamp', 'transaction', 'content', 'url']
|
||||
readonly_fields = ['timestamp', 'transaction']
|
||||
extra = 0
|
||||
can_delete = False
|
||||
|
||||
|
||||
class DataAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'schema', 'name', 'created', 'created_ts',
|
||||
'modified', 'modified_ts', 'deleted', 'deleted_ts']
|
||||
list_filter = ['schema']
|
||||
list_select_related = ('schema',)
|
||||
raw_id_fields = ['modified', 'created', 'deleted']
|
||||
|
||||
def name(self, instance):
|
||||
return instance.schema.make_caption(instance)
|
||||
|
||||
def created_ts(self, instance):
|
||||
return instance.created.created
|
||||
|
||||
def modified_ts(self, instance):
|
||||
return instance.modified and instance.modified.created
|
||||
|
||||
def deleted_ts(self, instance):
|
||||
return instance.deleted and instance.deleted.created
|
||||
|
||||
# def get_form(self, request, obj=None, **kwargs):
|
||||
# if obj and obj.schema:
|
||||
# kwargs['widgets'] = {
|
||||
# 'content': JSONEditor(schema=obj.schema.schema),
|
||||
# }
|
||||
# return super(DataAdmin, self).get_form(request, obj=obj, **kwargs)
|
||||
|
||||
|
||||
class EntityAdmin(DataAdmin):
|
||||
inlines = [LeftRelationInlineAdmin, RightRelationInlineAdmin, LogInlineAdmin]
|
||||
|
||||
|
||||
class RelationAdmin(DataAdmin):
|
||||
raw_id_fields = DataAdmin.raw_id_fields + ['left', 'right']
|
||||
|
||||
admin.site.register(Transaction, TransactionAdmin)
|
||||
admin.site.register(Entity, EntityAdmin)
|
||||
admin.site.register(Relation, RelationAdmin)
|
|
@ -0,0 +1,11 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class ZooDataConfig(AppConfig):
|
||||
name = 'zoo.zoo_data'
|
||||
verbose_name = _('datas')
|
||||
|
||||
def ready(self):
|
||||
# load our custom lookups
|
||||
import zoo.zoo_data.lookups
|
|
@ -0,0 +1,96 @@
|
|||
from django.contrib.postgres.fields import jsonb
|
||||
from django.db.models import Transform, TextField, DateTimeField
|
||||
|
||||
|
||||
class KeyTransform(Transform):
|
||||
operator = '->'
|
||||
nested_operator = '#>'
|
||||
|
||||
def __init__(self, key_name, *args, **kwargs):
|
||||
super(KeyTransform, self).__init__(*args, **kwargs)
|
||||
self.key_name = key_name
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
key_transforms = [self.key_name]
|
||||
previous = self.lhs
|
||||
while isinstance(previous, KeyTransform):
|
||||
key_transforms.insert(0, previous.key_name)
|
||||
previous = previous.lhs
|
||||
lhs, params = compiler.compile(previous)
|
||||
if len(key_transforms) > 1:
|
||||
return "(%s %s %%s)" % (lhs, self.nested_operator), [key_transforms] + params
|
||||
try:
|
||||
int(self.key_name)
|
||||
except ValueError:
|
||||
lookup = "'%s'" % self.key_name
|
||||
else:
|
||||
lookup = "%s" % self.key_name
|
||||
return "(%s %s %s)" % (lhs, self.operator, lookup), params
|
||||
|
||||
|
||||
jsonb.KeyTransform = KeyTransform
|
||||
|
||||
|
||||
class KeyTextTransform(KeyTransform):
|
||||
operator = '->>'
|
||||
nested_operator = '#>>'
|
||||
_output_field = TextField()
|
||||
|
||||
|
||||
class KeyTransformTextLookupMixin(object):
|
||||
"""
|
||||
Mixin for combining with a lookup expecting a text lhs from a JSONField
|
||||
key lookup. Make use of the ->> operator instead of casting key values to
|
||||
text and performing the lookup on the resulting representation.
|
||||
"""
|
||||
def __init__(self, key_transform, *args, **kwargs):
|
||||
assert isinstance(key_transform, KeyTransform)
|
||||
key_text_transform = KeyTextTransform(
|
||||
key_transform.key_name, *key_transform.source_expressions, **key_transform.extra
|
||||
)
|
||||
super(KeyTransformTextLookupMixin, self).__init__(key_text_transform, *args, **kwargs)
|
||||
|
||||
|
||||
class Lower(Transform):
|
||||
lookup_name = 'lower'
|
||||
function = 'LOWER'
|
||||
|
||||
TextField.register_lookup(Lower)
|
||||
|
||||
|
||||
class Unaccent(Transform):
|
||||
lookup_name = 'unaccent'
|
||||
function = 'immutable_unaccent'
|
||||
|
||||
TextField.register_lookup(Unaccent)
|
||||
|
||||
|
||||
class Normalize(Transform):
|
||||
lookup_name = 'normalize'
|
||||
function = 'immutable_normalize'
|
||||
|
||||
TextField.register_lookup(Normalize)
|
||||
|
||||
|
||||
class Timestamp(Transform):
|
||||
lookup_name = 'timestamp'
|
||||
function = 'immutable_timestamp'
|
||||
_output_field = DateTimeField()
|
||||
|
||||
TextField.register_lookup(Timestamp)
|
||||
|
||||
|
||||
class JSONUnaccent(KeyTransformTextLookupMixin, Unaccent):
|
||||
pass
|
||||
|
||||
|
||||
class JSONTimestamp(KeyTransformTextLookupMixin, Timestamp):
|
||||
pass
|
||||
|
||||
|
||||
class JSONNormalize(KeyTransformTextLookupMixin, Normalize):
|
||||
pass
|
||||
|
||||
KeyTransform.register_lookup(JSONUnaccent)
|
||||
KeyTransform.register_lookup(JSONTimestamp)
|
||||
KeyTransform.register_lookup(JSONNormalize)
|
|
@ -0,0 +1,34 @@
|
|||
# hobo - portal to configure and deploy applications
|
||||
# Copyright (C) 2016 Entr'ouvert
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import runpy
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('args', metavar='args', nargs='+', help='Fixture labels.')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
fullpath = os.path.dirname(os.path.abspath(args[0]))
|
||||
sys.path.insert(0, fullpath)
|
||||
module_name = os.path.splitext(os.path.basename(args[0]))[0]
|
||||
sys.argv = args
|
||||
runpy.run_module(module_name)
|
|
@ -0,0 +1,111 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-01-03 15:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('zoo_meta', '0002_auto_20161214_1545'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Entity',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('meta', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='meta')),
|
||||
('content', django.contrib.postgres.fields.jsonb.JSONField(blank=True, verbose_name='content')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('created',),
|
||||
'verbose_name': 'entity',
|
||||
'verbose_name_plural': 'entities',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Relation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('meta', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='meta')),
|
||||
('content', django.contrib.postgres.fields.jsonb.JSONField(blank=True, verbose_name='content')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('created',),
|
||||
'verbose_name': 'relation',
|
||||
'verbose_name_plural': 'relations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Transaction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||
('meta', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='meta')),
|
||||
('content', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='content')),
|
||||
('failed', models.BooleanField(default=False, verbose_name='failed')),
|
||||
('result', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='content')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('id',),
|
||||
'verbose_name': 'transaction',
|
||||
'verbose_name_plural': 'transactions',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='relation',
|
||||
name='created',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_relations', to='zoo_data.Transaction', verbose_name='created'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='relation',
|
||||
name='deleted',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='deleted_relations', to='zoo_data.Transaction', verbose_name='deleted'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='relation',
|
||||
name='left',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='zoo_data.Entity', verbose_name='left'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='relation',
|
||||
name='modified',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='modified_relations', to='zoo_data.Transaction', verbose_name='modified'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='relation',
|
||||
name='right',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='zoo_data.Entity', verbose_name='right'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='relation',
|
||||
name='schema',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='zoo_meta.RelationSchema', verbose_name='schema'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='entity',
|
||||
name='created',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_entities', to='zoo_data.Transaction', verbose_name='created'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='entity',
|
||||
name='deleted',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='deleted_entities', to='zoo_data.Transaction', verbose_name='deleted'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='entity',
|
||||
name='modified',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='modified_entities', to='zoo_data.Transaction', verbose_name='modified'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='entity',
|
||||
name='schema',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='zoo_meta.EntitySchema', verbose_name='schema'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-01-04 18:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('zoo_data', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Log',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='timestamp')),
|
||||
('content', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='content')),
|
||||
('url', models.URLField(blank=True, null=True, verbose_name='url')),
|
||||
('entity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='zoo_data.Entity', verbose_name='entity')),
|
||||
('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='zoo_data.Transaction', verbose_name='transaction')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('timestamp',),
|
||||
'verbose_name': 'log',
|
||||
'verbose_name_plural': 'logs',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-01-04 18:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('zoo_data', '0002_log'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='transaction',
|
||||
name='failed',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='transaction',
|
||||
name='result',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,197 @@
|
|||
from operator import __add__, __or__
|
||||
|
||||
from django.db import models, connection
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.query import QuerySet, Q
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.contrib.postgres.search import TrigramDistance
|
||||
|
||||
|
||||
from .search import Unaccent, Lower, JSONTextRef
|
||||
from zoo.zoo_meta.validators import schema_validator
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_('created'))
|
||||
meta = JSONField(
|
||||
verbose_name=_('meta'),
|
||||
blank=True,
|
||||
null=True)
|
||||
content = JSONField(
|
||||
verbose_name=_('content'),
|
||||
blank=True,
|
||||
null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.id)
|
||||
|
||||
class Meta:
|
||||
ordering = ('id',)
|
||||
verbose_name = _('transaction')
|
||||
verbose_name_plural = _('transactions')
|
||||
|
||||
|
||||
class EntityQuerySet(QuerySet):
|
||||
def content_search(self, schema, limit=0.3, **kwargs):
|
||||
qs = self
|
||||
qs = qs.filter(schema=schema)
|
||||
filters = []
|
||||
connection.cursor().execute('SELECT SET_LIMIT(%s)', (limit,))
|
||||
for key, value in kwargs.iteritems():
|
||||
filters.append(Q(**{
|
||||
'content__' + key + '__unaccent__lower__trigram_similar':
|
||||
Lower(Unaccent(Value(value))),
|
||||
}))
|
||||
qs = qs.filter(reduce(__or__, filters))
|
||||
expressions = []
|
||||
ordering = []
|
||||
for key, value in kwargs.iteritems():
|
||||
ordering.append(Lower(Unaccent(JSONTextRef(F('content'), *key.split('__')))))
|
||||
expressions.append(TrigramDistance(
|
||||
Lower(Unaccent(JSONTextRef(F('content'), *key.split('__')))),
|
||||
Lower(Unaccent(Value(value)))))
|
||||
expression = reduce(__add__, expressions)
|
||||
qs = qs.annotate(similarity=expression / len(kwargs))
|
||||
qs = qs.order_by('similarity', *ordering)
|
||||
return qs
|
||||
|
||||
|
||||
class CommonData(models.Model):
|
||||
def clean(self):
|
||||
if self.schema:
|
||||
try:
|
||||
schema_validator(self.schema.schema)(self.content)
|
||||
except ValidationError, e:
|
||||
raise ValidationError({'content': e})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# ensure we have a strict serialization of transactions
|
||||
# move elsewhere later
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('LOCK TABLE %s' % Transaction._meta.db_table)
|
||||
tr = Transaction.objects.create()
|
||||
if self.id:
|
||||
self.modified = tr
|
||||
else:
|
||||
self.created = tr
|
||||
return super(CommonData, self).save(*args, **kwargs)
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.id)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Entity(CommonData):
|
||||
schema = models.ForeignKey(
|
||||
'zoo_meta.EntitySchema',
|
||||
verbose_name=_('schema'))
|
||||
created = models.ForeignKey(
|
||||
Transaction,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('created'),
|
||||
related_name='created_entities')
|
||||
modified = models.ForeignKey(
|
||||
Transaction,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('modified'),
|
||||
related_name='modified_entities')
|
||||
deleted = models.ForeignKey(
|
||||
Transaction,
|
||||
verbose_name=_('deleted'),
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='deleted_entities')
|
||||
meta = JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('meta'))
|
||||
content = JSONField(
|
||||
blank=True,
|
||||
null=False,
|
||||
verbose_name=_('content'))
|
||||
|
||||
objects = EntityQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('created',)
|
||||
verbose_name = _('entity')
|
||||
verbose_name_plural = _('entities')
|
||||
|
||||
|
||||
class Relation(CommonData):
|
||||
schema = models.ForeignKey(
|
||||
'zoo_meta.RelationSchema',
|
||||
verbose_name=_('schema'))
|
||||
left = models.ForeignKey(
|
||||
'Entity',
|
||||
verbose_name=_('left'),
|
||||
related_name='left_relations')
|
||||
right = models.ForeignKey(
|
||||
'Entity',
|
||||
verbose_name=_('right'),
|
||||
related_name='right_relations')
|
||||
created = models.ForeignKey(
|
||||
Transaction,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('created'),
|
||||
related_name='created_relations')
|
||||
modified = models.ForeignKey(
|
||||
Transaction,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('modified'),
|
||||
related_name='modified_relations')
|
||||
deleted = models.ForeignKey(
|
||||
Transaction,
|
||||
verbose_name=_('deleted'),
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='deleted_relations')
|
||||
meta = JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('meta'))
|
||||
content = JSONField(
|
||||
blank=True,
|
||||
null=False,
|
||||
verbose_name=_('content'))
|
||||
|
||||
class Meta:
|
||||
ordering = ('created',)
|
||||
verbose_name = _('relation')
|
||||
verbose_name_plural = _('relations')
|
||||
|
||||
|
||||
class Log(models.Model):
|
||||
entity = models.ForeignKey(
|
||||
'Entity',
|
||||
verbose_name=_('entity'))
|
||||
transaction = models.ForeignKey(
|
||||
'Transaction',
|
||||
verbose_name=_('transaction'))
|
||||
timestamp = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
db_index=True,
|
||||
verbose_name=_('timestamp'))
|
||||
content = JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('content'))
|
||||
url = models.URLField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('url'))
|
||||
|
||||
class Meta:
|
||||
ordering = ('timestamp',)
|
||||
verbose_name = _('log')
|
||||
verbose_name_plural = _('logs')
|
|
@ -0,0 +1,39 @@
|
|||
from django.db.models import Func, Value
|
||||
|
||||
|
||||
class Unaccent(Func):
|
||||
function = 'immutable_unaccent'
|
||||
arity = 1
|
||||
|
||||
|
||||
class Normalize(Func):
|
||||
function = 'immutable_normalize'
|
||||
arity = 1
|
||||
|
||||
|
||||
class Lower(Func):
|
||||
function = 'LOWER'
|
||||
arity = 1
|
||||
|
||||
|
||||
class JSONRef(Func):
|
||||
function = ''
|
||||
arg_joiner = '->'
|
||||
|
||||
def __init__(self, *expressions, **extra):
|
||||
jsonb = expressions[0]
|
||||
refs = map(Value, expressions[1:])
|
||||
super(JSONRef, self).__init__(jsonb, *refs, **extra)
|
||||
|
||||
|
||||
class JSONTextRef(Func):
|
||||
function = ''
|
||||
arg_joiner = '->>'
|
||||
arity = 2
|
||||
|
||||
def __init__(self, *expressions, **extra):
|
||||
jsonb = expressions[0]
|
||||
if len(expressions) > 2:
|
||||
jsonb = JSONRef(jsonb, *expressions[1:-1])
|
||||
ref = Value(expressions[-1])
|
||||
super(JSONTextRef, self).__init__(jsonb, ref, **extra)
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,31 @@
|
|||
import json
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
from django import forms
|
||||
|
||||
|
||||
class JSONEditor(forms.Textarea):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.schema = kwargs.pop('schema', None)
|
||||
super(JSONEditor, self).__init__(*args, **kwargs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
if self.schema:
|
||||
attrs['style'] = 'display: none'
|
||||
s = super(JSONEditor, self).render(name, value, attrs=attrs)
|
||||
s += mark_safe('<div style="display: inline-block; width: 80%%" id="%s_editor_holder"></div>"' % attrs['id'])
|
||||
s += mark_safe('''<script>
|
||||
(function () {
|
||||
var schema = %s;
|
||||
var jsoneditor = new JSONEditor(document.getElementById("%s_editor_holder"),
|
||||
{
|
||||
schema: schema,
|
||||
disable_properties: true,
|
||||
show_errors: "always",
|
||||
});
|
||||
})();
|
||||
</script>''' % (json.dumps(self.schema), attrs['id']))
|
||||
return s
|
||||
|
||||
class Media:
|
||||
js = ('js/jsoneditor.min.js',)
|
|
@ -0,0 +1,39 @@
|
|||
from django.forms import Form, CharField, DecimalField, TextInput, NumberInput
|
||||
|
||||
from zoo.models import Entity
|
||||
|
||||
|
||||
class EntitySearchForm(Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.schema = kwargs.pop('schema')
|
||||
super(EntitySearchForm, self).__init__(*args, **kwargs)
|
||||
self.fields['limit'] = DecimalField(
|
||||
widget=NumberInput(attrs={
|
||||
'type': 'range',
|
||||
'min': '0',
|
||||
'max': '1',
|
||||
'step': '0.02',
|
||||
}),
|
||||
required=False)
|
||||
for path, _type in self.schema.paths():
|
||||
if _type != 'string':
|
||||
continue
|
||||
key = '__'.join(path)
|
||||
self.fields[key] = CharField(
|
||||
max_length=32,
|
||||
required=False,
|
||||
widget=TextInput(attrs={'autocomplete': 'off'}),
|
||||
)
|
||||
|
||||
def search(self):
|
||||
kwargs = {}
|
||||
for key in self.fields:
|
||||
if key == 'limit':
|
||||
continue
|
||||
if self.cleaned_data.get(key):
|
||||
kwargs[key] = self.cleaned_data[key]
|
||||
limit = self.cleaned_data.get('limit') or None
|
||||
if kwargs:
|
||||
return Entity.objects.content_search(self.schema, limit=limit, **kwargs)
|
||||
else:
|
||||
return Entity.objects.none()
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,22 @@
|
|||
<html>
|
||||
<body>
|
||||
<h1>{{ schema.name }}</h1>
|
||||
<p><a href="..">Retour</a></p>
|
||||
<h2>Définition</h2>
|
||||
<pre>{{ schema.schema|pprint }}</pre>
|
||||
<h2>Doublons</h2>
|
||||
{% if not doublons %}
|
||||
<p>Aucun doublon</p>
|
||||
{% else %}
|
||||
<table>
|
||||
{% for one, two in doublons %}
|
||||
<tr>
|
||||
<td>{{ two.similarity }}</td>
|
||||
<td><pre>{{ one.content|pprint }}</pre></td>
|
||||
<td><pre>{{ two.content|pprint }}</pre></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,86 @@
|
|||
{% load staticfiles %}<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="{% static "js/jquery.min.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$(document).on('change', 'form input', function () {
|
||||
var $form = $('form');
|
||||
$.ajax({
|
||||
url: '.',
|
||||
type: 'GET',
|
||||
data: $form.serialize(),
|
||||
success: function(html) {
|
||||
var $result = $(html);
|
||||
$form.replaceWith($('form', $result));
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
var timeout = null;
|
||||
|
||||
function reload(ev, $form, html) {
|
||||
var $result = $.parseHTML(html, document, true);
|
||||
$form.find('ul').replaceWith($('form', $result).find('ul'));
|
||||
var $input = $('form input[name=' + ev.target.name + ']');
|
||||
}
|
||||
$(document).on('keyup', 'form input', function (ev) {
|
||||
var $form = $('form');
|
||||
$.ajax({
|
||||
url: '.',
|
||||
type: 'GET',
|
||||
data: $form.serialize(),
|
||||
success: function (html) {
|
||||
if (timeout != null) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
timeout = setTimeout(function () {
|
||||
reload(ev, $form, html);
|
||||
}, 10);
|
||||
},
|
||||
});
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
form p {
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>{{ schema.name }}</h1>
|
||||
<p><a href="..">Retour</a></p>
|
||||
<h2>Recherche</h2>
|
||||
<form method="get">
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Recherche"/>
|
||||
{% if previous_page != -1 %}
|
||||
<a href="?{{ request.GET.urlencode }}&page={{ previous_page }}">précedents</a>
|
||||
{% endif %}
|
||||
{% if next_page %}
|
||||
<a href="?{{ request.GET.urlencode }}&page={{ next_page }}">suivants</a>
|
||||
{% endif %}
|
||||
{% if entities %}
|
||||
<ul>
|
||||
{% for entity in entities %}
|
||||
|
||||
<li>
|
||||
<div>Id: {{ entity.id }}</div>
|
||||
<div>Différence: {{ entity.similarity }}</div>
|
||||
<pre>{{ entity.content|pprint }}</pre>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Aucune entité trouvée</p>
|
||||
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<body>
|
||||
<ul>
|
||||
{% for schema in schemas %}
|
||||
<li><a href="{% url 'demo-schema' name=schema.name %}">{{ schema.name }}</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from .views import schemas, schema, doublons
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^schemas/$', schemas, name='demo-schemas'),
|
||||
url(r'^schemas/(?P<name>\w*)/$', schema, name='demo-schema'),
|
||||
url(r'^schemas/(?P<name>\w*)/doublons/$', doublons, name='demo-doublons'),
|
||||
]
|
|
@ -0,0 +1,53 @@
|
|||
from django.shortcuts import render, get_object_or_404
|
||||
|
||||
from zoo.models import EntitySchema, Entity
|
||||
|
||||
from .forms import EntitySearchForm
|
||||
|
||||
|
||||
def schemas(request):
|
||||
'''Show all schemas and give link to search page'''
|
||||
return render(request, 'zoo_demo/schemas.html', {
|
||||
'schemas': EntitySchema.objects.all(),
|
||||
})
|
||||
|
||||
|
||||
def schema(request, name):
|
||||
'''Display a search form for the targeted schema'''
|
||||
schema = get_object_or_404(EntitySchema, name=name)
|
||||
qs = Entity.objects.filter(schema=schema)
|
||||
form = EntitySearchForm(schema=schema, data=request.GET)
|
||||
if form.is_valid():
|
||||
qs = form.search()
|
||||
else:
|
||||
qs = Entity.objects.none()
|
||||
try:
|
||||
page = int(request.GET.get('page', 0))
|
||||
except ValueError:
|
||||
page = 0
|
||||
qs = qs[page * 10:(page + 1) * 10]
|
||||
next_page = page + 1
|
||||
if len(qs) < 10:
|
||||
next_page = None
|
||||
return render(request, 'zoo_demo/schema.html', {
|
||||
'schema': schema,
|
||||
'form': form,
|
||||
'entities': qs,
|
||||
'next_page': next_page,
|
||||
'previous_page': page - 1,
|
||||
})
|
||||
|
||||
|
||||
def doublons(request, name):
|
||||
'''Display potential doublons'''
|
||||
schema = get_object_or_404(EntitySchema, name=name)
|
||||
paths = [key.split('__') for key in request.GET]
|
||||
|
||||
if paths:
|
||||
doublons = Entity.objects.doublons(schema, *paths)
|
||||
else:
|
||||
doublons = []
|
||||
return render(request, 'zoo_demo/doublons.html', {
|
||||
'schema': schema,
|
||||
'doublons': doublons,
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'zoo.zoo_meta.apps.ZooMetaAppConfig'
|
|
@ -0,0 +1,13 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import EntitySchema, RelationSchema
|
||||
|
||||
|
||||
class SchemaAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ('name',),
|
||||
}
|
||||
|
||||
|
||||
admin.site.register(EntitySchema, SchemaAdmin)
|
||||
admin.site.register(RelationSchema, SchemaAdmin)
|
|
@ -0,0 +1,7 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ZooMetaAppConfig(AppConfig):
|
||||
name = 'zoo.zoo_meta'
|
||||
verbose_name = _('metadatas')
|
|
@ -0,0 +1,29 @@
|
|||
# zoo - store anything
|
||||
# Copyright (C) 2016 Entr'ouvert
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from zoo.zoo_meta.models import EntitySchema
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
for schema in EntitySchema.objects.all():
|
||||
if options['verbosity'] >= 1:
|
||||
print 'Rebuilding index for', unicode(schema),
|
||||
schema.rebuild_indexes()
|
||||
if options['verbosity'] >= 1:
|
||||
print ' Done.'
|
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-01-04 12:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import zoo.zoo_meta.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EntitySchema',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=64, unique=True, verbose_name='name')),
|
||||
('slug', models.SlugField(max_length=64, unique=True, verbose_name='slug')),
|
||||
('schema', django.contrib.postgres.fields.jsonb.JSONField(validators=[zoo.zoo_meta.validators.validate_schema], verbose_name='schema')),
|
||||
('caption_template', models.TextField(blank=True, verbose_name='caption template')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
'verbose_name': 'entity schema',
|
||||
'verbose_name_plural': 'entity schemas',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RelationSchema',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=64, unique=True, verbose_name='name')),
|
||||
('slug', models.SlugField(max_length=64, unique=True, verbose_name='slug')),
|
||||
('schema', django.contrib.postgres.fields.jsonb.JSONField(validators=[zoo.zoo_meta.validators.validate_schema], verbose_name='schema')),
|
||||
('caption_template', models.TextField(blank=True, verbose_name='caption template')),
|
||||
('is_symmetric', models.BooleanField(default=False, verbose_name='is symmetric')),
|
||||
('left', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='zoo_meta.EntitySchema', verbose_name='left schema')),
|
||||
('right', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='zoo_meta.EntitySchema', verbose_name='right schema')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
'verbose_name': 'relation schema',
|
||||
'verbose_name_plural': 'relation schemas',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2016-12-14 15:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
from django.contrib.postgres.operations import TrigramExtension, UnaccentExtension
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('zoo_meta', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
TrigramExtension(),
|
||||
UnaccentExtension(),
|
||||
migrations.RunSQL([
|
||||
"CREATE OR REPLACE FUNCTION immutable_unaccent(text) RETURNS varchar AS $$ "
|
||||
" SELECT unaccent('unaccent',$1::text); $$ LANGUAGE 'sql' IMMUTABLE",
|
||||
"CREATE OR REPLACE FUNCTION immutable_timestamp(text) RETURNS timestamp AS $$ "
|
||||
"SELECT $1::timestamp; $$ LANGUAGE sql IMMUTABLE",
|
||||
"CREATE OR REPLACE FUNCTION immutable_normalize(text) RETURNS varchar AS $$ "
|
||||
" SELECT regexp_replace(lower(unaccent('unaccent', $1::text)), ' *([''-] *)+', 'x', 'g'); "
|
||||
"$$ LANGUAGE sql IMMUTABLE",
|
||||
], []),
|
||||
]
|
|
@ -0,0 +1,143 @@
|
|||
from hashlib import md5
|
||||
|
||||
from django.db import models, connection
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
|
||||
|
||||
from .validators import validate_schema
|
||||
|
||||
|
||||
class GetBySlugManager(models.Manager):
|
||||
def get_by_natural_key(self, slug):
|
||||
return self.get(slug=slug)
|
||||
|
||||
|
||||
class CommonSchema(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=64,
|
||||
unique=True,
|
||||
verbose_name=_('name'))
|
||||
slug = models.SlugField(
|
||||
max_length=64,
|
||||
unique=True,
|
||||
verbose_name=_('slug'))
|
||||
schema = JSONField(
|
||||
verbose_name=_('schema'),
|
||||
validators=[validate_schema])
|
||||
caption_template = models.TextField(
|
||||
verbose_name=_('caption template'),
|
||||
blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def natural_key(self):
|
||||
return (self.slug,)
|
||||
|
||||
def paths(self, schema=None):
|
||||
schema = schema or self.schema
|
||||
paths = []
|
||||
if isinstance(schema, dict) and schema.get('type') == 'object':
|
||||
if 'properties' not in schema:
|
||||
return []
|
||||
for key in schema['properties']:
|
||||
subschema = schema['properties'][key]
|
||||
t = subschema.get('type')
|
||||
if t == 'object':
|
||||
paths.extend(([key] + subpath, _type)
|
||||
for subpath, _type in self.paths(subschema))
|
||||
elif t is not None:
|
||||
paths.append(([key], t, subschema.get('format')))
|
||||
return paths
|
||||
|
||||
def path_to_sql_expr(self, path):
|
||||
return ''.join('->\'%s\'' % elt for elt in path[:-1]) + '->>\'%s\'' % path[-1]
|
||||
|
||||
def rebuild_string_index(self, cursor, table, path):
|
||||
expr = 'immutable_normalize((content%s))' % self.path_to_sql_expr(path)
|
||||
key = md5(expr).hexdigest()[:8]
|
||||
sql = ('CREATE INDEX zoo_entity_%s_gin_%s_dynamic_idx ON %s USING gin ((%s) '
|
||||
' gin_trgm_ops) WHERE schema_id = %s' % (key, self.id, table, expr, self.id))
|
||||
cursor.execute(sql)
|
||||
sql = ('CREATE INDEX zoo_entity_%s_gist_%s_dynamic_idx ON %s USING gist ((%s) '
|
||||
' gist_trgm_ops) WHERE schema_id = %s' % (key, self.id, table, expr, self.id))
|
||||
cursor.execute(sql)
|
||||
|
||||
def rebuild_string_date_time_index(self, cursor, table, path):
|
||||
expr = 'immutable_timestamp(content%s)' % self.path_to_sql_expr(path)
|
||||
key = md5(expr).hexdigest()[:8]
|
||||
sql = ('CREATE INDEX zoo_entity_%s_%s_dynamic_idx ON %s (%s) '
|
||||
'WHERE schema_id = %s' % (key, self.id, table, expr, self.id))
|
||||
cursor.execute(sql)
|
||||
|
||||
def rebuild_indexes(self):
|
||||
from zoo.zoo_data.models import Entity
|
||||
|
||||
paths = self.paths()
|
||||
table = Entity._meta.db_table
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
constraints = connection.introspection.get_constraints(cursor, table)
|
||||
|
||||
# drop existing indexes
|
||||
for key in constraints:
|
||||
if not key.endswith('_%s_dynamic_idx' % self.pk):
|
||||
continue
|
||||
cursor.execute('DROP INDEX IF EXISTS %s' % key)
|
||||
|
||||
# rebuild them
|
||||
for path, _type, _format in paths:
|
||||
if _format:
|
||||
_format = _format.replace('-', '_')
|
||||
m = getattr(self, 'rebuild_%s_%s_index' % (_type, _format), None)
|
||||
if m is not None:
|
||||
m(cursor, table, path)
|
||||
continue
|
||||
m = getattr(self, 'rebuild_%s_index' % _type, None)
|
||||
if m is not None:
|
||||
m(cursor, table, path)
|
||||
|
||||
objects = GetBySlugManager()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(CommonSchema, self).save(*args, **kwargs)
|
||||
self.rebuild_indexes()
|
||||
|
||||
def make_caption(self, value):
|
||||
if self.caption_template:
|
||||
try:
|
||||
return eval(self.caption_template, {}, value.content)
|
||||
except Exception, e:
|
||||
return unicode(e)
|
||||
return unicode(value.id)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class EntitySchema(CommonSchema):
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('entity schema')
|
||||
verbose_name_plural = _('entity schemas')
|
||||
|
||||
|
||||
class RelationSchema(CommonSchema):
|
||||
left = models.ForeignKey(
|
||||
EntitySchema,
|
||||
verbose_name=_('left schema'),
|
||||
related_name='+')
|
||||
right = models.ForeignKey(
|
||||
EntitySchema,
|
||||
verbose_name=_('right schema'),
|
||||
related_name='+')
|
||||
is_symmetric = models.BooleanField(
|
||||
default=False,
|
||||
blank=True,
|
||||
verbose_name=_('is symmetric'))
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('relation schema')
|
||||
verbose_name_plural = _('relation schemas')
|
|
@ -0,0 +1,18 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
|
||||
from jsonschema import Draft4Validator
|
||||
|
||||
|
||||
def schema_validator(schema):
|
||||
def validate(value):
|
||||
errors = []
|
||||
validator = Draft4Validator(schema)
|
||||
for error in sorted(validator.iter_errors(value), key=str):
|
||||
errors.append(ValidationError(error.message))
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
return validate
|
||||
|
||||
|
||||
validate_schema = schema_validator(Draft4Validator.META_SCHEMA)
|
||||
validate_schema.__name__ = 'validate_schema'
|
|
@ -0,0 +1,105 @@
|
|||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from zoo.zoo_data.models import Entity
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def individu_to_response(individu):
|
||||
'''Serialize a person'''
|
||||
d = individu.content.copy()
|
||||
d['id'] = individu.id
|
||||
|
||||
if hasattr(individu, 'age_label'):
|
||||
d['age_label'] = individu.age_label
|
||||
if hasattr(individu, 'age'):
|
||||
d['age'] = individu.age
|
||||
if hasattr(individu, 'similarity'):
|
||||
d['score'] = individu.similarity
|
||||
if hasattr(individu, 'adresses'):
|
||||
d['adresses'] = individu.adresses
|
||||
if hasattr(individu, 'responsabilite_legale'):
|
||||
d['responsabilite_legale'] = individu.responsabilite_legale
|
||||
if hasattr(individu, 'enfants'):
|
||||
d['enfants'] = [individu_to_response(enfant) for enfant in individu.enfants]
|
||||
if hasattr(individu, 'parents'):
|
||||
d['parents'] = [individu_to_response(parent) for parent in individu.parents]
|
||||
if hasattr(individu, 'union'):
|
||||
d['union'] = individu_to_response(individu.union)
|
||||
d['union_statut'] = individu.union_statut
|
||||
return d
|
||||
|
||||
|
||||
class SearchView(APIView):
|
||||
def get(self, request, format=None):
|
||||
try:
|
||||
limit = int(request.GET.get('limit', ''))
|
||||
except ValueError:
|
||||
limit = 100
|
||||
try:
|
||||
offset = int(request.GET.get('offset', ''))
|
||||
except ValueError:
|
||||
offset = 0
|
||||
try:
|
||||
threshold = float(request.GET.get('threshold', ''))
|
||||
except ValueError:
|
||||
threshold = 0.2
|
||||
search = utils.PersonSearch(limit=threshold)
|
||||
if 'q' in request.GET:
|
||||
search = search.search_query(request.GET['q'])
|
||||
else:
|
||||
prenom = request.GET.get('prenom')
|
||||
nom = request.GET.get('nom')
|
||||
date_de_naissance = request.GET.get('date_de_naissance')
|
||||
cle = request.GET.get('cle')
|
||||
email = request.GET.get('email', '').strip()
|
||||
if prenom or nom:
|
||||
search = search.search_name(prenom, nom)
|
||||
if date_de_naissance and search.match_birthdate(date_de_naissance):
|
||||
search = search.search_birthdate(date_de_naissance)
|
||||
if cle:
|
||||
search = search.search_identifier(cle)
|
||||
if email:
|
||||
search = search.search_email(email)
|
||||
data = [individu_to_response(person) for person in search[offset:offset + limit]]
|
||||
return Response({
|
||||
'err': 0,
|
||||
'offset': offset,
|
||||
'limit': limit,
|
||||
'count': len(data),
|
||||
'data': data,
|
||||
'meta': {
|
||||
'applications': utils.PersonSearch.applications(),
|
||||
}
|
||||
})
|
||||
|
||||
search = SearchView.as_view()
|
||||
|
||||
|
||||
class ReseauView(APIView):
|
||||
def get(self, request, identifier, format=None):
|
||||
qs = Entity.objects.prefetch_related(
|
||||
'left_relations__schema', 'left_relations__right',
|
||||
'right_relations__schema', 'right_relations__left',
|
||||
)
|
||||
individu = get_object_or_404(qs, schema__slug='individu', id=identifier)
|
||||
utils.PersonSearch.decorate_individu(individu)
|
||||
return Response({
|
||||
'err': 0,
|
||||
'data': individu_to_response(individu),
|
||||
'meta': {
|
||||
'applications': utils.PersonSearch.applications(),
|
||||
}
|
||||
})
|
||||
|
||||
reseau = ReseauView.as_view()
|
||||
|
||||
|
||||
class CreateIndividu(APIView):
|
||||
def post(self, request):
|
||||
return Response({})
|
||||
|
||||
create_individu = CreateIndividu.as_view()
|
|
@ -0,0 +1,27 @@
|
|||
from zoo.zoo_data.models import Entity
|
||||
|
||||
|
||||
def doublons(schema, *args):
|
||||
'''Search for duplicate entities based on a list of field paths'''
|
||||
qs = Entity.objects.filter(schema=schema)
|
||||
paths = schema.paths_to_strings()
|
||||
for arg in args:
|
||||
if arg not in paths:
|
||||
raise ValueError('%s is not a path to a string value' % arg)
|
||||
|
||||
def value(item, path):
|
||||
for elt in path:
|
||||
item = item[elt]
|
||||
return item
|
||||
|
||||
seen = set()
|
||||
for item in qs:
|
||||
if item.pk in seen:
|
||||
continue
|
||||
for arg in args:
|
||||
kwargs = {}
|
||||
for path in paths:
|
||||
kwargs['__'.join(path)] = value(item.content, path)
|
||||
for doublon in qs.exclude(pk=item.pk).content_search(schema, **kwargs):
|
||||
yield item, doublon
|
||||
seen.add(doublon.pk)
|
|
@ -0,0 +1,246 @@
|
|||
[
|
||||
{
|
||||
"fields" : {
|
||||
"caption_template" : "{prenoms} {nom_de_naissance}",
|
||||
"schema" : {
|
||||
"title" : "individu",
|
||||
"properties" : {
|
||||
"telephones" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"numero" : {
|
||||
"pattern" : "^[0-9 .-]*$",
|
||||
"type" : "string"
|
||||
},
|
||||
"type" : {
|
||||
"enum" : [
|
||||
"maison",
|
||||
"mobile",
|
||||
"pro",
|
||||
"autre"
|
||||
],
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nom_" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"email" : {
|
||||
"type" : "string",
|
||||
"format" : "email"
|
||||
},
|
||||
"prenoms" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"nom_d_usage" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"date_de_naissance" : {
|
||||
"type" : "string",
|
||||
"format" : "date-time"
|
||||
},
|
||||
"statut_legal" : {
|
||||
"enum" : [
|
||||
"majeur",
|
||||
"mineur"
|
||||
],
|
||||
"type" : "string"
|
||||
},
|
||||
"genre" : {
|
||||
"enum" : [
|
||||
"femme",
|
||||
"homme",
|
||||
"autre"
|
||||
],
|
||||
"type" : "string"
|
||||
},
|
||||
"cles_de_federation" : {
|
||||
"type" : "object",
|
||||
"additionalProperties" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"id" : {
|
||||
"oneOf" : [
|
||||
{
|
||||
"type" : "string"
|
||||
},
|
||||
{
|
||||
"type" : "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nom_de_naissance" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"date_de_deces" : {
|
||||
"type" : "string",
|
||||
"format" : "date-time"
|
||||
}
|
||||
},
|
||||
"required" : [
|
||||
"prenoms",
|
||||
"nom_de_naissance",
|
||||
"date_de_naissance",
|
||||
"genre"
|
||||
],
|
||||
"type" : "object"
|
||||
},
|
||||
"slug" : "individu",
|
||||
"name" : "Individu"
|
||||
},
|
||||
"model" : "zoo_meta.entityschema"
|
||||
},
|
||||
{
|
||||
"model" : "zoo_meta.entityschema",
|
||||
"fields" : {
|
||||
"slug" : "adresse",
|
||||
"name" : "Adresse",
|
||||
"schema" : {
|
||||
"required" : [
|
||||
"streetname",
|
||||
"city",
|
||||
"country"
|
||||
],
|
||||
"properties" : {
|
||||
"streetname" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"inseecode" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"streetmatriculation" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"ext1" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"zipcode" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"at" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"adresse_inconnnue" : {
|
||||
"type" : "boolean",
|
||||
"default" : false
|
||||
},
|
||||
"city" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"streetnumberext" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"country" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"ext2" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"streetnumber" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
},
|
||||
"caption_template" : "{streetname} {city}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model" : "zoo_meta.relationschema",
|
||||
"fields" : {
|
||||
"name" : "Habite",
|
||||
"slug" : "habite",
|
||||
"left" : [
|
||||
"individu"
|
||||
],
|
||||
"right" : [
|
||||
"adresse"
|
||||
],
|
||||
"is_symmetric" : false,
|
||||
"caption_template" : "",
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"principale" : {
|
||||
"default" : false,
|
||||
"type" : "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fields" : {
|
||||
"right" : [
|
||||
"individu"
|
||||
],
|
||||
"name" : "Responsabilité légale",
|
||||
"slug" : "responsabilite-legale",
|
||||
"left" : [
|
||||
"individu"
|
||||
],
|
||||
"caption_template" : "",
|
||||
"schema" : {
|
||||
"required" : [
|
||||
"statut"
|
||||
],
|
||||
"properties" : {
|
||||
"facturation" : {
|
||||
"default" : false,
|
||||
"type" : "boolean"
|
||||
},
|
||||
"statut" : {
|
||||
"type" : "string",
|
||||
"enum" : [
|
||||
"parent",
|
||||
"tiers_de_confiance",
|
||||
"representant_personne_morale_qualifiee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type" : "object",
|
||||
"title" : "responsabilite_legale"
|
||||
},
|
||||
"is_symmetric" : false
|
||||
},
|
||||
"model" : "zoo_meta.relationschema"
|
||||
},
|
||||
{
|
||||
"fields" : {
|
||||
"right" : [
|
||||
"individu"
|
||||
],
|
||||
"left" : [
|
||||
"individu"
|
||||
],
|
||||
"slug" : "union",
|
||||
"name" : "Union",
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"required" : [
|
||||
"statut"
|
||||
],
|
||||
"properties" : {
|
||||
"statut" : {
|
||||
"type" : "string",
|
||||
"enum" : [
|
||||
"pacs",
|
||||
"mariage",
|
||||
"unionlibre"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"caption_template" : "",
|
||||
"is_symmetric" : true
|
||||
},
|
||||
"model" : "zoo_meta.relationschema"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
from django import forms
|
||||
|
||||
from .utils import PersonSearch
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
limit = forms.DecimalField(
|
||||
widget=forms.NumberInput(attrs={
|
||||
'type': 'range',
|
||||
'min': '0',
|
||||
'max': '1',
|
||||
'step': '0.02',
|
||||
}),
|
||||
required=False)
|
||||
query = forms.CharField(
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'off',
|
||||
}))
|
||||
|
||||
def results(self):
|
||||
query = self.cleaned_data['query']
|
||||
try:
|
||||
limit = float(self.cleaned_data.get('limit'))
|
||||
except ValueError:
|
||||
limit = 0.5
|
||||
return iter(PersonSearch(limit=limit).search_query(query))
|
|
@ -0,0 +1,198 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
import psycopg2
|
||||
|
||||
from zoo.models import *
|
||||
|
||||
Relation.objects.all().delete()
|
||||
Entity.objects.all().delete()
|
||||
|
||||
dbname = sys.argv[1]
|
||||
|
||||
connection = psycopg2.connect(dbname=dbname)
|
||||
|
||||
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
|
||||
psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY)
|
||||
|
||||
cursor = connection.cursor()
|
||||
|
||||
tr = Transaction.objects.create(meta="initial import")
|
||||
|
||||
cursor.execute('''SELECT id, gender, firstname, lastname, nameofuse, email, phones::json,
|
||||
legalstatus, birthdate, mappings::json
|
||||
FROM individual''')
|
||||
|
||||
individu_batch = []
|
||||
|
||||
individu_schema = EntitySchema.objects.get(slug='individu')
|
||||
adresse_schema = EntitySchema.objects.get(slug='adresse')
|
||||
habite_schema = RelationSchema.objects.get(slug='habite')
|
||||
responsabilite_legale_schema = RelationSchema.objects.get(slug='responsabilite-legale')
|
||||
union_schema = RelationSchema.objects.get(slug='union')
|
||||
|
||||
|
||||
individu_mapping = {}
|
||||
|
||||
for (individualid, gender, firstname, lastname, nameofuse, email, phones, legalstatus, birthdate,
|
||||
mappings) in cursor.fetchall():
|
||||
if gender == 'Female':
|
||||
genre = 'femme'
|
||||
elif gender == 'Male':
|
||||
genre = 'homme'
|
||||
elif gender == 'Other':
|
||||
genre = 'autre'
|
||||
else:
|
||||
raise NotImplementedError('unknown gender: %s' % gender)
|
||||
|
||||
if legalstatus == 'Adulte':
|
||||
statut_legal = 'majeur'
|
||||
elif legalstatus == 'Enfant':
|
||||
statut_legal = 'mineur'
|
||||
elif legalstatus == 'Emancipe':
|
||||
status_legal = 'emancipe'
|
||||
else:
|
||||
raise NotImplementedError('unknown legalstatus: %s' % legalstatus)
|
||||
|
||||
telephones = []
|
||||
for phone in phones:
|
||||
if phone['phoneType'] == 'OtherPhone':
|
||||
kind = 'autre'
|
||||
elif phone['phoneType'] == 'Pro':
|
||||
kind = 'pro'
|
||||
elif phone['phoneType'] == 'Mobile':
|
||||
kind = 'mobile'
|
||||
elif phone['phoneType'] == 'Home':
|
||||
kind = 'autre'
|
||||
else:
|
||||
raise NotImplementedError('unknown phoneType: %s' % phone['phoneType'])
|
||||
telephones.append({'type': kind, 'numero': phone['number']})
|
||||
|
||||
content = {
|
||||
'genre': genre,
|
||||
'prenoms': firstname.upper(),
|
||||
'nom_de_naissance': lastname.upper() if lastname else '',
|
||||
'nom_d_usage': nameofuse.upper(),
|
||||
'statut_legal': statut_legal,
|
||||
'date_de_naissance': birthdate.isoformat(),
|
||||
'cles_de_federation': mappings,
|
||||
}
|
||||
if telephones:
|
||||
content['telephones'] = telephones
|
||||
if email:
|
||||
content['email'] = email
|
||||
e = Entity(created=tr, schema=individu_schema, content=content)
|
||||
individu_mapping[individualid] = e
|
||||
individu_batch.append(e)
|
||||
|
||||
Entity.objects.bulk_create(individu_batch)
|
||||
|
||||
adresse_batch = []
|
||||
adresse_mapping = {}
|
||||
|
||||
individu_adresse_mapping = {}
|
||||
|
||||
# Création des adresses
|
||||
cursor.execute('''
|
||||
SELECT
|
||||
ia.individualid,
|
||||
a.id,
|
||||
ia.isprimary,
|
||||
a.streetnumber,
|
||||
a.streetnumberext,
|
||||
a.streetname,
|
||||
a.streetmatriculation,
|
||||
a.ext1,
|
||||
a.ext2,
|
||||
a.at,
|
||||
a.city,
|
||||
a.zipcode,
|
||||
a.country,
|
||||
a.inseecode
|
||||
FROM individualaddress as ia, address as a
|
||||
WHERE
|
||||
ia.addressid = a.id''')
|
||||
for individualid, addressid, is_primary, streetnumber, streetnumberext, streetname, streetmatriculation, \
|
||||
ext1, ext2, at, city, zipcode, country, inseecode in cursor.fetchall():
|
||||
if individualid not in individu_mapping:
|
||||
continue
|
||||
if addressid not in adresse_mapping:
|
||||
content = {
|
||||
'streetnumber': streetnumber,
|
||||
'streetnumberext': streetnumberext,
|
||||
'streetname': streetname,
|
||||
'streetmatriculation': streetmatriculation,
|
||||
'ext1': ext1,
|
||||
'ext2': ext2,
|
||||
'at': at,
|
||||
'city': city,
|
||||
'zipcode': zipcode,
|
||||
'country': country,
|
||||
'inseecode': inseecode
|
||||
}
|
||||
e = Entity(created=tr, schema=adresse_schema, content=content)
|
||||
adresse_batch.append(e)
|
||||
adresse_mapping[addressid] = e
|
||||
individu_adresse_mapping[(individualid, addressid)] = is_primary
|
||||
|
||||
Entity.objects.bulk_create(adresse_batch)
|
||||
|
||||
relation_batch = []
|
||||
for (a, b), is_primary in individu_adresse_mapping.iteritems():
|
||||
content = {
|
||||
'facturation': is_primary,
|
||||
}
|
||||
e = Relation(created=tr,
|
||||
left=individu_mapping[a],
|
||||
right=adresse_mapping[b],
|
||||
schema=habite_schema,
|
||||
content=content)
|
||||
relation_batch.append(e)
|
||||
|
||||
Relation.objects.bulk_create(relation_batch)
|
||||
|
||||
# Création des relations entre individus
|
||||
cursor.execute('''
|
||||
SELECT
|
||||
label,
|
||||
relationtype,
|
||||
responsibleid,
|
||||
subjectid
|
||||
FROM individualrelation''')
|
||||
|
||||
relation_batch = []
|
||||
|
||||
for label, relationtype, responsibleid, subjectid in cursor.fetchall():
|
||||
if relationtype == 'SituationFamiliale':
|
||||
schema = union_schema
|
||||
if label == 'Marie':
|
||||
kind = 'mariage'
|
||||
elif label == 'Pacse':
|
||||
kind = 'pacs'
|
||||
elif label == 'UnionLibre':
|
||||
kind = 'unionlibre'
|
||||
else:
|
||||
raise NotImplementedError('unknown label for relationtype: %s, %s' % (label,
|
||||
relationtype))
|
||||
content = {'statut': kind}
|
||||
elif relationtype == 'ResponsabiliteLegale':
|
||||
schema = responsabilite_legale_schema
|
||||
if label == 'Parent':
|
||||
kind = 'parent'
|
||||
elif label == 'TiersDeConfiance':
|
||||
kind = 'tiers_de_confiance'
|
||||
elif label == 'RepresentantPersonneMoraleQualifiee':
|
||||
kind = 'representant_personne_morale_qualifiee'
|
||||
elif label == 'Tuteur':
|
||||
kind = 'tuteur'
|
||||
else:
|
||||
raise NotImplementedError('unknown label for relationtype: %s, %s' % (label,
|
||||
relationtype))
|
||||
content = {'statut': kind}
|
||||
e = Relation(created=tr, schema=schema, left=individu_mapping[responsibleid],
|
||||
right=individu_mapping[subjectid], content=content)
|
||||
relation_batch.append(e)
|
||||
|
||||
Relation.objects.bulk_create(relation_batch)
|
||||
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
form label {
|
||||
display: none;
|
||||
}
|
||||
#id_query {
|
||||
width: 90%;
|
||||
display: inline;
|
||||
float: right;
|
||||
}
|
||||
#id_limit {
|
||||
width: 10%;
|
||||
display: inline;
|
||||
padding: 0.7ex 0.7em;
|
||||
height: calc(1.6em + 0.7ex + 2px);
|
||||
border: none;
|
||||
margin: 0.2em 0em;
|
||||
}
|
||||
.individu {
|
||||
border: 1px solid black;
|
||||
margin-bottom: 1ex;
|
||||
padding: 1ex;
|
||||
min-height: 3.5em;
|
||||
}
|
||||
|
||||
.individu-prenoms {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.individu-nom {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.individu-nom_de_naissance {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.individu-date_de_naissance {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.individu-score {
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
font-size: 1em;
|
||||
border: 5px solid;
|
||||
background-color: lightgreen;
|
||||
vertical-align: middle;
|
||||
line-height: 3em;
|
||||
letter-spacing: -3px;
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
.individu-age {
|
||||
}
|
||||
|
||||
.individu-numero {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.individu-numero:before {
|
||||
content: "#";
|
||||
}
|
||||
|
||||
.individu-email {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.individu-federations, .individu-adresse {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.disabled-results {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
#id_spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.spinner > div {
|
||||
background-color: #333;
|
||||
height: 100%;
|
||||
width: 6px;
|
||||
display: inline-block;
|
||||
|
||||
-webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
||||
animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.spinner .rect2 {
|
||||
-webkit-animation-delay: -1.1s;
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
|
||||
.spinner .rect3 {
|
||||
-webkit-animation-delay: -1.0s;
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
.spinner .rect4 {
|
||||
-webkit-animation-delay: -0.9s;
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
|
||||
.spinner .rect5 {
|
||||
-webkit-animation-delay: -0.8s;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-stretchdelay {
|
||||
0%, 40%, 100% { -webkit-transform: scaleY(0.4) }
|
||||
20% { -webkit-transform: scaleY(1.0) }
|
||||
}
|
||||
|
||||
@keyframes sk-stretchdelay {
|
||||
0%, 40%, 100% {
|
||||
transform: scaleY(0.4);
|
||||
-webkit-transform: scaleY(0.4);
|
||||
} 20% {
|
||||
transform: scaleY(1.0);
|
||||
-webkit-transform: scaleY(1.0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
$(function () {
|
||||
var xhr = null;
|
||||
var timeout = null;
|
||||
var last = null;
|
||||
|
||||
function submit() {
|
||||
var $form = $('form');
|
||||
if (xhr) {
|
||||
xhr.abort();
|
||||
}
|
||||
var query = $form.serialize();
|
||||
$('#id_spinner').addClass('spinner');
|
||||
xhr = $.ajax({
|
||||
url: '.',
|
||||
type: 'GET',
|
||||
data: $form.serialize(),
|
||||
success: function (html) {
|
||||
last = query;
|
||||
$('#id_spinner').removeClass('spinner');
|
||||
var $result = $.parseHTML(html, document, true);
|
||||
$('#id_results').replaceWith($('#id_results', $result));
|
||||
},
|
||||
});
|
||||
}
|
||||
function reload() {
|
||||
if (last && last == $('form').serialize()) {
|
||||
return;
|
||||
}
|
||||
$('#id_results').addClass('disabled-results');
|
||||
if (timeout != null) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
if (xhr) {
|
||||
xhr.abort();
|
||||
$('#id_spinner').removeClass('spinner');
|
||||
|
||||
}
|
||||
timeout = setTimeout(function () {
|
||||
submit();
|
||||
}, 300);
|
||||
}
|
||||
$(document).on('change keyup', 'form input', function (ev) {
|
||||
reload();
|
||||
})
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "gadjo/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Démo pour le RSU</h1>
|
||||
|
||||
<dl>
|
||||
<dt>Recherche</dt>
|
||||
<dd><a href="search/">Recherche</a></dd>
|
||||
</dl>
|
||||
{% endblock %}
|
|
@ -0,0 +1,61 @@
|
|||
{% extends "gadjo/base.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "js/rsu_search.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/rsu.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form id="id_form">
|
||||
{{ form }}
|
||||
<input type="submit" name="Rechercher">
|
||||
<span id="id_spinner"> <div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
</span>
|
||||
</form>
|
||||
|
||||
{% if results %}
|
||||
<div id="id_results">
|
||||
{% for individu in results %}
|
||||
<div class="individu individu-{{ individu.content.statut_legal }}-{{ individu.content.genre }}">
|
||||
<span class="individu-score">{% widthratio individu.similarity 1.0 100 %} %</span>
|
||||
<span class="individu-prenoms">{{ individu.content.prenoms|title }}</span>
|
||||
{% if individu.content.nom_d_usage %}
|
||||
<span class="individu-nom">{{ individu.content.nom_d_usage|upper }}</span>
|
||||
{% if individu.content.nom_de_naissance and individu.content.nom_d_usage != individu.content.nom_de_naissance %}
|
||||
<span class="individu-nom_de_naissance">(né{% if individu.content.genre == "femme" %}e{% endif %} {{ individu.content.nom_de_naissance}})</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="individu-nom">{{ individu.content.nom_d_usage|upper }}</span>
|
||||
{% endif %}
|
||||
-
|
||||
<span class="individu-age">{{ individu.age_label }}</span>
|
||||
<span class="individu-numero">{{ individu.id }}</span>
|
||||
<span class="individu-email">{{ individu.content.email }}</span>
|
||||
<span class="individu-date_de_naissance">Date de naissance : {{ individu.content.date_de_naissance }}</span>
|
||||
{% for adresse in individu.adresses %}
|
||||
<span class="individu-adresse">Adresse{% if individu.adresses|length > 1%} {{ forloop.counter }}{% endif %} : {{ adresse.streetnumber }} {{ adresse.streetname }}
|
||||
{{ adresse.zipcode }} {{ adresse.city }}</span>
|
||||
{% endfor %}
|
||||
{% if individu.federations %}
|
||||
<span class="individu-federations">Fédérations: {{ individu.federations|join:", " }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="id_results" class="big-msg-info">
|
||||
Aucune fiche trouvée
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from .views import demo, search
|
||||
from . import api_views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^demo/$', demo, name='demo'),
|
||||
url(r'^demo/search/$', search, name='demo'),
|
||||
url(r'^search/$', api_views.search, name='rsu-api-search'),
|
||||
url(r'^individu/(?P<identifier>\d+)/$', api_views.reseau, name='rsu-api-reseau'),
|
||||
]
|
|
@ -0,0 +1,386 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import datetime
|
||||
import isodate
|
||||
import operator
|
||||
import copy
|
||||
import itertools
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.search import TrigramDistance
|
||||
from django.db import connection
|
||||
from django.db.models import Q, F, Value
|
||||
from django.db.models.functions import Least, Greatest, Coalesce, Concat
|
||||
|
||||
from zoo.zoo_meta.models import EntitySchema
|
||||
from zoo.zoo_data.models import Entity
|
||||
from zoo.zoo_data.search import Unaccent, Lower, JSONTextRef, Normalize
|
||||
|
||||
today = datetime.date.today
|
||||
|
||||
|
||||
def make_date(date_var):
|
||||
'''Extract a date from a datetime, a date, a struct_time or a string'''
|
||||
if isinstance(date_var, datetime.datetime):
|
||||
return date_var.date()
|
||||
if isinstance(date_var, datetime.date):
|
||||
return date_var
|
||||
return isodate.parse_date(date_var)
|
||||
|
||||
|
||||
def date_delta(t1, t2):
|
||||
'''Return the timedelta between two date like values'''
|
||||
t1, t2 = make_date(t1), make_date(t2)
|
||||
return t1 - t2
|
||||
|
||||
|
||||
def age_in_years_and_months(born, today=None):
|
||||
'''Compute age since today as the number of years and months elapsed'''
|
||||
born = make_date(born)
|
||||
if today is None:
|
||||
today = datetime.date.today()
|
||||
today = make_date(today)
|
||||
before = (today.month, today.day) < (born.month, born.day)
|
||||
years = today.year - born.year
|
||||
months = today.month - born.month
|
||||
if before:
|
||||
years -= 1
|
||||
months += 12
|
||||
if today.day < born.day:
|
||||
months -= 1
|
||||
return years, months
|
||||
|
||||
|
||||
def age_in_years(born, today=None):
|
||||
'''Compute age since today as the number of years elapsed'''
|
||||
return age_in_years_and_months(born, today=today)[0]
|
||||
|
||||
|
||||
class PersonSearch(object):
|
||||
EMAIL_RE = re.compile(
|
||||
'^[a-zA-Z.+_-]*@[a-zA-Z.+_-]*$')
|
||||
DATE_RE1 = re.compile(
|
||||
'^(?:(?P<year>\d\d|\d\d\d\d)(?:-(?P<month>\d{1,2})(?:-(?P<day>\d{1,2}))?)?)$')
|
||||
DATE_RE2 = re.compile(
|
||||
'^(?:(?:(?:(?P<day>\d{1,2})/)?(?P<month>\d{1,2})/)?(?P<year>\d\d|\d\d\d\d))$')
|
||||
|
||||
@classmethod
|
||||
def match_birthdate(cls, birthdate):
|
||||
return cls.DATE_RE1.match(birthdate) or cls.DATE_RE2.match(birthdate)
|
||||
|
||||
@classmethod
|
||||
def lu(cls, x):
|
||||
return Normalize(x)
|
||||
|
||||
@classmethod
|
||||
def luv(cls, x):
|
||||
return cls.lu(Value(x))
|
||||
|
||||
@classmethod
|
||||
def applications(cls):
|
||||
return getattr(settings, 'ZOO_NANTERRE_APPLICATIONS', {}).iteritems()
|
||||
|
||||
def __init__(self, limit=0.5):
|
||||
self.birthdates_filters = []
|
||||
self.name_filters = []
|
||||
self.name_similarities = []
|
||||
self.email_similarities = []
|
||||
self.key_filters = []
|
||||
self.email_filters = []
|
||||
self.schema = EntitySchema.objects.get(slug='individu')
|
||||
self.limit = limit
|
||||
|
||||
def search_query(self, query):
|
||||
'''Take a one line query and try to build a search filter from it'''
|
||||
emails = []
|
||||
identifiers = []
|
||||
birthdates = []
|
||||
names = []
|
||||
|
||||
parts = query.strip().split()
|
||||
for part in parts:
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
if self.EMAIL_RE.match(part):
|
||||
emails.append(part)
|
||||
elif part.startswith('#'):
|
||||
if part[1:]:
|
||||
identifiers.append(part[1:])
|
||||
elif self.match_birthdate(part):
|
||||
birthdates.append(self.match_birthdate(part).groupdict())
|
||||
else:
|
||||
names.append(part)
|
||||
for email in emails:
|
||||
self = self.search_email(email)
|
||||
for identifier in identifiers:
|
||||
self = self.search_identifier(identifier)
|
||||
for birthdate in birthdates:
|
||||
self = self.search_birthdate(birthdate)
|
||||
self = self.search_names(names)
|
||||
return self
|
||||
|
||||
def search_email(self, email):
|
||||
self = copy.deepcopy(self)
|
||||
|
||||
f = self.q_normalize('email', email)
|
||||
self.email_filters.append(f)
|
||||
self.email_similarities.append(Value(1.0) - self.distance('email', email))
|
||||
return self
|
||||
|
||||
def search_identifier(self, identifier):
|
||||
self = copy.deepcopy(self)
|
||||
|
||||
q = Q(id=identifier)
|
||||
for key, name in self.applications():
|
||||
q |= Q(**{'content__cles_de_federation__%s' % key: identifier})
|
||||
self.key_filters.append(q)
|
||||
return self
|
||||
|
||||
def search_birthdate(self, birthdate):
|
||||
self = copy.deepcopy(self)
|
||||
|
||||
if hasattr(birthdate, 'keys'):
|
||||
# case of dict
|
||||
pass
|
||||
elif hasattr(birthdate, 'year'):
|
||||
# case of date or datetime
|
||||
birthdate = {
|
||||
'year': birthdate.year,
|
||||
'month': birthdate.month,
|
||||
'day': birthdate.day,
|
||||
}
|
||||
else:
|
||||
# case of strings
|
||||
birthdate = self.match_birthdate(birthdate).groupdict()
|
||||
|
||||
this_year = datetime.date.today().year % 100
|
||||
year = int(birthdate['year'])
|
||||
if year < 100:
|
||||
if year > this_year:
|
||||
year += 1900
|
||||
else:
|
||||
year += 2000
|
||||
q = Q(content__date_de_naissance__timestamp__year=year)
|
||||
if birthdate['month']:
|
||||
q &= Q(content__date_de_naissance__timestamp__month=birthdate['month'])
|
||||
if birthdate['day']:
|
||||
q &= Q(content__date_de_naissance__timestamp__day=birthdate['day'])
|
||||
self.birthdates_filters.append(q)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def distance(cls, field, value):
|
||||
return TrigramDistance(cls.lu(JSONTextRef(F('content'), field)), cls.luv(value))
|
||||
|
||||
@classmethod
|
||||
def q_normalize(self, field, value):
|
||||
return Q(**{'content__%s__normalize__trigram_similar' % field: self.luv(value)})
|
||||
|
||||
def search_name(self, first_name=None, last_name=None, factor=1.0, first_name_weight=1.0,
|
||||
last_name_weight=1.0):
|
||||
q = Q()
|
||||
if not first_name or not last_name:
|
||||
factor *= 0.8
|
||||
if last_name:
|
||||
q &= (self.q_normalize('nom_d_usage', last_name)
|
||||
| self.q_normalize('nom_de_naissance', last_name))
|
||||
if first_name:
|
||||
q &= self.q_normalize('prenoms', first_name)
|
||||
self.name_filters.append(q)
|
||||
|
||||
fname_d = self.distance('prenoms', first_name)
|
||||
name_of_use_d = self.distance('nom_d_usage', last_name)
|
||||
name_of_birth_d = self.distance('nom_de_naissance', last_name)
|
||||
|
||||
if first_name and last_name:
|
||||
similarity = Value(first_name_weight) * fname_d
|
||||
similarity += Value(last_name_weight) * Least(name_of_use_d, name_of_birth_d)
|
||||
similarity /= Value(first_name_weight + last_name_weight)
|
||||
elif first_name:
|
||||
similarity = fname_d
|
||||
else:
|
||||
similarity = Least(name_of_use_d, name_of_birth_d)
|
||||
similarity = (Value(1.0) - similarity) * Value(factor)
|
||||
self.name_similarities.append(similarity)
|
||||
return self
|
||||
|
||||
def search_names(self, names):
|
||||
if not names:
|
||||
return self
|
||||
|
||||
self = copy.deepcopy(self)
|
||||
|
||||
for i in range(0, len(names) + 1):
|
||||
first_name, last_name = ' '.join(names[:i]), ' '.join(names[i:])
|
||||
self = self.search_name(first_name, last_name)
|
||||
if len(names) > 1:
|
||||
self = self.search_name(last_name, first_name, factor=0.8)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def or_filters(self, filters):
|
||||
return reduce(operator.__or__, filters, Q())
|
||||
|
||||
@classmethod
|
||||
def add_age(cls, individu):
|
||||
birthdate = make_date(individu.content['date_de_naissance'])
|
||||
|
||||
if birthdate >= datetime.date.today():
|
||||
age = u'à naître'
|
||||
else:
|
||||
individu.age = years, months = age_in_years_and_months(
|
||||
individu.content['date_de_naissance'])
|
||||
if (months, years) == (0, 0):
|
||||
age = u'moins d\'un mois'
|
||||
elif years < 1:
|
||||
age = u'%s mois' % months
|
||||
elif years < 2:
|
||||
age = u'%s mois' % (months + 12)
|
||||
else:
|
||||
age = u'%s ans' % years
|
||||
individu.age_label = age
|
||||
|
||||
@classmethod
|
||||
def add_adresses(cls, individu):
|
||||
individu.adresses = []
|
||||
for relation in individu.left_relations.all():
|
||||
if relation.schema.slug != 'habite':
|
||||
continue
|
||||
individu.adresses.append(relation.right.content)
|
||||
individu.adresses[-1].update(relation.content)
|
||||
individu.adresses.sort(key=lambda x: int(not x['facturation']))
|
||||
|
||||
@classmethod
|
||||
def add_enfants(cls, individu):
|
||||
enfants = []
|
||||
for relation in individu.left_relations.all():
|
||||
if relation.schema.slug != 'responsabilite-legale':
|
||||
continue
|
||||
enfant = relation.right
|
||||
cls.add_age(enfant)
|
||||
cls.add_federations(enfant)
|
||||
enfant.responsabilite_legale = relation.content['statut']
|
||||
enfants.append(enfant)
|
||||
if enfants:
|
||||
individu.enfants = enfants
|
||||
|
||||
@classmethod
|
||||
def add_parents(cls, individu):
|
||||
parents = []
|
||||
for relation in individu.right_relations.all():
|
||||
if relation.schema.slug != 'responsabilite-legale':
|
||||
continue
|
||||
parent = relation.left
|
||||
cls.add_age(parent)
|
||||
cls.add_federations(parent)
|
||||
parent.responsabilite_legale = relation.content['statut']
|
||||
parents.append(parent)
|
||||
if parents:
|
||||
individu.parents = parents
|
||||
|
||||
@classmethod
|
||||
def add_union(cls, individu):
|
||||
conjoint = None
|
||||
for relation in individu.left_relations.all():
|
||||
if relation.schema.slug != 'union':
|
||||
continue
|
||||
conjoint = relation.right
|
||||
break
|
||||
else:
|
||||
for relation in individu.right_relations.all():
|
||||
if relation.schema.slug != 'union':
|
||||
continue
|
||||
conjoint = relation.left
|
||||
break
|
||||
if conjoint:
|
||||
cls.add_age(conjoint)
|
||||
cls.add_federations(conjoint)
|
||||
individu.union = conjoint
|
||||
individu.union_statut = relation.content['statut']
|
||||
|
||||
@classmethod
|
||||
def add_federations(cls, individu):
|
||||
individu.federations = []
|
||||
cles_de_federation = individu.content.get('cles_de_federation', {})
|
||||
for federation_key, federation_name in cls.applications():
|
||||
if cles_de_federation.get(federation_key):
|
||||
individu.federations.append(federation_name)
|
||||
|
||||
def queryset(self, prefetch=True):
|
||||
connection.cursor().execute('SELECT SET_LIMIT(0.3)')
|
||||
|
||||
qs = Entity.objects.filter(schema=self.schema)
|
||||
|
||||
qs = qs.filter(
|
||||
self.or_filters(
|
||||
self.birthdates_filters))
|
||||
qs = qs.filter(
|
||||
self.or_filters(self.key_filters))
|
||||
qs = qs.filter(
|
||||
self.or_filters(self.email_filters))
|
||||
qs = qs.filter(
|
||||
self.or_filters(self.name_filters))
|
||||
|
||||
qs = qs.annotate(
|
||||
fullname=Concat(
|
||||
Coalesce(
|
||||
JSONTextRef(F('content'), 'nom_d_usage'),
|
||||
JSONTextRef(F('content'), 'nom_de_naissance'),
|
||||
Value(' ')
|
||||
),
|
||||
Value(' '),
|
||||
JSONTextRef(F('content'), 'prenoms'))
|
||||
)
|
||||
|
||||
# order by similarities or fullname
|
||||
similarities = []
|
||||
|
||||
if self.name_similarities:
|
||||
e = (Greatest(*self.name_similarities) if len(self.name_similarities) > 1
|
||||
else self.name_similarities[0])
|
||||
similarities.append(e)
|
||||
|
||||
if self.email_similarities:
|
||||
e = (Greatest(*self.email_similarities) if len(self.email_similarities) > 1
|
||||
else self.email_similarities[0])
|
||||
similarities.append(e)
|
||||
|
||||
if similarities:
|
||||
qs = qs.annotate(similarity=reduce(operator.__add__, similarities) /
|
||||
Value(len(similarities)))
|
||||
qs = qs.filter(similarity__gte=self.limit)
|
||||
qs = qs.order_by('-similarity', 'fullname')
|
||||
else:
|
||||
qs = qs.order_by('fullname')
|
||||
|
||||
if prefetch:
|
||||
qs = qs.prefetch_related(
|
||||
'left_relations__schema', 'left_relations__right',
|
||||
'right_relations__schema', 'right_relations__left',
|
||||
)
|
||||
return qs
|
||||
|
||||
def __getitem__(self, item):
|
||||
if hasattr(item, 'start'):
|
||||
return self.decorate_iter(self.queryset()[item.start:item.stop])
|
||||
return self.decorate_individu(self.queryset()[item])
|
||||
|
||||
@classmethod
|
||||
def decorate_individu(self, individu):
|
||||
self.add_age(individu)
|
||||
self.add_adresses(individu)
|
||||
self.add_federations(individu)
|
||||
self.add_enfants(individu)
|
||||
self.add_parents(individu)
|
||||
self.add_union(individu)
|
||||
|
||||
@classmethod
|
||||
def decorate_iter(self, qs):
|
||||
for individu in qs:
|
||||
self.decorate_individu(individu)
|
||||
yield individu
|
||||
|
||||
def __iter__(self):
|
||||
return self.decorate_iter(self.queryset())
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from . import forms
|
||||
|
||||
|
||||
class Demo(TemplateView):
|
||||
template_name = 'zoo_nanterre/demo.html'
|
||||
|
||||
demo = Demo.as_view()
|
||||
|
||||
|
||||
class Search(TemplateView):
|
||||
template_name = 'zoo_nanterre/search.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(Search, self).get_context_data(**kwargs)
|
||||
form = forms.SearchForm(data=self.request.GET or None)
|
||||
ctx['form'] = form
|
||||
ctx['results'] = list(form.results()) if form.is_valid() else []
|
||||
return ctx
|
||||
|
||||
|
||||
search = Search.as_view()
|
Loading…
Reference in New Issue