diff --git a/.gitignore b/.gitignore index ca72d2e..e90e027 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,15 @@ htmlcov/ tags _build/ *.mo +.vagrant/ +gnuviechadmin/assets/ +coverage-report/ +.idea/ + +.env + +/docker/django_media +/docker/django_static +!/docker/django_media/.empty +!/docker/django_static/.empty +/static/ diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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 +. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..53ba4cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +ARG DEBIAN_RELEASE=buster +FROM debian:$DEBIAN_RELEASE +LABEL maintainer="Jan Dittberner " + +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + dumb-init \ + gettext \ + git \ + python3-dev \ + python3-pip \ + python3-setuptools \ + python3-virtualenv \ + python3-wheel \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/*.* + +RUN python3 -m pip install --prefix=/usr/local pipenv + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + libpq-dev \ + postgresql-client \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/*.* + +ARG GVAGID=2000 +ARG GVAUID=2000 + +ARG GVAAPP=gva + +VOLUME /srv/$GVAAPP/media /srv/$GVAAPP/static + +WORKDIR /srv/$GVAAPP + +COPY Pipfile Pipfile.lock /srv/$GVAAPP/ + +RUN addgroup --gid $GVAGID $GVAAPP ; \ + adduser --home /home/$GVAAPP --shell /bin/bash --uid $GVAUID --gid $GVAGID --disabled-password --gecos "User for gnuviechadmin component $GVAAPP" $GVAAPP + +USER $GVAAPP +RUN python3 -m virtualenv --python=python3 /home/$GVAAPP/$GVAAPP-venv ; \ + /home/$GVAAPP/$GVAAPP-venv/bin/python3 -m pip install -U pip ; \ + VIRTUAL_ENV=/home/$GVAAPP/$GVAAPP-venv pipenv install --deploy --ignore-pipfile --dev + +VOLUME /srv/$GVAAPP + +EXPOSE 8000 + +COPY gva.sh /srv/ + +ENTRYPOINT ["dumb-init", "/srv/gva.sh"] diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 268230d..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2014, 2015 Jan Dittberner. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..0c6343e --- /dev/null +++ b/Pipfile @@ -0,0 +1,33 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[[source]] +url = "https://pypi.gnuviech-server.de/simple" +verify_ssl = true +name = "gnuviech" + +[packages] +"psycopg2" = "*" +Django = "<3" +celery = "*" +django-allauth = "*" +django-braces = "*" +django-crispy-forms = "*" +django-model-utils = "*" +gvacommon = {version = "*",index = "gnuviech"} +passlib = "*" +redis = "*" +requests-oauthlib = "*" + +[dev-packages] +coverage = "*" +django-debug-toolbar = "*" +sphinx = "*" +releases = "*" +sphinxcontrib-blockdiag = "*" +pylama = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..50db093 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,617 @@ +{ + "_meta": { + "hash": { + "sha256": "1c0b7bdab385f10279c852fa7fe7ae2c022dc1c4495d0e55fd407aea947bc976" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + }, + { + "name": "gnuviech", + "url": "https://pypi.gnuviech-server.de/simple", + "verify_ssl": true + } + ] + }, + "default": { + "amqp": { + "hashes": [ + "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8", + "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d" + ], + "version": "==2.5.2" + }, + "billiard": { + "hashes": [ + "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede", + "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a" + ], + "version": "==3.6.3.0" + }, + "celery": { + "hashes": [ + "sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f", + "sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a" + ], + "index": "pypi", + "version": "==4.4.2" + }, + "certifi": { + "hashes": [ + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + ], + "version": "==2020.4.5.1" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "defusedxml": { + "hashes": [ + "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", + "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5" + ], + "version": "==0.6.0" + }, + "django": { + "hashes": [ + "sha256:69897097095f336d5aeef45b4103dceae51c00afa6d3ae198a2a18e519791b7a", + "sha256:6ecd229e1815d4fc5240fc98f1cca78c41e7a8cd3e3f2eefadc4735031077916" + ], + "index": "pypi", + "version": "==2.2.12" + }, + "django-allauth": { + "hashes": [ + "sha256:7ab91485b80d231da191d5c7999ba93170ef1bf14ab6487d886598a1ad03e1d8" + ], + "index": "pypi", + "version": "==0.41.0" + }, + "django-braces": { + "hashes": [ + "sha256:83705b78948de00804bfacf40c315d001bb39630f35bbdd8588211c2d5b4d43f", + "sha256:a6d9b34cf3e4949635e54884097c30410d7964fc7bec7231445ea7079b8c5722" + ], + "index": "pypi", + "version": "==1.14.0" + }, + "django-crispy-forms": { + "hashes": [ + "sha256:50032184708ce351e3c9f0008ac35d659d9d5973fa2db218066f2e0a76eb41d9", + "sha256:67e73ac863d3159500029fbbcdcb788f287a3fd357becebc1a0b51f73896dce3" + ], + "index": "pypi", + "version": "==1.9.0" + }, + "django-model-utils": { + "hashes": [ + "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c", + "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061" + ], + "index": "pypi", + "version": "==4.0.0" + }, + "gvacommon": { + "hashes": [ + "sha256:adf1ebc824433196d112764c61d9ca869481d33f612818c2840069f57ab42c25" + ], + "index": "gnuviech", + "version": "==0.5.0" + }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, + "importlib-metadata": { + "hashes": [ + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + ], + "markers": "python_version < '3.8'", + "version": "==1.6.0" + }, + "kombu": { + "hashes": [ + "sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76", + "sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2" + ], + "version": "==4.6.8" + }, + "oauthlib": { + "hashes": [ + "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", + "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" + ], + "version": "==3.1.0" + }, + "passlib": { + "hashes": [ + "sha256:68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177", + "sha256:8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8" + ], + "index": "pypi", + "version": "==1.7.2" + }, + "psycopg2": { + "hashes": [ + "sha256:132efc7ee46a763e68a815f4d26223d9c679953cd190f1f218187cb60decf535", + "sha256:2327bf42c1744a434ed8ed0bbaa9168cac7ee5a22a9001f6fc85c33b8a4a14b7", + "sha256:27c633f2d5db0fc27b51f1b08f410715b59fa3802987aec91aeb8f562724e95c", + "sha256:2c0afb40cfb4d53487ee2ebe128649028c9a78d2476d14a67781e45dc287f080", + "sha256:2df2bf1b87305bd95eb3ac666ee1f00a9c83d10927b8144e8e39644218f4cf81", + "sha256:440a3ea2c955e89321a138eb7582aa1d22fe286c7d65e26a2c5411af0a88ae72", + "sha256:6a471d4d2a6f14c97a882e8d3124869bc623f3df6177eefe02994ea41fd45b52", + "sha256:6b306dae53ec7f4f67a10942cf8ac85de930ea90e9903e2df4001f69b7833f7e", + "sha256:a0984ff49e176062fcdc8a5a2a670c9bb1704a2f69548bce8f8a7bad41c661bf", + "sha256:ac5b23d0199c012ad91ed1bbb971b7666da651c6371529b1be8cbe2a7bf3c3a9", + "sha256:acf56d564e443e3dea152efe972b1434058244298a94348fc518d6dd6a9fb0bb", + "sha256:d3b29d717d39d3580efd760a9a46a7418408acebbb784717c90d708c9ed5f055", + "sha256:f7d46240f7a1ae1dd95aab38bd74f7428d46531f69219954266d669da60c0818" + ], + "index": "pypi", + "version": "==2.8.5" + }, + "python3-openid": { + "hashes": [ + "sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa", + "sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502" + ], + "version": "==3.1.0" + }, + "pytz": { + "hashes": [ + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" + ], + "version": "==2019.3" + }, + "redis": { + "hashes": [ + "sha256:0dcfb335921b88a850d461dc255ff4708294943322bd55de6cfd68972490ca1f", + "sha256:b205cffd05ebfd0a468db74f0eedbff8df1a7bfc47521516ade4692991bb0833" + ], + "index": "pypi", + "version": "==3.4.1" + }, + "requests": { + "hashes": [ + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + ], + "version": "==2.23.0" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" + ], + "index": "pypi", + "version": "==1.3.0" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "sqlparse": { + "hashes": [ + "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", + "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" + ], + "version": "==0.3.1" + }, + "urllib3": { + "hashes": [ + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + ], + "version": "==1.25.8" + }, + "vine": { + "hashes": [ + "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", + "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" + ], + "version": "==1.3.0" + }, + "zipp": { + "hashes": [ + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + ], + "version": "==3.1.0" + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + ], + "version": "==0.7.12" + }, + "asgiref": { + "hashes": [ + "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5", + "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c" + ], + "version": "==3.2.7" + }, + "babel": { + "hashes": [ + "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", + "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" + ], + "version": "==2.8.0" + }, + "blockdiag": { + "hashes": [ + "sha256:16a69dd9f3b44c9e0869999ce82aa968586698febc86ece9ca0c902dba772397", + "sha256:fa0b47cf25bfc4d546b7fc284c70c3bac875a066e744b4a6b1d9ba457e4ed077" + ], + "version": "==2.0.1" + }, + "certifi": { + "hashes": [ + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + ], + "version": "==2020.4.5.1" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coverage": { + "hashes": [ + "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0", + "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30", + "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b", + "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0", + "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823", + "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe", + "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037", + "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6", + "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31", + "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd", + "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892", + "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1", + "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78", + "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac", + "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006", + "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014", + "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2", + "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7", + "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8", + "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7", + "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9", + "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1", + "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307", + "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a", + "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435", + "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0", + "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5", + "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441", + "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732", + "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de", + "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1" + ], + "index": "pypi", + "version": "==5.0.4" + }, + "django": { + "hashes": [ + "sha256:69897097095f336d5aeef45b4103dceae51c00afa6d3ae198a2a18e519791b7a", + "sha256:6ecd229e1815d4fc5240fc98f1cca78c41e7a8cd3e3f2eefadc4735031077916" + ], + "index": "pypi", + "version": "==2.2.12" + }, + "django-debug-toolbar": { + "hashes": [ + "sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943", + "sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c" + ], + "index": "pypi", + "version": "==2.2" + }, + "docutils": { + "hashes": [ + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + ], + "version": "==0.16" + }, + "funcparserlib": { + "hashes": [ + "sha256:b7992eac1a3eb97b3d91faa342bfda0729e990bd8a43774c1592c091e563c91d" + ], + "version": "==0.3.6" + }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, + "imagesize": { + "hashes": [ + "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", + "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" + ], + "version": "==1.2.0" + }, + "jinja2": { + "hashes": [ + "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", + "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" + ], + "version": "==2.11.1" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" + ], + "version": "==1.1.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "packaging": { + "hashes": [ + "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", + "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" + ], + "version": "==20.3" + }, + "pillow": { + "hashes": [ + "sha256:04a10558320eba9137d6a78ca6fc8f4a5801f1b971152938851dc4629d903579", + "sha256:0f89ddc77cf421b8cd34ae852309501458942bf370831b4a9b406156b599a14e", + "sha256:251e5618125ec12ac800265d7048f5857a8f8f1979db9ea3e11382e159d17f68", + "sha256:291bad7097b06d648222b769bbfcd61e40d0abdfe10df686d20ede36eb8162b6", + "sha256:2f0b52a08d175f10c8ea36685115681a484c55d24d0933f9fd911e4111c04144", + "sha256:3713386d1e9e79cea1c5e6aaac042841d7eef838cc577a3ca153c8bedf570287", + "sha256:433bbc2469a2351bea53666d97bb1eb30f0d56461735be02ea6b27654569f80f", + "sha256:4510c6b33277970b1af83c987277f9a08ec2b02cc20ac0f9234e4026136bb137", + "sha256:50a10b048f4dd81c092adad99fa5f7ba941edaf2f9590510109ac2a15e706695", + "sha256:670e58d3643971f4afd79191abd21623761c2ebe61db1c2cb4797d817c4ba1a7", + "sha256:6c1924ed7dbc6ad0636907693bbbdd3fdae1d73072963e71f5644b864bb10b4d", + "sha256:721c04d3c77c38086f1f95d1cd8df87f2f9a505a780acf8575912b3206479da1", + "sha256:8d5799243050c2833c2662b824dfb16aa98e408d2092805edea4300a408490e7", + "sha256:90cd441a1638ae176eab4d8b6b94ab4ec24b212ed4c3fbee2a6e74672481d4f8", + "sha256:a5dc9f28c0239ec2742d4273bd85b2aa84655be2564db7ad1eb8f64b1efcdc4c", + "sha256:b2f3e8cc52ecd259b94ca880fea0d15f4ebc6da2cd3db515389bb878d800270f", + "sha256:b7453750cf911785009423789d2e4e5393aae9cbb8b3f471dab854b85a26cb89", + "sha256:b99b2607b6cd58396f363b448cbe71d3c35e28f03e442ab00806463439629c2c", + "sha256:cd47793f7bc9285a88c2b5551d3f16a2ddd005789614a34c5f4a598c2a162383", + "sha256:d6bf085f6f9ec6a1724c187083b37b58a8048f86036d42d21802ed5d1fae4853", + "sha256:da737ab273f4d60ae552f82ad83f7cbd0e173ca30ca20b160f708c92742ee212", + "sha256:eb84e7e5b07ff3725ab05977ac56d5eeb0c510795aeb48e8b691491be3c5745b" + ], + "version": "==7.1.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pydocstyle": { + "hashes": [ + "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", + "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" + ], + "version": "==5.0.2" + }, + "pyflakes": { + "hashes": [ + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + ], + "version": "==2.2.0" + }, + "pygments": { + "hashes": [ + "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", + "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + ], + "version": "==2.6.1" + }, + "pylama": { + "hashes": [ + "sha256:9bae53ef9c1a431371d6a8dca406816a60d547147b60a4934721898f553b7d8f", + "sha256:fd61c11872d6256b019ef1235be37b77c922ef37ac9797df6bd489996dddeb15" + ], + "index": "pypi", + "version": "==7.7.1" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "version": "==2.4.7" + }, + "pytz": { + "hashes": [ + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" + ], + "version": "==2019.3" + }, + "releases": { + "hashes": [ + "sha256:555ae4c97a671a420281c1c782e9236be25157b449fdf20b4c4b293fe93db2f1", + "sha256:cb3435ba372a6807433800fbe473760cfa781171513f670f3c4b76983ac80f18" + ], + "index": "pypi", + "version": "==1.6.3" + }, + "requests": { + "hashes": [ + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + ], + "version": "==2.23.0" + }, + "semantic-version": { + "hashes": [ + "sha256:2a4328680073e9b243667b201119772aefc5fc63ae32398d6afafff07c4f54c0", + "sha256:2d06ab7372034bcb8b54f2205370f4aa0643c133b7e6dbd129c5200b83ab394b" + ], + "version": "==2.6.0" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", + "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" + ], + "version": "==2.0.0" + }, + "sphinx": { + "hashes": [ + "sha256:6a099e6faffdc3ceba99ca8c2d09982d43022245e409249375edf111caf79ed3", + "sha256:b63a0c879c4ff9a4dffcb05217fa55672ce07abdeb81e33c73303a563f8d8901" + ], + "index": "pypi", + "version": "==3.0.0" + }, + "sphinxcontrib-applehelp": { + "hashes": [ + "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", + "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" + ], + "version": "==1.0.2" + }, + "sphinxcontrib-blockdiag": { + "hashes": [ + "sha256:51ce7cff8d25dfd4c8a753d5aa5491e6dbf280004719c49e8001e583ecda7d91", + "sha256:91fd35b64f1f25db59d80b8a5196ed4ffadf57a81f63ee207e34d53ec36d8f97" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "sphinxcontrib-devhelp": { + "hashes": [ + "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", + "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" + ], + "version": "==1.0.2" + }, + "sphinxcontrib-htmlhelp": { + "hashes": [ + "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", + "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" + ], + "version": "==1.0.3" + }, + "sphinxcontrib-jsmath": { + "hashes": [ + "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", + "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-qthelp": { + "hashes": [ + "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", + "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" + ], + "version": "==1.0.3" + }, + "sphinxcontrib-serializinghtml": { + "hashes": [ + "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", + "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" + ], + "version": "==1.1.4" + }, + "sqlparse": { + "hashes": [ + "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", + "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" + ], + "version": "==0.3.1" + }, + "urllib3": { + "hashes": [ + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + ], + "version": "==1.25.8" + }, + "webcolors": { + "hashes": [ + "sha256:76f360636957d1c976db7466bc71dcb713bb95ac8911944dffc55c01cb516de6", + "sha256:b8cd5d865a25c51ff1218f0c90d0c0781fc64312a49b746b320cf50de1648f6e" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.rst b/README.rst index 3d7e316..ff40464 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,7 @@ Gnuviechadmin is based on Django_ and Celery_ .. _Django: https://djangoproject.com/ .. _Celery: http://www.celeryproject.com/ -The project page for gnuviechadmin is at http://dev.gnuviech-server.de/gva. If -you find some problem or have some feature suggestions you can post a new -ticket in our issue tracker on the project page. +The project page for gnuviechadmin is at +http://git.dittberner.info/gnuviech/gva. If you find some problem or have some +feature suggestions you can post a new ticket in our issue tracker on the +project page. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dd174b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,112 @@ +version: "3" +services: + db: + image: gnuviech/pgsql:buster + ports: + - "15432:5432" + env_file: .env + volumes: + - "pg_data:/var/lib/postgresql/11/main" + mq: + image: gnuviech/mq:buster + env_file: .env + volumes: + - "mq_data:/var/lib/rabbitmq/mnesia" + redis: + image: gnuviech/redis:buster + env_file: .env + volumes: + - "redis_data:/var/lib/redis" + gva: + image: gnuviech/gva:buster + build: + context: . + args: + GVAGID: 1000 + GVAUID: 1000 + ports: + - "8000:8000" + depends_on: + - db + - mq + - redis + env_file: .env + environment: + DJANGO_SETTINGS_MODULE: gnuviechadmin.settings + GVA_DOMAIN_NAME: localhost + GVA_SITE_NAME: localhost + volumes: + - "./docker/django_media:/srv/gva/media" + - "./docker/django_static:/srv/gva/static" + - ".:/srv/gva" + web: + image: gnuviech/gvaweb:buster + build: + context: ../gvaweb + args: + GVAGID: 1000 + GVAUID: 1000 + depends_on: + - mq + - redis + env_file: ../gvaweb/.env + volumes: + - "../gvaweb:/srv/gvaweb" + ldap: + image: gnuviech/gvaldap:buster + build: + context: ../gvaldap + args: + GVAGID: 1000 + GVAUID: 1000 + depends_on: + - mq + - redis + env_file: ../gvaldap/.env + volumes: + - "../gvaldap:/srv/gvaldap" + file: + image: gnuviech/gvafile:buster + build: + context: ../gvafile + args: + GVAGID: 1000 + GVAUID: 1000 + depends_on: + - mq + - redis + env_file: ../gvafile/.env + volumes: + - "../gvafile:/srv/gvafile" + pgsql: + image: gnuviech/gvapgsql:buster + build: + context: ../gvapgsql + args: + GVAGID: 1000 + GVAUID: 1000 + depends_on: + - mq + - redis + env_file: ../gvapgsql/.env + volumes: + - "../gvapgsql:/srv/gvapgsql" + mysql: + image: gnuviech/gvamysql:buster + build: + context: ../gvamysql + args: + GVAGID: 1000 + GVAUID: 1000 + depends_on: + - mq + - redis + env_file: ../gvamysql/.env + volumes: + - "../gvamysql:/srv/gvamysql" +volumes: + django_media: + django_static: + pg_data: + redis_data: + mq_data: diff --git a/gnuviechadmin/gvacommon/__init__.py b/docker/django_media/.empty similarity index 100% rename from gnuviechadmin/gvacommon/__init__.py rename to docker/django_media/.empty diff --git a/docker/django_static/.empty b/docker/django_static/.empty new file mode 100644 index 0000000..e69de29 diff --git a/docs/changelog.rst b/docs/changelog.rst index b597907..883156e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,33 @@ Changelog ========= +* :release:`0.12.0 <2020-04-10>` +* :support:`-` add architecture diagramm for documentation +* :support:`-` drop environment specific settings +* :support:`-` update to Python 3 +* :support:`-` use Pipenv for dependency management +* :support:`-` switch result backend to Redis +* :support:`-` use separate test vhost for celery queues +* :support:`-` switch licensing to AGPLv3+ +* :support:`-` add a Vagrant setup to ease development +* :support:`-` add Docker setup for local development +* :feature:`-` let all celery tasks run asynchronously and move task processing + to signal handlers +* :feature:`-` add unit tests for all the code +* :feature:`-` add proper configuration for coverage, flake8 and pep8 +* :feature:`-` update to Django 2.2.12 +* :support:`-` use gvacommon from separate repository +* :support:`-` update documentation + +* :release:`0.11.6 <2020-02-14>` +* :support:`-` Update dependencies to versions that work with Debian Stretch + +* :release:`0.11.5 <2018-12-26>` +* :support:`-` Remove Xing support from settings and templates + +* :release:`0.11.4 <2016-12-31>` +* :bug:`-` fix wrong tag in password reset done template + * :release:`0.11.3 <2015-02-21>` * :bug:`-` fix handling of OpenSSH formatted keys with whitespace in comments * :bug:`-` the ssh key list does not show SSH keys of other users anymore diff --git a/docs/code/gnuviechadmin.rst b/docs/code/gnuviechadmin.rst index 57fabec..8625299 100644 --- a/docs/code/gnuviechadmin.rst +++ b/docs/code/gnuviechadmin.rst @@ -35,28 +35,4 @@ The project module :py:mod:`gnuviechadmin` ------------------------------------------- .. automodule:: gnuviechadmin.settings - - -:py:mod:`base ` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: gnuviechadmin.settings.base :members: - - -:py:mod:`local ` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: gnuviechadmin.settings.local - - -:py:mod:`production ` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: gnuviechadmin.settings.production - - -:py:mod:`test ` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: gnuviechadmin.settings.test diff --git a/docs/code/osusers.rst b/docs/code/osusers.rst index 3353239..29d62e3 100644 --- a/docs/code/osusers.rst +++ b/docs/code/osusers.rst @@ -31,6 +31,12 @@ .. automodule:: osusers.models :members: +:py:mod:`signals ` +----------------------------------- + +.. automodule:: osusers.signals + :members: + :py:mod:`urls ` ----------------------------- diff --git a/docs/code/userdbs.rst b/docs/code/userdbs.rst index c7af8a1..d5b0da2 100644 --- a/docs/code/userdbs.rst +++ b/docs/code/userdbs.rst @@ -30,6 +30,12 @@ .. automodule:: userdbs.models :members: +:py:mod:`signals ` +----------------------------------- + +.. automodule:: userdbs.signals + :members: + :py:mod:`templatetags ` --------------------------------------------- diff --git a/docs/conf.py b/docs/conf.py index a1cec22..e778564 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,6 +22,7 @@ import django # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join('..', 'gnuviechadmin'))) +os.environ['DJANGO_SETTINGS_MODULE'] = 'gnuviechadmin.settings' os.environ['GVA_SITE_ADMINMAIL'] = 'admin@gva.example.org' django.setup() @@ -33,11 +34,16 @@ django.setup() # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['releases', 'sphinx.ext.autodoc', 'celery.contrib.sphinx'] +extensions = [ + 'releases', 'sphinx.ext.autodoc', 'celery.contrib.sphinx', + 'sphinxcontrib.blockdiag'] # configuration for releases extension -releases_issue_uri = 'https://dev.gnuviech-server.de/gva/ticket/%s' -releases_release_uri = 'https://dev.gnuviech-server.de/gva/milestone/%s' +releases_issue_uri = 'https://git.dittberner.info/gnuviech/gva/issues/%s' +releases_release_uri = 'https://git.dittberner.info/gnuviech/gva/src/tag/%s' + +# configuration for blockdiag extension +blockdiag_fontpath = '/usr/share/fonts/truetype/dejavu/' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -53,7 +59,7 @@ master_doc = 'index' # General information about the project. project = u'gnuviechadmin' -copyright = u'2014, 2015 Jan Dittberner' +copyright = u'2014-2020, Jan Dittberner' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -104,7 +110,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/gnuviechadmin_architecture.drawio b/docs/gnuviechadmin_architecture.drawio new file mode 100644 index 0000000..951a193 --- /dev/null +++ b/docs/gnuviechadmin_architecture.drawio @@ -0,0 +1 @@ +7Vpbb6M6EP41eQziDnls2mbPSl2pPX3Y06fIAYd4FzA1Jk32159xMAnEtElbcqu2qlo8tmE833zj8aVnXSeLbwxlsx80xHHP1MNFz7rpmaZhm2ZP/OrhspT4ulUKIkZC2WgjeCR/sBTqUlqQEOeNhpzSmJOsKQxomuKAN2SIMfrSbDalcfOrGYqwIngMUKxKf5KQz+QoTG8j/weTaFZ92XAHZU2CqsZyJPkMhfSlJrJue9Y1o5SXT8niGsfCeJVdyn6jV2rXijGc8n06PD48osH3+6uh9fw8mt4/3ekPP/vyLXMUF3LA/+KQ5FJjvqzMgPKstO6ULDC8cDjjSQxFAx4zSlK+MrMz7Dk3IEExiVIQBKAaZiAgycrMwylNucTYMDfyG5JEoHhMJvA3yQOE4f81CmZ4vFJnfM9oWARcy+cR9JI6Y8bx4lVjGGsTg29immDOltBEdhh4tua5ZS/pmbZjaE4pedkgPXA00y6lsxrOvgQVSfeK1l/YICDGUoLwDkBMX7E9DsEjZTGlqbAjo0UaChxudCj9KpKsaoBYABLK+IxGNEXxHaWZxOkX5nwprY8KTpsoTkkcX9OYstVHrRBhfypelXNGf+NajRv4eDIVNZVDCx0CmpBAPpcjEGq/jQ6MkhYswG+ZQ1IesQjzXX6sos1wjDiZN/XoHjOFRNEcZVH+HCtY0oLHJAVzVtFK2CtE+WwNpnBrAvHnDk1wfE9zwgkVXJpQzmlSa3AlScYFwCrlatACUJn4frKIRJDW0EtuaRnNecTwGLQckzTnKA2w6gbmrWcNfZBDz5DAu6s66YgdUNFYx3pJRMfUPJWIrq6y0LUOhaj3l4U1c1iXwEKrjYXJ8rxZuNJvHE7WFBybpyGhfn4ktBVAzxjIswmnpm0305oKs5PFUuf9sfQDkVMNv+cZS51LiKVOWywFe2IFyp38emvl0M69Jj7SPxRCtlJ/m5oB4YwsNKH5OMdsvurZAclc3WuSTOWYZ6gcG3iH4ph9FI6dJ6PcS2CU28aoOETZZTIqJAzaULbslFb2VhLSQivXUWnl+9XCvXtiuX8XAjVzeJdANa+Nai94cplMA8U75ZivD3ZOXa7WwrLBoba8DHXy+vRMBcZgy/9EfxiKLD7J160KN4tGaVkv3WNGYGjC4l0TyN+TQPYpCWSo6V+O07BnujHoPJwweIp4GWUSJCqOkHvgBeEloJ4ji0+1qg2eotCAs+yl601H0HTrQnzB0E/qDGo4zQrRbdsZGM6LmKtHBwdxhhquVhNWy/gIrOc53e7tIW9GfLCR6/uNoC8V2NuD5LvvxUlPrQmdTnNQbNvF1ip83Ot8xekqhwvJvPI4mNYrKXykVqH4YDW3VudQO+fxnalBLCqGKPgdrZy55iHT1U9PHm5dVSmE3pZPVOdfM87FeeaVsKM5CsJU10hA0ykBmjANvEuMD3EY7kjIc/Ff5BIp5n2RSUDZMQQK36V0fAfSvu71DdPXsjTqIHHY3qZfl2uZg1EdX9UTh7Ww+9hkdZ85dEhfS9+Tv/4nA/wWPdeH703SW6an6Y7pWobj+QPb21ozlcOR7+ie0cbgyGmC4dq1PGGdGrTnCVA44Exv7OsI7iln+vVFhWMhBA5ZR0jTDfciUHJOipJxFJROYVfvpHZVT5e+il2tk9pVnaO/il3NU9q10rJ+dQtNJoT/eFAMvjOb3ZkPK1tYrRtdW6soXR/ejsyWrS30p2BYey5wgccRTiGki7XW6h4ZZrdzXF4n28D6qbzVGWxtKrcciBptpzXuwW4qfCCE11eYnWwwN9avHbLHHBwp6f1cVBoo7GlZYF6FCRGLra+5xjR3rjFxngP+BMX9FzwR4W7kAHdGRY5ZH4VhP4uLvB8wjDiGMtiqw+Wm3WStr5LWb1lr+odaalpquFU8oHVbX1x5rd8AscyR+OxrN0C2L4xUzVvj7aFvumxf112V5Xhbfa0thLdcl7E14UJ5N/F9y1Esr+VAo8VTvPd7ChQ317XLdfHm0rt1+z8= \ No newline at end of file diff --git a/docs/gnuviechadmin_architecture.png b/docs/gnuviechadmin_architecture.png new file mode 100644 index 0000000..dde221a Binary files /dev/null and b/docs/gnuviechadmin_architecture.png differ diff --git a/docs/gnuviechadmin_architecture.svg b/docs/gnuviechadmin_architecture.svg new file mode 100644 index 0000000..a68effd --- /dev/null +++ b/docs/gnuviechadmin_architecture.svg @@ -0,0 +1,3 @@ + + +
Redis
Redis
gvapgsql
gvapgsql
gvamysql
gvamysql
gvafile
gvafile
gvaldap
gvaldap
gvaweb
gvaweb
send
command
send...
pull
results
pull...
gva
gva
RabbitMQ
RabbitMQ
Admins
Admins
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a4707d9..e5ae1d7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,27 +9,31 @@ Welcome to gnuviechadmin's documentation! .. include:: ../README.rst -License -------- - -gnuviechadmin is licensed under the terms of the MIT license: - -.. include:: ../LICENSE.txt - :literal: - Contents -------- .. toctree:: - :maxdepth: 2 + :maxdepth: 3 install deploy tests code ideas + task_flows changelog +License +------- + +gnuviechadmin 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. + +.. include:: ../COPYING + :literal: + Indices and tables ------------------ diff --git a/docs/install.rst b/docs/install.rst index 9e08482..3b22344 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,45 +1,22 @@ -Installation -============ +.. index:: installation -You have several options in setting up your working environment. We recommend -using virtualenv to separate the dependencies of your project from your -system's python environment. If on Linux or Mac OS X, you can also use -virtualenvwrapper to help manage multiple virtualenvs across different -projects. +======= +Install +======= -Virtualenv Only ---------------- +Working Environment +=================== -First, make sure you are using virtualenv (http://www.virtualenv.org). Once -that's installed, create your virtualenv:: +To get a running work environment use `pipenv`_. - $ virtualenv --distribute gnuviechadmin +.. _pipenv: https://pipenv.kennethreitz.org/en/latest/ -You will also need to ensure that the virtualenv has the project directory -added to the path. Adding the project directory will allow `django-admin.py` to -be able to change settings using the `--settings` flag. +To get started install `pip` and `pipenv` and use `pipenv install --dev`: -Virtualenv with virtualenvwrapper ------------------------------------- +.. code-block:: sh -In Linux and Mac OSX, you can install virtualenvwrapper -(http://virtualenvwrapper.readthedocs.org/en/latest/), which will take care of -managing your virtual environments and adding the project path to the -`site-directory` for you:: + $ apt install python3-pip + $ python3 -m pip install --user -U pipenv + $ pipenv install --dev - $ mkdir gnuviechadmin - $ mkvirtualenv -a gnuviechadmin gnuviechadmin-dev $ cd gnuviechadmin && add2virtualenv `pwd` - -Installation of Dependencies -============================= - -Depending on where you are installing dependencies: - -In development:: - - $ pip install -r requirements/local.txt - -For production:: - - $ pip install -r requirements.txt diff --git a/docs/pdns.local.gva_queries.conf b/docs/pdns.local.gva_queries.conf new file mode 100644 index 0000000..392b43b --- /dev/null +++ b/docs/pdns.local.gva_queries.conf @@ -0,0 +1,198 @@ +# Regular queries +gpgsql-basic-query=SELECT content, ttl, prio, type, domain_id, disabled::int, name, auth::int \ + FROM domains_dnsrecord \ + WHERE disabled=false AND type='%s' AND name=E'%s' +gpgsql-id-query=SELECT content, ttl, prio, type, domain_id, disabled::int, name, auth::int \ + FROM domains_dnsrecord \ + WHERE disabled=false AND type='%s' AND name=E'%s' AND domain_id=%d +gpgsql-any-query=SELECT content, ttl, prio, type, domain_id, disabled::int, name, auth::int \ + FROM domains_dnsrecord \ + WHERE disabled=false AND name=E'%s' +gpgsql-any-id-query=SELECT content, ttl, prio, type, domain_id, disabled::int, name, auth::int \ + FROM domains_dnsrecord \ + WHERE disabled=false AND name=E'%s' AND domain_id=%d +gpgsql-list-query=SELECT content, ttl, prio, type, domain_id, disabled::int, name, auth::int \ + FROM domains_dnsrecord \ + WHERE (disabled=false OR %d::bool) AND domain_id='%d' \ + ORDER BY name, type + +# Master/slave queries +gpgsql-master-zone-query=SELECT master \ + FROM domains_dnsdomain \ + WHERE domain=E'%s' AND type='SLAVE' +gpgsql-info-zone-query=SELECT id, domain, master, last_check, notified_serial, type \ + FROM domains_dnsdomain \ + WHERE domain=E'%s' +gpgsql-info-all-slaves-query=SELECT id, domain, master, last_check, type \ + FROM domains_dnsdomain \ + WHERE type='SLAVE' +gpgsql-supermaster-query=SELECT customer \ + FROM domains_dnssupermaster \ + WHERE ip='%s' AND nameserver=E'%s' +gpgsql-insert-slave-query=INSERT INTO domains_dnsdomain \ + (type, domain, master, account) \ + VALUES ('SLAVE', E'%s', E'%s', E'%s') +gpgsql-insert-record-query=INSERT INTO domains_dnsrecord \ + (content, ttl, prio, type, domain_id, disabled, name, auth) \ + VALUES (E'%s', %d, %d, '%s', %d, %d::bool, E'%s', '%d') +gpgsql-update-serial-query=UPDATE domains_dnsdomain \ + SET notified_serial=%d \ + WHERE id=%d +gpgsql-update-lastcheck-query=UPDATE domains_dnsdomain \ + SET last_check=%d \ + WHERE id=%d +gpgsql-info-all-master-query=SELECT id, domain, master, last_check, notified_serial, type \ + FROM domains_dnsdomain \ + WHERE type='MASTER' +gpgsql-delete-zone-query=DELETE FROM domains_dnsrecord \ + WHERE domain_id=%d + +# Comment queries +gpgsql-list-comments-query=SELECT domain_id, name, type, modified_at, customer, comment \ + FROM domains_dnscomment \ + WHERE domain_id=%d +gpgsql-insert-comment-query=INSERT INTO domains_dnscomment \ + (domain_id, name, type, modified_at, customer, comment) \ + VALUES (%d, E'%s', E'%s', %d, E'%s', E'%s') +gpgsql-delete-comment-rrset-query=DELETE FROM domains_dnscomment \ + WHERE domain_id=%d AND name=E'%s' AND type=E'%s' +gpgsql-delete-comments-query=DELETE FROM domains_dnscomment \ + WHERE domain_id=%d + +# Crypto key queries +gpgsql-activate-domain-key-query=UPDATE domains_dnscryptokey \ + SET active=true \ + WHERE domain_id=( \ + SELECT id \ + FROM domains_dnsdomain \ + WHERE domain=E'%s' \ + ) AND domains_dnscryptokey.id=%d +gpgsql-add-domain-key-query=INSERT INTO domains_dnscryptokey \ + (domain_id, flags, active, content) \ + SELECT id, %d, (%d = 1), '%s' FROM domains_dnsdomain \ + WHERE domain=E'%s' +gpgsql-clear-domain-all-keys-query=DELETE FROM domains_dnscryptokey \ + WHERE domain_id=( \ + SELECT id FROM domains_dnsdomain \ + WHERE domain=E'%s' \ + ) +gpgsql-deactivate-domain-key-query=UPDATE domains_dnscryptokey \ + SET active=false \ + WHERE domain_id=( \ + SELECT id FROM domains_dnsdomain \ + WHERE domain=E'%s' \ + ) AND domains_dnscryptokey.id=%d +gpgsql-list-domain-keys-query=SELECT domains_dnscryptokey.id, flags, CASE WHEN active THEN 1 ELSE 0 END AS active, content \ + FROM domains_dnsdomain, domains_cryptokey \ + WHERE domains_dnscryptokey.domain_id=domains_dnsdomain.id AND domain=E'%s' +gpgsql-remove-domain-key-query=DELETE FROM domains_dnscryptokey \ + WHERE domain_id=( \ + SELECT id FROM domains_dnsdomain \ + WHERE domain=E'%s' \ + ) AND domains_dnscryptokey.id=%d + +# TSIG key queries +gpgsql-delete-tsig-key-query=DELETE FROM domains_dnstsigkey \ + WHERE name='%s' +gpgsql-get-tsig-key-query=SELECT algorithm, secret \ + FROM domains_dnstsigkey \ + WHERE name=E'%s' +gpgsql-get-tsig-keys-query=SELECT name, algorithm, secret \ + FROM domains_dnstsigkey +gpgsql-set-tsig-key-query=INSERT INTO domains_dnstsigkey \ + (name, algorithm, secret) \ + VALUES ('%s', '%s', '%s') + +# Metadata queries +gpgsql-clear-domain-all-metadata-query=DELETE FROM domains_dnsdomainmetadata \ + WHERE domain_id=( \ + SELECT id FROM domains_dnsdomain \ + WHERE domain=E'%s' \ + ) +gpgsql-clear-domain-metadata-query=DELETE FROM domains_dnsdomainmetadata \ + WHERE domain_id=( \ + SELECT id FROM domains_dnsdomain \ + WHERE domain=E'%s' \ + ) AND domains_dnsdomainmetadata.kind=E'%s' +gpgsql-get-all-domain-metadata-query=SELECT kind, content \ + FROM domains_dnsdomain, domains_dnsdomainmetadata \ + WHERE domains_dnsdomainmetadata.domain_id=domains_dnsdomain.id AND domain=E'%s' +gpgsql-get-domain-metadata-query=SELECT content \ + FROM domains_dnsdomain, domains_dnsdomainmetadata \ + WHERE domains_dnsdomainmetadata.domain_id=domains_dnsdomain.id AND domain=E'%s' AND domains_dnsdomainmetadata.kind=E'%s' +gpgsql-set-domain-metadata-query=INSERT INTO domains_dnsdomainmetadata \ + (domain_id, kind, content) \ + SELECT id, '%s', '%s' FROM domains_dnsdomain \ + WHERE domain=E'%s' + +# Record queries +gpgsql-delete-empty-non-terminal-query=DELETE FROM domains_dnsrecord \ + WHERE domain_id='%d' AND name='%s' AND type IS NULL +gpgsql-delete-names-query=DELETE FROM domains_dnsrecord \ + WHERE domain_id=%d AND name=E'%s' +gpgsql-delete-rrset-query=DELETE FROM domains_dnsrecord \ + WHERE domain_id=%d AND name=E'%s' AND type=E'%s' +gpgsql-get-order-after-query=SELECT ordername FROM domains_dnsrecord \ + WHERE disabled=false AND ordername ~>~ E'%s' AND domain_id=%d AND ordername IS NOT NULL \ + ORDER BY 1 USING ~<~ LIMIT 1 +gpgsql-get-order-before-query=SELECT ordername, name FROM domains_dnsrecord \ + WHERE disabled=false AND ordername ~<=~ E'%s' AND domain_id=%d AND ordername IS NOT NULL \ + ORDER BY 1 USING ~>~ LIMIT 1 +gpgsql-get-order-first-query=SELECT ordername, name FROM domains_dnsrecord \ + WHERE disabled=false AND domain_id=%d AND ordername IS NOT NULL \ + ORDER BY 1 USING ~<~ LIMIT 1 +gpgsql-get-order-last-query=SELECT ordername, name FROM domains_dnsrecord \ + WHERE disabled=false AND ordername != '' AND domain_id=%d AND ordername IS NOT NULL \ + ORDER BY 1 USING ~>~ LIMIT 1 +gpgsql-insert-empty-non-terminal-query=INSERT INTO domains_dnsrecord \ + (domain_id, name, type, disabled, auth) \ + VALUES ('%d', '%s', null, false, true) + gpgsql-insert-ent-order-query=INSERT INTO domains_dnsrecord \ + (type, domain_id, disabled, name, ordername, auth) \ + VALUES (null, '%d', false, E'%s', E'%s', '%d') +gpgsql-insert-ent-query=INSERT INTO domains_dnsrecord \ + (type, domain_id, disabled, name, auth) \ + VALUES (null, '%d', false, E'%s', '%d') +gpgsql-insert-record-order-query=INSERT INTO domains_dnsrecord \ + (content, ttl, prio, type, domain_id, disabled, name, ordername, auth) \ + VALUES (E'%s', %d, %d, '%s', %d, %d::bool, E'%s', E'%s', '%d') +gpgsql-list-subzone-query=SELECT content, ttl, prio, type, domain_id, disabled::int, name, auth::int \ + FROM domains_dnsrecord \ + WHERE disabled=false AND (name=E'%s' OR name like E'%s') AND domain_id='%d' +gpgsql-nullify-ordername-and-auth-query=UPDATE domains_dnsrecord \ + SET ordername=NULL, auth=false \ + WHERE name=E'%s' AND type=E'%s' AND domain_id='%d' AND disabled=false +gpgsql-nullify-ordername-and-update-auth-query=UPDATE domains_dnsrecord \ + SET ordername=NULL, auth=%d::bool \ + WHERE domain_id='%d' AND name='%s' AND disabled=false +gpgsql-remove-empty-non-terminals-from-zone-query=DELETE FROM domains_dnsrecord \ + WHERE domain_id='%d' AND type IS NULL +gpgsql-set-auth-on-ds-record-query=UPDATE domains_dnsrecord \ + SET auth=true \ + WHERE domain_id='%d' AND name='%s' AND type='DS' AND disabled=false +gpgsql-set-order-and-auth-query=UPDATE domains_dnsrecord \ + SET ordername=E'%s', auth=%d::bool \ + WHERE name=E'%s' AND domain_id='%d' AND disabled=false +gpgsql-zone-lastchange-query=SELECT MAX(change_date) FROM domains_dnsrecord \ + WHERE domain_id=%d + +# Domain queries +gpgsql-delete-domain-query=DELETE FROM domains_dnsdomain \ + WHERE domain=E'%s' +gpgsql-insert-zone-query=INSERT INTO domains_dnsdomain \ + (type, domain) \ + VALUES ('NATIVE', E'%s') +gpgsql-update-kind-query=UPDATE domains_dnsdomain \ + SET type='%s' \ + WHERE domain='%s' +gpgsql-update-master-query=UPDATE domains_dnsdomain \ + SET master='%s' \ + WHERE domain='%s' + +# Mixed queries +gpgsql-get-all-domains-query=SELECT domains_dnsdomain.id, domains_dnsdomain.domain, domains_dnsrecord.content, \ + domains_dnsdomain.type, domains_dnsdomain.master, domains_dnsdomain.notified_serial, domains_dnsdomain.last_check \ + FROM domains_dnsdomain \ + LEFT JOIN domains_dnsrecord \ + ON domains_dnsrecord.domain_id=domains_dnsdomain.id AND domains_dnsrecord.type='SOA' AND domains_dnsrecord.name=domains_dnsdomain.domain \ + WHERE domains_dnsrecord.disabled=false OR %d::bool diff --git a/docs/task_flows.rst b/docs/task_flows.rst new file mode 100644 index 0000000..c75e878 --- /dev/null +++ b/docs/task_flows.rst @@ -0,0 +1,35 @@ +********** +Task Flows +********** + +gva uses Celery tasks to trigger actions on several servers, this chapter lists +the code parts that start tasks. See the code documentation for details on the +information flow. + +:py:mod:`osusers.admin` +======================= + + * :py:meth:`osusers.admin.SshPublicKeyAdmin.perform_delete_selected` + + +:py:mod:`osusers.signals` +========================= + + * :py:func:`osusers.signals.handle_group_created` + * :py:func:`osusers.signals.handle_group_deleted` + * :py:func:`osusers.signals.handle_ssh_keys_changed` + * :py:func:`osusers.signals.handle_user_added_to_group` + * :py:func:`osusers.signals.handle_user_created` + * :py:func:`osusers.signals.handle_user_deleted` + * :py:func:`osusers.signals.handle_user_password_set` + * :py:func:`osusers.signals.handle_user_removed_from_group` + + +:py:mod:`userdbs.signals` +========================= + + * :py:func:`userdbs.signals.handle_dbuser_created` + * :py:func:`userdbs.signals.handle_dbuser_deleted` + * :py:func:`userdbs.signals.handle_dbuser_deleted` + * :py:func:`userdbs.signals.handle_dbuser_password_set` + * :py:func:`userdbs.signals.handle_userdb_created` diff --git a/fixtures/default_hostingpackages.json b/fixtures/default_hostingpackages.json new file mode 100644 index 0000000..359662a --- /dev/null +++ b/fixtures/default_hostingpackages.json @@ -0,0 +1,227 @@ +[ + { + "fields": { + "description": "", + "diskspace": 256, + "created": "2015-01-18T10:50:38.392Z", + "modified": "2015-01-18T10:50:38.409Z", + "diskspace_unit": 0, + "mailboxcount": 10, + "name": "Basispaket I" + }, + "model": "hostingpackages.hostingpackagetemplate", + "pk": 1 + }, + { + "fields": { + "description": "", + "diskspace": 512, + "created": "2015-01-18T10:50:55.413Z", + "modified": "2015-01-18T10:50:55.425Z", + "diskspace_unit": 0, + "mailboxcount": 25, + "name": "Basispaket II" + }, + "model": "hostingpackages.hostingpackagetemplate", + "pk": 2 + }, + { + "fields": { + "description": "", + "diskspace": 1, + "created": "2015-01-18T10:51:13.988Z", + "modified": "2015-01-18T10:51:13.999Z", + "diskspace_unit": 1, + "mailboxcount": 50, + "name": "Basispaket III" + }, + "model": "hostingpackages.hostingpackagetemplate", + "pk": 3 + }, + { + "fields": { + "description": "Hostingpaket, dass ausschlie\u00dflich Mailweiterleitungen und Web-Redirects enth\u00e4lt", + "diskspace": 0, + "created": "2015-01-28T20:45:20.598Z", + "modified": "2015-01-28T20:45:20.616Z", + "diskspace_unit": 0, + "mailboxcount": 0, + "name": "Weiterleitung an externe Services" + }, + "model": "hostingpackages.hostingpackagetemplate", + "pk": 4 + }, + { + "fields": { + "diskspace_unit": 0, + "diskspace": 256 + }, + "model": "hostingpackages.diskspaceoption", + "pk": 3 + }, + { + "fields": { + "diskspace_unit": 0, + "diskspace": 512 + }, + "model": "hostingpackages.diskspaceoption", + "pk": 4 + }, + { + "fields": { + "diskspace_unit": 1, + "diskspace": 1 + }, + "model": "hostingpackages.diskspaceoption", + "pk": 5 + }, + { + "fields": { + "diskspace_unit": 1, + "diskspace": 2 + }, + "model": "hostingpackages.diskspaceoption", + "pk": 6 + }, + { + "fields": { + "diskspace_unit": 1, + "diskspace": 5 + }, + "model": "hostingpackages.diskspaceoption", + "pk": 7 + }, + { + "fields": { + "diskspace_unit": 1, + "diskspace": 10 + }, + "model": "hostingpackages.diskspaceoption", + "pk": 8 + }, + { + "fields": { + "db_type": 0, + "number": 1 + }, + "model": "hostingpackages.userdatabaseoption", + "pk": 1 + }, + { + "fields": { + "db_type": 1, + "number": 1 + }, + "model": "hostingpackages.userdatabaseoption", + "pk": 2 + }, + { + "fields": { + "number": 10 + }, + "model": "hostingpackages.mailboxoption", + "pk": 9 + }, + { + "fields": { + "number": 25 + }, + "model": "hostingpackages.mailboxoption", + "pk": 10 + }, + { + "fields": { + "number": 50 + }, + "model": "hostingpackages.mailboxoption", + "pk": 11 + }, + { + "fields": { + "modified": "2015-01-18T10:51:38.298Z", + "created": "2015-01-18T10:51:38.286Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 1 + }, + { + "fields": { + "modified": "2015-01-18T10:51:41.804Z", + "created": "2015-01-18T10:51:41.792Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 2 + }, + { + "fields": { + "modified": "2015-01-18T10:52:03.193Z", + "created": "2015-01-18T10:52:03.181Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 3 + }, + { + "fields": { + "modified": "2015-01-18T10:52:08.430Z", + "created": "2015-01-18T10:52:08.418Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 4 + }, + { + "fields": { + "modified": "2015-01-18T10:52:14.153Z", + "created": "2015-01-18T10:52:14.134Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 5 + }, + { + "fields": { + "modified": "2015-01-18T10:52:19.151Z", + "created": "2015-01-18T10:52:19.138Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 6 + }, + { + "fields": { + "modified": "2015-01-18T10:52:24.461Z", + "created": "2015-01-18T10:52:24.448Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 7 + }, + { + "fields": { + "modified": "2015-01-18T10:52:30.821Z", + "created": "2015-01-18T10:52:30.807Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 8 + }, + { + "fields": { + "modified": "2015-01-18T10:52:53.657Z", + "created": "2015-01-18T10:52:53.646Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 9 + }, + { + "fields": { + "modified": "2015-01-18T10:52:56.079Z", + "created": "2015-01-18T10:52:56.064Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 10 + }, + { + "fields": { + "modified": "2015-01-18T10:53:01.634Z", + "created": "2015-01-18T10:53:01.622Z" + }, + "model": "hostingpackages.hostingoption", + "pk": 11 + } +] \ No newline at end of file diff --git a/fixtures/default_osuser_groups.json b/fixtures/default_osuser_groups.json new file mode 100644 index 0000000..f570511 --- /dev/null +++ b/fixtures/default_osuser_groups.json @@ -0,0 +1,46 @@ +[ + { + "fields": { + "created": "2014-06-03T10:13:59.796Z", + "descr": "SFTP users", + "groupname": "sftponly", + "modified": "2014-06-03T10:13:59.804Z", + "passwd": "" + }, + "model": "osusers.group", + "pk": 2000 + }, + { + "fields": { + "created": "2014-06-03T10:14:14.353Z", + "descr": "", + "groupname": "wwwusers", + "modified": "2014-06-03T10:14:14.360Z", + "passwd": "" + }, + "model": "osusers.group", + "pk": 2001 + }, + { + "fields": { + "created": "2014-06-03T10:14:31.853Z", + "descr": "", + "groupname": "webserver", + "modified": "2014-06-03T10:14:31.860Z", + "passwd": "" + }, + "model": "osusers.group", + "pk": 2002 + }, + { + "fields": { + "created": "2016-06-18T10:32:26.490Z", + "descr": "Group for git only access", + "groupname": "gitonly", + "modified": "2016-06-18T10:32:26.498Z", + "passwd": "" + }, + "model": "osusers.group", + "pk": 2003 + } +] \ No newline at end of file diff --git a/gnuviechadmin/.coveragerc b/gnuviechadmin/.coveragerc deleted file mode 100644 index a787368..0000000 --- a/gnuviechadmin/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[run] -source = gnuviechadmin,contact_form,dashboard,domains,gvawebcore,managemails,osusers,taskresults,userdbs - -[report] -omit = */migrations/*,*/tests/*.py,*/tests.py,gnuviechadmin/settings/local.py,gnuviechadmin/settings/production.py diff --git a/gnuviechadmin/contact_form/forms.py b/gnuviechadmin/contact_form/forms.py index d703983..4427eb6 100644 --- a/gnuviechadmin/contact_form/forms.py +++ b/gnuviechadmin/contact_form/forms.py @@ -7,13 +7,13 @@ from __future__ import absolute_import, unicode_literals from django import forms from django.conf import settings from django.core.mail import send_mail -from django.core.urlresolvers import reverse from django.template import RequestContext from django.template import loader +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -from django.contrib.sites.models import RequestSite from django.contrib.sites.models import Site +from django.contrib.sites.requests import RequestSite from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit @@ -52,11 +52,16 @@ class ContactForm(forms.Form): self.request, dict(self.cleaned_data, site=site)) def message(self): - return loader.render_to_string(self.template_name, self.get_context()) + context = self.get_context() + template_context = context.flatten() + template_context.update({ + 'remote_ip': context.request.META['REMOTE_ADDR'] + }) + return loader.render_to_string(self.template_name, template_context) def subject(self): - subject = loader.render_to_string( - self.subject_template_name, self.get_context()) + context = self.get_context().flatten() + subject = loader.render_to_string(self.subject_template_name, context) return ''.join(subject.splitlines()) def save(self, fail_silently=False): diff --git a/gnuviechadmin/contact_form/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/contact_form/locale/de/LC_MESSAGES/django.po index f559bc8..ffeeaa3 100644 --- a/gnuviechadmin/contact_form/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/contact_form/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: contact_form\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-02-01 19:02+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" "PO-Revision-Date: 2015-02-01 19:03+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" @@ -19,18 +19,18 @@ msgstr "" "X-Generator: Poedit 1.6.10\n" "X-Poedit-SourceCharset: UTF-8\n" -#: forms.py:27 +#: contact_form/forms.py:27 msgid "Your name" msgstr "Ihr Name" -#: forms.py:28 +#: contact_form/forms.py:28 msgid "Your email address" msgstr "Ihre E-Mailadresse" -#: forms.py:29 +#: contact_form/forms.py:29 msgid "Your message" msgstr "Ihre Nachricht" -#: forms.py:41 +#: contact_form/forms.py:41 msgid "Send message" msgstr "Nachricht senden" diff --git a/gnuviechadmin/contact_form/tests/__init__.py b/gnuviechadmin/contact_form/tests/__init__.py new file mode 100644 index 0000000..88bdfc5 --- /dev/null +++ b/gnuviechadmin/contact_form/tests/__init__.py @@ -0,0 +1,4 @@ +""" +Tests for the :py:mod:`contact_form` app. + +""" diff --git a/gnuviechadmin/contact_form/tests/test_forms.py b/gnuviechadmin/contact_form/tests/test_forms.py new file mode 100644 index 0000000..b546657 --- /dev/null +++ b/gnuviechadmin/contact_form/tests/test_forms.py @@ -0,0 +1,83 @@ +""" +Tests for :py:mod:`contact_form.forms`. + +""" +from unittest.mock import MagicMock, Mock, patch + +from django.contrib.sites.models import Site +from django.test import TestCase +from django.urls import reverse + +from contact_form.forms import ContactForm + +TEST_DATA = {"name": "Test User", "email": "test@example.org", "body": "Test message"} + + +class ContactFormTest(TestCase): + def test_constructor_needs_request(self): + with self.assertRaises(KeyError): + ContactForm() + + def test_constructor(self): + request = MagicMock() + form = ContactForm(request=request) + self.assertTrue(hasattr(form, "request")) + self.assertEqual(form.request, request) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual(form.helper.form_action, reverse("contact_form")) + self.assertEqual(len(form.helper.inputs), 1) + self.assertEqual(form.helper.inputs[0].name, "submit") + + def test_constructor_fields(self): + request = MagicMock() + form = ContactForm(request=request) + self.assertEqual(len(form.fields), 3) + self.assertIn("email", form.fields) + self.assertIn("name", form.fields) + self.assertIn("body", form.fields) + self.assertEqual(len(form.data), 0) + + def test_get_context_invalid(self): + request = MagicMock() + form = ContactForm(request=request) + with self.assertRaisesMessage( + ValueError, "Cannot generate context from invalid contact form" + ): + form.get_context() + + def test_get_context_valid_site_installed(self): + request = MagicMock() + form = ContactForm(request=request, data=TEST_DATA) + context = form.get_context() + self.assertIn("site", context) + self.assertIn("name", context) + self.assertIn("email", context) + self.assertIn("body", context) + + def test_get_context_valid_site_not_installed(self): + request = MagicMock() + form = ContactForm(request=request, data=TEST_DATA) + with patch("contact_form.forms.Site") as sitemock: + sitemock._meta.installed = False + context = form.get_context() + self.assertIn("site", context) + self.assertIn("name", context) + self.assertIn("email", context) + self.assertIn("body", context) + + def test_message(self): + request = Mock() + request.META = {"REMOTE_ADDR": "127.0.0.1"} + form = ContactForm(request=request, data=TEST_DATA) + message = form.message() + self.assertIn(TEST_DATA["name"], message) + self.assertIn(TEST_DATA["email"], message) + self.assertIn(TEST_DATA["body"], message) + self.assertIn("127.0.0.1", message) + + def test_subject(self): + request = Mock() + form = ContactForm(request=request, data=TEST_DATA) + subject = form.subject() + self.assertIn(Site.objects.get_current().name, subject) + self.assertIn(TEST_DATA["name"], subject) diff --git a/gnuviechadmin/contact_form/tests/test_views.py b/gnuviechadmin/contact_form/tests/test_views.py new file mode 100644 index 0000000..f7df680 --- /dev/null +++ b/gnuviechadmin/contact_form/tests/test_views.py @@ -0,0 +1,123 @@ +""" +Tests for :py:mod:`contact_form.views`. + +""" +from django.contrib.auth import get_user_model +from django.core import mail +from django.test import TestCase +from django.urls import reverse + +User = get_user_model() + +TEST_USER = "test" +TEST_PASSWORD = "secret" +TEST_EMAIL = "test@example.org" +TEST_NAME = "Example Tester".split() +TEST_MESSAGE = """ +This is a really unimportant test message. +""" + + +class ContactFormViewTest(TestCase): + def _setup_user(self, **kwargs): + return User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD, **kwargs + ) + + def test_get_contact_form_template(self): + response = self.client.get(reverse("contact_form")) + self.assertTemplateUsed(response, "contact_form/contact_form.html") + + def test_get_contact_form_anonymous_status(self): + response = self.client.get(reverse("contact_form")) + self.assertEqual(response.status_code, 200) + + def test_get_contact_form_anonymous_has_empty_form(self): + response = self.client.get(reverse("contact_form")) + self.assertIn("form", response.context) + form = response.context["form"] + self.assertEqual(len(form.initial), 0) + + def test_get_contact_form_fields_anonymous(self): + response = self.client.get(reverse("contact_form")) + for name in ("name", "email", "body"): + self.assertIn(name, response.context["form"].fields) + + def test_post_empty_form_template(self): + response = self.client.post(reverse("contact_form"), {}) + self.assertTemplateUsed(response, "contact_form/contact_form.html") + + def test_post_empty_form_status(self): + response = self.client.post(reverse("contact_form"), {}) + self.assertEqual(response.status_code, 200) + + def test_post_empty_form_validation_errors(self): + response = self.client.post(reverse("contact_form"), {}) + self.assertIn("form", response.context) + form = response.context["form"] + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 3) + + def test_post_empty_form_no_mail(self): + self.client.post(reverse("contact_form"), {}) + self.assertEqual(len(mail.outbox), 0) + + def test_get_contact_form_logged_in_no_fullname_initial(self): + self._setup_user() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(reverse("contact_form")) + self.assertIn("form", response.context) + form = response.context["form"] + self.assertEqual(form.initial, {"name": TEST_USER, "email": TEST_EMAIL}) + + def test_get_contact_form_logged_in_fullname_initial(self): + self._setup_user(first_name=TEST_NAME[0], last_name=TEST_NAME[1]) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(reverse("contact_form")) + self.assertIn("form", response.context) + form = response.context["form"] + self.assertEqual( + form.initial, {"name": " ".join(TEST_NAME), "email": TEST_EMAIL} + ) + + def test_post_filled_form_anonymous_redirects(self): + response = self.client.post( + reverse("contact_form"), + {"name": TEST_USER, "email": TEST_EMAIL, "body": TEST_MESSAGE}, + ) + self.assertRedirects(response, reverse("contact_success")) + + def test_post_filled_form_anonymous_mail(self): + self.client.post( + reverse("contact_form"), + {"name": TEST_USER, "email": TEST_EMAIL, "body": TEST_MESSAGE}, + ) + self.assertEqual(len(mail.outbox), 1) + + def test_post_filled_form_logged_in_redirects(self): + self._setup_user(first_name=TEST_NAME[0], last_name=TEST_NAME[1]) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse("contact_form"), + {"name": " ".join(TEST_NAME), "email": TEST_EMAIL, "body": TEST_MESSAGE}, + ) + self.assertRedirects(response, reverse("contact_success")) + + def test_post_filled_form_logged_in_mail(self): + self._setup_user(first_name=TEST_NAME[0], last_name=TEST_NAME[1]) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + self.client.post( + reverse("contact_form"), + {"name": " ".join(TEST_NAME), "email": TEST_EMAIL, "body": TEST_MESSAGE}, + ) + self.assertEqual(len(mail.outbox), 1) + + +class ContactSuccessViewTest(TestCase): + def test_get_template(self): + response = self.client.get(reverse("contact_success")) + self.assertTemplateUsed(response, "contact_form/contact_success.html") + + def test_get_status(self): + response = self.client.get(reverse("contact_success")) + self.assertEqual(response.status_code, 200) diff --git a/gnuviechadmin/contact_form/urls.py b/gnuviechadmin/contact_form/urls.py index 6963969..e3f01ca 100644 --- a/gnuviechadmin/contact_form/urls.py +++ b/gnuviechadmin/contact_form/urls.py @@ -4,7 +4,7 @@ URL patterns for the contact_form views. """ from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( ContactFormView, @@ -12,8 +12,7 @@ from .views import ( ) -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^$', ContactFormView.as_view(), name='contact_form'), url(r'^success/$', ContactSuccessView.as_view(), name='contact_success'), -) +] diff --git a/gnuviechadmin/contact_form/views.py b/gnuviechadmin/contact_form/views.py index 0e27417..a1f3aba 100644 --- a/gnuviechadmin/contact_form/views.py +++ b/gnuviechadmin/contact_form/views.py @@ -5,7 +5,7 @@ This module defines the views of the contact_form app. from __future__ import absolute_import, unicode_literals from django.shortcuts import redirect -from django.core.urlresolvers import reverse_lazy +from django.urls import reverse_lazy from django.views.generic import ( FormView, TemplateView, @@ -31,7 +31,7 @@ class ContactFormView(FormView): def get_initial(self): initial = super(ContactFormView, self).get_initial() currentuser = self.request.user - if currentuser.is_authenticated(): + if currentuser.is_authenticated: initial['name'] = ( currentuser.get_full_name() or currentuser.username) initial['email'] = currentuser.email diff --git a/gnuviechadmin/dashboard/tests/__init__.py b/gnuviechadmin/dashboard/tests/__init__.py new file mode 100644 index 0000000..b01b093 --- /dev/null +++ b/gnuviechadmin/dashboard/tests/__init__.py @@ -0,0 +1,4 @@ +""" +Tests for :py:mod:`dashboard`. + +""" diff --git a/gnuviechadmin/dashboard/tests/test_views.py b/gnuviechadmin/dashboard/tests/test_views.py new file mode 100644 index 0000000..2ff835b --- /dev/null +++ b/gnuviechadmin/dashboard/tests/test_views.py @@ -0,0 +1,67 @@ +""" +Tests for :py:mod:`dashboard.views`. + +""" + +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse + +User = get_user_model() + +TEST_USER = "test" +TEST_PASSWORD = "secret" + + +class IndexViewTest(TestCase): + def test_index_view(self): + response = self.client.get(reverse("dashboard")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "dashboard/index.html") + + +class UserDashboardViewTest(TestCase): + def _create_test_user(self): + self.user = User.objects.create(username=TEST_USER) + self.user.set_password(TEST_PASSWORD) + self.user.save() + + def test_user_dashboard_view_no_user(self): + response = self.client.get( + reverse("customer_dashboard", kwargs={"slug": TEST_USER}) + ) + self.assertEqual(response.status_code, 404) + + def test_user_dashboard_view_anonymous(self): + User.objects.create(username=TEST_USER) + response = self.client.get( + reverse("customer_dashboard", kwargs={"slug": TEST_USER}) + ) + self.assertEqual(response.status_code, 403) + + def test_user_dashboard_view_logged_in_ok(self): + self._create_test_user() + self.assertTrue(self.client.login(username=TEST_USER, password=TEST_PASSWORD)) + response = self.client.get( + reverse("customer_dashboard", kwargs={"slug": TEST_USER}) + ) + self.assertEqual(response.status_code, 200) + + def test_user_dashboard_view_logged_in_template(self): + self._create_test_user() + self.assertTrue(self.client.login(username=TEST_USER, password=TEST_PASSWORD)) + response = self.client.get( + reverse("customer_dashboard", kwargs={"slug": TEST_USER}) + ) + self.assertTemplateUsed(response, "dashboard/user_dashboard.html") + + def test_user_dashboard_view_logged_in_context_fresh(self): + self._create_test_user() + self.assertTrue(self.client.login(username=TEST_USER, password=TEST_PASSWORD)) + response = self.client.get( + reverse("customer_dashboard", kwargs={"slug": TEST_USER}) + ) + self.assertIn("dashboard_user", response.context) + self.assertEqual(self.user, response.context["dashboard_user"]) + self.assertIn("hosting_packages", response.context) + self.assertEqual(len(response.context["hosting_packages"]), 0) diff --git a/gnuviechadmin/dashboard/urls.py b/gnuviechadmin/dashboard/urls.py index f7172d8..ffa741b 100644 --- a/gnuviechadmin/dashboard/urls.py +++ b/gnuviechadmin/dashboard/urls.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( IndexView, @@ -8,9 +8,8 @@ from .views import ( ) -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^$', IndexView.as_view(), name='dashboard'), url(r'^user/(?P[\w0-9@.+-_]+)/$', UserDashboardView.as_view(), name='customer_dashboard'), -) +] diff --git a/gnuviechadmin/domains/admin.py b/gnuviechadmin/domains/admin.py index 997b49d..87be497 100644 --- a/gnuviechadmin/domains/admin.py +++ b/gnuviechadmin/domains/admin.py @@ -6,9 +6,23 @@ with the django admin site. from django.contrib import admin from .models import ( - MailDomain, + DNSComment, + DNSCryptoKey, + DNSDomain, + DNSDomainMetadata, + DNSRecord, + DNSSupermaster, + DNSTSIGKey, HostingDomain, + MailDomain, ) admin.site.register(MailDomain) admin.site.register(HostingDomain) +admin.site.register(DNSComment) +admin.site.register(DNSCryptoKey) +admin.site.register(DNSDomain) +admin.site.register(DNSDomainMetadata) +admin.site.register(DNSRecord) +admin.site.register(DNSSupermaster) +admin.site.register(DNSTSIGKey) diff --git a/gnuviechadmin/domains/forms.py b/gnuviechadmin/domains/forms.py index fff18cb..451e108 100644 --- a/gnuviechadmin/domains/forms.py +++ b/gnuviechadmin/domains/forms.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, unicode_literals import re from django import forms -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils.translation import ugettext as _ from crispy_forms.helper import FormHelper @@ -21,7 +21,9 @@ from .models import HostingDomain def relative_domain_validator(value): """ - This validator ensures that the given value is a valid lowercase + This validator ensures that the given value is a valid lowercase domain + name. + """ if len(value) > 254: raise forms.ValidationError( diff --git a/gnuviechadmin/domains/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/domains/locale/de/LC_MESSAGES/django.po index 5dc5688..4e559af 100644 --- a/gnuviechadmin/domains/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/domains/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: gnuviechadmin domains\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-27 18:55+0100\n" -"PO-Revision-Date: 2015-01-27 19:06+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" +"PO-Revision-Date: 2015-11-08 12:02+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" "Language: de\n" @@ -16,57 +16,151 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 1.6.10\n" +"X-Generator: Poedit 1.8.6\n" "X-Poedit-SourceCharset: UTF-8\n" #: domains/apps.py:17 msgid "Domains" msgstr "Domains" -#: domains/forms.py:28 +#: domains/forms.py:30 domains/tests/test_forms.py:24 msgid "host name too long" msgstr "zu langer Hostname" -#: domains/forms.py:31 +#: domains/forms.py:33 domains/tests/test_forms.py:29 +#: domains/tests/test_forms.py:34 domains/tests/test_forms.py:39 +#: domains/tests/test_forms.py:44 msgid "invalid domain name" msgstr "ungültiger Domainname" -#: domains/forms.py:54 +#: domains/forms.py:56 msgid "Add Hosting Domain" msgstr "Hostingdomain hinzufügen" -#: domains/models.py:21 +#: domains/models.py:17 +msgid "Master" +msgstr "Master" + +#: domains/models.py:18 +msgid "Slave" +msgstr "Slave" + +#: domains/models.py:19 +msgid "Native" +msgstr "Native" + +#: domains/models.py:44 +msgid "HMAC MD5" +msgstr "HMAC MD5" + +#: domains/models.py:45 +msgid "HMAC SHA1" +msgstr "HMAC SHA1" + +#: domains/models.py:46 +msgid "HMAC SHA224" +msgstr "HMAC SHA224" + +#: domains/models.py:47 +msgid "HMAC SHA256" +msgstr "HMAC SHA256" + +#: domains/models.py:48 +msgid "HMAC SHA384" +msgstr "HMAC SHA384" + +#: domains/models.py:49 +msgid "HMAC SHA512" +msgstr "HMAC SHA512" + +#: domains/models.py:58 msgid "domain name" msgstr "Domainname" -#: domains/models.py:23 +#: domains/models.py:60 domains/models.py:258 domains/models.py:308 msgid "customer" msgstr "Kunde" -#: domains/models.py:42 +#: domains/models.py:76 msgid "Mail domain" msgstr "E-Maildomain" -#: domains/models.py:43 +#: domains/models.py:77 msgid "Mail domains" msgstr "E-Maildomains" -#: domains/models.py:87 +#: domains/models.py:121 msgid "mail domain" msgstr "E-Maildomain" -#: domains/models.py:88 +#: domains/models.py:122 msgid "assigned mail domain for this domain" msgstr "zugeordnete E-Maildomain für diese Domain" -#: domains/models.py:94 +#: domains/models.py:128 msgid "Hosting domain" msgstr "Hostingdomain" -#: domains/models.py:95 +#: domains/models.py:129 msgid "Hosting domains" msgstr "Hostingdomains" +#: domains/models.py:169 +msgid "DNS domain" +msgstr "DNS-Domain" + +#: domains/models.py:170 +msgid "DNS domains" +msgstr "DNS-Domains" + +#: domains/models.py:226 +msgid "DNS record" +msgstr "DNS-Record" + +#: domains/models.py:227 +msgid "DNS records" +msgstr "DNS-Records" + +#: domains/models.py:261 +msgid "DNS supermaster" +msgstr "DNS-Supermaster" + +#: domains/models.py:262 +msgid "DNS supermasters" +msgstr "DNS-Supermasters" + +#: domains/models.py:313 +msgid "DNS comment" +msgstr "DNS-Kommentar" + +#: domains/models.py:314 +msgid "DNS comments" +msgstr "DNS-Kommentare" + +#: domains/models.py:351 +msgid "DNS domain metadata item" +msgstr "DNS-Domainmetadaten-Eintrag" + +#: domains/models.py:352 +msgid "DNS domain metadata items" +msgstr "DNS-Domainmetadaten-Einträge" + +#: domains/models.py:385 +msgid "DNS crypto key" +msgstr "DNS-Cryposchlüssel" + +#: domains/models.py:386 +msgid "DNS crypto keys" +msgstr "DNS-Cryptoschlüssel" + +#: domains/models.py:420 +msgid "DNS TSIG key" +msgstr "DNS-TSIG-Schlüssel" + +#: domains/models.py:421 +msgid "DNS TSIG keys" +msgstr "DNS-TSIG-Schlüssel" + #: domains/views.py:58 #, python-brace-format msgid "Successfully created domain {domainname}" diff --git a/gnuviechadmin/domains/migrations/0002_auto_20150124_1909.py b/gnuviechadmin/domains/migrations/0002_auto_20150124_1909.py index 22ba714..27743bf 100644 --- a/gnuviechadmin/domains/migrations/0002_auto_20150124_1909.py +++ b/gnuviechadmin/domains/migrations/0002_auto_20150124_1909.py @@ -8,7 +8,6 @@ import model_utils.fields class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('domains', '0001_initial'), @@ -18,12 +17,31 @@ class Migration(migrations.Migration): migrations.CreateModel( name='HostingDomain', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('domain', models.CharField(unique=True, max_length=128, verbose_name='domain name')), - ('customer', models.ForeignKey(verbose_name='customer', blank=True, to=settings.AUTH_USER_MODEL, null=True)), - ('maildomain', models.OneToOneField(null=True, to='domains.MailDomain', blank=True, help_text='assigned mail domain for this domain', verbose_name='mail domain')), + ('id', + models.AutoField(verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('domain', + models.CharField( + unique=True, max_length=128, verbose_name='domain name')), + ('customer', + models.ForeignKey( + verbose_name='customer', blank=True, + to=settings.AUTH_USER_MODEL, null=True, + on_delete=models.CASCADE)), + ('maildomain', + models.OneToOneField( + null=True, to='domains.MailDomain', blank=True, + help_text='assigned mail domain for this domain', + verbose_name='mail domain', + on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Hosting domain', @@ -34,13 +52,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name='maildomain', name='customer', - field=models.ForeignKey(verbose_name='customer', blank=True, to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey( + verbose_name='customer', blank=True, + to=settings.AUTH_USER_MODEL, null=True, + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( model_name='maildomain', name='domain', - field=models.CharField(unique=True, max_length=128, verbose_name='domain name'), + field=models.CharField( + unique=True, max_length=128, verbose_name='domain name'), preserve_default=True, ), ] diff --git a/gnuviechadmin/domains/migrations/0003_auto_20151105_2133.py b/gnuviechadmin/domains/migrations/0003_auto_20151105_2133.py new file mode 100644 index 0000000..5891a27 --- /dev/null +++ b/gnuviechadmin/domains/migrations/0003_auto_20151105_2133.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +from django.conf import settings +import model_utils.fields + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('domains', '0002_auto_20150124_1909'), + ] + + operations = [ + migrations.CreateModel( + name='DNSComment', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=255)), + ('commenttype', + models.CharField(max_length=10, db_column='type')), + ('modified_at', models.IntegerField()), + ('comment', models.CharField(max_length=65535)), + ('customer', models.ForeignKey( + verbose_name='customer', + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ], + ), + migrations.RunSQL( + '''ALTER TABLE domains_dnscomment ADD CONSTRAINT c_lowercase_name + CHECK (((name)::TEXT = LOWER((name)::TEXT)))''' + ), + migrations.CreateModel( + name='DNSCryptoKey', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('flags', models.IntegerField()), + ('active', models.BooleanField(default=True)), + ('content', models.TextField()), + ], + ), + migrations.CreateModel( + name='DNSDomain', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('domain', models.CharField( + unique=True, max_length=255, verbose_name='domain name')), + ('master', + models.CharField(max_length=128, null=True, blank=True)), + ('last_check', models.IntegerField(null=True)), + ('domaintype', models.CharField( + max_length=6, db_column='type', + choices=[('MASTER', 'Master'), + ('SLAVE', 'Slave'), + ('NATIVE', 'Native')])), + ('notified_serial', models.IntegerField(null=True)), + ('customer', models.ForeignKey( + verbose_name='customer', blank=True, + to=settings.AUTH_USER_MODEL, null=True, + on_delete=models.CASCADE)), + ], + options={ + 'verbose_name': 'DNS domain', + 'verbose_name_plural': 'DNS domains', + }, + ), + migrations.RunSQL( + '''ALTER TABLE domains_dnsdomain ADD CONSTRAINT c_lowercase_name + CHECK (((domain)::TEXT = LOWER((domain)::TEXT)))''' + ), + migrations.CreateModel( + name='DNSDomainMetadata', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('kind', models.CharField(max_length=32)), + ('content', models.TextField()), + ('domain', models.ForeignKey( + to='domains.DNSDomain', on_delete=models.CASCADE)), + ], + ), + migrations.CreateModel( + name='DNSRecord', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('name', models.CharField( + db_index=True, max_length=255, null=True, blank=True)), + ('recordtype', models.CharField( + max_length=10, null=True, db_column='type', blank=True)), + ('content', models.CharField( + max_length=65535, null=True, blank=True)), + ('ttl', models.IntegerField(null=True)), + ('prio', models.IntegerField(null=True)), + ('change_date', models.IntegerField(null=True)), + ('disabled', models.BooleanField(default=False)), + ('ordername', models.CharField(max_length=255)), + ('auth', models.BooleanField(default=True)), + ('domain', models.ForeignKey( + to='domains.DNSDomain', on_delete=models.CASCADE)), + ], + options={ + 'verbose_name': 'DNS record', + 'verbose_name_plural': 'DNS records', + }, + ), + migrations.RunSQL( + '''ALTER TABLE domains_dnsrecord ADD CONSTRAINT c_lowercase_name + CHECK (((name)::TEXT = LOWER((name)::TEXT)))''' + ), + migrations.RunSQL( + '''CREATE INDEX recordorder ON domains_dnsrecord (domain_id, + ordername text_pattern_ops)''' + ), + migrations.CreateModel( + name='DNSSupermaster', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('ip', models.GenericIPAddressField()), + ('nameserver', models.CharField(max_length=255)), + ('customer', models.ForeignKey( + verbose_name='customer', to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE)), + ], + ), + migrations.CreateModel( + name='DNSTSIGKey', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=255)), + ('algorithm', models.CharField(max_length=50)), + ('secret', models.CharField(max_length=255)), + ], + ), + migrations.RunSQL( + '''ALTER TABLE domains_dnstsigkey ADD CONSTRAINT c_lowercase_name + CHECK (((name)::TEXT = LOWER((name)::TEXT)))''' + ), + migrations.AlterField( + model_name='hostingdomain', + name='domain', + field=models.CharField( + unique=True, max_length=255, verbose_name='domain name'), + ), + migrations.AlterField( + model_name='maildomain', + name='domain', + field=models.CharField( + unique=True, max_length=255, verbose_name='domain name'), + ), + migrations.AddField( + model_name='dnscryptokey', + name='domain', + field=models.ForeignKey( + to='domains.DNSDomain', on_delete=models.CASCADE), + ), + migrations.AddField( + model_name='dnscomment', + name='domain', + field=models.ForeignKey( + to='domains.DNSDomain', on_delete=models.CASCADE), + ), + migrations.AlterUniqueTogether( + name='dnssupermaster', + unique_together=set([('ip', 'nameserver')]), + ), + migrations.AlterUniqueTogether( + name='dnstsigkey', + unique_together=set([('name', 'algorithm')]), + ), + migrations.AlterIndexTogether( + name='dnsrecord', + index_together=set([('name', 'recordtype')]), + ), + migrations.AlterIndexTogether( + name='dnscomment', + index_together={('name', 'commenttype'), ('domain', 'modified_at')}, + ), + ] diff --git a/gnuviechadmin/domains/migrations/0004_auto_20151107_1708.py b/gnuviechadmin/domains/migrations/0004_auto_20151107_1708.py new file mode 100644 index 0000000..40d407d --- /dev/null +++ b/gnuviechadmin/domains/migrations/0004_auto_20151107_1708.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('domains', '0003_auto_20151105_2133'), + ] + + operations = [ + migrations.AlterModelOptions( + name='dnscomment', + options={'verbose_name': 'DNS comment', 'verbose_name_plural': 'DNS comments'}, + ), + migrations.AlterModelOptions( + name='dnscryptokey', + options={'verbose_name': 'DNS crypto key', 'verbose_name_plural': 'DNS crypto keys'}, + ), + migrations.AlterModelOptions( + name='dnsdomainmetadata', + options={'verbose_name': 'DNS domain metadata item', 'verbose_name_plural': 'DNS domain metadata items'}, + ), + migrations.AlterModelOptions( + name='dnssupermaster', + options={'verbose_name': 'DNS supermaster', 'verbose_name_plural': 'DNS supermasters'}, + ), + migrations.AlterModelOptions( + name='dnstsigkey', + options={'verbose_name': 'DNS TSIG key', 'verbose_name_plural': 'DNS TSIG keys'}, + ), + migrations.AlterField( + model_name='dnsdomainmetadata', + name='kind', + field=models.CharField(max_length=32, choices=[('ALLOW-DNSUPDATE-FROM', 'ALLOW-DNSUPDATE-FROM'), ('ALSO-NOTIFY', 'ALSO-NOTIFY'), ('AXFR-MASTER-TSIG', 'AXFR-MASTER-TSIG'), ('AXFR-SOURCE', 'AXFR-SOURCE'), ('FORWARD-DNSUPDATE', 'FORWARD-DNSUPDATE'), ('GSS-ACCEPTOR-PRINCIPAL', 'GSS-ACCEPTOR-PRINCIPAL'), ('GSS-ALLOW-AXFR-PRINCIPAL', 'GSS-ALLOW-AXFR-PRINCIPAL'), ('LUA-AXFR-SCRIPT', 'LUA-AXFR-SCRIPT'), ('NSEC3NARROW', 'NSEC3NARROW'), ('NSEC3PARAM', 'NSEC3PARAM'), ('PRESIGNED', 'PRESIGNED'), ('PUBLISH_CDNSKEY', 'PUBLISH_CDNSKEY'), ('PUBLISH_CDS', 'PUBLISH_CDS'), ('SOA-EDIT', 'SOA-EDIT'), ('SOA-EDIT-DNSUPDATE', 'SOA-EDIT-DNSUPDATE'), ('TSIG-ALLOW-AXFR', 'TSIG-ALLOW-AXFR'), ('TSIG-ALLOW-DNSUPDATE', 'TSIG-ALLOW-DNSUPDATE')]), + ), + migrations.AlterField( + model_name='dnstsigkey', + name='algorithm', + field=models.CharField(max_length=50, choices=[('hmac-md5', 'HMAC MD5'), ('hmac-sha1', 'HMAC SHA1'), ('hmac-sha224', 'HMAC SHA224'), ('hmac-sha256', 'HMAC SHA256'), ('hmac-sha384', 'HMAC SHA384'), ('hmac-sha512', 'HMAC SHA512')]), + ), + ] diff --git a/gnuviechadmin/domains/models.py b/gnuviechadmin/domains/models.py index c827f2c..4fff880 100644 --- a/gnuviechadmin/domains/models.py +++ b/gnuviechadmin/domains/models.py @@ -10,25 +10,59 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext as _ from model_utils.models import TimeStampedModel +from model_utils import Choices + + +DNS_DOMAIN_TYPES = Choices( + ('MASTER', _('Master')), + ('SLAVE', _('Slave')), + ('NATIVE', _('Native')), +) + +# see https://doc.powerdns.com/md/authoritative/domainmetadata/ +DNS_DOMAIN_METADATA_KINDS = Choices( + 'ALLOW-DNSUPDATE-FROM', + 'ALSO-NOTIFY', + 'AXFR-MASTER-TSIG', + 'AXFR-SOURCE', + 'FORWARD-DNSUPDATE', + 'GSS-ACCEPTOR-PRINCIPAL', + 'GSS-ALLOW-AXFR-PRINCIPAL', + 'LUA-AXFR-SCRIPT', + 'NSEC3NARROW', + 'NSEC3PARAM', + 'PRESIGNED', + 'PUBLISH_CDNSKEY', + 'PUBLISH_CDS', + 'SOA-EDIT', + 'SOA-EDIT-DNSUPDATE', + 'TSIG-ALLOW-AXFR', + 'TSIG-ALLOW-DNSUPDATE', +) + +DNS_TSIG_KEY_ALGORITHMS = Choices( + ('hmac-md5', _('HMAC MD5')), + ('hmac-sha1', _('HMAC SHA1')), + ('hmac-sha224', _('HMAC SHA224')), + ('hmac-sha256', _('HMAC SHA256')), + ('hmac-sha384', _('HMAC SHA384')), + ('hmac-sha512', _('HMAC SHA512')), +) -@python_2_unicode_compatible class DomainBase(TimeStampedModel): """ This is the base model for domains. """ - domain = models.CharField(_('domain name'), max_length=128, unique=True) + domain = models.CharField(_('domain name'), max_length=255, unique=True) customer = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('customer'), blank=True, - null=True) + null=True, on_delete=models.CASCADE) class Meta: abstract = True - def __str__(self): - return self.name - @python_2_unicode_compatible class MailDomain(DomainBase): @@ -85,7 +119,8 @@ class HostingDomain(DomainBase): """ maildomain = models.OneToOneField( MailDomain, verbose_name=_('mail domain'), blank=True, null=True, - help_text=_('assigned mail domain for this domain') + help_text=_('assigned mail domain for this domain'), + on_delete=models.CASCADE, ) objects = HostingDomainManager() @@ -96,3 +131,301 @@ class HostingDomain(DomainBase): def __str__(self): return self.domain + + +@python_2_unicode_compatible +class DNSDomain(DomainBase): + """ + This model represents a DNS zone. The model is similar to the domain table + in the PowerDNS schema specified in + https://doc.powerdns.com/md/authoritative/backend-generic-mypgsql/. + + .. code-block:: sql + + CREATE TABLE domains ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + master VARCHAR(128) DEFAULT NULL, + last_check INT DEFAULT NULL, + type VARCHAR(6) NOT NULL, + notified_serial INT DEFAULT NULL, + account VARCHAR(40) DEFAULT NULL, + CONSTRAINT c_lowercase_name CHECK ( + ((name)::TEXT = LOWER((name)::TEXT))) + ); + + CREATE UNIQUE INDEX name_index ON domains(name); + + """ + # name is represented by domain + master = models.CharField(max_length=128, blank=True, null=True) + last_check = models.IntegerField(null=True) + domaintype = models.CharField( + max_length=6, choices=DNS_DOMAIN_TYPES, db_column='type') + notified_serial = models.IntegerField(null=True) + # account is represented by customer_id + # check constraint is added via RunSQL in migration + + class Meta: + verbose_name = _('DNS domain') + verbose_name_plural = _('DNS domains') + + def __str__(self): + return self.domain + + +@python_2_unicode_compatible +class DNSRecord(models.Model): + """ + This model represents a DNS record. The model is similar to the record + table in the PowerDNS schema specified in + https://doc.powerdns.com/md/authoritative/backend-generic-mypgsql/. + + .. code-block:: sql + + CREATE TABLE records ( + id SERIAL PRIMARY KEY, + domain_id INT DEFAULT NULL, + name VARCHAR(255) DEFAULT NULL, + type VARCHAR(10) DEFAULT NULL, + content VARCHAR(65535) DEFAULT NULL, + ttl INT DEFAULT NULL, + prio INT DEFAULT NULL, + change_date INT DEFAULT NULL, + disabled BOOL DEFAULT 'f', + ordername VARCHAR(255), + auth BOOL DEFAULT 't', + CONSTRAINT domain_exists + FOREIGN KEY(domain_id) REFERENCES domains(id) + ON DELETE CASCADE, + CONSTRAINT c_lowercase_name CHECK ( + ((name)::TEXT = LOWER((name)::TEXT))) + ); + + CREATE INDEX rec_name_index ON records(name); + CREATE INDEX nametype_index ON records(name,type); + CREATE INDEX domain_id ON records(domain_id); + CREATE INDEX recordorder ON records ( + domain_id, ordername text_pattern_ops); + + """ + domain = models.ForeignKey('DNSDomain', on_delete=models.CASCADE) + name = models.CharField( + max_length=255, blank=True, null=True, db_index=True) + recordtype = models.CharField( + max_length=10, blank=True, null=True, db_column='type') + content = models.CharField(max_length=65535, blank=True, null=True) + ttl = models.IntegerField(null=True) + prio = models.IntegerField(null=True) + change_date = models.IntegerField(null=True) + disabled = models.BooleanField(default=False) + ordername = models.CharField(max_length=255) + auth = models.BooleanField(default=True) + # check constraint and index recordorder are added via RunSQL in migration + + class Meta: + verbose_name = _('DNS record') + verbose_name_plural = _('DNS records') + index_together = [ + ['name', 'recordtype'] + ] + + def __str__(self): + return "{name} IN {type} {content}".format( + name=self.name, type=self.recordtype, content=self.content) + + +@python_2_unicode_compatible +class DNSSupermaster(models.Model): + """ + This model represents the supermasters table in the PowerDNS schema + specified in + https://doc.powerdns.com/md/authoritative/backend-generic-mypgsql/. + + .. code-block:: sql + + CREATE TABLE supermasters ( + ip INET NOT NULL, + nameserver VARCHAR(255) NOT NULL, + account VARCHAR(40) NOT NULL, + PRIMARY KEY(ip, nameserver) + ); + + """ + ip = models.GenericIPAddressField() + nameserver = models.CharField(max_length=255) + # account is replaced by customer + customer = models.ForeignKey( + settings.AUTH_USER_MODEL, verbose_name=_('customer'), + on_delete=models.CASCADE) + + class Meta: + verbose_name = _('DNS supermaster') + verbose_name_plural = _('DNS supermasters') + unique_together = ( + ('ip', 'nameserver') + ) + + def __str__(self): + return "{ip} {nameserver}".format( + ip=self.ip, nameserver=self.nameserver) + + +@python_2_unicode_compatible +class DNSComment(models.Model): + """ + This model represents the comments table in the PowerDNS schema specified + in https://doc.powerdns.com/md/authoritative/backend-generic-mypgsql/. The + comments table is used to store user comments related to individual DNS + records. + + .. code-block:: sql + + CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + domain_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(10) NOT NULL, + modified_at INT NOT NULL, + account VARCHAR(40) DEFAULT NULL, + comment VARCHAR(65535) NOT NULL, + CONSTRAINT domain_exists + FOREIGN KEY(domain_id) REFERENCES domains(id) + ON DELETE CASCADE, + CONSTRAINT c_lowercase_name CHECK ( + ((name)::TEXT = LOWER((name)::TEXT))) + ); + + CREATE INDEX comments_domain_id_idx ON comments (domain_id); + CREATE INDEX comments_name_type_idx ON comments (name, type); + CREATE INDEX comments_order_idx ON comments (domain_id, modified_at); + + """ + domain = models.ForeignKey('DNSDomain', on_delete=models.CASCADE) + name = models.CharField(max_length=255) + commenttype = models.CharField(max_length=10, db_column='type') + modified_at = models.IntegerField() + # account is replaced by customer + customer = models.ForeignKey( + settings.AUTH_USER_MODEL, verbose_name=_('customer'), + on_delete=models.CASCADE) + comment = models.CharField(max_length=65535) + # check constraint is added via RunSQL in migration + + class Meta: + verbose_name = _('DNS comment') + verbose_name_plural = _('DNS comments') + index_together = [ + ['name', 'commenttype'], + ['domain', 'modified_at'] + ] + + def __str__(self): + return "{name} IN {type}: {comment}".format( + name=self.name, type=self.commenttype, comment=self.comment) + + +@python_2_unicode_compatible +class DNSDomainMetadata(models.Model): + """ + This model represents the domainmetadata table in the PowerDNS schema + specified in + https://doc.powerdns.com/md/authoritative/backend-generic-mypgsql/. + The domainmetadata table is used to store domain meta data as described in + https://doc.powerdns.com/md/authoritative/domainmetadata/. + + .. code-block:: sql + + CREATE TABLE domainmetadata ( + id SERIAL PRIMARY KEY, + domain_id INT REFERENCES domains(id) ON DELETE CASCADE, + kind VARCHAR(32), + content TEXT + ); + + CREATE INDEX domainidmetaindex ON domainmetadata(domain_id); + + """ + domain = models.ForeignKey('DNSDomain', on_delete=models.CASCADE) + kind = models.CharField(max_length=32, choices=DNS_DOMAIN_METADATA_KINDS) + content = models.TextField() + + class Meta: + verbose_name = _('DNS domain metadata item') + verbose_name_plural = _('DNS domain metadata items') + + def __str__(self): + return "{domain} {kind} {content}".format( + domain=self.domain.domain, kind=self.kind, content=self.content) + + +@python_2_unicode_compatible +class DNSCryptoKey(models.Model): + """ + This model represents the cryptokeys table in the PowerDNS schema + specified in + https://doc.powerdns.com/md/authoritative/backend-generic-mypgsql/. + + .. code-block:: sql + + CREATE TABLE cryptokeys ( + id SERIAL PRIMARY KEY, + domain_id INT REFERENCES domains(id) ON DELETE CASCADE, + flags INT NOT NULL, + active BOOL, + content TEXT + ); + + CREATE INDEX domainidindex ON cryptokeys(domain_id); + + """ + domain = models.ForeignKey('DNSDomain', on_delete=models.CASCADE) + flags = models.IntegerField() + active = models.BooleanField(default=True) + content = models.TextField() + + class Meta: + verbose_name = _('DNS crypto key') + verbose_name_plural = _('DNS crypto keys') + + def __str__(self): + return "{domain} {content}".format( + domain=self.domain.domain, content=self.content) + + +@python_2_unicode_compatible +class DNSTSIGKey(models.Model): + """ + This model represents the tsigkeys table in the PowerDNS schema specified + in https://doc.powerdns.com/md/authoritative/backend-generic-mypgsql/. + + .. code-block:: sql + + CREATE TABLE tsigkeys ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + algorithm VARCHAR(50), + secret VARCHAR(255), + CONSTRAINT c_lowercase_name CHECK ( + ((name)::TEXT = LOWER((name)::TEXT))) + ); + + CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm); + + """ + name = models.CharField(max_length=255) + algorithm = models.CharField( + max_length=50, choices=DNS_TSIG_KEY_ALGORITHMS) + secret = models.CharField(max_length=255) + # check constraint is added via RunSQL in migration + + class Meta: + verbose_name = _('DNS TSIG key') + verbose_name_plural = _('DNS TSIG keys') + unique_together = [ + ['name', 'algorithm'] + ] + + def __str__(self): + return "{name} {algorithm} XXXX".format( + name=self.name, algorithm=self.algorithm) diff --git a/gnuviechadmin/domains/tests/test_admin.py b/gnuviechadmin/domains/tests/test_admin.py index aa717a9..15bfaac 100644 --- a/gnuviechadmin/domains/tests/test_admin.py +++ b/gnuviechadmin/domains/tests/test_admin.py @@ -1,8 +1,8 @@ from django.test import TestCase -from django.core.urlresolvers import reverse +from django.urls import reverse class TestMailDomainAdmin(TestCase): def test_admin_for_maildomain(self): - admin_url = reverse('admin:domains_maildomain_changelist') + admin_url = reverse("admin:domains_maildomain_changelist") self.assertIsNotNone(admin_url) diff --git a/gnuviechadmin/domains/tests/test_forms.py b/gnuviechadmin/domains/tests/test_forms.py new file mode 100644 index 0000000..7bc1f64 --- /dev/null +++ b/gnuviechadmin/domains/tests/test_forms.py @@ -0,0 +1,99 @@ +""" +Tests for :py:mod:`domains.forms`. + +""" +from unittest.mock import MagicMock, Mock, patch + +from django.forms import ValidationError +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import ugettext as _ + +from domains.forms import relative_domain_validator, CreateHostingDomainForm + + +class RelativeDomainValidatorTest(TestCase): + def test_valid_domainname(self): + relative_domain_validator("example.org") + + def test_domain_name_too_long(self): + with self.assertRaisesMessage(ValidationError, _("host name too long")): + relative_domain_validator("e" * 255) + + def test_domain_name_part_too_long(self): + with self.assertRaisesMessage(ValidationError, _("invalid domain name")): + relative_domain_validator("a" * 64 + ".org") + + def test_domain_name_illegal_characters(self): + with self.assertRaisesMessage(ValidationError, _("invalid domain name")): + relative_domain_validator("eXampl3.org") + + def test_domain_name_starts_with_dash(self): + with self.assertRaisesMessage(ValidationError, _("invalid domain name")): + relative_domain_validator("-example.org") + + def test_domain_name_ends_with_dash(self): + with self.assertRaisesMessage(ValidationError, _("invalid domain name")): + relative_domain_validator("example-.org") + + +class CreateHostingDomainFormTest(TestCase): + def test_constructor_needs_hostingpackage(self): + instance = MagicMock() + with self.assertRaises(KeyError): + CreateHostingDomainForm(instance) + + def test_constructor(self): + hostingpackage = Mock(id=42) + instance = MagicMock() + form = CreateHostingDomainForm(instance, hostingpackage=hostingpackage) + self.assertTrue(hasattr(form, "hosting_package")) + self.assertEqual(form.hosting_package, hostingpackage) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse("create_hosting_domain", kwargs={"package": 42}), + ) + self.assertEqual(len(form.helper.layout.fields), 2) + self.assertEqual(form.helper.layout.fields[1].name, "submit") + + def test_domain_field_has_relative_domain_validator(self): + hostingpackage = Mock(id=42) + instance = MagicMock() + form = CreateHostingDomainForm(instance, hostingpackage=hostingpackage) + self.assertIn(relative_domain_validator, form.fields["domain"].validators) + + def test_clean(self): + hostingpackage = Mock(id=42) + instance = MagicMock() + form = CreateHostingDomainForm( + instance, hostingpackage=hostingpackage, data={"domain": "example.org"} + ) + self.assertTrue(form.is_valid()) + self.assertIn("hosting_package", form.cleaned_data) + self.assertEqual(hostingpackage, form.cleaned_data["hosting_package"]) + + def test_save(self): + hostingpackage = Mock(id=42) + instance = MagicMock() + form = CreateHostingDomainForm( + instance, hostingpackage=hostingpackage, data={"domain": "example.org"} + ) + self.assertTrue(form.is_valid()) + with patch("domains.forms.HostingDomain") as domain: + form.save() + domain.objects.create_for_hosting_package.assert_called_with( + commit=True, **form.cleaned_data + ) + form.save(commit=False) + domain.objects.create_for_hosting_package.assert_called_with( + commit=False, **form.cleaned_data + ) + + def test_save_m2m(self): + hostingpackage = Mock(id=42) + instance = MagicMock() + form = CreateHostingDomainForm( + instance, hostingpackage=hostingpackage, data={"domain": "example.org"} + ) + form.save_m2m() diff --git a/gnuviechadmin/domains/tests/test_models.py b/gnuviechadmin/domains/tests/test_models.py index 233844b..fd6d645 100644 --- a/gnuviechadmin/domains/tests/test_models.py +++ b/gnuviechadmin/domains/tests/test_models.py @@ -1,9 +1,125 @@ -from django.test import TestCase +""" +Tests for :py:mod:`domains.models`. -from domains.models import MailDomain +""" +from unittest.mock import patch + +from django.test import TestCase +from django.contrib.auth import get_user_model + +from domains.models import ( + DNSComment, + DNSCryptoKey, + DNSDomain, + DNSDomainMetadata, + DNSRecord, + DNSSupermaster, + DNSTSIGKey, + HostingDomain, + MailDomain, +) +from hostingpackages.models import CustomerHostingPackage, HostingPackageTemplate + + +User = get_user_model() + +TEST_USER = "test" class MailDomainTest(TestCase): - def test__str__(self): - md = MailDomain.objects.create(domain='example.org') - self.assertEqual(str(md), 'example.org') + def test___str__(self): + md = MailDomain.objects.create(domain="example.org") + self.assertEqual(str(md), "example.org") + + def test_get_mailaddresses(self): + md = MailDomain.objects.create(domain="example.org") + from managemails.models import MailAddress + + addrmock = MailAddress.objects.create(localpart="info", domain=md) + self.assertIn(addrmock, md.get_mailaddresses()) + self.assertIn(addrmock, md.mailaddresses) + + +class HostingDomainManagerTest(TestCase): + def _setup_hosting_package(self): + template = HostingPackageTemplate.objects.create( + name="testpackagetemplate", mailboxcount=0, diskspace=1, diskspace_unit=0 + ) + customer = User.objects.create_user(username=TEST_USER) + package = CustomerHostingPackage.objects.create_from_template( + customer, template, "testpackage" + ) + with patch("hostingpackages.models.settings") as hmsettings: + hmsettings.OSUSER_DEFAULT_GROUPS = [] + package.save() + return package + + def test_create_for_hosting_package_with_commit(self): + package = self._setup_hosting_package() + hostingdomain = HostingDomain.objects.create_for_hosting_package( + package, "example.org", True + ) + + self.assertIsNotNone(hostingdomain) + self.assertTrue(hostingdomain.customer, package.customer) + + def test_create_for_hosting_package_no_commit(self): + package = self._setup_hosting_package() + hostingdomain = HostingDomain.objects.create_for_hosting_package( + package, "example.org", False + ) + + self.assertIsNotNone(hostingdomain) + self.assertTrue(hostingdomain.customer, package.customer) + + +class HostingDomainTest(TestCase): + def test___str__(self): + hostingdomain = HostingDomain(domain="test") + self.assertEqual(str(hostingdomain), "test") + + +class DNSDomainTest(TestCase): + def test___str__(self): + dnsdomain = DNSDomain(domain="test") + self.assertEqual(str(dnsdomain), "test") + + +class DNSRecordTest(TestCase): + def test___str__(self): + dnsrecord = DNSRecord(name="localhost", recordtype="A", content="127.0.0.1") + self.assertEqual(str(dnsrecord), "localhost IN A 127.0.0.1") + + +class DNSSupermasterTest(TestCase): + def test___str__(self): + dnssupermaster = DNSSupermaster(ip="127.0.0.1", nameserver="dns.example.org") + self.assertEqual(str(dnssupermaster), "127.0.0.1 dns.example.org") + + +class DNSCommentTest(TestCase): + def test___str__(self): + dnscomment = DNSComment(name="localhost", commenttype="A", comment="good stuff") + self.assertEqual(str(dnscomment), "localhost IN A: good stuff") + + +class DNSDomainMetadataTest(TestCase): + def test___str__(self): + dnsdomain = DNSDomain(domain="test") + dnsdomainmetadata = DNSDomainMetadata( + domain=dnsdomain, kind="SOA-EDIT", content="INCEPTION" + ) + self.assertEqual(str(dnsdomainmetadata), "test SOA-EDIT INCEPTION") + + +class DNSCryptoKeyTest(TestCase): + def test___str__(self): + dnsdomain = DNSDomain(domain="test") + dnscryptokey = DNSCryptoKey(domain=dnsdomain, content="testvalue") + self.assertEqual(str(dnscryptokey), "test testvalue") + + +class DNSTSIGKeyTest(TestCase): + def test___str__(self): + dnstsigkey = DNSTSIGKey(name="testkey", algorithm="hmac-md5", secret="dummykey") + self.assertEqual(str(dnstsigkey), "testkey hmac-md5 XXXX") diff --git a/gnuviechadmin/domains/tests/test_views.py b/gnuviechadmin/domains/tests/test_views.py new file mode 100644 index 0000000..cc4e099 --- /dev/null +++ b/gnuviechadmin/domains/tests/test_views.py @@ -0,0 +1,153 @@ +""" +Tests for :py:mod:`domains.views`. + +""" +from unittest.mock import MagicMock, patch + +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse + +from domains.views import CreateHostingDomain +from hostingpackages.models import CustomerHostingPackage, HostingPackageTemplate + +User = get_user_model() + +TEST_USER = "test" +TEST_PASSWORD = "secret" +TEST_EMAIL = "test@example.org" +TEST_NAME = "Example Tester".split() + + +class CreateHostingDomainTest(TestCase): + def _setup_hosting_package(self, customer): + template = HostingPackageTemplate.objects.create( + name="testpackagetemplate", mailboxcount=0, diskspace=1, diskspace_unit=0 + ) + package = CustomerHostingPackage.objects.create_from_template( + customer, template, "testpackage" + ) + with patch("hostingpackages.models.settings") as hmsettings: + hmsettings.OSUSER_DEFAULT_GROUPS = [] + package.save() + return package + + def test_get_anonymous(self): + response = self.client.get( + reverse("create_hosting_domain", kwargs={"package": 1}) + ) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_hosting_domain", kwargs={"package": package.id}) + ) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + customer = User.objects.create_user("customer") + package = self._setup_hosting_package(customer) + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_hosting_domain", kwargs={"package": package.id}) + ) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + customer = User.objects.create_user("customer") + package = self._setup_hosting_package(customer) + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_hosting_domain", kwargs={"package": package.id}) + ) + self.assertTemplateUsed(response, "domains/hostingdomain_create.html") + + def test_get_no_package_found(self): + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_hosting_domain", kwargs={"package": 1}) + ) + self.assertEqual(response.status_code, 404) + + def test_get_get_form_kwargs(self): + customer = User.objects.create_user("customer") + package = self._setup_hosting_package(customer) + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = CreateHostingDomain( + request=MagicMock(), kwargs={"package": str(package.id)} + ) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], package) + + def test_get_context_data_has_hosting_package(self): + customer = User.objects.create_user("customer") + package = self._setup_hosting_package(customer) + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_hosting_domain", kwargs={"package": package.id}) + ) + self.assertIn("hostingpackage", response.context) + self.assertEqual(response.context["hostingpackage"], package) + + def test_get_context_data_has_customer(self): + customer = User.objects.create_user("customer") + package = self._setup_hosting_package(customer) + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_hosting_domain", kwargs={"package": package.id}) + ) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], customer) + + def test_form_valid_redirect(self): + customer = User.objects.create_user("customer") + package = self._setup_hosting_package(customer) + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse("create_hosting_domain", kwargs={"package": package.id}), + data={"domain": "example.org"}, + ) + self.assertRedirects(response, package.get_absolute_url()) + + def test_form_valid_message(self): + customer = User.objects.create_user("customer") + package = self._setup_hosting_package(customer) + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse("create_hosting_domain", kwargs={"package": package.id}), + follow=True, + data={"domain": "example.org"}, + ) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual("Successfully created domain example.org", str(messages[0])) diff --git a/gnuviechadmin/domains/urls.py b/gnuviechadmin/domains/urls.py index d762b84..2d63fe4 100644 --- a/gnuviechadmin/domains/urls.py +++ b/gnuviechadmin/domains/urls.py @@ -4,13 +4,12 @@ This module defines the URL patterns for domain related views. """ from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import CreateHostingDomain -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^(?P\d+)/create$', CreateHostingDomain.as_view(), name='create_hosting_domain'), -) +] diff --git a/gnuviechadmin/domains/views.py b/gnuviechadmin/domains/views.py index 2d16751..41ca975 100644 --- a/gnuviechadmin/domains/views.py +++ b/gnuviechadmin/domains/views.py @@ -4,58 +4,50 @@ This module defines views related to domains. """ from __future__ import absolute_import, unicode_literals -from django.shortcuts import redirect, get_object_or_404 -from django.views.generic.edit import CreateView -from django.utils.translation import ugettext as _ +from braces.views import StaffuserRequiredMixin from django.contrib import messages - -from braces.views import ( - LoginRequiredMixin, - StaffuserRequiredMixin, -) +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import ugettext as _ +from django.views.generic.edit import CreateView from hostingpackages.models import CustomerHostingPackage - from .forms import CreateHostingDomainForm from .models import HostingDomain -class CreateHostingDomain( - LoginRequiredMixin, StaffuserRequiredMixin, CreateView -): +class CreateHostingDomain(StaffuserRequiredMixin, CreateView): """ This view is used for creating a new HostingDomain instance for an existing hosting package. - """ + model = HostingDomain raise_exception = True - template_name_suffix = '_create' + template_name_suffix = "_create" form_class = CreateHostingDomainForm def _get_hosting_package(self): - return get_object_or_404( - CustomerHostingPackage, pk=int(self.kwargs['package'])) + return get_object_or_404(CustomerHostingPackage, pk=int(self.kwargs["package"])) def get_form_kwargs(self): kwargs = super(CreateHostingDomain, self).get_form_kwargs() - kwargs['hostingpackage'] = self._get_hosting_package() + kwargs["hostingpackage"] = self._get_hosting_package() return kwargs def get_context_data(self, **kwargs): context = super(CreateHostingDomain, self).get_context_data(**kwargs) hosting_package = self._get_hosting_package() - context.update({ - 'hostingpackage': hosting_package, - 'customer': hosting_package.customer, - }) + context.update( + {"hostingpackage": hosting_package, "customer": hosting_package.customer} + ) return context def form_valid(self, form): hostingdomain = form.save() messages.success( self.request, - _('Successfully created domain {domainname}').format( - domainname=hostingdomain.domain) + _("Successfully created domain {domainname}").format( + domainname=hostingdomain.domain + ), ) return redirect(self._get_hosting_package()) diff --git a/gnuviechadmin/fileservertasks/tasks.py b/gnuviechadmin/fileservertasks/tasks.py index c04d395..2de6d84 100644 --- a/gnuviechadmin/fileservertasks/tasks.py +++ b/gnuviechadmin/fileservertasks/tasks.py @@ -9,127 +9,388 @@ from celery import shared_task @shared_task -def setup_file_sftp_userdir(username): +def setup_file_sftp_userdir(username, *args, **kwargs): """ This task creates the home directory for an SFTP user if it does not exist yet. - :param str username: the user name + :param str username: the username :raises Exception: if the SFTP directory of the user cannot be created - :return: the created directory name - :rtype: str + :return: a dictionary with the key :py:const:`username` set to the + username value and a new key :py:const:`sftp_directory` set to the + path of the created SFTP directory + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use :py:func:`fileservertasks.tasks.setup_file_sftp_userdir_chained` + at other positions in the task chain. """ @shared_task -def delete_file_sftp_userdir(username): +def setup_file_sftp_userdir_chained(previous_result, *args, **kwargs): + """ + This task creates the home directory for an SFTP user if it does not exist + yet. + + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` key + :raises Exception: if the SFTP directory of the user cannot be created + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`sftp_directory` key set to the path of the created SFTP + directory + :rtype: dict + + """ + + +@shared_task +def delete_file_sftp_userdir(username, *args, **kwargs): """ This task recursively deletes the home directory of an SFTP user if it - does not exist yet. + exists. - :param str username: the user name + :param str username: the username :raises Exception: if the SFTP directory of the user cannot be removed - :return: the removed directory name - :rtype: str + :return: a dictionary with the key :py:const:`username` set to the username + value and the new key :py:const:`sftp_directory` set to the path of the + deleted SFTP directory + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use :py:func:`fileservertasks.tasks.delete_file_sftp_userdir_chained` + at other positions in the task chain. """ @shared_task -def setup_file_mail_userdir(username): +def delete_file_sftp_userdir_chained(previous_result, *args, **kwargs): + """ + This task recursively deletes the home directory of an SFTP user if it + exists. + + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` key + :raises Exception: if the SFTP directory of the user cannot be removed + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`sftp_directory` key set to the path of the removed SFTP + directory + :rtype: dict + + """ + + +@shared_task +def setup_file_mail_userdir(username, *args, **kwargs): """ This task creates the mail base directory for a user if it does not exist yet. - :param str username: the user name + :param str username: the username :raises Exception: if the mail base directory for the user cannot be created - :return: the created directory name - :rtype: str + :return: a dictionary with the key :py:const:`username` set to the + username value and a new key :py:const:`mail_directory` set to the path + of the created mail directory + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use :py:func:`fileservertasks.tasks.setup_file_mail_userdir_chained` + at other positions in the task chain. """ @shared_task -def delete_file_mail_userdir(username): +def setup_file_mail_userdir_chained(previous_result, *args, **kwargs): + """ + This task creates the mail base directory for a user if it does not exist + yet. + + :param dict previous_result: a dictionary containing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` key + :raises Exception: if the mail base directory for the user cannot be + created + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`mail_directory` key set to the path of the created mail + directory + :rtype: dict + + """ + + +@shared_task +def delete_file_mail_userdir(username, *args, **kwargs): """ This task recursively deletes the mail base directory for a user if it does not exist yet. - :param str username: the user name - :raises Exception: if the mail base directory of the user cannot be removed - :return: the removed directory name + :param str username: the username + :raises Exception: if the mail base directory of the user cannot be deleted + :return: a dictionary with the key :py:const:`username` set to the + username value and a new key :py:const:`mail_directory` set to the path + of the deleted mail directory + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use :py:func:`fileservertasks.tasks.delete_file_mail_userdir_chained` + at other positions in the task chain. + + """ + + +@shared_task +def delete_file_mail_userdir_chained(previous_result, *args, **kwargs): + """ + This task recursively deletes the mail base directory for a user if it + does not exist yet. + + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` key + :raises Exception: if the mail base directory of the user cannot be deleted + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`mail_directory` key set to the path of the deleted mail + directory :rtype: str """ @shared_task -def create_file_mailbox(username, mailboxname): +def create_file_mailbox(username, mailboxname, *args, **kwargs): """ This task creates a new mailbox directory for the given user and mailbox name. - :param str username: the user name + :param str username: the username :param str mailboxname: the mailbox name :raises Exception: if the mailbox directory cannot be created - :return: the created mailbox directory name - :rtype: str + :return: a dictionary with the keys :py:const:`username` and + :py:const:`mailboxname` set to the values of username and mailboxname + and a new key :py:const:`mailbox_directory` set to the path of the + created mailbox directory + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use :py:func:`fileservertasks.tasks.create_file_mailbox_chained` at + other positions in the task chain. """ @shared_task -def delete_file_mailbox(username, mailboxname): +def create_file_mailbox_chained(previous_result, *args, **kwargs): + """ + This task creates a new mailbox directory for the given user and mailbox + name. + + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` and a :py:const:`mailboxname` key + :raises Exception: if the mailbox directory cannot be created + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`mailbox_directory` key set to the path of the created + mailbox directory + :rtype: dict + + """ + + +@shared_task +def delete_file_mailbox(username, mailboxname, *args, **kwargs): """ This task deletes the given mailbox of the given user. - :param str username: the user name + :param str username: the username :param str mailboxname: the mailbox name :raises Exception: if the mailbox directory cannot be deleted - :return: the deleted mailbox directory name - :rtype: str + :return: a dictionary with the keys :py:const:`username` and + :py:const:`mailboxname` set to the values of username and mailboxname + and a new key :py:const:`mailbox_directory` set to the path of the + deleted mailbox directory + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use :py:func:`fileservertasks.tasks.delete_file_mailbox_chained` for + other positions in the task chain. """ @shared_task -def create_file_website_hierarchy(username, sitename): +def delete_file_mailbox_chained(previous_result, *args, **kwargs): + """ + This task deletes the given mailbox of the given user. + + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` and a :py:const:`mailboxname` key + :raises Exception: if the mailbox directory cannot be deleted + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`mailbox_directory` key set to the path of the deleted + mailbox directory + :rtype: dict + + """ + + +@shared_task +def create_file_website_hierarchy(username, sitename, *args, **kwargs): """ This task creates the directory hierarchy for a website. - :param str username: the user name - :param str sitename: name of the website - :return: the directory name - :rtype: str + :param str username: the username + :param str sitename: the sitename + :raises Exception: if the website directory hierarchy directory cannot be + created + :return: a dictionary with the keys :py:const:`username` and + :py:const:`sitename` set to the values of username and sitename and a + new key :py:const:`website_directory` set to the path of the created + website directory + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use + :py:func:`fileservertasks.tasks.create_file_website_hierarchy_chained` + at other positions in the task chain """ @shared_task -def delete_file_website_hierarchy(username, sitename): +def create_file_website_hierarchy_chained(previous_result, *args, **kwargs): + """ + This task creates the directory hierarchy for a website. + + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` and a :py:const:`sitename` key + :raises Exception: if the website directory hierarchy directory cannot be + created + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`website_directory` key set to the path of the created + website directory + :rtype: dict + + """ + + +@shared_task +def delete_file_website_hierarchy(username, sitename, *args, **kwargs): + """ + This task deletes a website hierarchy recursively. + + :param str username: a username + :param str sitename: a site name + :return: a dictionary with the keys :py:const:`username` and + :py:const:`sitename` set to their original values and a new key + :py:const:`website_directory` set to the path of the deleted website + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use + :py:func:`fileservertasks.tasks.delete_file_website_hierarchy_chained` + at other positions in the task chain + + """ + + +@shared_task +def delete_file_website_hierarchy_chained(previous_result, *args, **kwargs): """ This task deletes the website hierarchy recursively. - :param str username: the user name - :param str sitename: name of the website - :return: the directory name - :rtype: str + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` and a :py:const:`sitename` key + :raises Exception: if the website directory hierarchy directory cannot be + deleted + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`website_directory` set to the path of the deleted website + directory + :rtype: dict """ @shared_task -def set_file_ssh_authorized_keys(username, ssh_keys): +def set_file_ssh_authorized_keys(username, ssh_keys, *args, **kwargs): + """ + This task set the authorized keys for ssh logins. + + :param str username: a username + :param list ssh_keys: a list of ssh keys + :raises Exception: if the update of the creation or update of ssh + authorized_keys failed + :return: a dictionary with the keys :py:const:`username` and + :py:const:`ssh_keys` set to their original values and a new key + :py:const:`ssh_authorized_keys` set to the path of the SSH + authorized_keys file + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use + :py:func:`fileservertasks.tasks.set_file_ssh_authorized_keys_chained` + at other positions in the task chain + + """ + + +@shared_task +def set_file_ssh_authorized_keys_chained(previous_result, *args, **kwargs): """ This task sets the authorized keys for ssh logins. - :param str username: the user name - :param list ssh_key: an ssh_key + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` and a :py:const:`ssh_keys` key :raises Exception: if the update of the creation or update of ssh authorized_keys failed - :return: the name of the authorized_keys file - :rtype: str + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`ssh_authorized_keys` set to the path of the SSH + authorized_keys file + :rtype: dict """ diff --git a/gnuviechadmin/gnuviechadmin/__init__.py b/gnuviechadmin/gnuviechadmin/__init__.py index 2e822b8..4f7caa1 100644 --- a/gnuviechadmin/gnuviechadmin/__init__.py +++ b/gnuviechadmin/gnuviechadmin/__init__.py @@ -1,3 +1,4 @@ -from gnuviechadmin.celery import app as celery_app +# import celery_app to initialize it +from gnuviechadmin.celery import app as celery_app # NOQA -__version__ = '0.11.3' +__version__ = '0.12.0' diff --git a/gnuviechadmin/gnuviechadmin/celery.py b/gnuviechadmin/gnuviechadmin/celery.py index b8be2e5..b9a95d2 100644 --- a/gnuviechadmin/gnuviechadmin/celery.py +++ b/gnuviechadmin/gnuviechadmin/celery.py @@ -12,5 +12,9 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', app = Celery('gnuviechadmin') + +def get_installed_apps(): + return settings.INSTALLED_APPS + app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) +app.autodiscover_tasks(get_installed_apps) diff --git a/gnuviechadmin/gnuviechadmin/context_processors.py b/gnuviechadmin/gnuviechadmin/context_processors.py index 3e71aa2..09f1226 100644 --- a/gnuviechadmin/gnuviechadmin/context_processors.py +++ b/gnuviechadmin/gnuviechadmin/context_processors.py @@ -13,7 +13,6 @@ from gnuviechadmin import __version__ as gvaversion _LOGGER = logging.getLogger(__name__) - def navigation(request): """ Add navigation items to the request context. @@ -50,7 +49,7 @@ def navigation(request): request.path.endswith('/impressum/') ): context['active_item'] = 'imprint' - else: + elif not viewmodule.startswith('django.contrib.admin'): _LOGGER.debug( 'no special handling for view %s in module %s, fallback to ' 'default active menu item %s', diff --git a/gnuviechadmin/gnuviechadmin/settings.py b/gnuviechadmin/gnuviechadmin/settings.py new file mode 100644 index 0000000..0d9ee46 --- /dev/null +++ b/gnuviechadmin/gnuviechadmin/settings.py @@ -0,0 +1,496 @@ +# -*- python -*- +# pymode:lint_ignore=E501 +""" +Common settings and globals. + +""" + +from os.path import abspath, basename, dirname, join, normpath + +from django.contrib.messages import constants as messages + +from gvacommon.settings_utils import get_env_variable + + +# ######### PATH CONFIGURATION +# Absolute filesystem path to the Django project directory: +DJANGO_ROOT = dirname(dirname(abspath(__file__))) + +# Absolute filesystem path to the top-level project folder: +SITE_ROOT = dirname(DJANGO_ROOT) + +# Site name: +SITE_NAME = basename(DJANGO_ROOT) + +# ######### END PATH CONFIGURATION + + +# ######### DEBUG CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = False +# ######### END DEBUG CONFIGURATION + + +# ######### MANAGER CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins +ADMINS = ( + ( + get_env_variable("GVA_ADMIN_NAME", default="Admin"), + get_env_variable("GVA_ADMIN_EMAIL", default="admin@example.org"), + ), +) + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#managers +MANAGERS = ADMINS +# ######### END MANAGER CONFIGURATION + + +# ######### DATABASE CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": get_env_variable("GVA_PGSQL_DATABASE", default="gnuviechadmin"), + "USER": get_env_variable("GVA_PGSQL_USER", default="gnuviechadmin"), + "PASSWORD": get_env_variable("GVA_PGSQL_PASSWORD"), + "HOST": get_env_variable("GVA_PGSQL_HOSTNAME", default="db"), + "PORT": get_env_variable("GVA_PGSQL_PORT", int, default=5432), + } +} +# ######### END DATABASE CONFIGURATION + + +# ######### GENERAL CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#time-zone +TIME_ZONE = "Europe/Berlin" + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code +LANGUAGE_CODE = "en-us" + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id +SITE_ID = 1 +SITES_DOMAIN_NAME = get_env_variable("GVA_DOMAIN_NAME") +SITES_SITE_NAME = get_env_variable("GVA_SITE_NAME") + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n +USE_I18N = True + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n +USE_L10N = True + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz +USE_TZ = True +# ######### END GENERAL CONFIGURATION + + +LOCALE_PATHS = (normpath(join(SITE_ROOT, "gnuviechadmin", "locale")),) + + +# ######### MEDIA CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root +MEDIA_ROOT = normpath(join(SITE_ROOT, "media")) + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url +MEDIA_URL = "/media/" +# ######### END MEDIA CONFIGURATION + + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url +STATIC_URL = "/static/" + +# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS # noqa +STATICFILES_DIRS = (normpath(join(SITE_ROOT, "gnuviechadmin", "static")),) + +# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders # noqa +STATICFILES_FINDERS = ( + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +) +# ######### END STATIC FILE CONFIGURATION + + +# ######### SECRET CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key +# Note: This key should only be used for development and testing. +SECRET_KEY = get_env_variable("GVA_SITE_SECRET") +# ######### END SECRET CONFIGURATION + + +# ######### SITE CONFIGURATION +# Hosts/domain names that are valid for this site +# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] +# ######### END SITE CONFIGURATION + + +# ######### FIXTURE CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS # noqa +FIXTURE_DIRS = (normpath(join(SITE_ROOT, "fixtures")),) +# ######### END FIXTURE CONFIGURATION + + +# ######### TEMPLATE CONFIGURATION +# See: https://docs.djangoproject.com/en/1.9/ref/settings/#std:setting-TEMPLATES # noqa +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [normpath(join(DJANGO_ROOT, "templates"))], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.request", + # custom context processors + "gnuviechadmin.context_processors.navigation", + "gnuviechadmin.context_processors.version_info", + ] + }, + } +] +# ######### END TEMPLATE CONFIGURATION + + +# ######### MIDDLEWARE CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#middleware-classes +MIDDLEWARE = [ + # Default Django middleware. + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + # uncomment next line to enable translation to browser locale + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] +# ######### END MIDDLEWARE CONFIGURATION + + +AUTHENTICATION_BACKENDS = ( + # Needed to login by username in Django admin, regardless of `allauth` + "django.contrib.auth.backends.ModelBackend", + # `allauth` specific authentication methods, such as login by e-mail + "allauth.account.auth_backends.AuthenticationBackend", +) + + +# ######### URL CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf +ROOT_URLCONF = "%s.urls" % SITE_NAME +# ######### END URL CONFIGURATION + + +# ######### TEST RUNNER CONFIGURATION +TEST_RUNNER = "django.test.runner.DiscoverRunner" +# ######### END TEST RUNNER CONFIGURATION + + +# ######### APP CONFIGURATION +DJANGO_APPS = ( + # Default Django apps: + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + # Useful template tags: + "django.contrib.humanize", + # Admin panel and documentation: + "django.contrib.admin", + # Flatpages for about page + "django.contrib.flatpages", + "crispy_forms", +) + +ALLAUTH_APPS = ( + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.google", + "allauth.socialaccount.providers.linkedin_oauth2", + "allauth.socialaccount.providers.twitter", +) + +# Apps specific for this project go here. +LOCAL_APPS = ( + "dashboard", + "taskresults", + "ldaptasks", + "mysqltasks", + "pgsqltasks", + "fileservertasks", + "webtasks", + "domains", + "osusers", + "managemails", + "userdbs", + "hostingpackages", + "websites", + "contact_form", +) + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps +INSTALLED_APPS = DJANGO_APPS + ALLAUTH_APPS + LOCAL_APPS + +MESSAGE_TAGS = { + messages.DEBUG: "", + messages.ERROR: "alert-danger", + messages.INFO: "alert-info", + messages.SUCCESS: "alert-success", + messages.WARNING: "alert-warning", +} +# ######### END APP CONFIGURATION + + +# ######### ALLAUTH CONFIGURATION +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = "mandatory" +LOGIN_REDIRECT_URL = "/" +SOCIALACCOUNT_QUERY_EMAIL = True +# ######### END ALLAUTH CONFIGURATION + + +# ######### CRISPY FORMS CONFIGURATION +CRISPY_TEMPLATE_PACK = "bootstrap3" +# ######### END CRISPY_FORMS CONFIGURATION + + +# ######### LOGGING CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "%(levelname)s %(asctime)s %(name)s " + "%(module)s:%(lineno)d %(process)d %(thread)d %(message)s" + }, + "simple": {"format": "%(levelname)s %(name)s:%(lineno)d %(message)s"}, + }, + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "handlers": { + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + "class": "django.utils.log.AdminEmailHandler", + } + }, + "loggers": { + "django.request": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": True, + } + }, +} +# ######### END LOGGING CONFIGURATION + + +# ######### WSGI CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application +WSGI_APPLICATION = "%s.wsgi.application" % SITE_NAME +# ######### END WSGI CONFIGURATION + + +# ######### CELERY CONFIGURATION +BROKER_URL = get_env_variable( + "GVA_BROKER_URL", default="amqp://gnuviechadmin:gnuviechadmin@mq/gnuviechadmin" +) +BROKER_TRANSPORT_OPTIONS = { + "max_retries": 3, + "interval_start": 0, + "interval_step": 0.2, + "interval_max": 0.2, +} +CELERY_RESULT_BACKEND = get_env_variable( + "GVA_RESULTS_REDIS_URL", default="redis://:gnuviechadmin@redis:6379/0" +) +CELERY_TASK_RESULT_EXPIRES = None +CELERY_ROUTES = ("gvacommon.celeryrouters.GvaRouter",) +CELERY_TIMEZONE = "Europe/Berlin" +CELERY_ENABLE_UTC = True +CELERY_ACCEPT_CONTENT = ["json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" +# ######### END CELERY CONFIGURATION + + +# ######### CUSTOM APP CONFIGURATION +OSUSER_MINUID = get_env_variable("GVA_MIN_OS_UID", int, default=10000) +OSUSER_MINGID = get_env_variable("GVA_MIN_OS_GID", int, default=10000) +OSUSER_USERNAME_PREFIX = get_env_variable("GVA_OSUSER_PREFIX", default="usr") +OSUSER_HOME_BASEPATH = get_env_variable("GVA_OSUSER_HOME_BASEPATH", default="/home") +OSUSER_DEFAULT_SHELL = get_env_variable( + "GVA_OSUSER_DEFAULT_SHELL", default="/usr/bin/rssh" +) +OSUSER_SFTP_GROUP = "sftponly" +OSUSER_SSH_GROUP = "sshusers" +OSUSER_DEFAULT_GROUPS = [OSUSER_SFTP_GROUP] +OSUSER_UPLOAD_SERVER = get_env_variable("GVA_OSUSER_UPLOADSERVER", default="file") + +GVA_LINK_WEBMAIL = get_env_variable( + "GVA_WEBMAIL_URL", default="https://webmail.example.org/" +) +GVA_LINK_PHPMYADMIN = get_env_variable( + "GVA_PHPMYADMIN_URL", default="https://phpmyadmin.example.org/" +) +GVA_LINK_PHPPGADMIN = get_env_variable( + "GVA_PHPPGADMIN_URL", default="https://phppgadmin.example.org/" +) +# ######### END CUSTOM APP CONFIGURATION + +GVA_ENVIRONMENT = get_env_variable("GVA_ENVIRONMENT", default="prod") + +# ######### STATIC FILE CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root +STATIC_ROOT = "/srv/gva/static/" + + +def show_debug_toolbar(request): + return DEBUG and GVA_ENVIRONMENT == "local" + + +if GVA_ENVIRONMENT == "local": + # ######### DEBUG CONFIGURATION + # See: https://docs.djangoproject.com/en/dev/ref/settings/#debug + DEBUG = True + + # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug + TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG + # ######### END DEBUG CONFIGURATION + + # ######### EMAIL CONFIGURATION + # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + # ######### END EMAIL CONFIGURATION + + # ######### CACHE CONFIGURATION + # See: https://docs.djangoproject.com/en/dev/ref/settings/#caches + CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} + # ######### END CACHE CONFIGURATION + + # ######### TOOLBAR CONFIGURATION + # See: http://django-debug-toolbar.readthedocs.org/en/latest/installation.html#explicit-setup # noqa + INSTALLED_APPS += ("debug_toolbar",) + + MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] + + LOGGING["handlers"].update( + { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "simple", + } + } + ) + LOGGING["loggers"].update( + dict( + [ + (key, {"handlers": ["console"], "level": "DEBUG", "propagate": True}) + for key in [ + "dashboard", + "domains", + "fileservertasks", + "gvacommon", + "gvawebcore", + "hostingpackages", + "ldaptasks", + "managemails", + "mysqltasks", + "osusers", + "pgsqltasks", + "taskresults", + "userdbs", + "websites", + ] + ] + ) + ) + + DEBUG_TOOLBAR_PATCH_SETTINGS = False + DEBUG_TOOLBAR_CONFIG = { + "SHOW_TOOLBAR_CALLBACK": "gnuviechadmin.settings.show_debug_toolbar" + } + + # ######### END TOOLBAR CONFIGURATION +elif GVA_ENVIRONMENT == "test": + ALLOWED_HOSTS = ["localhost"] + PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) + LOGGING["handlers"].update( + { + "console": { + "level": "ERROR", + "class": "logging.StreamHandler", + "formatter": "simple", + } + } + ) + LOGGING["loggers"].update( + dict( + [ + (key, {"handlers": ["console"], "level": "ERROR", "propagate": True}) + for key in [ + "dashboard", + "domains", + "fileservertasks", + "gvacommon", + "gvawebcore", + "hostingpackages", + "ldaptasks", + "managemails", + "mysqltasks", + "osusers", + "pgsqltasks", + "taskresults", + "userdbs", + "websites", + ] + ] + ) + ) + BROKER_URL = BROKER_URL + "_test" + CELERY_RESULT_PERSISTENT = False +else: + # ######### HOST CONFIGURATION + # See: https://docs.djangoproject.com/en/1.5/releases/1.5/#allowed-hosts-required-in-production # noqa + ALLOWED_HOSTS = [SITES_DOMAIN_NAME] + # ######### END HOST CONFIGURATION + + # ######### EMAIL CONFIGURATION + # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend + EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" + + # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix + EMAIL_SUBJECT_PREFIX = "[%s] " % SITE_NAME + + # See: https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email + DEFAULT_FROM_EMAIL = get_env_variable( + "GVA_SITE_ADMINMAIL", default="admin@example.org" + ) + + # See: https://docs.djangoproject.com/en/dev/ref/settings/#server-email + SERVER_EMAIL = get_env_variable("GVA_SITE_ADMINMAIL", default="admin@example.org") + # ######### END EMAIL CONFIGURATION + + # ######### CACHE CONFIGURATION + # See: https://docs.djangoproject.com/en/dev/ref/settings/#caches + # CACHES = {} + # ######### END CACHE CONFIGURATION + + # ######### ALLAUTH PRODUCTION CONFIGURATION + ACCOUNT_EMAIL_SUBJECT_PREFIX = "[Jan Dittberner IT-Consulting & -Solutions] " + ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" + # ######### END ALLAUTH PRODUCTION CONFIGURATION diff --git a/gnuviechadmin/gnuviechadmin/settings/__init__.py b/gnuviechadmin/gnuviechadmin/settings/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/gnuviechadmin/gnuviechadmin/settings/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py deleted file mode 100644 index 66f3e7f..0000000 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ /dev/null @@ -1,380 +0,0 @@ -# -*- python -*- -# pymode:lint_ignore=E501 -"""Common settings and globals.""" - - -from os import environ -from os.path import abspath, basename, dirname, join, normpath -from sys import path - -# Normally you should not import ANYTHING from Django directly -# into your settings, but ImproperlyConfigured is an exception. -from django.core.exceptions import ImproperlyConfigured -from django.contrib.messages import constants as messages - - -def get_env_variable(var_name): - """ - Get a setting from an environment variable. - - :param str var_name: variable name - - """ - try: - return environ[var_name] - except KeyError: - error_msg = "Set the %s environment variable" % var_name - raise ImproperlyConfigured(error_msg) - - -########## PATH CONFIGURATION -# Absolute filesystem path to the Django project directory: -DJANGO_ROOT = dirname(dirname(abspath(__file__))) - -# Absolute filesystem path to the top-level project folder: -SITE_ROOT = dirname(DJANGO_ROOT) - -# Site name: -SITE_NAME = basename(DJANGO_ROOT) - -# Add our project to our pythonpath, this way we don't need to type our project -# name in our dotted import paths: -path.append(DJANGO_ROOT) -########## END PATH CONFIGURATION - - -########## DEBUG CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = False - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug -TEMPLATE_DEBUG = DEBUG -########## END DEBUG CONFIGURATION - - -########## MANAGER CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins -ADMINS = ( - (get_env_variable('GVA_ADMIN_NAME'), get_env_variable('GVA_ADMIN_EMAIL')), -) - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#managers -MANAGERS = ADMINS -########## END MANAGER CONFIGURATION - - -########## DATABASE CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': get_env_variable('GVA_PGSQL_DATABASE'), - 'USER': get_env_variable('GVA_PGSQL_USER'), - 'PASSWORD': get_env_variable('GVA_PGSQL_PASSWORD'), - 'HOST': get_env_variable('GVA_PGSQL_HOSTNAME'), - 'PORT': get_env_variable('GVA_PGSQL_PORT'), - } -} -########## END DATABASE CONFIGURATION - - -########## GENERAL CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#time-zone -TIME_ZONE = 'Europe/Berlin' - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code -LANGUAGE_CODE = 'en-us' - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id -SITE_ID = 1 -SITES_DOMAIN_NAME = get_env_variable('GVA_DOMAIN_NAME') -SITES_SITE_NAME = get_env_variable('GVA_SITE_NAME') - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n -USE_I18N = True - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n -USE_L10N = True - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz -USE_TZ = True -########## END GENERAL CONFIGURATION - - -LOCALE_PATHS = ( - normpath(join(SITE_ROOT, 'locale')), -) - - -########## MEDIA CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root -MEDIA_ROOT = normpath(join(SITE_ROOT, 'media')) - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url -MEDIA_URL = '/media/' -########## END MEDIA CONFIGURATION - - -########## STATIC FILE CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root -STATIC_ROOT = normpath(join(SITE_ROOT, 'assets')) - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url -STATIC_URL = '/static/' - -# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS -STATICFILES_DIRS = ( - normpath(join(SITE_ROOT, 'static')), -) - -# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -) -########## END STATIC FILE CONFIGURATION - - -########## SECRET CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key -# Note: This key should only be used for development and testing. -SECRET_KEY = get_env_variable('GVA_SITE_SECRET') -########## END SECRET CONFIGURATION - - -########## SITE CONFIGURATION -# Hosts/domain names that are valid for this site -# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts -ALLOWED_HOSTS = [] -########## END SITE CONFIGURATION - - -########## FIXTURE CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS -FIXTURE_DIRS = ( - normpath(join(SITE_ROOT, 'fixtures')), -) -########## END FIXTURE CONFIGURATION - - -########## TEMPLATE CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors -TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.contrib.auth.context_processors.auth', - 'django.core.context_processors.debug', - 'django.core.context_processors.i18n', - 'django.core.context_processors.media', - 'django.core.context_processors.static', - 'django.core.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - 'django.core.context_processors.request', - # allauth specific context processors - 'allauth.account.context_processors.account', - 'allauth.socialaccount.context_processors.socialaccount', - # custom context processors - 'gnuviechadmin.context_processors.navigation', - 'gnuviechadmin.context_processors.version_info', -) - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -) - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs -TEMPLATE_DIRS = ( - normpath(join(SITE_ROOT, 'templates')), -) -########## END TEMPLATE CONFIGURATION - - -########## MIDDLEWARE CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#middleware-classes -MIDDLEWARE_CLASSES = ( - # Default Django middleware. - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - # uncomment next line to enable translation to browser locale - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) -########## END MIDDLEWARE CONFIGURATION - - -AUTHENTICATION_BACKENDS = ( - # Needed to login by username in Django admin, regardless of `allauth` - "django.contrib.auth.backends.ModelBackend", - - # `allauth` specific authentication methods, such as login by e-mail - "allauth.account.auth_backends.AuthenticationBackend", -) - - -########## URL CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf -ROOT_URLCONF = '%s.urls' % SITE_NAME -########## END URL CONFIGURATION - - -########## TEST RUNNER CONFIGURATION -TEST_RUNNER = 'django.test.runner.DiscoverRunner' -########## END TEST RUNNER CONFIGURATION - - -########## APP CONFIGURATION -DJANGO_APPS = ( - # Default Django apps: - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - # Useful template tags: - 'django.contrib.humanize', - - # Admin panel and documentation: - 'django.contrib.admin', - - # Flatpages for about page - 'django.contrib.flatpages', - - 'crispy_forms', -) - -ALLAUTH_APPS = ( - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'allauth.socialaccount.providers.google', - 'allauth.socialaccount.providers.linkedin_oauth2', - 'allauth.socialaccount.providers.twitter', -) - -# Apps specific for this project go here. -LOCAL_APPS = ( - 'dashboard', - 'taskresults', - 'mysqltasks', - 'pgsqltasks', - 'fileservertasks', - 'webtasks', - 'domains', - 'osusers', - 'managemails', - 'userdbs', - 'hostingpackages', - 'websites', - 'contact_form', -) - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps -INSTALLED_APPS = DJANGO_APPS + ALLAUTH_APPS + LOCAL_APPS - -MESSAGE_TAGS = { - messages.DEBUG: '', - messages.ERROR: 'alert-danger', - messages.INFO: 'alert-info', - messages.SUCCESS: 'alert-success', - messages.WARNING: 'alert-warning', -} -########## END APP CONFIGURATION - - -########## ALLAUTH CONFIGURATION -ACCOUNT_EMAIL_REQUIRED = True -ACCOUNT_EMAIL_VERIFICATION = 'mandatory' -LOGIN_REDIRECT_URL = '/' -SOCIALACCOUNT_QUERY_EMAIL = True -########## END ALLAUTH CONFIGURATION - - -########## CRISPY FORMS CONFIGURATION -CRISPY_TEMPLATE_PACK = 'bootstrap3' -########## END CRISPY_FORMS CONFIGURATION - - -########## LOGGING CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error when DEBUG=False. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(name)s ' - '%(module)s:%(lineno)d %(process)d %(thread)d %(message)s', - }, - 'simple': { - 'format': '%(levelname)s %(name)s:%(lineno)d %(message)s', - }, - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} -########## END LOGGING CONFIGURATION - - -########## WSGI CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application -WSGI_APPLICATION = '%s.wsgi.application' % SITE_NAME -########## END WSGI CONFIGURATION - - -########## CELERY CONFIGURATION -BROKER_URL = get_env_variable('GVA_BROKER_URL') -CELERY_RESULT_BACKEND = 'amqp' -CELERY_RESULT_PERSISTENT = True -CELERY_TASK_RESULT_EXPIRES = None -CELERY_ROUTES = ( - 'gvacommon.celeryrouters.GvaRouter', -) -CELERY_TIMEZONE = 'Europe/Berlin' -CELERY_ENABLE_UTC = True -CELERY_ACCEPT_CONTENT = ['json'] -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' -########## END CELERY CONFIGURATION - - -########## CUSTOM APP CONFIGURATION -OSUSER_MINUID = int(get_env_variable('GVA_MIN_OS_UID')) -OSUSER_MINGID = int(get_env_variable('GVA_MIN_OS_GID')) -OSUSER_USERNAME_PREFIX = get_env_variable('GVA_OSUSER_PREFIX') -OSUSER_HOME_BASEPATH = get_env_variable('GVA_OSUSER_HOME_BASEPATH') -OSUSER_DEFAULT_SHELL = get_env_variable('GVA_OSUSER_DEFAULT_SHELL') -OSUSER_SFTP_GROUP = 'sftponly' -OSUSER_SSH_GROUP = 'sshusers' -OSUSER_DEFAULT_GROUPS = [OSUSER_SFTP_GROUP] -OSUSER_UPLOAD_SERVER = get_env_variable('GVA_OSUSER_UPLOADSERVER') - -GVA_LINK_WEBMAIL = get_env_variable('GVA_WEBMAIL_URL') -GVA_LINK_PHPMYADMIN = get_env_variable('GVA_PHPMYADMIN_URL') -GVA_LINK_PHPPGADMIN = get_env_variable('GVA_PHPPGADMIN_URL') -########## END CUSTOM APP CONFIGURATION diff --git a/gnuviechadmin/gnuviechadmin/settings/local.py b/gnuviechadmin/gnuviechadmin/settings/local.py deleted file mode 100644 index 6cc5823..0000000 --- a/gnuviechadmin/gnuviechadmin/settings/local.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- python -*- -# pymode:lint_ignore=W0401,E501 -"""Development settings and globals.""" - -from __future__ import absolute_import - -from .base import * - - -########## DEBUG CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = True - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug -TEMPLATE_DEBUG = DEBUG -########## END DEBUG CONFIGURATION - - -########## EMAIL CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -########## END EMAIL CONFIGURATION - - -########## CACHE CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#caches -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - } -} -########## END CACHE CONFIGURATION - - -########## TOOLBAR CONFIGURATION -# See: http://django-debug-toolbar.readthedocs.org/en/latest/installation.html#explicit-setup -INSTALLED_APPS += ( - 'debug_toolbar', -) - -MIDDLEWARE_CLASSES += ( - 'debug_toolbar.middleware.DebugToolbarMiddleware', -) - -LOGGING['handlers'].update({ - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'simple', - } -}) -LOGGING['loggers'].update({ - 'gnuviechadmin': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'dashboard': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'domains': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'gvacommon': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'gvawebcore': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'hostingpackages': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'managemails': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'osusers': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'taskresults': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'userdbs': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, - 'websites': { - 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True,}, -}) - -DEBUG_TOOLBAR_PATCH_SETTINGS = False - -# http://django-debug-toolbar.readthedocs.org/en/latest/installation.html -INTERNAL_IPS = ('127.0.0.1', '10.0.2.2') -########## END TOOLBAR CONFIGURATION diff --git a/gnuviechadmin/gnuviechadmin/settings/production.py b/gnuviechadmin/gnuviechadmin/settings/production.py deleted file mode 100644 index f19e735..0000000 --- a/gnuviechadmin/gnuviechadmin/settings/production.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- python -*- -# pymode:lint_ignore=W0401,E501 -"""Production settings and globals.""" - -from __future__ import absolute_import - -from .base import * - -########## HOST CONFIGURATION -# See: https://docs.djangoproject.com/en/1.5/releases/1.5/#allowed-hosts-required-in-production -ALLOWED_HOSTS = [SITES_DOMAIN_NAME] -########## END HOST CONFIGURATION - -########## EMAIL CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix -EMAIL_SUBJECT_PREFIX = '[%s] ' % SITE_NAME - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email -DEFAULT_FROM_EMAIL = get_env_variable('GVA_SITE_ADMINMAIL') - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#server-email -SERVER_EMAIL = get_env_variable('GVA_SITE_ADMINMAIL') -########## END EMAIL CONFIGURATION - -########## CACHE CONFIGURATION -# See: https://docs.djangoproject.com/en/dev/ref/settings/#caches -#CACHES = {} -########## END CACHE CONFIGURATION - -########## ALLAUTH PRODUCTION CONFIGURATION -ACCOUNT_EMAIL_SUBJECT_PREFIX = '[Jan Dittberner IT-Consulting & -Solutions] ' -ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'https' -########## END ALLAUTH PRODUCTION CONFIGURATION diff --git a/gnuviechadmin/gnuviechadmin/settings/test.py b/gnuviechadmin/gnuviechadmin/settings/test.py deleted file mode 100644 index e133281..0000000 --- a/gnuviechadmin/gnuviechadmin/settings/test.py +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import absolute_import - -from .base import * diff --git a/gnuviechadmin/gnuviechadmin/tests.py b/gnuviechadmin/gnuviechadmin/tests.py deleted file mode 100644 index 9c35fda..0000000 --- a/gnuviechadmin/gnuviechadmin/tests.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -from unittest import TestCase - -from django.core.exceptions import ImproperlyConfigured - -from gnuviechadmin.settings.base import get_env_variable - - -class GetEnvVariableTest(TestCase): - - def test_get_existing_env_variable(self): - os.environ['testvariable'] = 'myvalue' - self.assertEqual(get_env_variable('testvariable'), 'myvalue') - - def test_get_missing_env_variable(self): - if 'missingvariable' in os.environ: - del os.environ['missingvariable'] - with self.assertRaises(ImproperlyConfigured) as e: - get_env_variable('missingvariable') - self.assertEqual( - str(e.exception), 'Set the missingvariable environment variable') - - -class WSGITest(TestCase): - - def test_wsgi_application(self): - from gnuviechadmin import wsgi - self.assertIsNotNone(wsgi.application) diff --git a/gnuviechadmin/gnuviechadmin/tests/__init__.py b/gnuviechadmin/gnuviechadmin/tests/__init__.py new file mode 100644 index 0000000..df1ff9a --- /dev/null +++ b/gnuviechadmin/gnuviechadmin/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- python -*- +# -*- coding: utf-8 -*- diff --git a/gnuviechadmin/gnuviechadmin/tests/test_celery.py b/gnuviechadmin/gnuviechadmin/tests/test_celery.py new file mode 100644 index 0000000..3dd9795 --- /dev/null +++ b/gnuviechadmin/gnuviechadmin/tests/test_celery.py @@ -0,0 +1,10 @@ +from unittest import TestCase + +from gnuviechadmin.celery import get_installed_apps +from django.conf import settings + + +class GetInstalledAppsTest(TestCase): + + def test_get_installed_apps(self): + self.assertEqual(get_installed_apps(), settings.INSTALLED_APPS) diff --git a/gnuviechadmin/gnuviechadmin/tests/test_contextprocessors.py b/gnuviechadmin/gnuviechadmin/tests/test_contextprocessors.py new file mode 100644 index 0000000..664e8d2 --- /dev/null +++ b/gnuviechadmin/gnuviechadmin/tests/test_contextprocessors.py @@ -0,0 +1,111 @@ +# -*- python -*- +# -*- coding: utf-8 -*- +""" +This module contains tests for :py:mod:`gnuviechadmin.context_processors`. + +""" + +from unittest.mock import MagicMock + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.http import HttpRequest +from django.test import TestCase +from django.urls import reverse + +from gnuviechadmin import __version__ as gvaversion +from gnuviechadmin.context_processors import navigation + +User = get_user_model() + + +class NavigationContextProcessorTest(TestCase): + + EXPECTED_ITEMS = ("webmail_url", "phpmyadmin_url", "phppgadmin_url", "active_item") + + def test_ajax_request(self): + response = self.client.get("/", HTTP_X_REQUESTED_WITH="XMLHttpRequest") + for item in self.EXPECTED_ITEMS: + self.assertNotIn(item, response.context) + + def _check_static_urls(self, context): + self.assertEqual(context["webmail_url"], settings.GVA_LINK_WEBMAIL) + self.assertEqual(context["phpmyadmin_url"], settings.GVA_LINK_PHPMYADMIN) + self.assertEqual(context["phppgadmin_url"], settings.GVA_LINK_PHPPGADMIN) + + def test_index_page_context(self): + response = self.client.get("/") + for item in self.EXPECTED_ITEMS: + self.assertIn(item, response.context) + self._check_static_urls(response.context) + self.assertEqual(response.context["active_item"], "dashboard") + + def test_contact_page_context(self): + response = self.client.get(reverse("contact_form")) + for item in self.EXPECTED_ITEMS: + self.assertIn(item, response.context) + self._check_static_urls(response.context) + self.assertEqual(response.context["active_item"], "contact") + + def test_hostingpackage_page_context(self): + User.objects.create_user("test", password="test") + self.client.login(username="test", password="test") + response = self.client.get(reverse("hosting_packages", kwargs={"user": "test"})) + for item in self.EXPECTED_ITEMS: + self.assertIn(item, response.context) + self._check_static_urls(response.context) + self.assertEqual(response.context["active_item"], "hostingpackage") + + def _test_page_context_by_viewmodule(self, viewmodule, expecteditem): + request = HttpRequest() + request.resolver_match = MagicMock() + request.resolver_match.func.__module__ = viewmodule + context = navigation(request) + for item in self.EXPECTED_ITEMS: + self.assertIn(item, context) + self._check_static_urls(context) + self.assertEqual(context["active_item"], expecteditem) + + def test_osusers_page_context(self): + self._test_page_context_by_viewmodule("osusers.views", "hostingpackage") + + def test_userdbs_page_context(self): + self._test_page_context_by_viewmodule("userdbs.views", "hostingpackage") + + def test_managemails_page_context(self): + self._test_page_context_by_viewmodule("managemails.views", "hostingpackage") + + def test_websites_page_context(self): + self._test_page_context_by_viewmodule("websites.views", "hostingpackage") + + def test_domains_page_context(self): + self._test_page_context_by_viewmodule("domains.views", "hostingpackage") + + def test_allauth_account_page_context(self): + self._test_page_context_by_viewmodule("allauth.account.views", "account") + + def test_allauth_socialaccount_page_context(self): + self._test_page_context_by_viewmodule("allauth.socialaccount.views", "account") + + def test_imprint_page_context(self): + response = self.client.get(reverse("imprint")) + for item in self.EXPECTED_ITEMS: + self.assertIn(item, response.context) + self._check_static_urls(response.context) + self.assertEqual(response.context["active_item"], "imprint") + + def test_no_resolver_match(self): + request = HttpRequest() + context = navigation(request) + self._check_static_urls(context) + self.assertEqual(context["active_item"], "dashboard") + + def test_admin_module(self): + self._test_page_context_by_viewmodule("django.contrib.admin.foo", "dashboard") + + +class VersionInfoContextProcessorTest(TestCase): + def test_version_info_in_context(self): + response = self.client.get("/") + self.assertIn("gnuviechadmin_version", response.context) + self.assertEqual(response.context["gnuviechadmin_version"], gvaversion) diff --git a/gnuviechadmin/gnuviechadmin/tests/test_wsgi.py b/gnuviechadmin/gnuviechadmin/tests/test_wsgi.py new file mode 100644 index 0000000..0aa9b53 --- /dev/null +++ b/gnuviechadmin/gnuviechadmin/tests/test_wsgi.py @@ -0,0 +1,10 @@ +# -*- python -*- +# -*- coding: utf-8 -*- +from unittest import TestCase + + +class WSGITest(TestCase): + + def test_wsgi_application(self): + from gnuviechadmin import wsgi + self.assertIsNotNone(wsgi.application) diff --git a/gnuviechadmin/gnuviechadmin/urls.py b/gnuviechadmin/gnuviechadmin/urls.py index 7d1f972..2b6d0b1 100644 --- a/gnuviechadmin/gnuviechadmin/urls.py +++ b/gnuviechadmin/gnuviechadmin/urls.py @@ -1,13 +1,15 @@ from __future__ import absolute_import -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.conf import settings from django.contrib import admin +from django.contrib.flatpages import views +from django.contrib.staticfiles.urls import staticfiles_urlpatterns + admin.autodiscover() -urlpatterns = patterns( - '', +urlpatterns = [ url(r'', include('dashboard.urls')), url(r'^accounts/', include('allauth.urls')), url(r'^database/', include('userdbs.urls')), @@ -16,19 +18,19 @@ urlpatterns = patterns( url(r'^website/', include('websites.urls')), url(r'^mail/', include('managemails.urls')), url(r'^osuser/', include('osusers.urls')), - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), url(r'^contact/', include('contact_form.urls')), -) -urlpatterns += patterns( - 'django.contrib.flatpages.views', - url(r'^impressum/$', 'flatpage', {'url': '/impressum/'}, name='imprint'), -) + url(r'^impressum/$', views.flatpage, { + 'url': '/impressum/' + }, name='imprint'), +] # Uncomment the next line to serve media files in dev. # urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -if settings.DEBUG: +if settings.DEBUG: # pragma: no cover import debug_toolbar - urlpatterns += patterns('', - url(r'^__debug__/', include(debug_toolbar.urls)), - ) + + urlpatterns = [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + staticfiles_urlpatterns() + urlpatterns diff --git a/gnuviechadmin/gnuviechadmin/wsgi.py b/gnuviechadmin/gnuviechadmin/wsgi.py index df7c0c9..04d5488 100644 --- a/gnuviechadmin/gnuviechadmin/wsgi.py +++ b/gnuviechadmin/gnuviechadmin/wsgi.py @@ -24,12 +24,13 @@ path.append(SITE_ROOT) # if running multiple sites in the same mod_wsgi process. To fix this, use # mod_wsgi daemon mode with each site in its own daemon process, or use # os.environ["DJANGO_SETTINGS_MODULE"] = "jajaja.settings" -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gnuviechadmin.settings.production") +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "gnuviechadmin.settings.production") # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. -from django.core.wsgi import get_wsgi_application +from django.core.wsgi import get_wsgi_application # noqa application = get_wsgi_application() # Apply WSGI middleware here. diff --git a/gnuviechadmin/gvacommon/.gitignore b/gnuviechadmin/gvacommon/.gitignore deleted file mode 100644 index 5f1ace6..0000000 --- a/gnuviechadmin/gvacommon/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.*.swp -*.pyc -.ropeproject/ diff --git a/gnuviechadmin/gvacommon/celeryrouters.py b/gnuviechadmin/gvacommon/celeryrouters.py deleted file mode 100644 index 44c4b4f..0000000 --- a/gnuviechadmin/gvacommon/celeryrouters.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - -class GvaRouter(object): - - def route_for_task(self, task, args=None, kwargs=None): - for route in ['ldap', 'file', 'mysql', 'pgsql', 'web']: - if route in task: - return { - 'exchange': route, - 'exchange_type': 'direct', - 'queue': route, - } - return None diff --git a/gnuviechadmin/gvacommon/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/gvacommon/locale/de/LC_MESSAGES/django.po deleted file mode 100644 index 15de43e..0000000 --- a/gnuviechadmin/gvacommon/locale/de/LC_MESSAGES/django.po +++ /dev/null @@ -1,24 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -msgid "" -msgstr "" -"Project-Id-Version: gvacommon\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-27 18:55+0100\n" -"PO-Revision-Date: 2015-01-24 18:25+0100\n" -"Last-Translator: Jan Dittberner \n" -"Language-Team: Jan Dittberner \n" -"Language: de\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" -"X-Generator: Poedit 1.6.10\n" -"X-Poedit-SourceCharset: UTF-8\n" - -#: gvacommon/viewmixins.py:29 -msgid "You are not allowed to view this page." -msgstr "Sie haben nicht die nötigen Berechtigungen um diese Seite zu sehen." diff --git a/gnuviechadmin/gvacommon/viewmixins.py b/gnuviechadmin/gvacommon/viewmixins.py deleted file mode 100644 index fc7f106..0000000 --- a/gnuviechadmin/gvacommon/viewmixins.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -This module defines mixins for gnuviechadmin views. - -""" -from __future__ import unicode_literals - -from django.http import HttpResponseForbidden -from django.utils.translation import ugettext as _ - -from braces.views import LoginRequiredMixin - - -class StaffOrSelfLoginRequiredMixin(LoginRequiredMixin): - """ - Mixin that makes sure that a user is logged in and matches the current - customer or is a staff user. - - """ - - def dispatch(self, request, *args, **kwargs): - if ( - request.user.is_staff or - request.user == self.get_customer_object() - ): - return super(StaffOrSelfLoginRequiredMixin, self).dispatch( - request, *args, **kwargs - ) - return HttpResponseForbidden( - _('You are not allowed to view this page.') - ) - - def get_customer_object(self): - """ - Views based on this mixin have to implement this method to return - the customer that must be an object of the same class as the - django.contrib.auth user type. - - :return: customer - :rtype: settings.AUTH_USER_MODEL - - """ - raise NotImplemented("subclass has to implement get_customer_object") diff --git a/gnuviechadmin/gvawebcore/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/gvawebcore/locale/de/LC_MESSAGES/django.po index 8fffff1..6b80207 100644 --- a/gnuviechadmin/gvawebcore/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/gvawebcore/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: gvawebcore\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-27 18:55+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" "PO-Revision-Date: 2015-01-25 11:49+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" diff --git a/gnuviechadmin/gvawebcore/tests/__init__.py b/gnuviechadmin/gvawebcore/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/gvawebcore/tests/test_forms.py b/gnuviechadmin/gvawebcore/tests/test_forms.py new file mode 100644 index 0000000..b4bb213 --- /dev/null +++ b/gnuviechadmin/gvawebcore/tests/test_forms.py @@ -0,0 +1,30 @@ +""" +This module contains tests for :py:mod:`gvawebcore.forms`. + +""" + +from unittest import TestCase + +from gvawebcore.forms import PASSWORD_MISMATCH_ERROR, PasswordModelFormMixin + + +class PasswordModelFormMixinTest(TestCase): + def test_form_properties(self): + form = PasswordModelFormMixin() + self.assertIn("password1", form.fields) + self.assertIn("password2", form.fields) + + def test_clean_password_same(self): + form = PasswordModelFormMixin( + data={"password1": "secret", "password2": "secret"} + ) + self.assertTrue(form.is_valid()) + self.assertEqual("secret", form.clean_password2()) + + def test_clean_password_different(self): + form = PasswordModelFormMixin( + data={"password1": "onesecret", "password2": "other"} + ) + self.assertFalse(form.is_valid()) + self.assertIn("password2", form.errors) + self.assertIn(PASSWORD_MISMATCH_ERROR, form.errors["password2"]) diff --git a/gnuviechadmin/gvawebcore/tests/test_views.py b/gnuviechadmin/gvawebcore/tests/test_views.py new file mode 100644 index 0000000..30cd39c --- /dev/null +++ b/gnuviechadmin/gvawebcore/tests/test_views.py @@ -0,0 +1,32 @@ +""" +This model contains tests for :py:mod:`gvawebcore.views`. + +""" + +from unittest import TestCase +from unittest.mock import Mock, patch + +from gvawebcore.views import HostingPackageAndCustomerMixin + + +class HostingPackageAndCustomerMixinTest(TestCase): + class TestView(HostingPackageAndCustomerMixin): + + kwargs = {"package": "1"} + + @patch("gvawebcore.views.get_object_or_404") + def test_get_hosting_package(self, get_object_or_404): + get_object_or_404.return_value = "A package" + view = self.TestView() + self.assertEqual("A package", view.get_hosting_package()) + + def test_get_hosting_package_cached(self): + view = self.TestView() + view.hostingpackage = "Cached package" + self.assertEqual("Cached package", view.get_hosting_package()) + + @patch("gvawebcore.views.get_object_or_404") + def test_get_customer_object(self, get_object_or_404): + get_object_or_404.return_value = Mock(customer="A customer") + view = self.TestView() + self.assertEqual("A customer", view.get_customer_object()) diff --git a/gnuviechadmin/hostingpackages/forms.py b/gnuviechadmin/hostingpackages/forms.py index b424ec7..ddf5f6b 100644 --- a/gnuviechadmin/hostingpackages/forms.py +++ b/gnuviechadmin/hostingpackages/forms.py @@ -5,7 +5,7 @@ This module contains the form classes related to hosting packages. from __future__ import absolute_import, unicode_literals from django import forms -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils.translation import ugettext as _ from crispy_forms.helper import FormHelper diff --git a/gnuviechadmin/hostingpackages/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/hostingpackages/locale/de/LC_MESSAGES/django.po index e9f078b..d8edc2e 100644 --- a/gnuviechadmin/hostingpackages/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/hostingpackages/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: gnuviechadmin hostingpackages\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-27 18:55+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" "PO-Revision-Date: 2015-01-25 15:49+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" @@ -167,67 +167,67 @@ msgstr "Kundenhostingpakete" msgid "{name} for {customer}" msgstr "{name} für {customer}" -#: hostingpackages/models.py:405 hostingpackages/models.py:427 +#: hostingpackages/models.py:404 hostingpackages/models.py:426 msgid "hosting package" msgstr "Hostingpaket" -#: hostingpackages/models.py:408 +#: hostingpackages/models.py:407 msgid "hosting domain" msgstr "Hostingdomain" -#: hostingpackages/models.py:430 +#: hostingpackages/models.py:429 msgid "customer hosting option" msgstr "kundenspezifische Hostingoption" -#: hostingpackages/models.py:431 +#: hostingpackages/models.py:430 msgid "customer hosting options" msgstr "kundenspezifische Hostingoptionen" -#: hostingpackages/models.py:443 +#: hostingpackages/models.py:442 msgid "disk space option template" msgstr "Speicherplatzoptionsvorlage" -#: hostingpackages/models.py:445 +#: hostingpackages/models.py:444 msgid "The disk space option template that this disk space option is based on" msgstr "" "Die Speicherplatzoptionsvorlage auf der diese Speicherplatzoption aufgebaut " "ist" -#: hostingpackages/models.py:459 +#: hostingpackages/models.py:458 msgid "user database option template" msgstr "Nutzerdatenbankoptionsvorlage" -#: hostingpackages/models.py:461 +#: hostingpackages/models.py:460 msgid "The user database option template that this database option is based on" msgstr "" "Die Nutzerdatenbankoptionsvorlage auf der diese Datenbankoption aufgebaut ist" -#: hostingpackages/models.py:475 +#: hostingpackages/models.py:474 msgid "mailbox option template" msgstr "Postfachoptionsvorlage" -#: hostingpackages/models.py:477 +#: hostingpackages/models.py:476 msgid "The mailbox option template that this mailbox option is based on" msgstr "Die Postfachoptionsvorlage auf der diese Postfachoption aufgebaut ist" -#: hostingpackages/views.py:60 hostingpackages/views.py:93 +#: hostingpackages/views.py:60 hostingpackages/views.py:94 #, python-brace-format msgid "Started setup of new hosting package {name}." msgstr "Einrichtung des Hostingpakets {name} wurde gestartet." -#: hostingpackages/views.py:152 +#: hostingpackages/views.py:186 msgid "Disk space" msgstr "Speicherplatz" -#: hostingpackages/views.py:155 +#: hostingpackages/views.py:189 msgid "Mailboxes" msgstr "Postfächer" -#: hostingpackages/views.py:158 +#: hostingpackages/views.py:192 msgid "Databases" msgstr "Datenbanken" -#: hostingpackages/views.py:228 +#: hostingpackages/views.py:262 #, python-brace-format msgid "Successfully added option {option} to hosting package {package}." msgstr "Option {option} erfolgreich zum Hostingpaket {package} hinzugefügt." diff --git a/gnuviechadmin/hostingpackages/migrations/0001_initial.py b/gnuviechadmin/hostingpackages/migrations/0001_initial.py index b70934d..51ec210 100644 --- a/gnuviechadmin/hostingpackages/migrations/0001_initial.py +++ b/gnuviechadmin/hostingpackages/migrations/0001_initial.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone -from django.conf import settings import model_utils.fields +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -17,15 +16,30 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CustomerHostingPackage', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('name', models.CharField(unique=True, max_length=128, verbose_name='name')), - ('description', models.TextField(verbose_name='description', blank=True)), - ('mailboxcount', models.PositiveIntegerField(verbose_name='mailbox count')), - ('diskspace', models.PositiveIntegerField(help_text='disk space for the hosting package', verbose_name='disk space')), - ('diskspace_unit', models.PositiveSmallIntegerField(verbose_name='unit of disk space', choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), - ('customer', models.ForeignKey(verbose_name='customer', to=settings.AUTH_USER_MODEL)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('name', models.CharField( + unique=True, max_length=128, verbose_name='name')), + ('description', models.TextField( + verbose_name='description', blank=True)), + ('mailboxcount', models.PositiveIntegerField( + verbose_name='mailbox count')), + ('diskspace', models.PositiveIntegerField( + help_text='disk space for the hosting package', + verbose_name='disk space')), + ('diskspace_unit', models.PositiveSmallIntegerField( + verbose_name='unit of disk space', + choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), + ('customer', models.ForeignKey( + verbose_name='customer', to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE)), ], options={ 'verbose_name': 'customer hosting package', @@ -36,9 +50,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CustomerHostingPackageOption', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), ], options={ 'verbose_name': 'customer hosting option', @@ -49,9 +69,16 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CustomerDiskSpaceOption', fields=[ - ('customerhostingpackageoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.CustomerHostingPackageOption')), - ('diskspace', models.PositiveIntegerField(verbose_name='disk space')), - ('diskspace_unit', models.PositiveSmallIntegerField(verbose_name='unit of disk space', choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), + ('customerhostingpackageoption_ptr', models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, + to='hostingpackages.CustomerHostingPackageOption', + on_delete=models.CASCADE)), + ('diskspace', models.PositiveIntegerField( + verbose_name='disk space')), + ('diskspace_unit', models.PositiveSmallIntegerField( + verbose_name='unit of disk space', + choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), ], options={ 'ordering': ['diskspace_unit', 'diskspace'], @@ -59,13 +86,19 @@ class Migration(migrations.Migration): 'verbose_name': 'Disk space option', 'verbose_name_plural': 'Disk space options', }, - bases=('hostingpackages.customerhostingpackageoption', models.Model), + bases=( + 'hostingpackages.customerhostingpackageoption', models.Model), ), migrations.CreateModel( name='CustomerMailboxOption', fields=[ - ('customerhostingpackageoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.CustomerHostingPackageOption')), - ('number', models.PositiveIntegerField(unique=True, verbose_name='number of mailboxes')), + ('customerhostingpackageoption_ptr', models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, + to='hostingpackages.CustomerHostingPackageOption', + on_delete=models.CASCADE)), + ('number', models.PositiveIntegerField( + unique=True, verbose_name='number of mailboxes')), ], options={ 'ordering': ['number'], @@ -73,14 +106,22 @@ class Migration(migrations.Migration): 'verbose_name': 'Mailbox option', 'verbose_name_plural': 'Mailbox options', }, - bases=('hostingpackages.customerhostingpackageoption', models.Model), + bases=( + 'hostingpackages.customerhostingpackageoption', models.Model), ), migrations.CreateModel( name='CustomerUserDatabaseOption', fields=[ - ('customerhostingpackageoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.CustomerHostingPackageOption')), - ('number', models.PositiveIntegerField(default=1, verbose_name='number of databases')), - ('db_type', models.PositiveSmallIntegerField(verbose_name='database type', choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), + ('customerhostingpackageoption_ptr', models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, + to='hostingpackages.CustomerHostingPackageOption', + on_delete=models.CASCADE)), + ('number', models.PositiveIntegerField( + default=1, verbose_name='number of databases')), + ('db_type', models.PositiveSmallIntegerField( + verbose_name='database type', + choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), ], options={ 'ordering': ['db_type', 'number'], @@ -88,14 +129,21 @@ class Migration(migrations.Migration): 'verbose_name': 'Database option', 'verbose_name_plural': 'Database options', }, - bases=('hostingpackages.customerhostingpackageoption', models.Model), + bases=( + 'hostingpackages.customerhostingpackageoption', models.Model), ), migrations.CreateModel( name='HostingOption', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), ], options={ 'verbose_name': 'Hosting option', @@ -106,9 +154,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='DiskSpaceOption', fields=[ - ('hostingoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.HostingOption')), - ('diskspace', models.PositiveIntegerField(verbose_name='disk space')), - ('diskspace_unit', models.PositiveSmallIntegerField(verbose_name='unit of disk space', choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), + ('hostingoption_ptr', models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, to='hostingpackages.HostingOption', + on_delete=models.CASCADE)), + ('diskspace', models.PositiveIntegerField( + verbose_name='disk space')), + ('diskspace_unit', models.PositiveSmallIntegerField( + verbose_name='unit of disk space', + choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), ], options={ 'ordering': ['diskspace_unit', 'diskspace'], @@ -121,14 +175,27 @@ class Migration(migrations.Migration): migrations.CreateModel( name='HostingPackageTemplate', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('name', models.CharField(unique=True, max_length=128, verbose_name='name')), - ('description', models.TextField(verbose_name='description', blank=True)), - ('mailboxcount', models.PositiveIntegerField(verbose_name='mailbox count')), - ('diskspace', models.PositiveIntegerField(help_text='disk space for the hosting package', verbose_name='disk space')), - ('diskspace_unit', models.PositiveSmallIntegerField(verbose_name='unit of disk space', choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('name', models.CharField( + unique=True, max_length=128, verbose_name='name')), + ('description', models.TextField( + verbose_name='description', blank=True)), + ('mailboxcount', models.PositiveIntegerField( + verbose_name='mailbox count')), + ('diskspace', models.PositiveIntegerField( + help_text='disk space for the hosting package', + verbose_name='disk space')), + ('diskspace_unit', models.PositiveSmallIntegerField( + verbose_name='unit of disk space', + choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), ], options={ 'verbose_name': 'Hosting package', @@ -139,8 +206,12 @@ class Migration(migrations.Migration): migrations.CreateModel( name='MailboxOption', fields=[ - ('hostingoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.HostingOption')), - ('number', models.PositiveIntegerField(unique=True, verbose_name='number of mailboxes')), + ('hostingoption_ptr', models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, to='hostingpackages.HostingOption', + on_delete=models.CASCADE)), + ('number', models.PositiveIntegerField( + unique=True, verbose_name='number of mailboxes')), ], options={ 'ordering': ['number'], @@ -153,9 +224,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserDatabaseOption', fields=[ - ('hostingoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.HostingOption')), - ('number', models.PositiveIntegerField(default=1, verbose_name='number of databases')), - ('db_type', models.PositiveSmallIntegerField(verbose_name='database type', choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), + ('hostingoption_ptr', models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, to='hostingpackages.HostingOption', + on_delete=models.CASCADE)), + ('number', models.PositiveIntegerField( + default=1, verbose_name='number of databases')), + ('db_type', models.PositiveSmallIntegerField( + verbose_name='database type', + choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), ], options={ 'ordering': ['db_type', 'number'], @@ -167,48 +244,71 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='userdatabaseoption', - unique_together=set([('number', 'db_type')]), + unique_together={('number', 'db_type')}, ), migrations.AlterUniqueTogether( name='diskspaceoption', - unique_together=set([('diskspace', 'diskspace_unit')]), + unique_together={('diskspace', 'diskspace_unit')}, ), migrations.AddField( model_name='customeruserdatabaseoption', name='template', - field=models.ForeignKey(verbose_name='user database option template', to='hostingpackages.UserDatabaseOption', help_text='The user database option template that this hosting option is based on'), + field=models.ForeignKey( + verbose_name='user database option template', + to='hostingpackages.UserDatabaseOption', + help_text='The user database option template that this ' + 'hosting option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( name='customeruserdatabaseoption', - unique_together=set([('number', 'db_type')]), + unique_together={('number', 'db_type')}, ), migrations.AddField( model_name='customermailboxoption', name='template', - field=models.ForeignKey(verbose_name='mailbox option template', to='hostingpackages.UserDatabaseOption', help_text='The mailbox option template that this hosting option is based on'), + field=models.ForeignKey( + verbose_name='mailbox option template', + to='hostingpackages.UserDatabaseOption', + help_text='The mailbox option template that this hosting ' + 'option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='customerhostingpackageoption', name='hosting_package', - field=models.ForeignKey(verbose_name='hosting package', to='hostingpackages.CustomerHostingPackage'), + field=models.ForeignKey( + verbose_name='hosting package', + to='hostingpackages.CustomerHostingPackage', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='customerhostingpackage', name='template', - field=models.ForeignKey(verbose_name='hosting package template', to='hostingpackages.HostingPackageTemplate', help_text='The hosting package template that this hosting package is based on.'), + field=models.ForeignKey( + verbose_name='hosting package template', + to='hostingpackages.HostingPackageTemplate', + help_text='The hosting package template that this hosting ' + 'package is based on.', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='customerdiskspaceoption', name='template', - field=models.ForeignKey(verbose_name='disk space option template', to='hostingpackages.DiskSpaceOption', help_text='The disk space option template that this hosting option is based on'), + field=models.ForeignKey( + verbose_name='disk space option template', + to='hostingpackages.DiskSpaceOption', + help_text='The disk space option template that this hosting ' + 'option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( name='customerdiskspaceoption', - unique_together=set([('diskspace', 'diskspace_unit')]), + unique_together={('diskspace', 'diskspace_unit')}, ), ] diff --git a/gnuviechadmin/hostingpackages/migrations/0001_squashed_0005_auto_20150118_1303.py b/gnuviechadmin/hostingpackages/migrations/0001_squashed_0005_auto_20150118_1303.py index 09ca357..1bf5616 100644 --- a/gnuviechadmin/hostingpackages/migrations/0001_squashed_0005_auto_20150118_1303.py +++ b/gnuviechadmin/hostingpackages/migrations/0001_squashed_0005_auto_20150118_1303.py @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone -from django.conf import settings import model_utils.fields +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - - replaces = [('hostingpackages', '0001_initial'), ('hostingpackages', '0002_auto_20150118_1149'), ('hostingpackages', '0003_auto_20150118_1221'), ('hostingpackages', '0004_customerhostingpackage_osuser'), ('hostingpackages', '0005_auto_20150118_1303')] + replaces = [('hostingpackages', '0001_initial'), + ('hostingpackages', '0002_auto_20150118_1149'), + ('hostingpackages', '0003_auto_20150118_1221'), + ('hostingpackages', '0004_customerhostingpackage_osuser'), + ('hostingpackages', '0005_auto_20150118_1303')] dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), @@ -20,15 +23,30 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CustomerHostingPackage', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('name', models.CharField(unique=True, max_length=128, verbose_name='name')), - ('description', models.TextField(verbose_name='description', blank=True)), - ('mailboxcount', models.PositiveIntegerField(verbose_name='mailbox count')), - ('diskspace', models.PositiveIntegerField(help_text='disk space for the hosting package', verbose_name='disk space')), - ('diskspace_unit', models.PositiveSmallIntegerField(verbose_name='unit of disk space', choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), - ('customer', models.ForeignKey(verbose_name='customer', to=settings.AUTH_USER_MODEL)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('name', models.CharField( + unique=True, max_length=128, verbose_name='name')), + ('description', models.TextField( + verbose_name='description', blank=True)), + ('mailboxcount', models.PositiveIntegerField( + verbose_name='mailbox count')), + ('diskspace', models.PositiveIntegerField( + help_text='disk space for the hosting package', + verbose_name='disk space')), + ('diskspace_unit', models.PositiveSmallIntegerField( + verbose_name='unit of disk space', + choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), + ('customer', models.ForeignKey( + verbose_name='customer', + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'verbose_name': 'customer hosting package', @@ -39,9 +57,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CustomerHostingPackageOption', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), ], options={ 'verbose_name': 'customer hosting option', @@ -52,9 +76,17 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CustomerDiskSpaceOption', fields=[ - ('customerhostingpackageoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.CustomerHostingPackageOption')), - ('diskspace', models.PositiveIntegerField(verbose_name='disk space')), - ('diskspace_unit', models.PositiveSmallIntegerField(verbose_name='unit of disk space', choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), + ('customerhostingpackageoption_ptr', + models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, + to='hostingpackages.CustomerHostingPackageOption', + on_delete=models.CASCADE)), + ('diskspace', models.PositiveIntegerField( + verbose_name='disk space')), + ('diskspace_unit', models.PositiveSmallIntegerField( + verbose_name='unit of disk space', + choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), ], options={ 'ordering': ['diskspace_unit', 'diskspace'], @@ -62,13 +94,20 @@ class Migration(migrations.Migration): 'verbose_name': 'Disk space option', 'verbose_name_plural': 'Disk space options', }, - bases=('hostingpackages.customerhostingpackageoption', models.Model), + bases=( + 'hostingpackages.customerhostingpackageoption', models.Model), ), migrations.CreateModel( name='CustomerMailboxOption', fields=[ - ('customerhostingpackageoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.CustomerHostingPackageOption')), - ('number', models.PositiveIntegerField(unique=True, verbose_name='number of mailboxes')), + ('customerhostingpackageoption_ptr', + models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, + to='hostingpackages.CustomerHostingPackageOption', + on_delete=models.CASCADE)), + ('number', models.PositiveIntegerField( + unique=True, verbose_name='number of mailboxes')), ], options={ 'ordering': ['number'], @@ -76,14 +115,23 @@ class Migration(migrations.Migration): 'verbose_name': 'Mailbox option', 'verbose_name_plural': 'Mailbox options', }, - bases=('hostingpackages.customerhostingpackageoption', models.Model), + bases=( + 'hostingpackages.customerhostingpackageoption', models.Model), ), migrations.CreateModel( name='CustomerUserDatabaseOption', fields=[ - ('customerhostingpackageoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.CustomerHostingPackageOption')), - ('number', models.PositiveIntegerField(default=1, verbose_name='number of databases')), - ('db_type', models.PositiveSmallIntegerField(verbose_name='database type', choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), + ('customerhostingpackageoption_ptr', + models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, + to='hostingpackages.CustomerHostingPackageOption', + on_delete=models.CASCADE)), + ('number', models.PositiveIntegerField( + default=1, verbose_name='number of databases')), + ('db_type', models.PositiveSmallIntegerField( + verbose_name='database type', + choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), ], options={ 'ordering': ['db_type', 'number'], @@ -91,14 +139,21 @@ class Migration(migrations.Migration): 'verbose_name': 'Database option', 'verbose_name_plural': 'Database options', }, - bases=('hostingpackages.customerhostingpackageoption', models.Model), + bases=( + 'hostingpackages.customerhostingpackageoption', models.Model), ), migrations.CreateModel( name='HostingOption', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), ], options={ 'verbose_name': 'Hosting option', @@ -109,9 +164,16 @@ class Migration(migrations.Migration): migrations.CreateModel( name='DiskSpaceOption', fields=[ - ('hostingoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.HostingOption')), - ('diskspace', models.PositiveIntegerField(verbose_name='disk space')), - ('diskspace_unit', models.PositiveSmallIntegerField(verbose_name='unit of disk space', choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), + ('hostingoption_ptr', + models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, to='hostingpackages.HostingOption', + on_delete=models.CASCADE)), + ('diskspace', models.PositiveIntegerField( + verbose_name='disk space')), + ('diskspace_unit', models.PositiveSmallIntegerField( + verbose_name='unit of disk space', + choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), ], options={ 'ordering': ['diskspace_unit', 'diskspace'], @@ -124,14 +186,27 @@ class Migration(migrations.Migration): migrations.CreateModel( name='HostingPackageTemplate', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('name', models.CharField(unique=True, max_length=128, verbose_name='name')), - ('description', models.TextField(verbose_name='description', blank=True)), - ('mailboxcount', models.PositiveIntegerField(verbose_name='mailbox count')), - ('diskspace', models.PositiveIntegerField(help_text='disk space for the hosting package', verbose_name='disk space')), - ('diskspace_unit', models.PositiveSmallIntegerField(verbose_name='unit of disk space', choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('name', models.CharField( + unique=True, max_length=128, verbose_name='name')), + ('description', models.TextField( + verbose_name='description', blank=True)), + ('mailboxcount', models.PositiveIntegerField( + verbose_name='mailbox count')), + ('diskspace', models.PositiveIntegerField( + help_text='disk space for the hosting package', + verbose_name='disk space')), + ('diskspace_unit', models.PositiveSmallIntegerField( + verbose_name='unit of disk space', + choices=[(0, 'MiB'), (1, 'GiB'), (2, 'TiB')])), ], options={ 'verbose_name': 'Hosting package', @@ -142,8 +217,13 @@ class Migration(migrations.Migration): migrations.CreateModel( name='MailboxOption', fields=[ - ('hostingoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.HostingOption')), - ('number', models.PositiveIntegerField(unique=True, verbose_name='number of mailboxes')), + ('hostingoption_ptr', + models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, to='hostingpackages.HostingOption', + on_delete=models.CASCADE)), + ('number', models.PositiveIntegerField( + unique=True, verbose_name='number of mailboxes')), ], options={ 'ordering': ['number'], @@ -156,9 +236,17 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserDatabaseOption', fields=[ - ('hostingoption_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='hostingpackages.HostingOption')), - ('number', models.PositiveIntegerField(default=1, verbose_name='number of databases')), - ('db_type', models.PositiveSmallIntegerField(verbose_name='database type', choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), + ('hostingoption_ptr', + models.OneToOneField( + parent_link=True, auto_created=True, primary_key=True, + serialize=False, to='hostingpackages.HostingOption', + on_delete=models.CASCADE)), + ('number', models.PositiveIntegerField( + default=1, verbose_name='number of databases')), + ('db_type', + models.PositiveSmallIntegerField( + verbose_name='database type', + choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), ], options={ 'ordering': ['db_type', 'number'], @@ -170,60 +258,93 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='userdatabaseoption', - unique_together=set([('number', 'db_type')]), + unique_together={('number', 'db_type')}, ), migrations.AlterUniqueTogether( name='diskspaceoption', - unique_together=set([('diskspace', 'diskspace_unit')]), + unique_together={('diskspace', 'diskspace_unit')}, ), migrations.AddField( model_name='customeruserdatabaseoption', name='template', - field=models.ForeignKey(verbose_name='user database option template', to='hostingpackages.UserDatabaseOption', help_text='The user database option template that this hosting option is based on'), + field=models.ForeignKey( + verbose_name='user database option template', + to='hostingpackages.UserDatabaseOption', + help_text='The user database option template that this ' + 'hosting option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( name='customeruserdatabaseoption', - unique_together=set([('number', 'db_type')]), + unique_together={('number', 'db_type')}, ), migrations.AddField( model_name='customermailboxoption', name='template', - field=models.ForeignKey(verbose_name='mailbox option template', to='hostingpackages.UserDatabaseOption', help_text='The mailbox option template that this mailbox option is based on'), + field=models.ForeignKey( + verbose_name='mailbox option template', + to='hostingpackages.UserDatabaseOption', + help_text='The mailbox option template that this mailbox ' + 'option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='customerhostingpackageoption', name='hosting_package', - field=models.ForeignKey(verbose_name='hosting package', to='hostingpackages.CustomerHostingPackage'), + field=models.ForeignKey( + verbose_name='hosting package', + to='hostingpackages.CustomerHostingPackage', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='customerhostingpackage', name='template', - field=models.ForeignKey(verbose_name='hosting package template', to='hostingpackages.HostingPackageTemplate', help_text='The hosting package template that this hosting package is based on'), + field=models.ForeignKey( + verbose_name='hosting package template', + to='hostingpackages.HostingPackageTemplate', + help_text='The hosting package template that this hosting ' + 'package is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='customerdiskspaceoption', name='template', - field=models.ForeignKey(verbose_name='disk space option template', to='hostingpackages.DiskSpaceOption', help_text='The disk space option template that this hosting option is based on'), + field=models.ForeignKey( + verbose_name='disk space option template', + to='hostingpackages.DiskSpaceOption', + help_text='The disk space option template that this hosting ' + 'option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( name='customerdiskspaceoption', - unique_together=set([('diskspace', 'diskspace_unit')]), + unique_together={('diskspace', 'diskspace_unit')}, ), migrations.AlterField( model_name='customerdiskspaceoption', name='template', - field=models.ForeignKey(verbose_name='disk space option template', to='hostingpackages.DiskSpaceOption', help_text='The disk space option template that this disk space option is based on'), + field=models.ForeignKey( + verbose_name='disk space option template', + to='hostingpackages.DiskSpaceOption', + help_text='The disk space option template that this disk ' + 'space option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( model_name='customeruserdatabaseoption', name='template', - field=models.ForeignKey(verbose_name='user database option template', to='hostingpackages.UserDatabaseOption', help_text='The user database option template that this database option is based on'), + field=models.ForeignKey( + verbose_name='user database option template', + to='hostingpackages.UserDatabaseOption', + help_text='The user database option template that this ' + 'database option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( @@ -234,12 +355,14 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='customerhostingpackage', - unique_together=set([('customer', 'name')]), + unique_together={('customer', 'name')}, ), migrations.AddField( model_name='customerhostingpackage', name='osuser', - field=models.OneToOneField(null=True, blank=True, to='osusers.User', verbose_name='Operating system user'), + field=models.OneToOneField( + null=True, blank=True, to='osusers.User', + verbose_name='Operating system user', on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/gnuviechadmin/hostingpackages/migrations/0002_auto_20150118_1149.py b/gnuviechadmin/hostingpackages/migrations/0002_auto_20150118_1149.py index 61103ed..515829f 100644 --- a/gnuviechadmin/hostingpackages/migrations/0002_auto_20150118_1149.py +++ b/gnuviechadmin/hostingpackages/migrations/0002_auto_20150118_1149.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('hostingpackages', '0001_initial'), ] @@ -14,25 +13,45 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='customerdiskspaceoption', name='template', - field=models.ForeignKey(verbose_name='disk space option template', to='hostingpackages.DiskSpaceOption', help_text='The disk space option template that this disk space option is based on'), + field=models.ForeignKey( + verbose_name='disk space option template', + to='hostingpackages.DiskSpaceOption', + help_text='The disk space option template that this disk ' + 'space option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( model_name='customerhostingpackage', name='template', - field=models.ForeignKey(verbose_name='hosting package template', to='hostingpackages.HostingPackageTemplate', help_text='The hosting package template that this hosting package is based on'), + field=models.ForeignKey( + verbose_name='hosting package template', + to='hostingpackages.HostingPackageTemplate', + help_text='The hosting package template that this hosting ' + 'package is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( model_name='customermailboxoption', name='template', - field=models.ForeignKey(verbose_name='mailbox option template', to='hostingpackages.UserDatabaseOption', help_text='The mailbox option template that this mailbox option is based on'), + field=models.ForeignKey( + verbose_name='mailbox option template', + to='hostingpackages.UserDatabaseOption', + help_text='The mailbox option template that this mailbox ' + 'option is based on', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( model_name='customeruserdatabaseoption', name='template', - field=models.ForeignKey(verbose_name='user database option template', to='hostingpackages.UserDatabaseOption', help_text='The user database option template that this database option is based on'), + field=models.ForeignKey( + verbose_name='user database option template', + to='hostingpackages.UserDatabaseOption', + help_text='The user database option template that this ' + 'database option is based on', + on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/gnuviechadmin/hostingpackages/migrations/0003_auto_20150118_1407.py b/gnuviechadmin/hostingpackages/migrations/0003_auto_20150118_1407.py index 8d7e82d..98979c5 100644 --- a/gnuviechadmin/hostingpackages/migrations/0003_auto_20150118_1407.py +++ b/gnuviechadmin/hostingpackages/migrations/0003_auto_20150118_1407.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('hostingpackages', '0002_auto_20150118_1319'), ] @@ -14,7 +13,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='customermailboxoption', name='template', - field=models.ForeignKey(verbose_name='mailbox option template', to='hostingpackages.MailboxOption', help_text='The mailbox option template that this mailbox option is based on'), + field=models.ForeignKey( + verbose_name='mailbox option template', + to='hostingpackages.MailboxOption', + help_text='The mailbox option template that this mailbox ' + 'option is based on', + on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/gnuviechadmin/hostingpackages/migrations/0004_customerhostingpackage_osuser.py b/gnuviechadmin/hostingpackages/migrations/0004_customerhostingpackage_osuser.py index c5b47e3..616d13f 100644 --- a/gnuviechadmin/hostingpackages/migrations/0004_customerhostingpackage_osuser.py +++ b/gnuviechadmin/hostingpackages/migrations/0004_customerhostingpackage_osuser.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('osusers', '0004_auto_20150104_1751'), ('hostingpackages', '0003_auto_20150118_1221'), @@ -15,7 +14,9 @@ class Migration(migrations.Migration): migrations.AddField( model_name='customerhostingpackage', name='osuser', - field=models.ForeignKey(verbose_name='Operating system user', blank=True, to='osusers.User', null=True), + field=models.ForeignKey( + verbose_name='Operating system user', blank=True, + to='osusers.User', null=True, on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/gnuviechadmin/hostingpackages/migrations/0004_customerhostingpackagedomain.py b/gnuviechadmin/hostingpackages/migrations/0004_customerhostingpackagedomain.py index 9a5ee1d..446c730 100644 --- a/gnuviechadmin/hostingpackages/migrations/0004_customerhostingpackagedomain.py +++ b/gnuviechadmin/hostingpackages/migrations/0004_customerhostingpackagedomain.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone import model_utils.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('domains', '0002_auto_20150124_1909'), ('hostingpackages', '0003_auto_20150118_1407'), @@ -17,11 +16,22 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CustomerHostingPackageDomain', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('domain', models.OneToOneField(verbose_name='hosting domain', to='domains.HostingDomain')), - ('hosting_package', models.ForeignKey(related_name='domains', verbose_name='hosting package', to='hostingpackages.CustomerHostingPackage')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('domain', models.OneToOneField( + verbose_name='hosting domain', to='domains.HostingDomain', + on_delete=models.CASCADE)), + ('hosting_package', models.ForeignKey( + related_name='domains', verbose_name='hosting package', + to='hostingpackages.CustomerHostingPackage', + on_delete=models.CASCADE)), ], options={ 'abstract': False, diff --git a/gnuviechadmin/hostingpackages/migrations/0005_auto_20150118_1303.py b/gnuviechadmin/hostingpackages/migrations/0005_auto_20150118_1303.py index b66739e..9034ae0 100644 --- a/gnuviechadmin/hostingpackages/migrations/0005_auto_20150118_1303.py +++ b/gnuviechadmin/hostingpackages/migrations/0005_auto_20150118_1303.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('hostingpackages', '0004_customerhostingpackage_osuser'), ] @@ -14,7 +13,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='customerhostingpackage', name='osuser', - field=models.OneToOneField(null=True, blank=True, to='osusers.User', verbose_name='Operating system user'), + field=models.OneToOneField( + null=True, blank=True, to='osusers.User', + verbose_name='Operating system user', on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/gnuviechadmin/hostingpackages/models.py b/gnuviechadmin/hostingpackages/models.py index 2e5ff43..c1a5267 100644 --- a/gnuviechadmin/hostingpackages/models.py +++ b/gnuviechadmin/hostingpackages/models.py @@ -5,9 +5,9 @@ This module contains the hosting package models. from __future__ import absolute_import, unicode_literals from django.conf import settings -from django.core.urlresolvers import reverse from django.db import transaction from django.db import models +from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _, ungettext @@ -16,38 +16,24 @@ from model_utils.models import TimeStampedModel from domains.models import HostingDomain from managemails.models import Mailbox -from osusers.models import ( - AdditionalGroup, - Group, - User as OsUser, -) -from userdbs.models import ( - DB_TYPES, - UserDatabase, -) +from osusers.models import AdditionalGroup, Group, User as OsUser +from userdbs.models import DB_TYPES, UserDatabase +DISK_SPACE_UNITS = Choices((0, "M", _("MiB")), (1, "G", _("GiB")), (2, "T", _("TiB"))) -DISK_SPACE_UNITS = Choices( - (0, 'M', _('MiB')), - (1, 'G', _('GiB')), - (2, 'T', _('TiB')), -) - -DISK_SPACE_FACTORS = ( - (1, None, None), - (1024, 1, None), - (1024 * 1024, 1024, 1), -) +DISK_SPACE_FACTORS = ((1, None, None), (1024, 1, None), (1024 * 1024, 1024, 1)) @python_2_unicode_compatible class HostingPackageBase(TimeStampedModel): - description = models.TextField(_('description'), blank=True) - mailboxcount = models.PositiveIntegerField(_('mailbox count')) + description = models.TextField(_("description"), blank=True) + mailboxcount = models.PositiveIntegerField(_("mailbox count")) diskspace = models.PositiveIntegerField( - _('disk space'), help_text=_('disk space for the hosting package')) + _("disk space"), help_text=_("disk space for the hosting package") + ) diskspace_unit = models.PositiveSmallIntegerField( - _('unit of disk space'), choices=DISK_SPACE_UNITS) + _("unit of disk space"), choices=DISK_SPACE_UNITS + ) class Meta: abstract = True @@ -57,11 +43,11 @@ class HostingPackageBase(TimeStampedModel): class HostingPackageTemplate(HostingPackageBase): - name = models.CharField(_('name'), max_length=128, unique=True) + name = models.CharField(_("name"), max_length=128, unique=True) class Meta: - verbose_name = _('Hosting package') - verbose_name_plural = _('Hosting packages') + verbose_name = _("Hosting package") + verbose_name_plural = _("Hosting packages") class HostingOption(TimeStampedModel): @@ -73,19 +59,21 @@ class HostingOption(TimeStampedModel): @python_2_unicode_compatible class DiskSpaceOptionBase(models.Model): - diskspace = models.PositiveIntegerField(_('disk space')) + diskspace = models.PositiveIntegerField(_("disk space")) diskspace_unit = models.PositiveSmallIntegerField( - _('unit of disk space'), choices=DISK_SPACE_UNITS) + _("unit of disk space"), choices=DISK_SPACE_UNITS + ) class Meta: abstract = True - ordering = ['diskspace_unit', 'diskspace'] - verbose_name = _('Disk space option') - verbose_name_plural = _('Disk space options') + ordering = ["diskspace_unit", "diskspace"] + verbose_name = _("Disk space option") + verbose_name_plural = _("Disk space options") def __str__(self): return _("Additional disk space {space} {unit}").format( - space=self.diskspace, unit=self.get_diskspace_unit_display()) + space=self.diskspace, unit=self.get_diskspace_unit_display() + ) class DiskSpaceOption(DiskSpaceOptionBase, HostingOption): @@ -94,31 +82,26 @@ class DiskSpaceOption(DiskSpaceOptionBase, HostingOption): existing hosting packages. """ + class Meta: - unique_together = ['diskspace', 'diskspace_unit'] + unique_together = ["diskspace", "diskspace_unit"] @python_2_unicode_compatible class UserDatabaseOptionBase(models.Model): - number = models.PositiveIntegerField( - _('number of databases'), default=1) - db_type = models.PositiveSmallIntegerField( - _('database type'), choices=DB_TYPES) + number = models.PositiveIntegerField(_("number of databases"), default=1) + db_type = models.PositiveSmallIntegerField(_("database type"), choices=DB_TYPES) class Meta: abstract = True - ordering = ['db_type', 'number'] - verbose_name = _('Database option') - verbose_name_plural = _('Database options') + ordering = ["db_type", "number"] + verbose_name = _("Database option") + verbose_name_plural = _("Database options") def __str__(self): return ungettext( - '{type} database', - '{count} {type} databases', - self.number - ).format( - type=self.get_db_type_display(), count=self.number - ) + "{type} database", "{count} {type} databases", self.number + ).format(type=self.get_db_type_display(), count=self.number) class UserDatabaseOption(UserDatabaseOptionBase, HostingOption): @@ -127,8 +110,9 @@ class UserDatabaseOption(UserDatabaseOptionBase, HostingOption): hosting packages. """ + class Meta: - unique_together = ['number', 'db_type'] + unique_together = ["number", "db_type"] @python_2_unicode_compatible @@ -137,23 +121,19 @@ class MailboxOptionBase(models.Model): Base class for mailbox options. """ - number = models.PositiveIntegerField( - _('number of mailboxes'), unique=True) + + number = models.PositiveIntegerField(_("number of mailboxes"), unique=True) class Meta: abstract = True - ordering = ['number'] - verbose_name = _('Mailbox option') - verbose_name_plural = _('Mailbox options') + ordering = ["number"] + verbose_name = _("Mailbox option") + verbose_name_plural = _("Mailbox options") def __str__(self): return ungettext( - '{count} additional mailbox', - '{count} additional mailboxes', - self.number - ).format( - count=self.number - ) + "{count} additional mailbox", "{count} additional mailboxes", self.number + ).format(count=self.number) class MailboxOption(MailboxOptionBase, HostingOption): @@ -188,10 +168,11 @@ class CustomerHostingPackageManager(models.Manager): """ package = CustomerHostingPackage( - customer=customer, template=template, name=name) + customer=customer, template=template, name=name + ) package.description = template.description package.copy_template_attributes() - if 'commit' in kwargs and kwargs['commit'] is True: + if "commit" in kwargs and kwargs["commit"] is True: package.save(**kwargs) return package @@ -202,43 +183,49 @@ class CustomerHostingPackage(HostingPackageBase): This class defines customer specific hosting packages. """ + customer = models.ForeignKey( - settings.AUTH_USER_MODEL, verbose_name=_('customer')) + settings.AUTH_USER_MODEL, verbose_name=_("customer"), on_delete=models.CASCADE + ) template = models.ForeignKey( - HostingPackageTemplate, verbose_name=_('hosting package template'), + HostingPackageTemplate, + verbose_name=_("hosting package template"), help_text=_( - 'The hosting package template that this hosting package is based' - ' on' - )) - name = models.CharField(_('name'), max_length=128) + "The hosting package template that this hosting package is based" " on" + ), + on_delete=models.CASCADE, + ) + name = models.CharField(_("name"), max_length=128) osuser = models.OneToOneField( - OsUser, verbose_name=_('Operating system user'), - blank=True, null=True) + OsUser, + verbose_name=_("Operating system user"), + blank=True, + null=True, + on_delete=models.CASCADE, + ) objects = CustomerHostingPackageManager() class Meta: - unique_together = ['customer', 'name'] - verbose_name = _('customer hosting package') - verbose_name_plural = _('customer hosting packages') + unique_together = ["customer", "name"] + verbose_name = _("customer hosting package") + verbose_name_plural = _("customer hosting packages") def __str__(self): - return _("{name} for {customer}").format( - name=self.name, customer=self.customer - ) + return _("{name} for {customer}").format(name=self.name, customer=self.customer) def get_absolute_url(self): - return reverse('hosting_package_details', kwargs={ - 'user': self.customer.username, - 'pk': self.id, - }) + return reverse( + "hosting_package_details", + kwargs={"user": self.customer.username, "pk": self.id}, + ) def copy_template_attributes(self): """ Copy the attributes of the hosting package's template to the package. """ - for attrname in ('diskspace', 'diskspace_unit', 'mailboxcount'): + for attrname in ("diskspace", "diskspace_unit", "mailboxcount"): setattr(self, attrname, getattr(self.template, attrname)) def get_hostingoptions(self): @@ -246,10 +233,11 @@ class CustomerHostingPackage(HostingPackageBase): for opt_type in [ CustomerDiskSpaceOption, CustomerMailboxOption, - CustomerUserDatabaseOption + CustomerUserDatabaseOption, ]: opts.extend(opt_type.objects.filter(hosting_package=self)) return opts + hostingoptions = property(get_hostingoptions) def get_disk_space(self, unit=None): @@ -272,15 +260,16 @@ class CustomerHostingPackage(HostingPackageBase): diskspace += option.diskspace elif option.diskspace_unit > min_unit: diskspace += ( - DISK_SPACE_FACTORS[option.diskspace_unit][min_unit] * - option.diskspace) + DISK_SPACE_FACTORS[option.diskspace_unit][min_unit] + * option.diskspace + ) else: diskspace = ( - DISK_SPACE_FACTORS[min_unit][option.diskspace_unit] * - diskspace) + option.diskspace + DISK_SPACE_FACTORS[min_unit][option.diskspace_unit] * diskspace + ) + option.diskspace min_unit = option.diskspace_unit if unit is None: - return DISK_SPACE_FACTORS[min_unit][0] * diskspace * 1024**2 + return DISK_SPACE_FACTORS[min_unit][0] * diskspace * 1024 ** 2 if unit > min_unit: return DISK_SPACE_FACTORS[unit][min_unit] * diskspace return DISK_SPACE_FACTORS[min_unit][unit] * diskspace @@ -297,21 +286,22 @@ class CustomerHostingPackage(HostingPackageBase): """ if unit is None: - return (DISK_SPACE_FACTORS[self.diskspace_unit][0] * - self.diskspace * 1024**2) + return ( + DISK_SPACE_FACTORS[self.diskspace_unit][0] * self.diskspace * 1024 ** 2 + ) if unit > self.diskspace_unit: - return (DISK_SPACE_FACTORS[unit][self.diskspace_unit] * - self.diskspace) + return DISK_SPACE_FACTORS[unit][self.diskspace_unit] * self.diskspace return DISK_SPACE_FACTORS[self.diskspace_unit][unit] * self.diskspace def get_quota(self): soft = 1024 * self.get_disk_space(DISK_SPACE_UNITS.M) - hard = soft * 105 / 100 + hard = soft * 105 // 100 return (soft, hard) def get_mailboxes(self): if self.osuser: return Mailbox.objects.filter(osuser=self.osuser).all() + mailboxes = property(get_mailboxes) def get_used_mailbox_count(self): @@ -322,6 +312,7 @@ class CustomerHostingPackage(HostingPackageBase): if self.osuser: return Mailbox.objects.filter(osuser=self.osuser).count() return 0 + used_mailbox_count = property(get_used_mailbox_count) def get_mailbox_count(self): @@ -330,12 +321,11 @@ class CustomerHostingPackage(HostingPackageBase): of its mailbox options. """ - result = CustomerMailboxOption.objects.filter( - hosting_package=self - ).aggregate( - mailbox_sum=models.Sum('number') + result = CustomerMailboxOption.objects.filter(hosting_package=self).aggregate( + mailbox_sum=models.Sum("number") ) - return self.mailboxcount + (result['mailbox_sum'] or 0) + return self.mailboxcount + (result["mailbox_sum"] or 0) + mailbox_count = property(get_mailbox_count) def may_add_mailbox(self): @@ -347,25 +337,23 @@ class CustomerHostingPackage(HostingPackageBase): options for this hosting package. """ - return CustomerUserDatabaseOption.objects.values( - 'db_type' - ).filter(hosting_package=self).annotate( - number=models.Sum('number') - ).all() + return ( + CustomerUserDatabaseOption.objects.values("db_type") + .filter(hosting_package=self) + .annotate(number=models.Sum("number")) + .all() + ) def get_databases_flat(self): if self.osuser: - return UserDatabase.objects.filter( - db_user__osuser=self.osuser).all() + return UserDatabase.objects.filter(db_user__osuser=self.osuser).all() + databases = property(get_databases_flat) def may_add_database(self): return ( - CustomerUserDatabaseOption.objects.filter( - hosting_package=self).count() - > - UserDatabase.objects.filter( - db_user__osuser=self.osuser).count() + CustomerUserDatabaseOption.objects.filter(hosting_package=self).count() + > UserDatabase.objects.filter(db_user__osuser=self.osuser).count() ) @transaction.atomic @@ -401,11 +389,16 @@ class CustomerHostingPackageDomain(TimeStampedModel): domain. """ + hosting_package = models.ForeignKey( - CustomerHostingPackage, verbose_name=_('hosting package'), - related_name='domains') + CustomerHostingPackage, + verbose_name=_("hosting package"), + related_name="domains", + on_delete=models.CASCADE, + ) domain = models.OneToOneField( - HostingDomain, verbose_name=_('hosting domain')) + HostingDomain, verbose_name=_("hosting domain"), on_delete=models.CASCADE + ) def __str__(self): return self.domain.domain @@ -423,56 +416,62 @@ class CustomerHostingPackageOption(TimeStampedModel): This class defines options for customer hosting packages. """ + hosting_package = models.ForeignKey( - CustomerHostingPackage, verbose_name=_('hosting package')) + CustomerHostingPackage, + verbose_name=_("hosting package"), + on_delete=models.CASCADE, + ) class Meta: - verbose_name = _('customer hosting option') - verbose_name_plural = _('customer hosting options') + verbose_name = _("customer hosting option") + verbose_name_plural = _("customer hosting options") -class CustomerDiskSpaceOption(DiskSpaceOptionBase, - CustomerHostingPackageOption): +class CustomerDiskSpaceOption(DiskSpaceOptionBase, CustomerHostingPackageOption): """ This is a class for customer hosting package options adding additional disk space to existing customer hosting package. """ + template = models.ForeignKey( DiskSpaceOption, - verbose_name=_('disk space option template'), + verbose_name=_("disk space option template"), help_text=_( - 'The disk space option template that this disk space option is' - ' based on' - )) + "The disk space option template that this disk space option is" " based on" + ), + on_delete=models.CASCADE, + ) -class CustomerUserDatabaseOption(UserDatabaseOptionBase, - CustomerHostingPackageOption): +class CustomerUserDatabaseOption(UserDatabaseOptionBase, CustomerHostingPackageOption): """ This is a class for customer hosting package options adding user databases to existing customer hosting packages. """ + template = models.ForeignKey( UserDatabaseOption, - verbose_name=_('user database option template'), + verbose_name=_("user database option template"), help_text=_( - 'The user database option template that this database option is' - ' based on' - )) + "The user database option template that this database option is" " based on" + ), + on_delete=models.CASCADE, + ) -class CustomerMailboxOption(MailboxOptionBase, - CustomerHostingPackageOption): +class CustomerMailboxOption(MailboxOptionBase, CustomerHostingPackageOption): """ This is a class for customer hosting package options adding additional mailboxes to existing customer hosting packages. """ + template = models.ForeignKey( MailboxOption, - verbose_name=_('mailbox option template'), - help_text=_( - 'The mailbox option template that this mailbox option is based on' - )) + verbose_name=_("mailbox option template"), + help_text=_("The mailbox option template that this mailbox option is based on"), + on_delete=models.CASCADE, + ) diff --git a/gnuviechadmin/hostingpackages/tests/test_models.py b/gnuviechadmin/hostingpackages/tests/test_models.py index b762d05..66f579c 100644 --- a/gnuviechadmin/hostingpackages/tests/test_models.py +++ b/gnuviechadmin/hostingpackages/tests/test_models.py @@ -5,10 +5,7 @@ Test for models. from django.test import TestCase -from hostingpackages.models import ( - DISK_SPACE_UNITS, - CustomerHostingPackage, -) +from hostingpackages.models import DISK_SPACE_UNITS, CustomerHostingPackage class CustomerHostingPackageTest(TestCase): @@ -16,7 +13,7 @@ class CustomerHostingPackageTest(TestCase): package = CustomerHostingPackage( diskspace=10, diskspace_unit=DISK_SPACE_UNITS.G ) - self.assertEqual(package.get_disk_space(), 10 * 1024 * 1024**2) + self.assertEqual(package.get_disk_space(), 10 * 1024 ** 3) def test_get_disk_space_mib(self): package = CustomerHostingPackage( diff --git a/gnuviechadmin/hostingpackages/urls.py b/gnuviechadmin/hostingpackages/urls.py index 150256e..4d0201b 100644 --- a/gnuviechadmin/hostingpackages/urls.py +++ b/gnuviechadmin/hostingpackages/urls.py @@ -4,7 +4,7 @@ This module defines the URL patterns for hosting package related views. """ from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( AddHostingOption, @@ -17,8 +17,7 @@ from .views import ( ) -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^create$', CreateHostingPackage.as_view(), name='create_hosting_package'), url(r'^allpackages/', @@ -35,4 +34,4 @@ urlpatterns = patterns( HostingOptionChoices.as_view(), name='hosting_option_choices'), url(r'^(?P\d+)/add-option/(?P\w+)/(?P\d+)$', AddHostingOption.as_view(), name='add_hosting_option'), -) +] diff --git a/gnuviechadmin/hostingpackages/views.py b/gnuviechadmin/hostingpackages/views.py index abe265e..d07f4cc 100644 --- a/gnuviechadmin/hostingpackages/views.py +++ b/gnuviechadmin/hostingpackages/views.py @@ -80,7 +80,8 @@ class CreateCustomerHostingPackage(CreateHostingPackage): get_user_model(), username=self.kwargs['user']) def get_context_data(self, **kwargs): - context = super(CreateCustomerHostingPackage, self).get_context_data(**kwargs) + context = super( + CreateCustomerHostingPackage, self).get_context_data(**kwargs) context['customer'] = self.get_customer_object() return context diff --git a/gnuviechadmin/ldaptasks/tasks.py b/gnuviechadmin/ldaptasks/tasks.py index 72acd8d..f47437c 100644 --- a/gnuviechadmin/ldaptasks/tasks.py +++ b/gnuviechadmin/ldaptasks/tasks.py @@ -9,7 +9,7 @@ from celery import shared_task @shared_task -def create_ldap_group(groupname, gid, descr): +def create_ldap_group(groupname, gid, description): """ This task creates an :py:class:`LDAP group ` if it does not exist yet. @@ -19,9 +19,12 @@ def create_ldap_group(groupname, gid, descr): :param str groupname: the group name :param int gid: the group id - :param str descr: description text for the group - :return: the distinguished name of the group - :rtype: str + :param str description: description text for the group + :return: dictionary containing groupname, gid, description and + :py:const:`group_dn` set to the distinguished name of the newly created + or existing LDAP group + :rtype: dict + """ @@ -45,8 +48,10 @@ def create_ldap_user(username, uid, gid, gecos, homedir, shell, password): is passed the password is not touched :raises celery.exceptions.Reject: if the specified primary group does not exist - :return: the distinguished name of the user - :rtype: str + :return: dictionary containing username, uid, gid, gecos, homedir, shell, + password and :py:const:`user_dn` set to the distinguished name of the + newly created or existing LDAP user + :rtype: dict """ @@ -59,8 +64,10 @@ def set_ldap_user_password(username, password): :param str username: the user name :param str password: teh clear text password - :return: :py:const:`True` if the password has been set, :py:const:`False` - if the user does not exist. + :return: dictionary containing the username and a flag + :py:const:`password_set` that is set to :py:const:`True` if the + password has been set, :py:const:`False` if the user does not exist. + :rtype: dict """ @@ -76,8 +83,10 @@ def add_ldap_user_to_group(username, groupname): :param str groupname: the group name :raises celery.exceptions.Retry: if the user does not exist yet, :py:func:`create_ldap_user` should be called before - :return: True if the user has been added to the group otherwise False - :rtype: boolean + :return: dictionary containing the username, groupname and a flag + :py:const`added` that is as a :py:const:`True` if the user has been + added to the group otherwise to :py:const:`False` + :rtype: dict """ @@ -89,20 +98,48 @@ def remove_ldap_user_from_group(username, groupname): :param str username: the user name :param str groupname: the group name - :return: True if the user has been removed, False otherwise - :rtype: boolean + :return: dictionary containing the input parameters and a flag + :py:const:`removed` that is set to :py:const:`True` if the user has + been removed, False otherwise + :rtype: dict """ @shared_task -def delete_ldap_user(username): +def delete_ldap_user(username, *args, **kwargs): """ This task deletes the given user. :param str username: the user name - :return: True if the user has been deleted, False otherwise - :rtype: boolean + :return: dictionary containing the username and a flag :py:const:`deleted` + that is set to :py:const:`True` if the user has been deleted and is set + to :py:const:`False` otherwise + :rtype: dict + + .. note:: + + This variant can only be used at the beginning of a Celery task chain + or as a standalone task. + + Use :py:func:`ldaptasks.tasks.delete_ldap_user_chained` at other + positions in the task chain + + """ + + +@shared_task +def delete_ldap_user_chained(previous_result, *args, **kwargs): + """ + This task deletes the given user. + + :param dict previous_result: a dictionary describing the result of the + previous step in the Celery task chain. This dictionary must contain a + :py:const:`username` key + :return: a copy of the :py:obj:`previous_result` dictionary with a new + :py:const:`deleted` key set to :py:const:`True` if the user has been + deleted and set to :py:const:`False` otherwise + :rtype: dict """ @@ -113,8 +150,10 @@ def delete_ldap_group_if_empty(groupname): This task deletes the given group if it is empty. :param str groupname: the group name - :return: True if the user has been deleted, False otherwise - :rtype: boolean + :return: dictionary that contains the groupname and a flag + :py:const:`deleted` that is set to :py:const:`True` if the group has + been deleted and is set to :py:const:`False` otherwise + :rtype: dict """ @@ -122,10 +161,12 @@ def delete_ldap_group_if_empty(groupname): @shared_task def delete_ldap_group(groupname): """ - This taks deletes the given group. + This task deletes the given group. :param str groupname: the group name - :return: True if the user has been deleted, False otherwise - :rtype: boolean + :return: dictionary that contains the groupname and a flag + :py:const:`deleted` that is set to :py:const:`True` if the group has + been deleted and is set to :py:const:`False` otherwise + :rtype: dict """ diff --git a/gnuviechadmin/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/locale/de/LC_MESSAGES/django.po index d141bce..b67e270 100644 --- a/gnuviechadmin/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: gnuviechadmin\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-02-01 19:04+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" "PO-Revision-Date: 2015-02-01 19:04+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" @@ -181,12 +181,12 @@ msgstr "" "Mailadresse des Benutzers %(user_display)s ist." #: templates/account/login.html:4 templates/account/login.html.py:5 -#: templates/account/login.html:29 templates/base.html:82 +#: templates/account/login.html:30 templates/base.html.py:81 #: templates/registration/login.html:4 msgid "Sign In" msgstr "Anmelden" -#: templates/account/login.html:9 +#: templates/account/login.html:10 #, python-format msgid "" "Please sign in with one\n" @@ -198,11 +198,11 @@ msgstr "" "href=\"%(signup_url)s\">registrieren Sie sich für ein Konto auf " "%(site_name)s und melden Sie sich unten an:" -#: templates/account/login.html:16 +#: templates/account/login.html:17 msgid "or" msgstr "oder" -#: templates/account/login.html:18 +#: templates/account/login.html:19 #, python-format msgid "" "If you have not created an account yet, then please\n" @@ -211,7 +211,7 @@ msgstr "" "Wenn Sie noch kein Konto haben, Registrieren Sie " "sich bitte erst." -#: templates/account/login.html:28 +#: templates/account/login.html:29 msgid "Forgot Password?" msgstr "Passwort vergessen?" @@ -342,12 +342,12 @@ msgstr "Ihr Passwort wurde geändert." msgid "Set Password" msgstr "Passwort setzen" -#: templates/account/signup.html:4 templates/socialaccount/signup.html:4 +#: templates/account/signup.html:4 templates/socialaccount/signup.html.py:4 msgid "Signup" msgstr "Registrieren" #: templates/account/signup.html:5 templates/account/signup.html.py:15 -#: templates/base.html:83 templates/socialaccount/signup.html:5 +#: templates/base.html:82 templates/socialaccount/signup.html.py:5 #: templates/socialaccount/signup.html:15 msgid "Sign Up" msgstr "Registrieren" @@ -425,86 +425,86 @@ msgstr "" "Hinweis: Sie können Ihre E-" "Mailadresse noch ändern." -#: templates/base.html:44 +#: templates/base.html:43 msgid "Dashboard" msgstr "Dashboard" -#: templates/base.html:50 templates/base.html.py:57 +#: templates/base.html:49 templates/base.html.py:56 msgid "Hosting" msgstr "Hosting" -#: templates/base.html:52 +#: templates/base.html:51 #: templates/hostingpackages/customerhostingpackage_list.html:5 #: templates/hostingpackages/customerhostingpackage_list.html:13 msgid "Your hosting packages" msgstr "Ihre Hostingpakete" -#: templates/base.html:53 +#: templates/base.html:52 #: templates/hostingpackages/customerhostingpackage_admin_list.html:3 #: templates/hostingpackages/customerhostingpackage_admin_list.html:4 msgid "All hosting packages" msgstr "Alle Hostingpakete" -#: templates/base.html:60 +#: templates/base.html:59 msgid "Links" msgstr "Links" -#: templates/base.html:62 +#: templates/base.html:61 msgid "Web based mail system" msgstr "Webbasiertes E-Mailsystem" -#: templates/base.html:62 +#: templates/base.html:61 msgid "Webmail" msgstr "Webmail" -#: templates/base.html:63 +#: templates/base.html:62 msgid "phpMyAdmin - MySQL database administration tool" msgstr "phpMyAdmin - MySQL-Datenbankverwaltungswerkzeug" -#: templates/base.html:63 +#: templates/base.html:62 msgid "phpMyAdmin" msgstr "phpMyAdmin" -#: templates/base.html:64 +#: templates/base.html:63 msgid "phpPgAdmin - PostgreSQL database administration tool" msgstr "phpPgAdmin - PostgreSQL-Datenbankverwaltungswerkzeug" -#: templates/base.html:64 +#: templates/base.html:63 msgid "phpPgAdmin" msgstr "phpPgAdmin" -#: templates/base.html:67 +#: templates/base.html:66 msgid "Imprint" msgstr "Impressum" -#: templates/base.html:68 templates/contact_form/contact_form.html:4 +#: templates/base.html:67 templates/contact_form/contact_form.html.py:4 #: templates/contact_form/contact_form.html:5 #: templates/contact_form/contact_success.html:4 #: templates/contact_form/contact_success.html:5 msgid "Contact" msgstr "Kontakt" -#: templates/base.html:73 +#: templates/base.html:72 msgid "My Account" msgstr "Mein Konto" -#: templates/base.html:75 +#: templates/base.html:74 msgid "Admin site" msgstr "Adminsite" -#: templates/base.html:76 +#: templates/base.html:75 msgid "Change Email" msgstr "E-Mail ändern" -#: templates/base.html:77 +#: templates/base.html:76 msgid "Social Accounts" msgstr "Konten in sozialen Netzwerken" -#: templates/base.html:78 +#: templates/base.html:77 msgid "Logout" msgstr "Abmelden" -#: templates/base.html:89 +#: templates/base.html:88 #, python-format msgid "" "Signed in as %(user_display)s" -#: templates/base.html:102 +#: templates/base.html:101 msgid "Close" msgstr "Schließen" diff --git a/gnuviechadmin/manage.py b/gnuviechadmin/manage.py index c950b97..d28aed8 100755 --- a/gnuviechadmin/manage.py +++ b/gnuviechadmin/manage.py @@ -3,7 +3,8 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gnuviechadmin.settings.local") + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "gnuviechadmin.settings") from django.core.management import execute_from_command_line diff --git a/gnuviechadmin/managemails/admin.py b/gnuviechadmin/managemails/admin.py index f059fd4..8bb386f 100644 --- a/gnuviechadmin/managemails/admin.py +++ b/gnuviechadmin/managemails/admin.py @@ -1,7 +1,7 @@ from django.utils.html import format_html from django.contrib import admin from django import forms -from django.forms.util import flatatt +from django.forms.utils import flatatt from django.utils.translation import ugettext as _ from .models import ( @@ -32,7 +32,7 @@ class ReadOnlyPasswordHashField(forms.Field): def bound_data(self, data, initial): return initial - def _has_changed(self, initial, data): + def has_changed(self, initial, data): return False diff --git a/gnuviechadmin/managemails/forms.py b/gnuviechadmin/managemails/forms.py index bd6a056..dc1032a 100644 --- a/gnuviechadmin/managemails/forms.py +++ b/gnuviechadmin/managemails/forms.py @@ -5,8 +5,8 @@ This module defines form classes for mailbox and mail address editing. from __future__ import absolute_import, unicode_literals from django import forms -from django.core.urlresolvers import reverse from django.core.validators import validate_email +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from crispy_forms.helper import FormHelper @@ -120,6 +120,7 @@ class MailAddressFieldMixin(forms.Form): label=_('Mailbox'), required=False, ) + # TODO: refactor as separate field class returning a list forwards = forms.CharField( label=_('Forwards'), required=False, @@ -187,6 +188,7 @@ class AddMailAddressForm(forms.ModelForm, MailAddressFieldMixin): return localpart def clean(self): + super(AddMailAddressForm, self).clean() data = self.cleaned_data if data['mailbox_or_forwards'] == MAILBOX_OR_FORWARDS.mailbox: if not data['mailbox']: @@ -194,7 +196,8 @@ class AddMailAddressForm(forms.ModelForm, MailAddressFieldMixin): elif data['mailbox_or_forwards'] == MAILBOX_OR_FORWARDS.forwards: if 'forwards' not in data or not data['forwards']: self.add_error('forwards', _('No forward addresses selected')) - else: + else: # pragma: no cover + # should not happen because of the field's validation raise forms.ValidationError( _('Illegal choice for target of the mail address')) @@ -213,7 +216,7 @@ class AddMailAddressForm(forms.ModelForm, MailAddressFieldMixin): if target_choice == MAILBOX_OR_FORWARDS.mailbox: mabox = self.instance.set_mailbox(data['mailbox'], commit=False) elif target_choice == MAILBOX_OR_FORWARDS.forwards: - targets = [part.strip() for part in data['forwards'].split()] + targets = [part.strip() for part in data['forwards'].split(',')] fwds = self.instance.set_forward_addresses(targets, commit=False) mailaddress = super(AddMailAddressForm, self).save(commit) if commit: @@ -275,7 +278,8 @@ class EditMailAddressForm(forms.ModelForm, MailAddressFieldMixin): elif data['mailbox_or_forwards'] == MAILBOX_OR_FORWARDS.forwards: if 'forwards' not in data or not data['forwards']: self.add_error('forwards', _('No forward addresses selected')) - else: + else: # pragma: no cover + # should not happen because of the field's validation raise forms.ValidationError( _('Illegal choice for target of the mail address')) @@ -291,4 +295,4 @@ class EditMailAddressForm(forms.ModelForm, MailAddressFieldMixin): elif data['mailbox_or_forwards'] == MAILBOX_OR_FORWARDS.forwards: targets = [part.strip() for part in data['forwards'].split(',')] self.instance.set_forward_addresses(targets, commit) - return self.instance + return super(EditMailAddressForm, self).save(commit) diff --git a/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po index 9aefc44..6e53cbc 100644 --- a/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: managemails\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-27 18:55+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" "PO-Revision-Date: 2015-01-25 22:17+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" @@ -23,7 +23,7 @@ msgstr "" msgid "Passwords don't match" msgstr "Passwörter stimmen nicht überein" -#: managemails/admin.py:21 managemails/tests/test_admin.py:37 +#: managemails/admin.py:21 managemails/tests/test_admin.py:40 msgid "Hash" msgstr "Hash-Code" @@ -47,11 +47,11 @@ msgstr "Deaktivieren" msgid "Mailboxes and Mail Addresses" msgstr "Postfächer und E-Mailadressen" -#: managemails/forms.py:29 managemails/forms.py:120 managemails/models.py:103 +#: managemails/forms.py:29 managemails/forms.py:120 managemails/models.py:120 msgid "Mailbox" msgstr "Postfach" -#: managemails/forms.py:30 managemails/forms.py:124 +#: managemails/forms.py:30 managemails/forms.py:125 msgid "Forwards" msgstr "Weiterleitungen" @@ -67,84 +67,84 @@ msgstr "Passwort setzen" msgid "Mailbox or Forwards" msgstr "Postfach oder Weiterleitungen" -#: managemails/forms.py:128 +#: managemails/forms.py:129 msgid "Please enter one or more email addresses separated by commas." msgstr "" "Bitte geben Sie eine oder mehrere durch Kommata getrennte E-Mailadressen ein." -#: managemails/forms.py:175 +#: managemails/forms.py:176 msgid "Add mail address" msgstr "E-Mailadresse hinzufügen" -#: managemails/forms.py:185 +#: managemails/forms.py:186 msgid "This mail address is already in use." msgstr "Diese E-Mailadresse wird bereits verwendet." -#: managemails/forms.py:193 managemails/forms.py:274 +#: managemails/forms.py:195 managemails/forms.py:277 msgid "No mailbox selected" msgstr "Kein Postfach ausgewählt" -#: managemails/forms.py:196 managemails/forms.py:277 +#: managemails/forms.py:198 managemails/forms.py:280 msgid "No forward addresses selected" msgstr "Keine Weiterleitungsadressen ausgewählt" -#: managemails/forms.py:199 managemails/forms.py:280 +#: managemails/forms.py:202 managemails/forms.py:284 msgid "Illegal choice for target of the mail address" msgstr "Ungültige Auswahl für das Ziel der E-Mailadresse" -#: managemails/forms.py:267 +#: managemails/forms.py:270 msgid "Change mail address targets" msgstr "E-Mailadressziele ändern" -#: managemails/models.py:104 +#: managemails/models.py:121 msgid "Mailboxes" msgstr "Postfächer" -#: managemails/models.py:145 +#: managemails/models.py:164 msgid "local part" msgstr "Lokaler Teil" -#: managemails/models.py:146 +#: managemails/models.py:165 msgid "domain" msgstr "Domain" -#: managemails/models.py:151 +#: managemails/models.py:170 msgid "Mail address" msgstr "E-Mailadresse" -#: managemails/models.py:152 +#: managemails/models.py:171 msgid "Mail addresses" msgstr "E-Mailadressen" -#: managemails/models.py:229 +#: managemails/models.py:253 msgid "mailaddress" msgstr "E-Mailadresse" -#: managemails/models.py:230 +#: managemails/models.py:254 msgid "mailbox" msgstr "Postfach" -#: managemails/views.py:51 +#: managemails/views.py:52 msgid "You are not allowed to add more mailboxes to this hosting package" msgstr "Sie können keine weiteren Postfächer zu diesem Hostingpaket hinzufügen" -#: managemails/views.py:70 +#: managemails/views.py:71 #, python-brace-format msgid "Mailbox {mailbox} created successfully." msgstr "Postfach {mailbox} erfolgreich angelegt." -#: managemails/views.py:105 +#: managemails/views.py:106 #, python-brace-format msgid "Successfully set new password for mailbox {mailbox}." msgstr "" "Für das Postfach {mailbox} wurde erfolgreich ein neues Passwort gesetzt." -#: managemails/views.py:144 +#: managemails/views.py:145 #, python-brace-format msgid "Successfully added mail address {mailaddress}" msgstr "E-Mailadresse {mailaddress} erfolgreich hinzugefügt" -#: managemails/views.py:222 +#: managemails/views.py:223 #, python-brace-format msgid "Successfully updated mail address {mailaddress} targets." msgstr "Ziele der E-Mailadresse {mailaddress} erfolgreich aktualisiert." diff --git a/gnuviechadmin/managemails/migrations/0001_initial.py b/gnuviechadmin/managemails/migrations/0001_initial.py index 293f59d..7b7b71f 100644 --- a/gnuviechadmin/managemails/migrations/0001_initial.py +++ b/gnuviechadmin/managemails/migrations/0001_initial.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone import model_utils.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('domains', '0001_initial'), ('osusers', '0001_initial'), @@ -17,9 +16,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='MailAddress', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), ('active', models.BooleanField(default=True)), ('localpart', models.CharField(max_length=128)), ], @@ -32,9 +37,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='MailAddressForward', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), ('target', models.EmailField(max_length=254)), ], options={ @@ -44,9 +55,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='MailAddressMailbox', fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('mailaddress', models.OneToOneField(primary_key=True, serialize=False, to='managemails.MailAddress')), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('mailaddress', models.OneToOneField( + primary_key=True, serialize=False, + to='managemails.MailAddress', on_delete=models.CASCADE)), ], options={ }, @@ -55,13 +72,20 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Mailbox', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), ('active', models.BooleanField(default=True)), ('username', models.CharField(unique=True, max_length=128)), ('password', models.CharField(max_length=255)), - ('osuser', models.ForeignKey(to='osusers.User')), + ('osuser', models.ForeignKey( + to='osusers.User', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Mailbox', @@ -72,31 +96,34 @@ class Migration(migrations.Migration): migrations.AddField( model_name='mailaddressmailbox', name='mailbox', - field=models.ForeignKey(to='managemails.Mailbox'), + field=models.ForeignKey( + to='managemails.Mailbox', on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( name='mailaddressmailbox', - unique_together=set([('mailaddress', 'mailbox')]), + unique_together={('mailaddress', 'mailbox')}, ), migrations.AddField( model_name='mailaddressforward', name='mailaddress', - field=models.ForeignKey(to='managemails.MailAddress'), + field=models.ForeignKey( + to='managemails.MailAddress', on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( name='mailaddressforward', - unique_together=set([('mailaddress', 'target')]), + unique_together={('mailaddress', 'target')}, ), migrations.AddField( model_name='mailaddress', name='domain', - field=models.ForeignKey(to='domains.MailDomain'), + field=models.ForeignKey( + to='domains.MailDomain', on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( name='mailaddress', - unique_together=set([('localpart', 'domain')]), + unique_together={('localpart', 'domain')}, ), ] diff --git a/gnuviechadmin/managemails/migrations/0003_auto_20150124_2029.py b/gnuviechadmin/managemails/migrations/0003_auto_20150124_2029.py index d9c31a0..7863dbb 100644 --- a/gnuviechadmin/managemails/migrations/0003_auto_20150124_2029.py +++ b/gnuviechadmin/managemails/migrations/0003_auto_20150124_2029.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('managemails', '0002_auto_20150117_1238'), ] @@ -14,13 +13,17 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='mailaddressmailbox', name='mailaddress', - field=models.OneToOneField(primary_key=True, serialize=False, to='managemails.MailAddress', verbose_name='mailaddress'), + field=models.OneToOneField( + primary_key=True, serialize=False, to='managemails.MailAddress', + verbose_name='mailaddress', on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( model_name='mailaddressmailbox', name='mailbox', - field=models.ForeignKey(verbose_name='mailbox', to='managemails.Mailbox'), + field=models.ForeignKey( + verbose_name='mailbox', to='managemails.Mailbox', + on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/gnuviechadmin/managemails/migrations/0004_auto_20150125_1825.py b/gnuviechadmin/managemails/migrations/0004_auto_20150125_1825.py index 893ca63..d1afccf 100644 --- a/gnuviechadmin/managemails/migrations/0004_auto_20150125_1825.py +++ b/gnuviechadmin/managemails/migrations/0004_auto_20150125_1825.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('managemails', '0003_auto_20150124_2029'), ] @@ -14,7 +13,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='mailaddress', name='domain', - field=models.ForeignKey(verbose_name='domain', to='domains.MailDomain'), + field=models.ForeignKey( + verbose_name='domain', to='domains.MailDomain', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( diff --git a/gnuviechadmin/managemails/models.py b/gnuviechadmin/managemails/models.py index 397189e..5d30164 100644 --- a/gnuviechadmin/managemails/models.py +++ b/gnuviechadmin/managemails/models.py @@ -85,6 +85,23 @@ class MailboxManager(models.Manager): active=True, osuser=osuser, ) + def create_mailbox(self, osuser, password=None, commit=True): + """ + Create a new mailbox for the given operating system user. + + :param osuser: a :py:class:`osuser.models.OsUser` instance + :param password: an optional password + :param commit: whether the mailbox should be commited to the database + :return: mailbox instance + :rtype: :py:class:`managemails.models.Mailbox` + + """ + mailbox = self.create( + osuser=osuser, username=self.get_next_mailbox_name(osuser)) + if password is not None: + mailbox.set_password(password) + return mailbox + @python_2_unicode_compatible class Mailbox(ActivateAbleMixin, TimeStampedModel): @@ -92,7 +109,7 @@ class Mailbox(ActivateAbleMixin, TimeStampedModel): This is the model class for a mailbox. """ - osuser = models.ForeignKey(OsUser) + osuser = models.ForeignKey(OsUser, on_delete=models.CASCADE) username = models.CharField(max_length=128, unique=True) password = models.CharField(max_length=255) @@ -113,10 +130,12 @@ class Mailbox(ActivateAbleMixin, TimeStampedModel): self.password = sha512_crypt.encrypt(password) def save(self, *args, **kwargs): + # TODO: refactor to use signals create_file_mailbox.delay(self.osuser.username, self.username).get() super(Mailbox, self).save(*args, **kwargs) def delete(self, *args, **kwargs): + # TODO: refactor to use signals delete_file_mailbox.delay(self.osuser.username, self.username).get() super(Mailbox, self).delete(*args, **kwargs) @@ -143,7 +162,8 @@ class MailAddress(ActivateAbleMixin, TimeStampedModel, models.Model): """ localpart = models.CharField(_('local part'), max_length=128) - domain = models.ForeignKey(MailDomain, verbose_name=_('domain')) + domain = models.ForeignKey( + MailDomain, verbose_name=_('domain'), on_delete=models.CASCADE) class Meta: ordering = ['domain', 'localpart'] @@ -176,9 +196,12 @@ class MailAddress(ActivateAbleMixin, TimeStampedModel, models.Model): for mafwd in MailAddressForward.objects.filter(mailaddress=self): mafwd.delete() else: - mabox = MailAddressMailbox(mailaddress=self, mailbox=mailbox) if commit: + self.save() + mabox = MailAddressMailbox(mailaddress=self, mailbox=mailbox) mabox.save() + else: + mabox = MailAddressMailbox(mailaddress=self, mailbox=mailbox) return mabox def set_forward_addresses(self, addresses, commit=True): @@ -200,17 +223,19 @@ class MailAddress(ActivateAbleMixin, TimeStampedModel, models.Model): forwards = MailAddressForward.objects.filter( mailaddress=self).all() for item in forwards: - if not item.target in addresses: + if item.target not in addresses: item.delete() else: retval.append(item) for target in addresses: - if not target in [item.target for item in forwards]: + if target not in [item.target for item in forwards]: mafwd = MailAddressForward(mailaddress=self, target=target) if commit: mafwd.save() retval.append(mafwd) else: + if commit: + self.save() for target in addresses: mafwd = MailAddressForward(mailaddress=self, target=target) if commit: @@ -226,8 +251,10 @@ class MailAddressMailbox(TimeStampedModel, models.Model): """ mailaddress = models.OneToOneField( - MailAddress, verbose_name=_('mailaddress'), primary_key=True) - mailbox = models.ForeignKey(Mailbox, verbose_name=_('mailbox')) + MailAddress, verbose_name=_('mailaddress'), primary_key=True, + on_delete=models.CASCADE) + mailbox = models.ForeignKey( + Mailbox, verbose_name=_('mailbox'), on_delete=models.CASCADE) class Meta: unique_together = ('mailaddress', 'mailbox') @@ -241,7 +268,7 @@ class MailAddressForward(TimeStampedModel, models.Model): This is a model class to map mail addresses to forwarding addresses. """ - mailaddress = models.ForeignKey(MailAddress) + mailaddress = models.ForeignKey(MailAddress, on_delete=models.CASCADE) target = models.EmailField(max_length=254) class Meta: diff --git a/gnuviechadmin/managemails/tests/test_admin.py b/gnuviechadmin/managemails/tests/test_admin.py index 91a6808..ddacf4b 100644 --- a/gnuviechadmin/managemails/tests/test_admin.py +++ b/gnuviechadmin/managemails/tests/test_admin.py @@ -1,14 +1,14 @@ from django import forms -from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings +from django.urls import reverse from django.utils.html import format_html from django.utils.translation import ugettext as _ from django.contrib.admin import AdminSite from django.contrib.auth import get_user_model -from mock import Mock +from unittest.mock import Mock from osusers.models import User @@ -21,9 +21,7 @@ from managemails.admin import ( ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget, ) -from managemails.models import ( - Mailbox, -) +from managemails.models import Mailbox Customer = get_user_model() @@ -31,14 +29,14 @@ Customer = get_user_model() class ReadOnlyPasswordHashWidgetTest(TestCase): def test_render(self): widget = ReadOnlyPasswordHashWidget() - rendered = widget.render('password', 'secret', {'class': 'test'}) + rendered = widget.render("password", "secret", {"class": "test"}) self.assertEqual( rendered, format_html( '
{0}
', - format_html('{0}: secret ', - _('Hash')) - )) + format_html("{0}: secret ", _("Hash")), + ), + ) class ReadOnlyPasswordHashFieldTest(TestCase): @@ -48,24 +46,24 @@ class ReadOnlyPasswordHashFieldTest(TestCase): def test_bound_data(self): field = ReadOnlyPasswordHashField() - self.assertEqual(field.bound_data('new', 'old'), 'old') + self.assertEqual(field.bound_data("new", "old"), "old") def test__has_changed(self): field = ReadOnlyPasswordHashField() - self.assertFalse(field._has_changed('new', 'old')) + self.assertFalse(field.has_changed("new", "old")) class CustomerTestCase(TestCase): def setUp(self): super(CustomerTestCase, self).setUp() - self.customer = Customer.objects.create(username='test') + self.customer = Customer.objects.create(username="test") class MailboxCreationFormTest(CustomerTestCase): def test_clean_password2_same(self): form = MailboxCreationForm() - form.cleaned_data = {'password1': 'secret', 'password2': 'secret'} - self.assertEqual(form.clean_password2(), 'secret') + form.cleaned_data = {"password1": "secret", "password2": "secret"} + self.assertEqual(form.clean_password2(), "secret") def test_clean_password2_empty(self): form = MailboxCreationForm() @@ -74,59 +72,47 @@ class MailboxCreationFormTest(CustomerTestCase): def test_clean_password2_mismatch(self): form = MailboxCreationForm() - form.cleaned_data = {'password1': 'secretx', 'password2': 'secrety'} + form.cleaned_data = {"password1": "secretx", "password2": "secrety"} with self.assertRaises(forms.ValidationError) as cm: form.clean_password2() self.assertEqual(cm.exception.message, PASSWORD_MISMATCH_ERROR) @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_save_commit(self): user = User.objects.create_user(customer=self.customer) - form = MailboxCreationForm(data={ - 'osuser': user.uid, - 'password1': 'secret', - 'password2': 'secret', - }) + form = MailboxCreationForm( + data={"osuser": user.uid, "password1": "secret", "password2": "secret"} + ) mailbox = form.save() self.assertIsNotNone(mailbox) - self.assertEqual( - len(Mailbox.objects.filter(osuser=user)), 1) + self.assertEqual(len(Mailbox.objects.filter(osuser=user)), 1) @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_save_no_commit(self): user = User.objects.create_user(customer=self.customer) - form = MailboxCreationForm(data={ - 'osuser': user.uid, - 'password1': 'secret', - 'password2': 'secret', - }) + form = MailboxCreationForm( + data={"osuser": user.uid, "password1": "secret", "password2": "secret"} + ) mailbox = form.save(commit=False) self.assertIsNotNone(mailbox) - self.assertEqual( - len(Mailbox.objects.filter(osuser=user)), 0) + self.assertEqual(len(Mailbox.objects.filter(osuser=user)), 0) class MailboxChangeFormTest(CustomerTestCase): @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_clean_password(self): mailbox = Mailbox( - username='test', - osuser=User.objects.create_user(customer=self.customer)) - mailbox.set_password('test') + username="test", osuser=User.objects.create_user(customer=self.customer) + ) + mailbox.set_password("test") mailbox.save() - form = MailboxChangeForm(instance=mailbox, data={'password': 'blub'}) + form = MailboxChangeForm(instance=mailbox, data={"password": "blub"}) self.assertEqual(form.clean_password(), mailbox.password) @@ -151,55 +137,43 @@ class MailBoxAdminTest(CustomerTestCase): self.mbadmin = MailboxAdmin(Mailbox, site) def test_get_fieldsets_without_object(self): - self.assertEqual( - self.mbadmin.get_fieldsets(Mock()), - self.mbadmin.add_fieldsets) + self.assertEqual(self.mbadmin.get_fieldsets(Mock()), self.mbadmin.add_fieldsets) @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_get_fieldsets_with_object(self): mailbox = Mailbox( - username='test', - osuser=User.objects.create_user(customer=self.customer)) - mailbox.set_password('test') + username="test", osuser=User.objects.create_user(customer=self.customer) + ) + mailbox.set_password("test") mailbox.save() self.assertEqual( - self.mbadmin.get_fieldsets(Mock(), mailbox), - self.mbadmin.fieldsets) + self.mbadmin.get_fieldsets(Mock(), mailbox), self.mbadmin.fieldsets + ) def test_get_form_without_object(self): form = self.mbadmin.get_form(Mock) - self.assertEqual( - form.Meta.fields, - ['osuser', 'password1', 'password2'] - ) + self.assertEqual(form.Meta.fields, ["osuser", "password1", "password2"]) @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_get_form_with_object(self): mailbox = Mailbox( - username='test', - osuser=User.objects.create_user(customer=self.customer)) - mailbox.set_password('test') + username="test", osuser=User.objects.create_user(customer=self.customer) + ) + mailbox.set_password("test") mailbox.save() form = self.mbadmin.get_form(Mock, mailbox) - self.assertEqual( - form.Meta.fields, - ['osuser', 'username', 'password', 'active'] - ) + self.assertEqual(form.Meta.fields, ["osuser", "username", "password", "active"]) def test_admin_for_mailbox(self): - admin_url = reverse('admin:managemails_mailaddress_changelist') + admin_url = reverse("admin:managemails_mailaddress_changelist") self.assertIsNotNone(admin_url) class MailAddressAdminTest(TestCase): def test_admin_for_mailaddress(self): - admin_url = reverse('admin:managemails_mailaddress_changelist') + admin_url = reverse("admin:managemails_mailaddress_changelist") self.assertIsNotNone(admin_url) diff --git a/gnuviechadmin/managemails/tests/test_forms.py b/gnuviechadmin/managemails/tests/test_forms.py index e69de29..be72a61 100644 --- a/gnuviechadmin/managemails/tests/test_forms.py +++ b/gnuviechadmin/managemails/tests/test_forms.py @@ -0,0 +1,599 @@ +""" +This module provides tests for :py:mod:`managemails.forms`. + +""" +from unittest.mock import MagicMock, Mock, patch, ANY + +from django.forms import ValidationError +from django.test import TestCase +from django.urls import reverse + +from managemails.forms import ( + AddMailAddressForm, + ChangeMailboxPasswordForm, + CreateMailboxForm, + EditMailAddressForm, + MAILBOX_OR_FORWARDS, + MailAddressFieldMixin, + multiple_email_validator, +) + + +class CreateMailboxFormTest(TestCase): + def test_constructor_needs_hostingpackage(self): + instance = MagicMock() + with self.assertRaises(KeyError): + CreateMailboxForm(instance) + + def test_constructor(self): + hostingpackage = Mock(id=42) + instance = MagicMock() + form = CreateMailboxForm(instance, hostingpackage=hostingpackage) + self.assertTrue(hasattr(form, "hosting_package")) + self.assertEqual(form.hosting_package, hostingpackage) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, reverse("create_mailbox", kwargs={"package": 42}) + ) + self.assertIn("password1", form.fields) + self.assertIn("password2", form.fields) + self.assertEqual(form.helper.inputs[0].name, "submit") + + @patch("managemails.forms.Mailbox.objects") + def test_save(self, mailbox_objects): + osuser = MagicMock() + hostingpackage = Mock(id=42, osuser=osuser) + instance = MagicMock() + form = CreateMailboxForm( + instance=instance, + hostingpackage=hostingpackage, + data={"password1": "secret", "password2": "secret"}, + ) + mailbox_objects.get_next_mailbox_name.return_value = "mailbox23" + self.assertTrue(form.is_valid()) + form.save(commit=False) + self.assertEqual(osuser, form.instance.osuser) + self.assertEqual("mailbox23", form.instance.username) + instance.set_password.assert_called_with("secret") + + +class ChangeMailboxPasswordFormTest(TestCase): + def test_constructor_needs_hostingpackage(self): + instance = MagicMock() + with self.assertRaises(KeyError): + ChangeMailboxPasswordForm(instance) + + def test_constructor(self): + hostingpackage = Mock(id=42) + instance = MagicMock(username="testuser") + form = ChangeMailboxPasswordForm( + instance=instance, hostingpackage=hostingpackage + ) + self.assertTrue(hasattr(form, "hosting_package")) + self.assertEqual(form.hosting_package, hostingpackage) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse( + "change_mailbox_password", kwargs={"package": 42, "slug": "testuser"} + ), + ) + self.assertIn("password1", form.fields) + self.assertIn("password2", form.fields) + self.assertEqual(form.helper.inputs[0].name, "submit") + + def test_save(self): + hostingpackage = Mock(id=42) + instance = MagicMock(username="testuser") + form = ChangeMailboxPasswordForm( + instance=instance, + hostingpackage=hostingpackage, + data={"password1": "newsecret", "password2": "newsecret"}, + ) + self.assertTrue(form.is_valid()) + form.save(commit=False) + instance.set_password.assert_called_with("newsecret") + + +class MultipleEmailValidatorTest(TestCase): + def test_valid_single_address(self): + self.assertEqual( + "test@example.org", multiple_email_validator("test@example.org") + ) + + def test_valid_multiple_addresses(self): + self.assertEqual( + "test1@example.org,test2@example.org", + multiple_email_validator("test1@example.org,test2@example.org"), + ) + + def test_empty(self): + self.assertEqual("", multiple_email_validator("")) + + def test_none(self): + self.assertIsNone(multiple_email_validator(None)) + + def test_invalid_single_address(self): + with self.assertRaises(ValidationError): + multiple_email_validator("no@ddress") + + def test_invalid_multiple_addresses(self): + with self.assertRaises(ValidationError): + multiple_email_validator("test1@example.org,no@ddress") + + +class MailAddressFieldMixinTest(TestCase): + def test_fields_defined(self): + form = MailAddressFieldMixin() + self.assertIn("mailbox_or_forwards", form.fields) + self.assertIn("mailbox", form.fields) + self.assertIn("forwards", form.fields) + + +class AddMailAddressFormTest(TestCase): + def setUp(self): + self.patcher1 = patch("managemails.forms.Mailbox.objects") + self.patcher2 = patch("managemails.forms.MailAddress.objects") + self.mailbox_objects = self.patcher1.start() + self.mailaddress_objects = self.patcher2.start() + + def tearDown(self): + self.patcher2.stop() + self.patcher1.stop() + + def test_constructor_needs_hostingpackage(self): + instance = MagicMock() + with self.assertRaises(KeyError): + AddMailAddressForm(instance=instance, maildomain=MagicMock()) + + def test_constructor_needs_maildomain(self): + instance = MagicMock() + with self.assertRaises(KeyError): + AddMailAddressForm(instance=instance, hostingpackage=MagicMock()) + + def test_constructor(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, hostingpackage=hostingpackage, maildomain=maildomain + ) + self.mailbox_objects.unused.assert_called_with(osuser=osuser) + self.assertIn("mailbox_or_forwards", form.fields) + self.assertIn("mailbox", form.fields) + self.assertIn("forwards", form.fields) + self.assertTrue(hasattr(form, "hosting_package")) + self.assertEqual(form.hosting_package, hostingpackage) + self.assertTrue(hasattr(form, "maildomain")) + self.assertEqual(form.maildomain, maildomain) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse("add_mailaddress", kwargs={"package": 42, "domain": "example.org"}), + ) + self.assertEqual(len(form.helper.layout), 2) + self.assertEqual(form.helper.layout[1].name, "submit") + + def test_clean_localpart_valid(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "forwards": "test2@example.org", + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = False + self.assertTrue(form.is_valid()) + self.assertEqual("test", form.clean_localpart()) + + def test_clean_localpart_duplicate(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "forwards": "test2@example.org", + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = True + self.assertFalse(form.is_valid()) + self.assertIn("localpart", form.errors) + + def test_clean_no_mailbox_choice(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.mailbox, + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = False + self.assertFalse(form.is_valid()) + self.assertIn("mailbox", form.errors) + + def test_clean_no_forward_address_choice(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = False + self.assertFalse(form.is_valid()) + self.assertIn("forwards", form.errors) + + def test_save_with_forwards_no_commit(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "forwards": "test2@example.org,test3@example.org", + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = False + self.assertTrue(form.is_valid()) + address1 = MagicMock(mailaddress="test2@example.org") + address2 = MagicMock(mailaddress="test3@example.org") + instance.set_forward_addresses.return_value = [address1, address2] + form.save(commit=False) + self.assertEqual(maildomain, instance.domain) + instance.set_forward_addresses.assert_called_with( + ["test2@example.org", "test3@example.org"], commit=False + ) + address1.save.assert_not_called() + address2.save.assert_not_called() + instance.save.assert_not_called() + + def test_save_with_forwards_commit(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "forwards": "test2@example.org,test3@example.org", + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = False + self.assertTrue(form.is_valid()) + address1 = MagicMock(mailaddress="test2@example.org") + address2 = MagicMock(mailaddress="test3@example.org") + instance.set_forward_addresses.return_value = [address1, address2] + form.save(commit=True) + self.assertEqual(maildomain, instance.domain) + instance.set_forward_addresses.assert_called_with( + ["test2@example.org", "test3@example.org"], commit=False + ) + address1.save.assert_called_with() + address2.save.assert_called_with() + instance.save.assert_called_with() + + def test_save_with_mailbox_no_commit(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.mailbox, + "mailbox": "mailbox23", + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = False + self.assertTrue(form.is_valid()) + mailbox = MagicMock(osuser=osuser, username="testuserp01") + instance.set_mailbox.return_value = mailbox + form.save(commit=False) + self.assertEqual(maildomain, instance.domain) + instance.set_mailbox.assert_called_with(ANY, commit=False) + mailbox.save.assert_not_called() + instance.save.assert_not_called() + + def test_save_with_mailbox_commit(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.mailbox, + "mailbox": "mailbox23", + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = False + self.assertTrue(form.is_valid()) + mailbox = MagicMock(osuser=osuser, username="testuserp01") + instance.set_mailbox.return_value = mailbox + form.save(commit=True) + self.assertEqual(maildomain, instance.domain) + instance.set_mailbox.assert_called_with(ANY, commit=False) + instance.set_mailbox.return_value.save.assert_called_with() + mailbox.save.assert_called_with() + instance.save.assert_called_with() + + def test_save_with_other_choice(self): + instance = MagicMock() + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = AddMailAddressForm( + instance=instance, + hostingpackage=hostingpackage, + maildomain=maildomain, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.mailbox, + "mailbox": "mailbox23", + }, + ) + self.mailaddress_objects.filter( + domain=maildomain, localpart="test" + ).exists.return_value = False + self.assertTrue(form.is_valid()) + form.cleaned_data["mailbox_or_forwards"] = -1 + address1 = MagicMock(mailaddress="test2@example.org") + address2 = MagicMock(mailaddress="test3@example.org") + instance.set_forward_addresses.return_value = [address1, address2] + mailbox = MagicMock(osuser=osuser, username="testuserp01") + instance.set_mailbox.return_value = mailbox + form.save(commit=True) + instance.set_mailbox.assert_not_called() + instance.set_forward_addresses.assert_not_called() + address1.save.assert_not_called() + address2.save.assert_not_called() + mailbox.save.assert_not_called() + instance.save.assert_called_with() + + +class EditMailAddressFormTest(TestCase): + def setUp(self): + self.patcher1 = patch("managemails.forms.Mailbox.objects") + self.patcher2 = patch("managemails.forms.MailAddress.objects") + self.mailbox_objects = self.patcher1.start() + self.mailaddress_objects = self.patcher2.start() + + def tearDown(self): + self.patcher2.stop() + self.patcher1.stop() + + def test_constructor_needs_hostingpackage(self): + instance = MagicMock() + with self.assertRaises(KeyError): + EditMailAddressForm(instance=instance, maildomain=MagicMock()) + + def test_constructor_needs_maildomain(self): + instance = MagicMock() + with self.assertRaises(KeyError): + EditMailAddressForm(instance=instance, hostingpackage=MagicMock()) + + def test_constructor(self): + instance = MagicMock(id=23) + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = EditMailAddressForm( + instance=instance, maildomain=maildomain, hostingpackage=hostingpackage + ) + self.mailbox_objects.unused_or_own.assert_called_with(instance, osuser) + self.assertIn("mailbox_or_forwards", form.fields) + self.assertIn("mailbox", form.fields) + self.assertIn("forwards", form.fields) + self.assertTrue(hasattr(form, "hosting_package")) + self.assertEqual(form.hosting_package, hostingpackage) + self.assertTrue(hasattr(form, "maildomain")) + self.assertEqual(form.maildomain, maildomain) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse( + "edit_mailaddress", + kwargs={"package": 42, "domain": "example.org", "pk": 23}, + ), + ) + self.assertEqual(len(form.helper.layout), 2) + self.assertEqual(form.helper.layout[1].name, "submit") + + def test_clean_no_mailbox_choice(self): + instance = MagicMock(id=23) + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = EditMailAddressForm( + instance=instance, + maildomain=maildomain, + hostingpackage=hostingpackage, + data={"mailbox_or_forwards": MAILBOX_OR_FORWARDS.mailbox}, + ) + self.assertFalse(form.is_valid()) + self.assertIn("mailbox", form.errors) + + def test_clean_no_forward_address_choice(self): + instance = MagicMock(id=23) + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = EditMailAddressForm( + instance=instance, + maildomain=maildomain, + hostingpackage=hostingpackage, + data={"mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards}, + ) + self.assertFalse(form.is_valid()) + self.assertIn("forwards", form.errors) + + def test_save_with_forwards_no_commit(self): + instance = MagicMock(id=23) + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = EditMailAddressForm( + instance=instance, + maildomain=maildomain, + hostingpackage=hostingpackage, + data={ + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "forwards": "test2@example.org,test3@example.org", + }, + ) + self.assertTrue(form.is_valid()) + address1 = MagicMock(mailaddress="test2@example.org") + address2 = MagicMock(mailaddress="test3@example.org") + instance.set_forward_addresses.return_value = [address1, address2] + form.save(commit=False) + instance.set_forward_addresses.assert_called_with( + ["test2@example.org", "test3@example.org"], False + ) + address1.save.assert_not_called() + address2.save.assert_not_called() + instance.save.assert_not_called() + + def test_save_with_forwards_commit(self): + instance = MagicMock(id=23) + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = EditMailAddressForm( + instance=instance, + maildomain=maildomain, + hostingpackage=hostingpackage, + data={ + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "forwards": "test2@example.org,test3@example.org", + }, + ) + self.assertTrue(form.is_valid()) + address1 = MagicMock(mailaddress="test2@example.org") + address2 = MagicMock(mailaddress="test3@example.org") + instance.set_forward_addresses.return_value = [address1, address2] + form.save(commit=True) + instance.set_forward_addresses.assert_called_with( + ["test2@example.org", "test3@example.org"], True + ) + instance.save.assert_called_with() + + def test_save_with_mailbox_no_commit(self): + instance = MagicMock(id=23) + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = EditMailAddressForm( + instance=instance, + maildomain=maildomain, + hostingpackage=hostingpackage, + data={ + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.mailbox, + "mailbox": "mailbox23", + }, + ) + self.assertTrue(form.is_valid()) + mailbox = MagicMock(osuser=osuser, username="testuserp01") + instance.set_mailbox.return_value = mailbox + form.save(commit=False) + instance.set_mailbox.assert_called_with(ANY, False) + mailbox.save.assert_not_called() + instance.save.assert_not_called() + + def test_save_with_mailbox_commit(self): + instance = MagicMock(id=23) + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = EditMailAddressForm( + instance=instance, + maildomain=maildomain, + hostingpackage=hostingpackage, + data={ + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.mailbox, + "mailbox": "mailbox23", + }, + ) + self.assertTrue(form.is_valid()) + mailbox = MagicMock(osuser=osuser, username="testuserp01") + instance.set_mailbox.return_value = mailbox + self.mailbox_objects.unused_or_own.get.return_value = mailbox + form.save(commit=True) + instance.set_mailbox.assert_called_with(ANY, True) + instance.save.assert_called_with() + + def test_save_with_other_choice(self): + instance = MagicMock(id=23) + osuser = Mock(username="testuser") + hostingpackage = MagicMock(id=42, osuser=osuser) + maildomain = MagicMock(domain="example.org") + form = EditMailAddressForm( + instance=instance, + maildomain=maildomain, + hostingpackage=hostingpackage, + data={ + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.mailbox, + "mailbox": "mailbox23", + }, + ) + self.assertTrue(form.is_valid()) + form.cleaned_data["mailbox_or_forwards"] = -1 + form.save(commit=True) + instance.set_mailbox.assert_not_called() + instance.save.assert_called_with() diff --git a/gnuviechadmin/managemails/tests/test_models.py b/gnuviechadmin/managemails/tests/test_models.py index 4b20b0c..895a078 100644 --- a/gnuviechadmin/managemails/tests/test_models.py +++ b/gnuviechadmin/managemails/tests/test_models.py @@ -1,4 +1,9 @@ -from django.test import TestCase +""" +This module contains tests for :py:mod:`managemails.models` +""" +from unittest.mock import patch + +from django.test import TestCase, TransactionTestCase from django.test.utils import override_settings from django.contrib.auth import get_user_model @@ -7,39 +12,306 @@ from passlib.hash import sha512_crypt from domains.models import MailDomain from osusers.models import User -from managemails.models import ( - MailAddress, - Mailbox, -) +from managemails.models import MailAddress, Mailbox Customer = get_user_model() @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) class MailboxTest(TestCase): def setUp(self): super(MailboxTest, self).setUp() - self.customer = Customer.objects.create_user('test') + self.customer = Customer.objects.create_user("test") def test_set_password(self): user = User.objects.create_user(self.customer) - mb = Mailbox.objects.create(username='test', osuser=user) - mb.set_password('test') - self.assertTrue(sha512_crypt.verify('test', mb.password)) + mb = Mailbox.objects.create(username="test", osuser=user) + mb.set_password("test") + self.assertTrue(sha512_crypt.verify("test", mb.password)) def test___str__(self): user = User.objects.create_user(self.customer) - mb = Mailbox.objects.create(username='test', osuser=user) - mb.set_password('test') - self.assertEqual(str(mb), 'test') + mb = Mailbox.objects.create(username="test", osuser=user) + mb.set_password("test") + self.assertEqual(str(mb), "test") + + @patch("managemails.models.create_file_mailbox") + def test_save(self, create_file_mailbox_task): + user = User.objects.create_user(self.customer) + mb = Mailbox.objects.create_mailbox(user) + self.assertIsNotNone(mb.pk) + create_file_mailbox_task.delay.assert_called_with(user.username, mb.username) + + @patch("managemails.models.delete_file_mailbox") + def test_delete(self, delete_file_mailbox_task): + user = User.objects.create_user(self.customer) + mb = Mailbox.objects.create_mailbox(user) + mb.delete() + self.assertIsNone(mb.pk) + delete_file_mailbox_task.delay.assert_called_with(user.username, mb.username) + + def test_get_mailaddresses(self): + user = User.objects.create_user(self.customer) + mb = Mailbox.objects.create_mailbox(user) + md = MailDomain.objects.create(domain="example.org") + address = MailAddress.objects.create(localpart="test", domain=md) + address.set_mailbox(mb) + mailaddresses = mb.get_mailaddresses() + self.assertEqual(len(mailaddresses), 1) + self.assertIn(address, mailaddresses) -class MailAddressTest(TestCase): +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class MailAddressTest(TransactionTestCase): def test__str__(self): - md = MailDomain.objects.create(domain='example.org') - ma = MailAddress.objects.create(localpart='test', domain=md) - self.assertEqual(str(ma), 'test@example.org') + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + self.assertEqual(str(ma), "test@example.org") + + def test_set_mailbox_fresh(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer) + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + mb = Mailbox.objects.create_mailbox(user) + ma.set_mailbox(mb) + self.assertIn(ma, mb.get_mailaddresses()) + + def test_set_mailbox_reassing(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer) + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + mb = Mailbox.objects.create_mailbox(user) + ma.set_mailbox(mb) + mb2 = Mailbox.objects.create_mailbox(user) + ma.set_mailbox(mb2) + self.assertIn(ma, mb2.get_mailaddresses()) + self.assertNotIn(ma, mb.get_mailaddresses()) + + def test_set_mailbox_with_forwards(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer) + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + mb = Mailbox.objects.create_mailbox(user) + ma.set_forward_addresses(["test2@example.org"]) + ma.set_mailbox(mb) + self.assertEqual(ma.mailaddressforward_set.count(), 0) + self.assertIn(ma, mb.get_mailaddresses()) + + def test_set_mailbox_with_unsaved_address(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer) + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress(localpart="test", domain=md) + mb = Mailbox.objects.create_mailbox(user) + ma.set_mailbox(mb) + self.assertIn(ma, mb.get_mailaddresses()) + + def test_set_mailbox_fresh_no_commit(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer) + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + mb = Mailbox.objects.create_mailbox(user) + ma.set_mailbox(mb, commit=False) + self.assertNotIn(ma, mb.get_mailaddresses()) + + def test_set_mailbox_with_unsaved_address_no_commit(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer) + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress(localpart="test", domain=md) + mb = Mailbox.objects.create_mailbox(user) + ma.set_mailbox(mb, commit=False) + self.assertNotIn(ma, mb.get_mailaddresses()) + + def test_set_forward_addresses_fresh(self): + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + ma.set_forward_addresses(["test2@example.org"]) + + def get_target(maf): + return maf.target + + self.assertQuerysetEqual( + ma.mailaddressforward_set.all(), ["test2@example.org"], get_target + ) + + def test_set_forward_addresses_unsaved(self): + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress(localpart="test", domain=md) + ma.set_forward_addresses(["test2@example.org"]) + + def get_target(maf): + return maf.target + + self.assertQuerysetEqual( + ma.mailaddressforward_set.all(), ["test2@example.org"], get_target + ) + + def test_set_forward_addresses_replace_forwards(self): + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + ma.set_forward_addresses(["test2@example.org"]) + ma.set_forward_addresses(["test3@example.org"]) + + def get_target(maf): + return maf.target + + self.assertQuerysetEqual( + ma.mailaddressforward_set.all(), ["test3@example.org"], get_target + ) + + def test_set_forward_addresses_add_forwards(self): + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + ma.set_forward_addresses(["test2@example.org"]) + ma.set_forward_addresses(["test2@example.org", "test3@example.org"]) + + def get_target(maf): + return maf.target + + self.assertQuerysetEqual( + ma.mailaddressforward_set.all(), + ["test2@example.org", "test3@example.org"], + get_target, + ordered=False, + ) + + def test_set_forward_addresses_replace_mailbox(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer) + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + mb = Mailbox.objects.create_mailbox(user) + ma.set_mailbox(mb) + ma.set_forward_addresses(["test2@example.org"]) + self.assertNotIn(ma, mb.get_mailaddresses()) + + def get_target(maf): + return maf.target + + self.assertQuerysetEqual( + ma.mailaddressforward_set.all(), ["test2@example.org"], get_target + ) + + def test_set_forward_addresses_fresh_no_commit(self): + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress.objects.create(localpart="test", domain=md) + mafwds = ma.set_forward_addresses(["test2@example.org"], commit=False) + self.assertEqual(ma.mailaddressforward_set.count(), 0) + self.assertEqual(mafwds[0].target, "test2@example.org") + + def test_set_forward_address_unsaved_no_commit(self): + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress(localpart="test", domain=md) + mafwds = ma.set_forward_addresses(["test2@example.org"], commit=False) + self.assertEqual(ma.mailaddressforward_set.count(), 0) + self.assertEqual(mafwds[0].target, "test2@example.org") + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class MailboxManagerTest(TransactionTestCase): + def setUp(self): + super(MailboxManagerTest, self).setUp() + self.customer = Customer.objects.create_user("test") + self.user = User.objects.create_user(self.customer) + + def test_get_next_mailbox_name_fresh(self): + mailboxname = Mailbox.objects.get_next_mailbox_name(self.user) + self.assertEqual(mailboxname, "{}p01".format(self.user.username)) + + def test_get_next_mailbox_name_second(self): + Mailbox.objects.create_mailbox(self.user) + mailboxname = Mailbox.objects.get_next_mailbox_name(self.user) + self.assertEqual(mailboxname, "{}p02".format(self.user.username)) + + def test_get_next_mailbox_name_gap_detection(self): + mailboxes = [Mailbox.objects.create_mailbox(self.user) for _ in range(3)] + mailboxes[1].delete() + mailboxname = Mailbox.objects.get_next_mailbox_name(self.user) + self.assertEqual(mailboxname, "{}p02".format(self.user.username)) + + def test_unused_or_own_fresh(self): + md = MailDomain.objects.create(domain="example.org") + address = MailAddress.objects.create(localpart="test", domain=md) + mailboxes = Mailbox.objects.unused_or_own(address, self.user) + self.assertQuerysetEqual(mailboxes, []) + + def test_unused_or_own_unassigned(self): + md = MailDomain.objects.create(domain="example.org") + address = MailAddress.objects.create(localpart="test", domain=md) + mailboxes = [Mailbox.objects.create_mailbox(self.user) for _ in range(2)] + assignable = Mailbox.objects.unused_or_own(address, self.user) + self.assertQuerysetEqual(assignable, [repr(mb) for mb in mailboxes]) + + def test_unused_or_own_assigned(self): + md = MailDomain.objects.create(domain="example.org") + address = MailAddress.objects.create(localpart="test", domain=md) + mailboxes = [Mailbox.objects.create_mailbox(self.user) for _ in range(2)] + address.set_mailbox(mailboxes[0]) + assignable = Mailbox.objects.unused_or_own(address, self.user) + self.assertQuerysetEqual(assignable, [repr(mb) for mb in mailboxes]) + + def test_unused_or_own_assigned_other(self): + md = MailDomain.objects.create(domain="example.org") + address = MailAddress.objects.create(localpart="test", domain=md) + address2 = MailAddress.objects.create(localpart="test2", domain=md) + mailboxes = [Mailbox.objects.create_mailbox(self.user) for _ in range(2)] + address2.set_mailbox(mailboxes[0]) + assignable = Mailbox.objects.unused_or_own(address, self.user) + self.assertQuerysetEqual(assignable, [repr(mailboxes[1])]) + + def test_unused_fresh(self): + mailboxes = Mailbox.objects.unused(self.user) + self.assertQuerysetEqual(mailboxes, []) + + def test_unused_unassigned(self): + mailbox = Mailbox.objects.create_mailbox(self.user) + mailboxes = Mailbox.objects.unused(self.user) + self.assertQuerysetEqual(mailboxes, [repr(mailbox)]) + + def test_unused_assigned(self): + md = MailDomain.objects.create(domain="example.org") + address = MailAddress.objects.create(localpart="test", domain=md) + mailboxes = [Mailbox.objects.create_mailbox(self.user) for _ in range(2)] + address.set_mailbox(mailboxes[0]) + assignable = Mailbox.objects.unused(self.user) + self.assertQuerysetEqual(assignable, [repr(mailboxes[1])]) + + def test_create_mailbox_no_password(self): + mailbox = Mailbox.objects.create_mailbox(self.user) + self.assertEqual(mailbox.osuser, self.user) + self.assertEqual(mailbox.username, "{}p01".format(self.user.username)) + self.assertEqual(mailbox.password, "") + + def test_create_mailbox_with_password(self): + mailbox = Mailbox.objects.create_mailbox(self.user, "test") + self.assertEqual(mailbox.osuser, self.user) + self.assertEqual(mailbox.username, "{}p01".format(self.user.username)) + self.assertTrue(sha512_crypt.verify("test", mailbox.password)) + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class MailAddressMailboxTest(TestCase): + def setUp(self): + super(MailAddressMailboxTest, self).setUp() + self.customer = Customer.objects.create_user("test") + + def test___str__(self): + user = User.objects.create_user(self.customer) + md = MailDomain.objects.create(domain="example.org") + ma = MailAddress(localpart="test", domain=md) + mb = Mailbox.objects.create_mailbox(user) + ma.set_mailbox(mb) + self.assertEqual(str(ma.mailaddressmailbox), mb.username) diff --git a/gnuviechadmin/managemails/tests/test_views.py b/gnuviechadmin/managemails/tests/test_views.py index e69de29..ca8608f 100644 --- a/gnuviechadmin/managemails/tests/test_views.py +++ b/gnuviechadmin/managemails/tests/test_views.py @@ -0,0 +1,787 @@ +""" +Tests for :py:mod:`managemails.views`. + +""" +from unittest.mock import patch, MagicMock + +from django.test import TestCase +from django.test.utils import override_settings +from django.contrib.auth import get_user_model +from django.urls import reverse + +from domains.models import MailDomain +from hostingpackages.models import CustomerHostingPackage, HostingPackageTemplate + +from managemails.forms import MAILBOX_OR_FORWARDS +from managemails.models import MailAddress, Mailbox +from managemails.views import ( + AddMailAddress, + ChangeMailboxPassword, + CreateMailbox, + EditMailAddress, +) + + +User = get_user_model() + +TEST_USER = "test" +TEST_PASSWORD = "secret" +TEST_EMAIL = "test@example.org" +TEST_NAME = "Example Tester".split() + + +class HostingPackageAwareTestMixin(object): + + # noinspection PyMethodMayBeStatic + def _setup_hosting_package(self, customer, mailboxcount): + template = HostingPackageTemplate.objects.create( + name="testpackagetemplate", + mailboxcount=mailboxcount, + diskspace=1, + diskspace_unit=0, + ) + package = CustomerHostingPackage.objects.create_from_template( + customer, template, "testpackage" + ) + with patch("hostingpackages.models.settings") as hmsettings: + hmsettings.OSUSER_DEFAULT_GROUPS = [] + package.save() + return package + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class CreateMailboxTest(HostingPackageAwareTestMixin, TestCase): + def test_get_anonymous(self): + response = self.client.get(reverse("create_mailbox", kwargs={"package": 1})) + self.assertEqual(response.status_code, 404) + + def test_get_regular_user(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer, 1) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_mailbox", kwargs={"package": package.pk}) + ) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + User.objects.create_user( + "test2", email="test2@example.org", password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer, 1) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get( + reverse("create_mailbox", kwargs={"package": package.pk}) + ) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + customer = User.objects.create_user("customer") + package = self._setup_hosting_package(customer, 1) + User.objects.create_superuser( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_mailbox", kwargs={"package": package.pk}) + ) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer, 1) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_mailbox", kwargs={"package": package.pk}) + ) + self.assertTemplateUsed(response, "managemails/mailbox_create.html") + + def test_get_no_more_mailboxes(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer, 0) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_mailbox", kwargs={"package": package.pk}) + ) + self.assertEqual(response.status_code, 403) + self.assertEqual( + response.content, + b"You are not allowed to add more mailboxes to this hosting" b" package", + ) + + def test_get_context_data(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer, 1) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse("create_mailbox", kwargs={"package": package.pk}) + ) + self.assertIn("hostingpackage", response.context) + self.assertEqual(response.context["hostingpackage"], package) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], customer) + + def test_get_form_kwargs(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer, 1) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = CreateMailbox(request=MagicMock(), kwargs={"package": str(package.pk)}) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], package) + + def test_form_valid_redirect(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer, 1) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse("create_mailbox", kwargs={"package": package.pk}), + data={"password1": TEST_PASSWORD, "password2": TEST_PASSWORD}, + ) + self.assertRedirects(response, package.get_absolute_url()) + + def test_form_valid_message(self): + customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + package = self._setup_hosting_package(customer, 1) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse("create_mailbox", kwargs={"package": package.pk}), + follow=True, + data={"password1": TEST_PASSWORD, "password2": TEST_PASSWORD}, + ) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual("Mailbox usr01p01 created successfully.", str(messages[0])) + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class ChangeMailboxPasswordTest(HostingPackageAwareTestMixin, TestCase): + def setUp(self): + self.customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer, 1) + self.mailbox = Mailbox.objects.create_mailbox( + self.package.osuser, TEST_PASSWORD + ) + + def test_get_anonymous(self): + response = self.client.get( + reverse( + "change_mailbox_password", + kwargs={"package": self.package.pk, "slug": self.mailbox.username}, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "change_mailbox_password", + kwargs={"package": self.package.pk, "slug": self.mailbox.username}, + ) + ) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user( + "test2", email="test2@example.org", password=TEST_PASSWORD + ) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get( + reverse( + "change_mailbox_password", + kwargs={"package": self.package.pk, "slug": self.mailbox.username}, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser( + "admin", email="admin@example.org", password=TEST_PASSWORD + ) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get( + reverse( + "change_mailbox_password", + kwargs={"package": self.package.pk, "slug": self.mailbox.username}, + ) + ) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "change_mailbox_password", + kwargs={"package": self.package.pk, "slug": self.mailbox.username}, + ) + ) + self.assertTemplateUsed(response, "managemails/mailbox_setpassword.html") + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "change_mailbox_password", + kwargs={"package": self.package.pk, "slug": self.mailbox.username}, + ) + ) + self.assertIn("hostingpackage", response.context) + self.assertEqual(response.context["hostingpackage"], self.package) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], self.customer) + + def test_get_form_kwargs(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = ChangeMailboxPassword( + request=MagicMock(), + kwargs={"package": str(self.package.pk), "slug": self.mailbox.username}, + ) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], self.package) + + def test_form_valid_redirect(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse( + "change_mailbox_password", + kwargs={"package": self.package.pk, "slug": self.mailbox.username}, + ), + data={"password1": TEST_PASSWORD, "password2": TEST_PASSWORD}, + ) + self.assertRedirects(response, self.package.get_absolute_url()) + + def test_form_valid_message(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse( + "change_mailbox_password", + kwargs={"package": self.package.pk, "slug": self.mailbox.username}, + ), + follow=True, + data={"password1": TEST_PASSWORD, "password2": TEST_PASSWORD}, + ) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), "Successfully set new password for mailbox usr01p01." + ) + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class AddMailAddressTest(HostingPackageAwareTestMixin, TestCase): + def setUp(self): + self.customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer, 1) + self.maildomain = MailDomain.objects.create( + domain="example.org", customer=self.customer + ) + + def test_get_anonymous(self): + response = self.client.get( + reverse( + "add_mailaddress", + kwargs={"package": self.package.pk, "domain": self.maildomain.domain}, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "add_mailaddress", + kwargs={"package": self.package.pk, "domain": self.maildomain.domain}, + ) + ) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user( + "test2", email="test2@example.org", password=TEST_PASSWORD + ) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get( + reverse( + "add_mailaddress", + kwargs={"package": self.package.pk, "domain": self.maildomain.domain}, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser( + "admin", email="admin@example.org", password=TEST_PASSWORD + ) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get( + reverse( + "add_mailaddress", + kwargs={"package": self.package.pk, "domain": self.maildomain.domain}, + ) + ) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "add_mailaddress", + kwargs={"package": self.package.pk, "domain": self.maildomain.domain}, + ) + ) + self.assertTemplateUsed(response, "managemails/mailaddress_create.html") + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "add_mailaddress", + kwargs={"package": self.package.pk, "domain": self.maildomain.domain}, + ) + ) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], self.customer) + + def test_get_form_kwargs(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = AddMailAddress( + request=MagicMock(), + kwargs={"package": str(self.package.pk), "domain": self.maildomain.domain}, + ) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], self.package) + self.assertIn("maildomain", the_kwargs) + self.assertEqual(the_kwargs["maildomain"], self.maildomain) + + def test_form_valid_redirect(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse( + "add_mailaddress", + kwargs={"package": self.package.pk, "domain": self.maildomain.domain}, + ), + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "mailbox": "", + "forwards": "test2@example.org", + }, + ) + self.assertRedirects(response, self.package.get_absolute_url()) + + def test_form_valid_message(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse( + "add_mailaddress", + kwargs={"package": self.package.pk, "domain": self.maildomain.domain}, + ), + follow=True, + data={ + "localpart": "test", + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "mailbox": "", + "forwards": "test2@example.org", + }, + ) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), "Successfully added mail address test@example.org" + ) + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class DeleteMailAddressTest(HostingPackageAwareTestMixin, TestCase): + def setUp(self): + self.customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer, 1) + self.maildomain = MailDomain.objects.create( + domain="example.org", customer=self.customer + ) + self.mailaddress = MailAddress.objects.create( + localpart="test", domain=self.maildomain + ) + self.mailaddress.set_forward_addresses(["test2@example.org"]) + + def test_get_anonymous(self): + response = self.client.get( + reverse( + "delete_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "delete_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user( + "test2", email="test2@example.org", password=TEST_PASSWORD + ) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get( + reverse( + "delete_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser( + "admin", email="admin@example.org", password=TEST_PASSWORD + ) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get( + reverse( + "delete_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "delete_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertTemplateUsed(response, "managemails/mailaddress_confirm_delete.html") + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "delete_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], self.customer) + self.assertIn("hostingpackage", response.context) + self.assertEqual(response.context["hostingpackage"], self.package) + self.assertIn("maildomain", response.context) + self.assertEqual(response.context["maildomain"], self.maildomain) + + def test_form_valid_redirect(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse( + "delete_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ), + data={}, + ) + self.assertRedirects(response, self.package.get_absolute_url()) + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class EditMailAddressTest(HostingPackageAwareTestMixin, TestCase): + def setUp(self): + self.customer = User.objects.create_user( + TEST_USER, email=TEST_EMAIL, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer, 1) + self.maildomain = MailDomain.objects.create( + domain="example.org", customer=self.customer + ) + + def _set_mailaddress_with_forward(self): + self.mailaddress = MailAddress.objects.create( + localpart="test", domain=self.maildomain + ) + self.mailaddress.set_forward_addresses(["test2@example.org"]) + + def _set_mailaddress_with_mailbox(self): + self.mailaddress = MailAddress.objects.create( + localpart="test", domain=self.maildomain + ) + self.mailbox = Mailbox.objects.create_mailbox(self.package.osuser) + self.mailaddress.set_mailbox(self.mailbox) + + def test_get_anonymous(self): + self._set_mailaddress_with_forward() + response = self.client.get( + reverse( + "edit_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self._set_mailaddress_with_forward() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "edit_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + self._set_mailaddress_with_forward() + User.objects.create_user( + "test2", email="test2@example.org", password=TEST_PASSWORD + ) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get( + reverse( + "edit_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + self._set_mailaddress_with_forward() + User.objects.create_superuser( + "admin", email="admin@example.org", password=TEST_PASSWORD + ) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get( + reverse( + "edit_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self._set_mailaddress_with_forward() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "edit_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertTemplateUsed(response, "managemails/mailaddress_edit.html") + + def test_get_context_data(self): + self._set_mailaddress_with_forward() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get( + reverse( + "edit_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ) + ) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], self.customer) + + def test_get_form_kwargs(self): + self._set_mailaddress_with_forward() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = EditMailAddress( + request=MagicMock(), + kwargs={ + "package": str(self.package.pk), + "domain": self.maildomain.domain, + "pk": str(self.mailaddress.pk), + }, + ) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], self.package) + self.assertIn("maildomain", the_kwargs) + self.assertEqual(the_kwargs["maildomain"], self.maildomain) + + def test_get_initial_with_forwards(self): + self._set_mailaddress_with_forward() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = EditMailAddress( + request=MagicMock(), + kwargs={ + "package": str(self.package.pk), + "domain": self.maildomain.domain, + "pk": str(self.mailaddress.pk), + }, + ) + initial = view.get_initial() + self.assertIn("mailbox_or_forwards", initial) + self.assertEqual(initial["mailbox_or_forwards"], MAILBOX_OR_FORWARDS.forwards) + self.assertIn("forwards", initial) + self.assertEqual(initial["forwards"], "test2@example.org") + self.assertNotIn("mailbox", initial) + + def test_get_initial_with_mailbox(self): + self._set_mailaddress_with_mailbox() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = EditMailAddress( + request=MagicMock(), + kwargs={ + "package": str(self.package.pk), + "domain": self.maildomain.domain, + "pk": str(self.mailaddress.pk), + }, + ) + initial = view.get_initial() + self.assertIn("mailbox_or_forwards", initial) + self.assertEqual(initial["mailbox_or_forwards"], MAILBOX_OR_FORWARDS.mailbox) + self.assertIn("mailbox", initial) + self.assertEqual(initial["mailbox"], self.mailbox) + self.assertNotIn("forwards", initial) + + def test_get_initial_with_unassigned_address(self): + self.mailaddress = MailAddress.objects.create( + localpart="test", domain=self.maildomain + ) + view = EditMailAddress( + request=MagicMock(), + kwargs={ + "package": str(self.package.pk), + "domain": self.maildomain.domain, + "pk": str(self.mailaddress.pk), + }, + ) + initial = view.get_initial() + self.assertNotIn("mailbox_or_forwards", initial) + self.assertNotIn("forwards", initial) + self.assertNotIn("mailbox", initial) + + def test_form_valid_redirect(self): + self._set_mailaddress_with_forward() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse( + "edit_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ), + data={ + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "mailbox": "", + "forwards": "test2@example.org,test3@example.org", + }, + ) + self.assertRedirects(response, self.package.get_absolute_url()) + + def test_form_valid_message(self): + self._set_mailaddress_with_forward() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + reverse( + "edit_mailaddress", + kwargs={ + "package": self.package.pk, + "domain": self.maildomain.domain, + "pk": self.mailaddress.pk, + }, + ), + follow=True, + data={ + "mailbox_or_forwards": MAILBOX_OR_FORWARDS.forwards, + "mailbox": "", + "forwards": "test2@example.org,test3@example.org", + }, + ) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), + "Successfully updated mail address test@example.org targets.", + ) diff --git a/gnuviechadmin/managemails/urls.py b/gnuviechadmin/managemails/urls.py index c37a83e..fcd4ef7 100644 --- a/gnuviechadmin/managemails/urls.py +++ b/gnuviechadmin/managemails/urls.py @@ -5,7 +5,7 @@ views. """ from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( AddMailAddress, @@ -15,8 +15,7 @@ from .views import ( EditMailAddress, ) -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^(?P\d+)/mailbox/create$', CreateMailbox.as_view(), name='create_mailbox'), url(r'^(?P\d+)/mailbox/(?P[\w0-9]+)/setpassword$', @@ -29,4 +28,4 @@ urlpatterns = patterns( url(r'^(?P\d+)/mailaddress/(?P[\w0-9-.]+)/(?P\d+)' r'/delete$', DeleteMailAddress.as_view(), name='delete_mailaddress'), -) +] diff --git a/gnuviechadmin/managemails/views.py b/gnuviechadmin/managemails/views.py index e5590b1..80a9337 100644 --- a/gnuviechadmin/managemails/views.py +++ b/gnuviechadmin/managemails/views.py @@ -46,10 +46,11 @@ class CreateMailbox( def dispatch(self, request, *args, **kwargs): resp = super(CreateMailbox, self).dispatch(request, *args, **kwargs) - if not self.get_hosting_package().may_add_mailbox(): - resp = HttpResponseForbidden( - _('You are not allowed to add more mailboxes to this' - ' hosting package')) + if request.method != 'POST': + if not self.get_hosting_package().may_add_mailbox(): + resp = HttpResponseForbidden( + _('You are not allowed to add more mailboxes to this' + ' hosting package')) return resp def get_context_data(self, **kwargs): diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index 91df5a8..b6688f6 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -1,28 +1,20 @@ """ This module contains the Django admin classes of the :py:mod:`osusers` app. +The module starts Celery_ tasks. + +.. _Celery: http://www.celeryproject.org/ + """ from django import forms -from django.utils.translation import ugettext_lazy as _ from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ from fileservertasks.tasks import set_file_ssh_authorized_keys -from gvawebcore.forms import ( - PASSWORD_MISMATCH_ERROR -) +from gvawebcore.forms import PASSWORD_MISMATCH_ERROR from taskresults.models import TaskResult - -from .forms import ( - INVALID_SSH_PUBLIC_KEY, - DUPLICATE_SSH_PUBLIC_KEY_FOR_USER, -) -from .models import ( - AdditionalGroup, - Group, - Shadow, - SshPublicKey, - User, -) +from .forms import DUPLICATE_SSH_PUBLIC_KEY_FOR_USER, INVALID_SSH_PUBLIC_KEY +from .models import AdditionalGroup, Group, Shadow, SshPublicKey, User class AdditionalGroupInline(admin.TabularInline): @@ -30,6 +22,7 @@ class AdditionalGroupInline(admin.TabularInline): Inline for :py:class:`osusers.models.AdditionalGroup` instances. """ + model = AdditionalGroup @@ -38,8 +31,9 @@ class ShadowInline(admin.TabularInline): Inline for :py:class:`osusers.models.ShadowInline` instances. """ + model = Shadow - readonly_fields = ['passwd'] + readonly_fields = ["passwd"] can_delete = False @@ -49,18 +43,17 @@ class UserCreationForm(forms.ModelForm): `. """ + password1 = forms.CharField( - label=_('Password'), widget=forms.PasswordInput, - required=False, + label=_("Password"), widget=forms.PasswordInput, required=False ) password2 = forms.CharField( - label=_('Password (again)'), widget=forms.PasswordInput, - required=False, + label=_("Password (again)"), widget=forms.PasswordInput, required=False ) class Meta: model = User - fields = ['customer'] + fields = ["customer"] def clean_password2(self): """ @@ -70,8 +63,8 @@ class UserCreationForm(forms.ModelForm): :rtype: str or None """ - password1 = self.cleaned_data.get('password1') - password2 = self.cleaned_data.get('password2') + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: raise forms.ValidationError(PASSWORD_MISMATCH_ERROR) return password2 @@ -86,8 +79,10 @@ class UserCreationForm(forms.ModelForm): """ user = User.objects.create_user( - customer=self.cleaned_data['customer'], - password=self.cleaned_data['password1'], commit=commit) + customer=self.cleaned_data["customer"], + password=self.cleaned_data["password1"], + commit=commit, + ) return user def save_m2m(self): @@ -104,14 +99,16 @@ class UserAdmin(admin.ModelAdmin): `. """ - actions = ['perform_delete_selected'] + + actions = ["perform_delete_selected"] add_form = UserCreationForm inlines = [AdditionalGroupInline, ShadowInline] add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('customer', 'password1', 'password2')}), + ( + None, + {"classes": ("wide",), "fields": ("customer", "password1", "password2")}, + ), ) def get_form(self, request, obj=None, **kwargs): @@ -128,10 +125,12 @@ class UserAdmin(admin.ModelAdmin): """ defaults = {} if obj is None: - defaults.update({ - 'form': self.add_form, - 'fields': admin.options.flatten_fieldsets(self.add_fieldsets), - }) + defaults.update( + { + "form": self.add_form, + "fields": admin.options.flatten_fieldsets(self.add_fieldsets), + } + ) defaults.update(kwargs) return super(UserAdmin, self).get_form(request, obj, **defaults) @@ -147,7 +146,7 @@ class UserAdmin(admin.ModelAdmin): """ if obj: - return ['uid'] + return ["uid"] return [] def perform_delete_selected(self, request, queryset): @@ -163,7 +162,8 @@ class UserAdmin(admin.ModelAdmin): """ for user in queryset.all(): user.delete() - perform_delete_selected.short_description = _('Delete selected users') + + perform_delete_selected.short_description = _("Delete selected users") def get_actions(self, request): """ @@ -178,8 +178,8 @@ class UserAdmin(admin.ModelAdmin): """ actions = super(UserAdmin, self).get_actions(request) - if 'delete_selected' in actions: - del actions['delete_selected'] + if "delete_selected" in actions: # pragma: no cover + del actions["delete_selected"] return actions @@ -189,7 +189,8 @@ class GroupAdmin(admin.ModelAdmin): `. """ - actions = ['perform_delete_selected'] + + actions = ["perform_delete_selected"] def perform_delete_selected(self, request, queryset): """ @@ -204,7 +205,8 @@ class GroupAdmin(admin.ModelAdmin): """ for group in queryset.all(): group.delete() - perform_delete_selected.short_description = _('Delete selected groups') + + perform_delete_selected.short_description = _("Delete selected groups") def get_actions(self, request): """ @@ -219,8 +221,8 @@ class GroupAdmin(admin.ModelAdmin): """ actions = super(GroupAdmin, self).get_actions(request) - if 'delete_selected' in actions: - del actions['delete_selected'] + if "delete_selected" in actions: # pragma: no cover + del actions["delete_selected"] return actions @@ -230,33 +232,38 @@ class SshPublicKeyCreationForm(forms.ModelForm): `. """ + publickeytext = forms.CharField( - label=_('Key text'), widget=forms.Textarea, - help_text=_('A SSH public key in either OpenSSH or RFC 4716 format')) + label=_("Key text"), + widget=forms.Textarea, + help_text=_("A SSH public key in either OpenSSH or RFC 4716 format"), + ) class Meta: model = SshPublicKey - fields = ['user'] + fields = ["user"] def clean_publickeytext(self): - keytext = self.cleaned_data.get('publickeytext') + keytext = self.cleaned_data.get("publickeytext") try: - SshPublicKey.objects.parse_keytext(keytext) + SshPublicKey.objects.parse_key_text(keytext) except: raise forms.ValidationError(INVALID_SSH_PUBLIC_KEY) return keytext def clean(self): - user = self.cleaned_data.get('user') - keytext = self.cleaned_data.get('publickeytext') + user = self.cleaned_data.get("user") + keytext = self.cleaned_data.get("publickeytext") if user and keytext: - alg, data, comment = SshPublicKey.objects.parse_keytext(keytext) + alg, data, comment = SshPublicKey.objects.parse_key_text(keytext) if SshPublicKey.objects.filter( user=user, algorithm=alg, data=data ).exists(): self.add_error( - 'publickeytext', - forms.ValidationError(DUPLICATE_SSH_PUBLIC_KEY_FOR_USER)) + "publickeytext", + forms.ValidationError(DUPLICATE_SSH_PUBLIC_KEY_FOR_USER), + ) + super(SshPublicKeyCreationForm, self).clean() def save(self, commit=True): """ @@ -267,8 +274,9 @@ class SshPublicKeyCreationForm(forms.ModelForm): :rtype: :py:class:`osusers.models.SshPublicKey` """ - algorithm, keydata, comment = SshPublicKey.objects.parse_keytext( - self.cleaned_data.get('publickeytext')) + algorithm, keydata, comment = SshPublicKey.objects.parse_key_text( + self.cleaned_data.get("publickeytext") + ) self.instance.algorithm = algorithm self.instance.data = keydata self.instance.comment = comment @@ -281,14 +289,13 @@ class SshPublicKeyAdmin(admin.ModelAdmin): `. """ - actions = ['perform_delete_selected'] + + actions = ["perform_delete_selected"] add_form = SshPublicKeyCreationForm - list_display = ['user', 'algorithm', 'comment'] + list_display = ["user", "algorithm", "comment"] add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('user', 'publickeytext')}), + (None, {"classes": ("wide",), "fields": ("user", "publickeytext")}), ) def get_form(self, request, obj=None, **kwargs): @@ -306,13 +313,14 @@ class SshPublicKeyAdmin(admin.ModelAdmin): """ defaults = {} if obj is None: - defaults.update({ - 'form': self.add_form, - 'fields': admin.options.flatten_fieldsets(self.add_fieldsets), - }) + defaults.update( + { + "form": self.add_form, + "fields": admin.options.flatten_fieldsets(self.add_fieldsets), + } + ) defaults.update(kwargs) - return super(SshPublicKeyAdmin, self).get_form( - request, obj, **defaults) + return super(SshPublicKeyAdmin, self).get_form(request, obj, **defaults) def get_readonly_fields(self, request, obj=None): """ @@ -327,7 +335,7 @@ class SshPublicKeyAdmin(admin.ModelAdmin): """ if obj: - return ['algorithm', 'data'] + return ["algorithm", "data"] return [] def perform_delete_selected(self, request, queryset): @@ -340,23 +348,44 @@ class SshPublicKeyAdmin(admin.ModelAdmin): :param request: the current HTTP request :param queryset: Django ORM queryset representing the selected ssh keys + This method starts a Celery_ task to update the list of authorized keys + for each affected user. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + + A [ label = "", shape = beginpoint, + description = "this method" + ]; + B [ label = "set file ssh authorized_keys", + description = ":py:func:`set_file_ssh_authorized_keys() + ` + called with username and a list of keys, returning the path + of the ssh authorized_keys file", + color = "LightGreen", + stacked + ]; + } + """ - users = set([ - item['user'] for item in - queryset.values('user').distinct() - ]) + users = set([item["user"] for item in queryset.values("user").distinct()]) queryset.delete() for user in users: + # TODO: move to model/signal TaskResult.objects.create_task_result( - set_file_ssh_authorized_keys.delay( - User.objects.get(uid=user).username, [ - str(key) for key in SshPublicKey.objects.filter( - user_id=user) - ]), - 'set_file_ssh_authorized_keys' + "perform_delete_selected", + set_file_ssh_authorized_keys.s( + User.objects.get(uid=user).username, + [str(key) for key in SshPublicKey.objects.filter(user_id=user)], + ), ) - perform_delete_selected.short_description = _( - 'Delete selected SSH public keys') + + perform_delete_selected.short_description = _("Delete selected SSH public keys") def get_actions(self, request): """ @@ -371,8 +400,8 @@ class SshPublicKeyAdmin(admin.ModelAdmin): """ actions = super(SshPublicKeyAdmin, self).get_actions(request) - if 'delete_selected' in actions: - del actions['delete_selected'] + if "delete_selected" in actions: # pragma: no cover + del actions["delete_selected"] return actions diff --git a/gnuviechadmin/osusers/apps.py b/gnuviechadmin/osusers/apps.py index 26f373d..79276a1 100644 --- a/gnuviechadmin/osusers/apps.py +++ b/gnuviechadmin/osusers/apps.py @@ -4,6 +4,7 @@ This module contains the :py:class:`django.apps.AppConfig` instance for the """ from __future__ import unicode_literals + from django.apps import AppConfig from django.utils.translation import ugettext_lazy as _ @@ -15,3 +16,11 @@ class OsusersAppConfig(AppConfig): """ name = 'osusers' verbose_name = _('Operating System Users and Groups') + + def ready(self): + """ + Takes care of importing the signal handlers of the :py:mod:`osusers` + app. + + """ + import osusers.signals # NOQA diff --git a/gnuviechadmin/osusers/forms.py b/gnuviechadmin/osusers/forms.py index 975f102..6ca67c5 100644 --- a/gnuviechadmin/osusers/forms.py +++ b/gnuviechadmin/osusers/forms.py @@ -5,7 +5,7 @@ This module defines operating system user related forms. from __future__ import unicode_literals from django import forms -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from crispy_forms.helper import FormHelper @@ -13,14 +13,12 @@ from crispy_forms.layout import Submit from gvawebcore.forms import PasswordModelFormMixin -from .models import ( - SshPublicKey, - User, -) +from .models import SshPublicKey, User -INVALID_SSH_PUBLIC_KEY = _('Invalid SSH public key data format.') +INVALID_SSH_PUBLIC_KEY = _("Invalid SSH public key data format.") DUPLICATE_SSH_PUBLIC_KEY_FOR_USER = _( - 'This SSH public key is already assigned to this user.') + "This SSH public key is already assigned to this user." +) class ChangeOsUserPasswordForm(PasswordModelFormMixin, forms.ModelForm): @@ -28,6 +26,7 @@ class ChangeOsUserPasswordForm(PasswordModelFormMixin, forms.ModelForm): A form for setting an OS user's password. """ + class Meta: model = User fields = [] @@ -36,8 +35,9 @@ class ChangeOsUserPasswordForm(PasswordModelFormMixin, forms.ModelForm): self.helper = FormHelper() super(ChangeOsUserPasswordForm, self).__init__(*args, **kwargs) self.helper.form_action = reverse( - 'set_osuser_password', kwargs={'slug': self.instance.username}) - self.helper.add_input(Submit('submit', _('Set password'))) + "set_osuser_password", kwargs={"slug": self.instance.username} + ) + self.helper.add_input(Submit("submit", _("Set password"))) def save(self, commit=True): """ @@ -48,7 +48,7 @@ class ChangeOsUserPasswordForm(PasswordModelFormMixin, forms.ModelForm): :rtype: :py:class:`osusers.models.User` """ - self.instance.set_password(self.cleaned_data['password1']) + self.instance.set_password(self.cleaned_data["password1"]) return super(ChangeOsUserPasswordForm, self).save(commit=commit) @@ -58,41 +58,45 @@ class AddSshPublicKeyForm(forms.ModelForm): `. """ + publickeytext = forms.CharField( - label=_('Key text'), widget=forms.Textarea, - help_text=_('A SSH public key in either OpenSSH or RFC 4716 format')) + label=_("Key text"), + widget=forms.Textarea, + help_text=_("A SSH public key in either OpenSSH or RFC 4716 format"), + ) class Meta: model = SshPublicKey fields = [] def __init__(self, *args, **kwargs): - hosting_package = kwargs.pop('hostingpackage') + hosting_package = kwargs.pop("hostingpackage") self.osuser = hosting_package.osuser super(AddSshPublicKeyForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_action = reverse( - 'add_ssh_key', kwargs={'package': hosting_package.id}) - self.helper.add_input(Submit('submit', _('Add SSH public key'))) + "add_ssh_key", kwargs={"package": hosting_package.id} + ) + self.helper.add_input(Submit("submit", _("Add SSH public key"))) def clean_publickeytext(self): - keytext = self.cleaned_data.get('publickeytext') + keytext = self.cleaned_data.get("publickeytext") try: - SshPublicKey.objects.parse_keytext(keytext) - except: + SshPublicKey.objects.parse_key_text(keytext) + except ValueError: raise forms.ValidationError(INVALID_SSH_PUBLIC_KEY) return keytext def clean(self): - keytext = self.cleaned_data.get('publickeytext') + keytext = self.cleaned_data.get("publickeytext") if keytext is not None: - alg, data, comment = SshPublicKey.objects.parse_keytext(keytext) + alg, data, comment = SshPublicKey.objects.parse_key_text(keytext) if SshPublicKey.objects.filter( user=self.osuser, algorithm=alg, data=data ).exists(): self.add_error( - 'publickeytext', - forms.ValidationError(DUPLICATE_SSH_PUBLIC_KEY_FOR_USER) + "publickeytext", + forms.ValidationError(DUPLICATE_SSH_PUBLIC_KEY_FOR_USER), ) def save(self, commit=True): @@ -104,8 +108,9 @@ class AddSshPublicKeyForm(forms.ModelForm): :rtype: :py:class:`osusers.models.SshPublicKey` """ - algorithm, keydata, comment = SshPublicKey.objects.parse_keytext( - self.cleaned_data.get('publickeytext')) + algorithm, keydata, comment = SshPublicKey.objects.parse_key_text( + self.cleaned_data.get("publickeytext") + ) self.instance.user = self.osuser self.instance.algorithm = algorithm self.instance.data = keydata @@ -119,17 +124,19 @@ class EditSshPublicKeyCommentForm(forms.ModelForm): ` comment fields. """ + class Meta: model = SshPublicKey - fields = ['comment'] + fields = ["comment"] def __init__(self, *args, **kwargs): - hosting_package = kwargs.pop('hostingpackage') + hosting_package = kwargs.pop("hostingpackage") self.osuser = hosting_package.osuser super(EditSshPublicKeyCommentForm, self).__init__(*args, **kwargs) - self.fields['comment'].widget = forms.TextInput() + self.fields["comment"].widget = forms.TextInput() self.helper = FormHelper() self.helper.form_action = reverse( - 'edit_ssh_key_comment', - kwargs={'package': hosting_package.id, 'pk': self.instance.id}) - self.helper.add_input(Submit('submit', _('Change Comment'))) + "edit_ssh_key_comment", + kwargs={"package": hosting_package.id, "pk": self.instance.id}, + ) + self.helper.add_input(Submit("submit", _("Change Comment"))) diff --git a/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po index b2723bd..81dba23 100644 --- a/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: osusers\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-02-01 02:12+0100\n" -"PO-Revision-Date: 2015-02-01 02:17+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" +"PO-Revision-Date: 2016-01-29 11:07+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" "Language: de\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 1.6.10\n" +"X-Generator: Poedit 1.8.6\n" "X-Poedit-SourceCharset: UTF-8\n" #: osusers/admin.py:53 @@ -45,11 +45,11 @@ msgstr "" "Öffentlicher Teil eines SSH-Schlüssels entweder im OpenSSH- oder im RFC-4716-" "Format" -#: osusers/admin.py:359 +#: osusers/admin.py:361 msgid "Delete selected SSH public keys" msgstr "Ausgewählte SSH-Schlüssel löschen" -#: osusers/apps.py:17 +#: osusers/apps.py:18 msgid "Operating System Users and Groups" msgstr "Betriebssystemnutzer- und Gruppen" @@ -69,108 +69,108 @@ msgstr "Passwort setzen" msgid "Add SSH public key" msgstr "SSH-Schlüssel hinzufügen" -#: osusers/forms.py:134 +#: osusers/forms.py:135 msgid "Change Comment" msgstr "Kommentar ändern" -#: osusers/models.py:50 +#: osusers/models.py:34 msgid "You can not use a user's primary group." msgstr "Sie können nicht die primäre Gruppe des Nutzers verwenden." -#: osusers/models.py:80 +#: osusers/models.py:64 msgid "Group name" msgstr "Gruppenname" -#: osusers/models.py:82 +#: osusers/models.py:66 msgid "Group ID" msgstr "Gruppen-ID" -#: osusers/models.py:83 +#: osusers/models.py:67 msgid "Description" msgstr "Beschreibung" -#: osusers/models.py:85 +#: osusers/models.py:69 msgid "Group password" msgstr "Gruppenpasswort" -#: osusers/models.py:90 osusers/models.py:224 +#: osusers/models.py:74 osusers/models.py:201 msgid "Group" msgstr "Gruppe" -#: osusers/models.py:91 +#: osusers/models.py:75 msgid "Groups" msgstr "Gruppen" -#: osusers/models.py:221 +#: osusers/models.py:198 msgid "User name" msgstr "Nutzername" -#: osusers/models.py:223 +#: osusers/models.py:200 msgid "User ID" msgstr "Nutzer-ID" -#: osusers/models.py:225 +#: osusers/models.py:202 msgid "Gecos field" msgstr "GECOS-Feld" -#: osusers/models.py:226 +#: osusers/models.py:203 msgid "Home directory" msgstr "Home-Verzeichnis" -#: osusers/models.py:227 +#: osusers/models.py:204 msgid "Login shell" msgstr "Loginshell" -#: osusers/models.py:233 osusers/models.py:373 osusers/models.py:566 +#: osusers/models.py:210 osusers/models.py:310 osusers/models.py:501 msgid "User" msgstr "Nutzer" -#: osusers/models.py:234 +#: osusers/models.py:211 msgid "Users" msgstr "Nutzer" -#: osusers/models.py:374 +#: osusers/models.py:311 msgid "Encrypted password" msgstr "Verschlüsseltes Passwort" -#: osusers/models.py:376 +#: osusers/models.py:313 msgid "Date of last change" msgstr "Datum der letzten Änderung" -#: osusers/models.py:377 +#: osusers/models.py:314 msgid "This is expressed in days since Jan 1, 1970" msgstr "Ausgedrückt als Tage seit dem 1. Januar 1970" -#: osusers/models.py:380 +#: osusers/models.py:317 msgid "Minimum age" msgstr "Minimales Alter" -#: osusers/models.py:381 +#: osusers/models.py:318 msgid "Minimum number of days before the password can be changed" msgstr "Minmale Anzahl von Tagen bevor das Passwort geändert werden kann" -#: osusers/models.py:385 +#: osusers/models.py:322 msgid "Maximum age" msgstr "Maximales Alter" -#: osusers/models.py:386 +#: osusers/models.py:323 msgid "Maximum number of days after which the password has to be changed" msgstr "" "Maximale Anzahl von Tagen, nach denen das Passwort geändert werden muss" -#: osusers/models.py:390 +#: osusers/models.py:327 msgid "Grace period" msgstr "Duldungsperiode" -#: osusers/models.py:391 +#: osusers/models.py:328 msgid "The number of days before the password is going to expire" msgstr "Anzahl von Tagen nach denen das Passwort verfällt" -#: osusers/models.py:395 +#: osusers/models.py:332 msgid "Inactivity period" msgstr "Inaktivitätsperiode" -#: osusers/models.py:396 +#: osusers/models.py:333 msgid "" "The number of days after the password has expired during which the password " "should still be accepted" @@ -178,53 +178,53 @@ msgstr "" "Die Anzahl von Tagen für die ein verfallenes Passwort noch akzeptiert werden " "soll" -#: osusers/models.py:400 +#: osusers/models.py:337 msgid "Account expiration date" msgstr "Kontoverfallsdatum" -#: osusers/models.py:401 +#: osusers/models.py:338 msgid "" "The date of expiration of the account, expressed as number of days since Jan " "1, 1970" msgstr "Kontoverfallsdatum in Tagen seit dem 1. Januar 1970" -#: osusers/models.py:408 +#: osusers/models.py:345 msgid "Shadow password" msgstr "Shadow-Passwort" -#: osusers/models.py:409 +#: osusers/models.py:346 msgid "Shadow passwords" msgstr "Shadow-Passwörter" -#: osusers/models.py:435 +#: osusers/models.py:372 msgid "Additional group" msgstr "Weitere Gruppe" -#: osusers/models.py:436 +#: osusers/models.py:373 msgid "Additional groups" msgstr "Weitere Gruppen" -#: osusers/models.py:567 +#: osusers/models.py:502 msgid "Algorithm" msgstr "Algorithmus" -#: osusers/models.py:568 +#: osusers/models.py:503 msgid "Key bytes" msgstr "Schlüsselbytes" -#: osusers/models.py:569 +#: osusers/models.py:504 msgid "Base64 encoded key bytes" msgstr "Base64-kodierte Schlüsselbytes" -#: osusers/models.py:570 +#: osusers/models.py:505 msgid "Comment" msgstr "Kommentar" -#: osusers/models.py:575 +#: osusers/models.py:510 msgid "SSH public key" msgstr "Öffentlicher SSH-Schlüssel" -#: osusers/models.py:576 +#: osusers/models.py:511 msgid "SSH public keys" msgstr "Öffentliche SSH-Schlüssel" @@ -235,8 +235,8 @@ msgstr "Für {username} wurde erfolgreich ein neues Passwort gesetzt." #: osusers/views.py:92 #, python-brace-format -msgid "Successfully added new {algorithm} SSH public key" -msgstr "Neuer {algorithm}-SSH-Schlüssel erfolgreich hinzugefügt" +msgid "Successfully added new {algorithm} SSH public key." +msgstr "Neuer {algorithm}-SSH-Schlüssel erfolgreich hinzugefügt." #~ msgid "Passwords don't match" #~ msgstr "Passwörter stimmen nicht überein" diff --git a/gnuviechadmin/osusers/migrations/0001_initial.py b/gnuviechadmin/osusers/migrations/0001_initial.py index 7c3d023..ddc97e8 100644 --- a/gnuviechadmin/osusers/migrations/0001_initial.py +++ b/gnuviechadmin/osusers/migrations/0001_initial.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone import model_utils.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ] @@ -15,9 +14,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='AdditionalGroup', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), ], options={ 'verbose_name': 'Additional group', @@ -28,9 +33,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='DeleteTaskResult', fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('task_uuid', models.CharField(max_length=64, serialize=False, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('task_uuid', models.CharField( + max_length=64, serialize=False, primary_key=True)), ('task_name', models.CharField(max_length=255, db_index=True)), ('is_finished', models.BooleanField(default=False)), ('is_success', models.BooleanField(default=False)), @@ -47,12 +57,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Group', fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('groupname', models.CharField(unique=True, max_length=16, verbose_name='Group name')), - ('gid', models.PositiveSmallIntegerField(unique=True, serialize=False, verbose_name='Group ID', primary_key=True)), - ('descr', models.TextField(verbose_name='Description', blank=True)), - ('passwd', models.CharField(max_length=128, verbose_name='Group password', blank=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('groupname', models.CharField( + unique=True, max_length=16, verbose_name='Group name')), + ('gid', models.PositiveSmallIntegerField( + unique=True, serialize=False, verbose_name='Group ID', + primary_key=True)), + ('descr', models.TextField( + verbose_name='Description', blank=True)), + ('passwd', models.CharField( + max_length=128, verbose_name='Group password', blank=True)), ], options={ 'verbose_name': 'Group', @@ -63,15 +82,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name='GroupTaskResult', fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('task_uuid', models.CharField(max_length=64, serialize=False, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('task_uuid', models.CharField( + max_length=64, serialize=False, primary_key=True)), ('task_name', models.CharField(max_length=255, db_index=True)), ('is_finished', models.BooleanField(default=False)), ('is_success', models.BooleanField(default=False)), ('state', models.CharField(max_length=10)), ('result_body', models.TextField(blank=True)), - ('group', models.ForeignKey(to='osusers.Group')), + ('group', models.ForeignKey( + to='osusers.Group', on_delete=models.CASCADE)), ], options={ 'abstract': False, @@ -81,13 +106,23 @@ class Migration(migrations.Migration): migrations.CreateModel( name='User', fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('username', models.CharField(unique=True, max_length=64, verbose_name='User name')), - ('uid', models.PositiveSmallIntegerField(unique=True, serialize=False, verbose_name='User ID', primary_key=True)), - ('gecos', models.CharField(max_length=128, verbose_name='Gecos field', blank=True)), - ('homedir', models.CharField(max_length=256, verbose_name='Home directory')), - ('shell', models.CharField(max_length=64, verbose_name='Login shell')), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('username', models.CharField( + unique=True, max_length=64, verbose_name='User name')), + ('uid', models.PositiveSmallIntegerField( + unique=True, serialize=False, verbose_name='User ID', + primary_key=True)), + ('gecos', models.CharField( + max_length=128, verbose_name='Gecos field', blank=True)), + ('homedir', models.CharField( + max_length=256, verbose_name='Home directory')), + ('shell', models.CharField( + max_length=64, verbose_name='Login shell')), ], options={ 'verbose_name': 'Benutzer', @@ -98,16 +133,43 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Shadow', fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('user', models.OneToOneField(primary_key=True, serialize=False, to='osusers.User', verbose_name='Benutzer')), - ('passwd', models.CharField(max_length=128, verbose_name='Encrypted password')), - ('changedays', models.PositiveSmallIntegerField(help_text='This is expressed in days since Jan 1, 1970', null=True, verbose_name='Date of last change', blank=True)), - ('minage', models.PositiveSmallIntegerField(help_text='Minimum number of days before the password can be changed', null=True, verbose_name='Minimum age', blank=True)), - ('maxage', models.PositiveSmallIntegerField(help_text='Maximum number of days after which the password has to be changed', null=True, verbose_name='Maximum age', blank=True)), - ('gracedays', models.PositiveSmallIntegerField(help_text='The number of days before the password is going to expire', null=True, verbose_name='Grace period', blank=True)), - ('inactdays', models.PositiveSmallIntegerField(help_text='The number of days after the password has expired during which the password should still be accepted', null=True, verbose_name='Inactivity period', blank=True)), - ('expiredays', models.PositiveSmallIntegerField(default=None, help_text='The date of expiration of the account, expressed as number of days since Jan 1, 1970', null=True, verbose_name='Account expiration date', blank=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('user', models.OneToOneField( + primary_key=True, serialize=False, to='osusers.User', + verbose_name='Benutzer', on_delete=models.CASCADE)), + ('passwd', models.CharField( + max_length=128, verbose_name='Encrypted password')), + ('changedays', models.PositiveSmallIntegerField( + help_text='This is expressed in days since Jan 1, 1970', + null=True, verbose_name='Date of last change', blank=True)), + ('minage', models.PositiveSmallIntegerField( + help_text='Minimum number of days before the password can ' + 'be changed', + null=True, verbose_name='Minimum age', blank=True)), + ('maxage', models.PositiveSmallIntegerField( + help_text='Maximum number of days after which the ' + 'password has to be changed', + null=True, verbose_name='Maximum age', blank=True)), + ('gracedays', models.PositiveSmallIntegerField( + help_text='The number of days before the password is ' + 'going to expire', + null=True, verbose_name='Grace period', blank=True)), + ('inactdays', models.PositiveSmallIntegerField( + help_text='The number of days after the password has ' + 'expired during which the password should still ' + 'be accepted', + null=True, verbose_name='Inactivity period', blank=True)), + ('expiredays', models.PositiveSmallIntegerField( + default=None, + help_text='The date of expiration of the account, ' + 'expressed as number of days since Jan 1, 1970', + null=True, verbose_name='Account expiration date', + blank=True)), ], options={ 'verbose_name': 'Shadow password', @@ -118,15 +180,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserTaskResult', fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('task_uuid', models.CharField(max_length=64, serialize=False, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('task_uuid', models.CharField( + max_length=64, serialize=False, primary_key=True)), ('task_name', models.CharField(max_length=255, db_index=True)), ('is_finished', models.BooleanField(default=False)), ('is_success', models.BooleanField(default=False)), ('state', models.CharField(max_length=10)), ('result_body', models.TextField(blank=True)), - ('user', models.ForeignKey(to='osusers.User')), + ('user', models.ForeignKey( + to='osusers.User', on_delete=models.CASCADE)), ], options={ 'abstract': False, @@ -136,23 +204,27 @@ class Migration(migrations.Migration): migrations.AddField( model_name='user', name='group', - field=models.ForeignKey(verbose_name='Group', to='osusers.Group'), + field=models.ForeignKey( + verbose_name='Group', to='osusers.Group', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='additionalgroup', name='group', - field=models.ForeignKey(to='osusers.Group'), + field=models.ForeignKey( + to='osusers.Group', on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='additionalgroup', name='user', - field=models.ForeignKey(to='osusers.User'), + field=models.ForeignKey( + to='osusers.User', on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( name='additionalgroup', - unique_together=set([('user', 'group')]), + unique_together={('user', 'group')}, ), ] diff --git a/gnuviechadmin/osusers/migrations/0003_user_customer.py b/gnuviechadmin/osusers/migrations/0003_user_customer.py index b8043ef..6226187 100644 --- a/gnuviechadmin/osusers/migrations/0003_user_customer.py +++ b/gnuviechadmin/osusers/migrations/0003_user_customer.py @@ -6,7 +6,6 @@ from django.conf import settings class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('osusers', '0002_auto_20141226_1456'), @@ -16,7 +15,9 @@ class Migration(migrations.Migration): migrations.AddField( model_name='user', name='customer', - field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + default=1, to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE), preserve_default=False, ), ] diff --git a/gnuviechadmin/osusers/migrations/0004_auto_20150104_1751.py b/gnuviechadmin/osusers/migrations/0004_auto_20150104_1751.py index b1dcd60..288db36 100644 --- a/gnuviechadmin/osusers/migrations/0004_auto_20150104_1751.py +++ b/gnuviechadmin/osusers/migrations/0004_auto_20150104_1751.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('osusers', '0003_user_customer'), ] @@ -18,7 +17,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='shadow', name='user', - field=models.OneToOneField(primary_key=True, serialize=False, to='osusers.User', verbose_name='User'), + field=models.OneToOneField( + primary_key=True, serialize=False, to='osusers.User', + verbose_name='User', on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/gnuviechadmin/osusers/migrations/0005_auto_20150131_2009.py b/gnuviechadmin/osusers/migrations/0005_auto_20150131_2009.py index 809f1a2..d930c57 100644 --- a/gnuviechadmin/osusers/migrations/0005_auto_20150131_2009.py +++ b/gnuviechadmin/osusers/migrations/0005_auto_20150131_2009.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone import model_utils.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('osusers', '0004_auto_20150104_1751'), ] @@ -16,13 +15,25 @@ class Migration(migrations.Migration): migrations.CreateModel( name='SshPublicKey', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('algorithm', models.CharField(max_length=20, verbose_name='Algorithm')), - ('data', models.TextField(help_text='Base64 encoded key bytes', verbose_name='Key bytes')), - ('comment', models.TextField(verbose_name='Comment', blank=True)), - ('user', models.ForeignKey(verbose_name='User', to='osusers.User')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('algorithm', models.CharField( + max_length=20, verbose_name='Algorithm')), + ('data', models.TextField( + help_text='Base64 encoded key bytes', + verbose_name='Key bytes')), + ('comment', models.TextField( + verbose_name='Comment', blank=True)), + ('user', models.ForeignKey( + verbose_name='User', to='osusers.User', + on_delete=models.CASCADE)), ], options={ 'verbose_name': 'SSH public key', @@ -32,6 +43,6 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='sshpublickey', - unique_together=set([('user', 'algorithm', 'data')]), + unique_together={('user', 'algorithm', 'data')}, ), ] diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py index 28e93a7..14d36ff 100644 --- a/gnuviechadmin/osusers/models.py +++ b/gnuviechadmin/osusers/models.py @@ -2,19 +2,16 @@ This module defines the database models of operating system users. """ -from __future__ import unicode_literals - import base64 from datetime import date import logging import os -import six from django.db import models, transaction from django.conf import settings from django.core.exceptions import ValidationError +from django.dispatch import Signal from django.utils import timezone -from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext as _ from model_utils.models import TimeStampedModel @@ -22,32 +19,14 @@ from model_utils.models import TimeStampedModel from passlib.hash import sha512_crypt from passlib.utils import generate_password -from taskresults.models import TaskResult - -from ldaptasks.tasks import ( - add_ldap_user_to_group, - create_ldap_group, - create_ldap_user, - delete_ldap_group, - delete_ldap_user, - remove_ldap_user_from_group, - set_ldap_user_password, -) - -from fileservertasks.tasks import ( - delete_file_mail_userdir, - delete_file_sftp_userdir, - set_file_ssh_authorized_keys, - setup_file_mail_userdir, - setup_file_sftp_userdir, -) - _LOGGER = logging.getLogger(__name__) -CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _( - "You can not use a user's primary group.") +password_set = Signal(providing_args=["instance", "password"]) + + +CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _("You can not use a user's primary group.") class GroupManager(models.Manager): @@ -64,34 +43,31 @@ class GroupManager(models.Manager): :rtype: int """ - q = self.aggregate(models.Max('gid')) - if q['gid__max'] is None: + q = self.aggregate(models.Max("gid")) + if q["gid__max"] is None: return settings.OSUSER_MINGID - return max(settings.OSUSER_MINGID, q['gid__max'] + 1) + return max(settings.OSUSER_MINGID, q["gid__max"] + 1) -@python_2_unicode_compatible class Group(TimeStampedModel, models.Model): """ This entity class corresponds to an operating system group. """ - groupname = models.CharField( - _('Group name'), max_length=16, unique=True) - gid = models.PositiveSmallIntegerField( - _('Group ID'), unique=True, primary_key=True) - descr = models.TextField(_('Description'), blank=True) - passwd = models.CharField( - _('Group password'), max_length=128, blank=True) + + groupname = models.CharField(_("Group name"), max_length=16, unique=True) + gid = models.PositiveSmallIntegerField(_("Group ID"), unique=True, primary_key=True) + descr = models.TextField(_("Description"), blank=True) + passwd = models.CharField(_("Group password"), max_length=128, blank=True) objects = GroupManager() class Meta: - verbose_name = _('Group') - verbose_name_plural = _('Groups') + verbose_name = _("Group") + verbose_name_plural = _("Groups") def __str__(self): - return '{0} ({1})'.format(self.groupname, self.gid) + return "{0} ({1})".format(self.groupname, self.gid) @transaction.atomic def save(self, *args, **kwargs): @@ -108,9 +84,6 @@ class Group(TimeStampedModel, models.Model): """ super(Group, self).save(*args, **kwargs) - dn = create_ldap_group.delay( - self.groupname, self.gid, self.descr).get() - _LOGGER.info("created LDAP group with dn %s", dn) return self @transaction.atomic @@ -124,10 +97,6 @@ class Group(TimeStampedModel, models.Model): :py:meth:`django.db.Model.delete` """ - TaskResult.objects.create_task_result( - delete_ldap_group.delay(self.groupname), - 'delete_ldap_group' - ) super(Group, self).delete(*args, **kwargs) @@ -145,10 +114,10 @@ class UserManager(models.Manager): :rtype: int """ - q = self.aggregate(models.Max('uid')) - if q['uid__max'] is None: + q = self.aggregate(models.Max("uid")) + if q["uid__max"] is None: return settings.OSUSER_MINUID - return max(settings.OSUSER_MINUID, q['uid__max'] + 1) + return max(settings.OSUSER_MINUID, q["uid__max"] + 1) def get_next_username(self): """ @@ -160,23 +129,21 @@ class UserManager(models.Manager): """ count = 1 usernameformat = "{0}{1:02d}" - nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX, - count) - for user in self.values('username').filter( - username__startswith=settings.OSUSER_USERNAME_PREFIX - ).order_by('username'): - if user['username'] == nextuser: + nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX, count) + for user in ( + self.values("username") + .filter(username__startswith=settings.OSUSER_USERNAME_PREFIX) + .order_by("username") + ): + if user["username"] == nextuser: count += 1 - nextuser = usernameformat.format( - settings.OSUSER_USERNAME_PREFIX, count) + nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX, count) else: break return nextuser @transaction.atomic - def create_user( - self, customer, username=None, password=None, commit=False - ): + def create_user(self, customer, username=None, password=None, commit=False): """ Create a new user with a primary group named the same as the user and an initial password. @@ -202,39 +169,42 @@ class UserManager(models.Manager): password = generate_password() homedir = os.path.join(settings.OSUSER_HOME_BASEPATH, username) group = Group.objects.create(groupname=username, gid=gid) - user = self.create(username=username, group=group, uid=uid, - homedir=homedir, customer=customer, - shell=settings.OSUSER_DEFAULT_SHELL) + user = self.create( + username=username, + group=group, + uid=uid, + homedir=homedir, + customer=customer, + shell=settings.OSUSER_DEFAULT_SHELL, + ) user.set_password(password) if commit: user.save() return user -@python_2_unicode_compatible class User(TimeStampedModel, models.Model): """ This entity class corresponds to an operating system user. """ - username = models.CharField( - _('User name'), max_length=64, unique=True) - uid = models.PositiveSmallIntegerField( - _('User ID'), unique=True, primary_key=True) - group = models.ForeignKey(Group, verbose_name=_('Group')) - gecos = models.CharField(_('Gecos field'), max_length=128, blank=True) - homedir = models.CharField(_('Home directory'), max_length=256) - shell = models.CharField(_('Login shell'), max_length=64) - customer = models.ForeignKey(settings.AUTH_USER_MODEL) + + username = models.CharField(_("User name"), max_length=64, unique=True) + uid = models.PositiveSmallIntegerField(_("User ID"), unique=True, primary_key=True) + group = models.ForeignKey(Group, verbose_name=_("Group"), on_delete=models.CASCADE) + gecos = models.CharField(_("Gecos field"), max_length=128, blank=True) + homedir = models.CharField(_("Home directory"), max_length=256) + shell = models.CharField(_("Login shell"), max_length=64) + customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) objects = UserManager() class Meta: - verbose_name = _('User') - verbose_name_plural = _('Users') + verbose_name = _("User") + verbose_name_plural = _("Users") def __str__(self): - return '{0} ({1})'.format(self.username, self.uid) + return "{0} ({1})".format(self.username, self.uid) @transaction.atomic def set_password(self, password): @@ -247,29 +217,15 @@ class User(TimeStampedModel, models.Model): :param str password: the new password """ - if hasattr(self, 'shadow'): + if hasattr(self, "shadow"): self.shadow.set_password(password) - success = set_ldap_user_password.delay( - self.username, password).get() - if success: - _LOGGER.info( - "successfully set LDAP password for %s", self.username) - else: - _LOGGER.error( - "setting the LDAP password for %s failed", self.username) - return success else: - self.shadow = Shadow.objects.create_shadow( - user=self, password=password - ) - dn = create_ldap_user.delay( - self.username, self.uid, self.group.gid, self.gecos, - self.homedir, self.shell, password - ).get() - _LOGGER.info("set LDAP password for %s", dn) - return True + self.shadow = Shadow.objects.create_shadow(user=self, password=password) + password_set.send(sender=self.__class__, password=password, instance=self) + return True def is_sftp_user(self): + # noinspection PyUnresolvedReferences return self.additionalgroup_set.filter( group__groupname=settings.OSUSER_SFTP_GROUP ).exists() @@ -288,22 +244,6 @@ class User(TimeStampedModel, models.Model): :rtype: :py:class:`osusers.models.User` """ - dn = create_ldap_user.delay( - self.username, self.uid, self.group.gid, self.gecos, - self.homedir, self.shell, password=None).get() - TaskResult.objects.create_task_result( - setup_file_sftp_userdir.delay(self.username), - 'setup_file_sftp_userdir' - ) - TaskResult.objects.create_task_result( - setup_file_mail_userdir.delay(self.username), - 'setup_file_mail_userdir' - ) - _LOGGER.info( - "created user %(user)s with LDAP dn %(dn)s, scheduled home " - "directory and mail base directory creation.", { - 'user': self, 'dn': dn, - }) return super(User, self).save(*args, **kwargs) @transaction.atomic @@ -318,18 +258,7 @@ class User(TimeStampedModel, models.Model): :py:meth:`django.db.Model.delete` """ - TaskResult.objects.create_task_result( - delete_file_mail_userdir.delay(self.username), - 'delete_file_mail_userdir' - ) - TaskResult.objects.create_task_result( - delete_file_sftp_userdir.delay(self.username), - 'delete_file_sftp_userdir' - ) - for group in [ag.group for ag in self.additionalgroup_set.all()]: - remove_ldap_user_from_group.delay( - self.username, group.groupname).get() - delete_ldap_user.delay(self.username).get() + # noinspection PyUnresolvedReferences self.group.delete() super(User, self).delete(*args, **kwargs) @@ -354,62 +283,84 @@ class ShadowManager(models.Manager): """ changedays = (timezone.now().date() - date(1970, 1, 1)).days shadow = self.create( - user=user, changedays=changedays, - minage=0, maxage=None, gracedays=7, - inactdays=30, expiredays=None + user=user, + changedays=changedays, + minage=0, + maxage=None, + gracedays=7, + inactdays=30, + expiredays=None, ) shadow.set_password(password) shadow.save() return shadow -@python_2_unicode_compatible class Shadow(TimeStampedModel, models.Model): """ This entity class corresponds to an operating system user's shadow file entry. """ - user = models.OneToOneField(User, primary_key=True, verbose_name=_('User')) - passwd = models.CharField(_('Encrypted password'), max_length=128) + + user = models.OneToOneField( + User, primary_key=True, verbose_name=_("User"), on_delete=models.CASCADE + ) + passwd = models.CharField(_("Encrypted password"), max_length=128) changedays = models.PositiveSmallIntegerField( - _('Date of last change'), - help_text=_('This is expressed in days since Jan 1, 1970'), - blank=True, null=True) + _("Date of last change"), + help_text=_("This is expressed in days since Jan 1, 1970"), + blank=True, + null=True, + ) minage = models.PositiveSmallIntegerField( - _('Minimum age'), - help_text=_('Minimum number of days before the password can be' - ' changed'), - blank=True, null=True) + _("Minimum age"), + help_text=_("Minimum number of days before the password can be" " changed"), + blank=True, + null=True, + ) maxage = models.PositiveSmallIntegerField( - _('Maximum age'), - help_text=_('Maximum number of days after which the password has to' - ' be changed'), - blank=True, null=True) + _("Maximum age"), + help_text=_( + "Maximum number of days after which the password has to" " be changed" + ), + blank=True, + null=True, + ) gracedays = models.PositiveSmallIntegerField( - _('Grace period'), - help_text=_('The number of days before the password is going to' - ' expire'), - blank=True, null=True) + _("Grace period"), + help_text=_("The number of days before the password is going to" " expire"), + blank=True, + null=True, + ) inactdays = models.PositiveSmallIntegerField( - _('Inactivity period'), - help_text=_('The number of days after the password has expired during' - ' which the password should still be accepted'), - blank=True, null=True) + _("Inactivity period"), + help_text=_( + "The number of days after the password has expired during" + " which the password should still be accepted" + ), + blank=True, + null=True, + ) expiredays = models.PositiveSmallIntegerField( - _('Account expiration date'), - help_text=_('The date of expiration of the account, expressed as' - ' number of days since Jan 1, 1970'), - blank=True, null=True, default=None) + _("Account expiration date"), + help_text=_( + "The date of expiration of the account, expressed as" + " number of days since Jan 1, 1970" + ), + blank=True, + null=True, + default=None, + ) objects = ShadowManager() class Meta: - verbose_name = _('Shadow password') - verbose_name_plural = _('Shadow passwords') + verbose_name = _("Shadow password") + verbose_name_plural = _("Shadow passwords") def __str__(self): - return 'for user {0}'.format(self.user) + return "for user {0}".format(self.user) def set_password(self, password): """ @@ -420,23 +371,23 @@ class Shadow(TimeStampedModel, models.Model): self.passwd = sha512_crypt.encrypt(password) -@python_2_unicode_compatible class AdditionalGroup(TimeStampedModel, models.Model): """ This entity class corresponds to additional group assignments for an :py:class:`operating system user `. """ - user = models.ForeignKey(User) - group = models.ForeignKey(Group) + + user = models.ForeignKey(User, on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE) class Meta: - unique_together = ('user', 'group') - verbose_name = _('Additional group') - verbose_name_plural = _('Additional groups') + unique_together = ("user", "group") + verbose_name = _("Additional group") + verbose_name_plural = _("Additional groups") def __str__(self): - return '{0} in {1}'.format(self.user, self.group) + return "{0} in {1}".format(self.user, self.group) def clean(self): """ @@ -444,6 +395,7 @@ class AdditionalGroup(TimeStampedModel, models.Model): group. """ + # noinspection PyUnresolvedReferences if self.user.group == self.group: raise ValidationError(CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL) @@ -460,8 +412,6 @@ class AdditionalGroup(TimeStampedModel, models.Model): :rtype: :py:class:`AdditionalGroup ` """ - add_ldap_user_to_group.delay( - self.user.username, self.group.groupname).get() return super(AdditionalGroup, self).save(*args, **kwargs) @transaction.atomic @@ -474,11 +424,6 @@ class AdditionalGroup(TimeStampedModel, models.Model): :param kwargs: keyword arguments to be passed on to :py:meth:`django.db.Model.delete` """ - TaskResult.objects.create_task_result( - remove_ldap_user_from_group.delay( - self.user.username, self.group.groupname), - 'remove_ldap_user_from_group' - ) super(AdditionalGroup, self).delete(*args, **kwargs) @@ -489,55 +434,62 @@ class SshPublicKeyManager(models.Manager): """ - def parse_keytext(self, keytext): + def parse_key_text(self, key_text: str): """ Parse a SSH public key in OpenSSH or :rfc:`4716` format into its components algorithm, key data and comment. - :param str keytext: key text + :param str key_text: key text :return: triple of algorithm name, key data and comment :rtype: triple of str """ - if keytext.startswith('---- BEGIN SSH2 PUBLIC KEY ----'): - comment = '' - data = '' - continued = '' + if key_text.startswith("---- BEGIN SSH2 PUBLIC KEY ----"): + comment = "" + data = "" + continued = "" headers = {} - for line in keytext.splitlines(): - if line == '---- BEGIN SSH2 PUBLIC KEY ----': + header_tag = None + for line in key_text.splitlines(): + if line == "---- BEGIN SSH2 PUBLIC KEY ----": continue - elif ':' in line: # a header line + elif ":" in line: # a header line header_tag, header_value = [ - item.strip() for item in line.split(':', 1)] - if header_value.endswith('\\'): + item.strip() for item in line.split(":", 1) + ] + if header_value.endswith("\\"): continued = header_value[:-1] else: headers[header_tag.lower()] = header_value elif continued: - if line.endswith('\\'): + if line.endswith("\\"): continued += line[:-1] continue header_value = continued + line headers[header_tag.lower()] = header_value - continued = '' - elif line == '---- END SSH2 PUBLIC KEY ----': + continued = "" + elif line == "---- END SSH2 PUBLIC KEY ----": break elif line: # ignore empty lines data += line - if 'comment' in headers: - comment = headers['comment'] + if "comment" in headers: + comment = headers["comment"] else: - parts = keytext.split(None, 2) + parts = key_text.split(None, 2) if len(parts) < 2: - raise ValueError('invalid SSH public key') + raise ValueError("invalid SSH public key") data = parts[1] comment = len(parts) == 3 and parts[2] or "" - keybytes = base64.b64decode(data) - parts = keybytes.split(b'\x00' * 3) - alglength = six.byte2int(parts[1]) - algname = parts[1][1:1+alglength] - return algname, data, comment + try: + keybytes = base64.b64decode(data) + except TypeError: + raise ValueError("invalid SSH public key") + parts = keybytes.split(b"\x00" * 3) + if len(parts) < 2: + raise ValueError("invalid SSH public key") + key_length = int.from_bytes(parts[1], byteorder="big") + key_algorithm = parts[1][1 : 1 + key_length].decode("utf-8") + return key_algorithm, data, comment def create_ssh_public_key(self, user, keytext): """ @@ -551,53 +503,30 @@ class SshPublicKeyManager(models.Manager): :retype: :py:class:`osusers.models.SshPublicKey` """ - algorithm, data, comment = self.parse_keytext(keytext) - return self.create( - user=user, algorithm=algorithm, data=data, comment=comment) + algorithm, data, comment = self.parse_key_text(keytext) + return self.create(user=user, algorithm=algorithm, data=data, comment=comment) -@python_2_unicode_compatible class SshPublicKey(TimeStampedModel): """ This entity class represents single SSH keys for an :py:class:`operating system user `. """ - user = models.ForeignKey(User, verbose_name=_('User')) - algorithm = models.CharField(_('Algorithm'), max_length=20) - data = models.TextField(_('Key bytes'), - help_text=_('Base64 encoded key bytes')) - comment = models.TextField(_('Comment'), blank=True) + + user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE) + algorithm = models.CharField(_("Algorithm"), max_length=20) + data = models.TextField(_("Key bytes"), help_text=_("Base64 encoded key bytes")) + comment = models.TextField(_("Comment"), blank=True) objects = SshPublicKeyManager() class Meta: - verbose_name = _('SSH public key') - verbose_name_plural = _('SSH public keys') - unique_together = [('user', 'algorithm', 'data')] + verbose_name = _("SSH public key") + verbose_name_plural = _("SSH public keys") + unique_together = [("user", "algorithm", "data")] def __str__(self): return "{algorithm} {data} {comment}".format( algorithm=self.algorithm, data=self.data, comment=self.comment ).strip() - - def save(self, **kwargs): - key = super(SshPublicKey, self).save(**kwargs) - TaskResult.objects.create_task_result( - set_file_ssh_authorized_keys.delay( - self.user.username, [ - str(key) for key in - SshPublicKey.objects.filter(user=self.user)]), - 'set_file_ssh_authorized_keys' - ) - return key - - def delete(self, **kwargs): - super(SshPublicKey, self).delete(**kwargs) - TaskResult.objects.create_task_result( - set_file_ssh_authorized_keys.delay( - self.user.username, [ - str(key) for key in - SshPublicKey.objects.filter(user=self.user)]), - 'set_file_ssh_authorized_keys' - ) diff --git a/gnuviechadmin/osusers/signals.py b/gnuviechadmin/osusers/signals.py new file mode 100644 index 0000000..e062cb9 --- /dev/null +++ b/gnuviechadmin/osusers/signals.py @@ -0,0 +1,401 @@ +""" +This module contains the signal handlers of the :py:mod:`osusers` app. + +The module starts Celery_ tasks. + +.. _Celery: http://www.celeryproject.org/ + +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from django.db.models.signals import ( + post_delete, + post_save, +) +from django.dispatch import receiver + +from fileservertasks.tasks import ( + delete_file_mail_userdir, + delete_file_sftp_userdir_chained, + set_file_ssh_authorized_keys, + setup_file_mail_userdir_chained, + setup_file_sftp_userdir_chained, +) +from ldaptasks.tasks import ( + add_ldap_user_to_group, + create_ldap_group, + create_ldap_user, + delete_ldap_group, + delete_ldap_user_chained, + remove_ldap_user_from_group, + set_ldap_user_password, +) +from taskresults.models import TaskResult + +from .models import ( + AdditionalGroup, + Group, + SshPublicKey, + User, + password_set, +) + + +_LOGGER = logging.getLogger(__name__) + + +@receiver(password_set, sender=User) +def handle_user_password_set(sender, instance, password, **kwargs): + """ + Handles password changes on :py:class:`User ` + instances. + + :param sender: sender of the signal + :param instance: User instance + :param str password: the new password + + This signal handler starts a Celery_ task. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "set ldap user password", color = "Wheat", + description = ":py:func:`set_ldap_user_password() + ` called with + username and password, returning :py:const:`True` if the + password has been set" ]; + } + """ + taskresult = TaskResult.objects.create_task_result( + 'handle_user_password_set', + set_ldap_user_password.s(instance.username, password)) + _LOGGER.info( + 'LDAP password change has been requested in task %s', + taskresult.task_id) + + +@receiver(post_save, sender=Group) +def handle_group_created(sender, instance, created, **kwargs): + """ + Handles post creation actions on :py:class:`Group ` + instances. + + :param sender: sender of the signal + :param instance: Group instance + :param bool created: whether the instance has just been created + + This signal handler starts a Celery_ task. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "create ldap group", color = "Wheat", + description = ":py:func:`create_ldap_group() + ` called with groupname, + gid and description, returning group DN" ]; + } + + """ + if created: + taskresult = TaskResult.objects.create_task_result( + 'handle_group_created', + create_ldap_group.s( + instance.groupname, instance.gid, instance.descr)) + _LOGGER.info( + 'LDAP group creation has been requested in task %s', + taskresult.task_id) + _LOGGER.debug( + 'group %s has been %s', instance, created and "created" or "updated") + + +@receiver(post_save, sender=User) +def handle_user_created(sender, instance, created, **kwargs): + """ + Handles post creation actions on :py:class:`User ` + instances. + + :param sender: sender of the signal + :param instance: User instance + :param bool created: whether the instance has just bean created + + This signal handler starts a chain of Celery_ tasks. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B -> C -> D; + B -> C [folded]; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "create ldap user", color = "Wheat", + description = ":py:func:`create_ldap_user() + ` called with username, uid, + gid, gecos, homeidr, shell, :py:const:`None`, returning + username" ]; + C [ label = "setup file sftp userdir", color = "LightGreen", + description = ":py:func:`setup_file_sftp_userdir_chained() + ` + called with the result of create ldap user task, returning a + dictionary containing username and sftp_directory"]; + D [ label = "setup file mail userdir", color = "LightGreen", + description = ":py:func:`setup_file_mail_userdir_chained() + ` called + with result of setup file sftp userdir task, returning + dictionary containing username, sftp_directory and + mail_directory" ]; + } + + """ + if created: + chain = create_ldap_user.s( + instance.username, instance.uid, instance.group.gid, + instance.gecos, instance.homedir, instance.shell, None + ) | setup_file_sftp_userdir_chained.s() | ( + setup_file_mail_userdir_chained.s()) + taskresult = TaskResult.objects.create_task_result( + 'handle_user_created', chain) + _LOGGER.info( + 'LDAP user creation has been requested in task %s', + taskresult.task_id) + _LOGGER.debug( + 'user %s has been %s', instance, created and "created" or "updated") + + +@receiver(post_save, sender=AdditionalGroup) +def handle_user_added_to_group(sender, instance, created, **kwargs): + """ + Handles post creation actions on :py:class:`AdditionalGroup + ` instances. + + :param sender: sender of the signal + :param instance: AdditionalGroup instance + :param bool created: whether the instance has just bean created + + This signal handler starts a Celery_ task. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "add ldap user to group", color = "Wheat", + description = ":py:func:`add_ldap_user_to_group() + ` called with username + and groupname, returning :py:const:`True` if the user has been + added to the group" ]; + } + + """ + if created: + taskresult = TaskResult.objects.create_task_result( + 'handle_user_added_to_group', + add_ldap_user_to_group.s( + instance.user.username, instance.group.groupname)) + _LOGGER.info( + 'Adding user to LDAP group has been requested in task %s', + taskresult.task_id) + + +@receiver(post_save, sender=SshPublicKey) +@receiver(post_delete, sender=SshPublicKey) +def handle_ssh_keys_changed(sender, instance, **kwargs): + """ + Handles changes to :py:class:`SshPublicKey ` + instances related to a user. + + :param sender: sender of the signal + :param instance: SshPublicKey instance + + This signal handler starts a Celery_ task. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "set file ssh authorized_keys", color = "LightGreen", + description = ":py:func:`set_file_ssh_authorized_keys() + ` called + with username and the corresponding list of keys, returning the + path of the ssh_authorized_keys_file" ]; + } + + """ + sig = set_file_ssh_authorized_keys.s( + instance.user.username, [ + str(key) for key in + SshPublicKey.objects.filter(user=instance.user)]) + taskresult = TaskResult.objects.create_task_result( + 'handle_ssh_keys_changed', sig) + _LOGGER.info( + 'Change of SSH keys has been requested in task %s', + taskresult.task_id) + + +# @receiver(post_delete) +# def handle_post_delete(sender, **kwargs): +# _LOGGER.debug( +# 'handling post_delete signal for %s with args %s', +# sender, kwargs) + + +@receiver(post_delete, sender=Group) +def handle_group_deleted(sender, instance, **kwargs): + """ + Handles cleanup actions to be done after deletion of a :py:class:`Group + ` instance. + + :param sender: sender of the signal + :param instance: Group instance + + This signal handler starts a Celery_ task. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + + A [ label = "", shape = beginpoint, + description = "this signal handler" + ]; + B [ label = "delete ldap group", color = "Wheat", + description = ":py:func:`delete_ldap_group() + ` called with groupname, + returning :py:const:`True` if the group has been deleted" ]; + } + + """ + taskresult = TaskResult.objects.create_task_result( + 'handle_group_deleted', + delete_ldap_group.s(instance.groupname)) + _LOGGER.info( + 'LDAP group deletion has been requested in task %s', + taskresult.task_id) + + +@receiver(post_delete, sender=User) +def handle_user_deleted(sender, instance, **kwargs): + """ + Handles cleanup actions to be done after deletion of a :py:class:`User + ` instance. + + :param sender: sender of the signal + :param instance: User instance + + This signal handler starts a chain of Celery_ tasks. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B -> C -> D; + B -> C [folded]; + + A [ label = "", shape = beginpoint, + description = "this signal handler" + ]; + B [ label = "delete file mail userdir", color = "LightGreen", + description = ":py:func:`delete_file_mail_userdir() + ` called with + username, returning a dictionary containing the username and + the deleted mail_directory" ]; + C [ label = "delete file sftp userdir", color = "LightGreen", + description = ":py:func:`delete_file_sftp_userdir_chained() + ` + called with the result of delete mail userdir, returning + dictionary containing username, deleted mail_directory and + deleted sftp_directory" ]; + D [ label = "delete ldap user", color = "Wheat", + description = ":py:func:`delete_ldap_user_chained() + ` called with the + result of delete file sftp userdir and adding the deleted user + DN to the result" ]; + } + + """ + chain = delete_file_mail_userdir.s( + instance.username + ) | delete_file_sftp_userdir_chained.s() | delete_ldap_user_chained.s() + _LOGGER.debug('chain signature %s', chain) + taskresult = TaskResult.objects.create_task_result( + 'handle_user_deleted', chain) + _LOGGER.info( + 'LDAP user deletion has been requested in task %s', + taskresult.task_id) + + +@receiver(post_delete, sender=AdditionalGroup) +def handle_user_removed_from_group(sender, instance, **kwargs): + """ + Handles cleanup actions to be done after removing a user from a group by + deleting the :py:class:`AdditionalGroup ` + instance. + + :param sender: sender of the signal + :param instance: AdditionalGroup instance + + This signal handler starts a Celery_ task. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + + A [ label = "", shape = beginpoint, + description = "this signal handler" + ]; + B [ label = "remove ldap user from group", color = "Wheat", + description = ":py:func:`remove_ldap_user_from_group() + ` called with + username and groupname, returning :py:const:`True` if the user + has been a member of the group and has been removed from the + group" + ]; + } + + """ + taskresult = TaskResult.objects.create_task_result( + 'handle_user_removed_from_group', + remove_ldap_user_from_group.s( + instance.user.username, instance.group.groupname)) + _LOGGER.info( + 'Removing user from LDAP group has been requested in task %s', + taskresult.task_id) diff --git a/gnuviechadmin/osusers/tests/test_admin.py b/gnuviechadmin/osusers/tests/test_admin.py index 405116f..1f2e7f3 100644 --- a/gnuviechadmin/osusers/tests/test_admin.py +++ b/gnuviechadmin/osusers/tests/test_admin.py @@ -5,15 +5,15 @@ from django.test.utils import override_settings from django.contrib.auth import get_user_model -from mock import Mock +from unittest.mock import MagicMock, Mock, patch -from osusers.models import ( - Group, - User, -) +from osusers.forms import INVALID_SSH_PUBLIC_KEY, DUPLICATE_SSH_PUBLIC_KEY_FOR_USER +from osusers.models import Group, SshPublicKey, User from osusers.admin import ( GroupAdmin, PASSWORD_MISMATCH_ERROR, + SshPublicKeyAdmin, + SshPublicKeyCreationForm, UserAdmin, UserCreationForm, ) @@ -23,20 +23,19 @@ Customer = get_user_model() class CustomerTestCase(TestCase): def setUp(self): - self.customer = Customer.objects.create_user('test') + self.customer = Customer.objects.create_user("test") super(CustomerTestCase, self).setUp() class UserCreationFormTest(CustomerTestCase): - def test_clean_password2_same(self): form = UserCreationForm() form.cleaned_data = { - 'customer': self.customer, - 'password1': 'secret', - 'password2': 'secret' + "customer": self.customer, + "password1": "secret", + "password2": "secret", } - self.assertEqual(form.clean_password2(), 'secret') + self.assertEqual(form.clean_password2(), "secret") def test_clean_password2_empty(self): form = UserCreationForm() @@ -46,25 +45,23 @@ class UserCreationFormTest(CustomerTestCase): def test_clean_password2_mismatch(self): form = UserCreationForm() form.cleaned_data = { - 'customer': self.customer, - 'password1': 'secretx', - 'password2': 'secrety' + "customer": self.customer, + "password1": "secretx", + "password2": "secrety", } with self.assertRaises(forms.ValidationError) as cm: form.clean_password2() self.assertEqual(cm.exception.message, PASSWORD_MISMATCH_ERROR) @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_save_commit(self): form = UserCreationForm() form.cleaned_data = { - 'customer': self.customer, - 'password1': 'secret', - 'password2': 'secret' + "customer": self.customer, + "password1": "secret", + "password2": "secret", } user = form.save() self.assertIsNotNone(user) @@ -82,43 +79,49 @@ class UserAdminTest(CustomerTestCase): super(UserAdminTest, self).setUp() def test_get_form_without_object(self): - form = self.uadmin.get_form(Mock(name='request')) - self.assertEqual( - form.Meta.fields, - ['customer', 'password1', 'password2'] - ) + form = self.uadmin.get_form(Mock(name="request")) + self.assertEqual(form.Meta.fields, ["customer", "password1", "password2"]) @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_get_form_with_object(self): user = User.objects.create_user(customer=self.customer) - form = self.uadmin.get_form(Mock(name='request'), user) + form = self.uadmin.get_form(Mock(name="request"), user) self.assertEqual( form.Meta.fields, - ['username', 'group', 'gecos', 'homedir', 'shell', 'customer', - 'uid'] + ["username", "group", "gecos", "homedir", "shell", "customer", "uid"], ) def test_get_inline_instances_without_object(self): - inlines = self.uadmin.get_inline_instances(Mock(name='request')) + inlines = self.uadmin.get_inline_instances(Mock(name="request")) self.assertEqual(len(inlines), 2) @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_get_inline_instances_with_object(self): user = User.objects.create_user(customer=self.customer) - inlines = self.uadmin.get_inline_instances( - Mock(name='request'), user) + inlines = self.uadmin.get_inline_instances(Mock(name="request"), user) self.assertEqual(len(inlines), len(UserAdmin.inlines)) for index in range(len(inlines)): self.assertIsInstance(inlines[index], UserAdmin.inlines[index]) + @override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" + ) + def test_perform_delete_selected(self): + user = User.objects.create_user(customer=self.customer) + self.uadmin.perform_delete_selected( + Mock(name="request"), User.objects.filter(uid=user.uid) + ) + self.assertEqual(User.objects.filter(uid=user.uid).count(), 0) + + def test_get_actions(self): + requestmock = MagicMock(name="request") + self.assertNotIn("delete_selected", self.uadmin.get_actions(requestmock)) + self.assertIn("perform_delete_selected", self.uadmin.get_actions(requestmock)) + class GroupAdminTest(TestCase): def setUp(self): @@ -127,18 +130,132 @@ class GroupAdminTest(TestCase): super(GroupAdminTest, self).setUp() def test_get_inline_instances_without_object(self): - inlines = self.gadmin.get_inline_instances(Mock(name='request')) + inlines = self.gadmin.get_inline_instances(Mock(name="request")) self.assertEqual(inlines, []) @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) def test_get_inline_instances_with_object(self): - group = Group.objects.create(gid=1000, groupname='test') - inlines = self.gadmin.get_inline_instances( - Mock(name='request'), group) + group = Group.objects.create(gid=1000, groupname="test") + inlines = self.gadmin.get_inline_instances(Mock(name="request"), group) self.assertEqual(len(inlines), len(GroupAdmin.inlines)) for index in range(len(inlines)): self.assertIsInstance(inlines[index], GroupAdmin.inlines[index]) + + def test_perform_delete_selected(self): + group = Group.objects.create(gid=1000, groupname="test") + self.gadmin.perform_delete_selected( + Mock(name="request"), Group.objects.filter(gid=group.gid) + ) + self.assertEqual(Group.objects.filter(gid=group.gid).count(), 0) + + def test_get_actions(self): + requestmock = MagicMock(name="request") + self.assertNotIn("delete_selected", self.gadmin.get_actions(requestmock)) + self.assertIn("perform_delete_selected", self.gadmin.get_actions(requestmock)) + + +class SshPublicKeyCreationFormTest(CustomerTestCase): + @patch("osusers.admin.SshPublicKey.objects") + def test_clean_publickeytext_valid_key(self, sshpkmanager): + form = SshPublicKeyCreationForm() + sshpkmanager.parse_key_text = MagicMock(side_effect=ValueError) + form.cleaned_data = {"publickeytext": "wrongkey"} + with self.assertRaises(forms.ValidationError) as ve: + form.clean_publickeytext() + self.assertEqual(ve.exception.message, INVALID_SSH_PUBLIC_KEY) + + @patch("osusers.admin.SshPublicKey.objects") + def test_clean_publickeytext_invalid_key(self, sshpkmanager): + form = SshPublicKeyCreationForm() + sshpkmanager.parse_key_text = MagicMock(return_value="goodkey") + form.cleaned_data = {"publickeytext": "goodkey"} + self.assertEqual(form.clean_publickeytext(), "goodkey") + + def test_clean_missing_data(self): + form = SshPublicKeyCreationForm() + form.cleaned_data = {} + form.clean() + self.assertEqual(len(form.errors), 0) + + @patch("osusers.admin.SshPublicKey.objects.parse_key_text") + def test_clean_once(self, parse_key_text): + parse_key_text.return_value = ("good", "key", "comment") + user = User.objects.create_user(customer=self.customer) + form = SshPublicKeyCreationForm() + form.cleaned_data = {"user": user, "publickeytext": "good key comment"} + form.clean() + self.assertEqual(len(form.errors), 0) + + @patch("osusers.admin.SshPublicKey.objects.parse_key_text") + def test_clean_again(self, parse_key_text): + parse_key_text.return_value = ("good", "key", "comment") + user = User.objects.create_user(customer=self.customer) + SshPublicKey.objects.create( + user=user, algorithm="good", data="key", comment="comment" + ) + form = SshPublicKeyCreationForm() + form.cleaned_data = {"user": user, "publickeytext": "good key comment"} + form.clean() + self.assertIn("publickeytext", form.errors) + self.assertEqual( + form.errors["publickeytext"], [DUPLICATE_SSH_PUBLIC_KEY_FOR_USER] + ) + + @patch("osusers.admin.SshPublicKey.objects.parse_key_text") + def test_save(self, parse_key_text): + parse_key_text.return_value = ("good", "key", "comment") + user = User.objects.create_user(customer=self.customer) + form = SshPublicKeyCreationForm() + form.cleaned_data = {"user": user, "publickeytext": "good key comment"} + form.instance.user = user + form.save() + self.assertTrue( + SshPublicKey.objects.filter(user=user, algorithm="good", data="key") + ) + + +class SshPublicKeyAdminTest(CustomerTestCase): + def setUp(self): + site = AdminSite() + self.sadmin = SshPublicKeyAdmin(SshPublicKey, site) + super(SshPublicKeyAdminTest, self).setUp() + + def test_get_form_no_instance(self): + form = self.sadmin.get_form(request=Mock(name="request")) + self.assertEqual(form.Meta.model, SshPublicKey) + + def test_get_form_with_instance(self): + user = User.objects.create_user(customer=self.customer) + key = SshPublicKey.objects.create( + user=user, algorithm="good", data="key", comment="comment" + ) + form = self.sadmin.get_form(request=Mock(name="request"), obj=key) + self.assertEqual(form.Meta.model, SshPublicKey) + self.assertEqual(form.Meta.fields, ["user", "comment", "algorithm", "data"]) + + def test_get_readonly_fields_no_instance(self): + readonly_fields = self.sadmin.get_readonly_fields(request=Mock(name="request")) + self.assertEqual(readonly_fields, []) + + def test_get_readonly_fields_with_instance(self): + readonly_fields = self.sadmin.get_readonly_fields( + request=Mock(name="request"), obj=Mock() + ) + self.assertEqual(readonly_fields, ["algorithm", "data"]) + + def test_perform_delete_selected(self): + user = User.objects.create_user(customer=self.customer) + key = SshPublicKey.objects.create( + user=user, algorithm="good", data="key", comment="comment" + ) + self.sadmin.perform_delete_selected( + Mock(name="request"), SshPublicKey.objects.filter(id=key.id) + ) + self.assertFalse(SshPublicKey.objects.filter(id=key.id).exists()) + + def test_get_actions(self): + requestmock = MagicMock(name="request") + self.assertNotIn("delete_selected", self.sadmin.get_actions(requestmock)) + self.assertIn("perform_delete_selected", self.sadmin.get_actions(requestmock)) diff --git a/gnuviechadmin/osusers/tests/test_forms.py b/gnuviechadmin/osusers/tests/test_forms.py new file mode 100644 index 0000000..e3c5eb6 --- /dev/null +++ b/gnuviechadmin/osusers/tests/test_forms.py @@ -0,0 +1,195 @@ +""" +This module provides tests for :py:mod:`osusers.forms`. + +""" +from unittest.mock import MagicMock, Mock, patch + +from django import forms +from django.test import TestCase + +from django.contrib.auth import get_user_model +from django.urls import reverse + +from passlib.hash import sha512_crypt + +from osusers.forms import ( + AddSshPublicKeyForm, + ChangeOsUserPasswordForm, + DUPLICATE_SSH_PUBLIC_KEY_FOR_USER, + EditSshPublicKeyCommentForm, + INVALID_SSH_PUBLIC_KEY, +) + +from osusers.models import SshPublicKey, User + +Customer = get_user_model() + + +class AddSshPublicKeyFormTest(TestCase): + """ + Test for :py:class:`osusers.forms.AddSshPublicKeyForm`. + + """ + + def _setup_hostingpackage(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer=customer) + self.hostingpackage = Mock(id=42, osuser=user) + + def test_constructor_needs_hostingpackage(self): + instance = MagicMock() + with self.assertRaises(KeyError) as ke: + AddSshPublicKeyForm(instance) + self.assertEqual(ke.exception.args[0], "hostingpackage") + + def test_constructor(self): + self._setup_hostingpackage() + instance = MagicMock() + form = AddSshPublicKeyForm(instance, hostingpackage=self.hostingpackage) + self.assertTrue(hasattr(form, "osuser")) + self.assertEqual(form.osuser, self.hostingpackage.osuser) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse("add_ssh_key", kwargs={"package": self.hostingpackage.id}), + ) + self.assertIn("publickeytext", form.fields) + self.assertEqual(form.helper.inputs[0].name, "submit") + + @patch("osusers.forms.SshPublicKey.objects.parse_key_text") + def test_clean_publickeytext_invalid(self, parse_key_text): + self._setup_hostingpackage() + instance = MagicMock() + form = AddSshPublicKeyForm(instance, hostingpackage=self.hostingpackage) + form.cleaned_data = {"publickeytext": "a bad key"} + parse_key_text.side_effect = ValueError + with self.assertRaises(forms.ValidationError) as ve: + form.clean_publickeytext() + self.assertEqual(ve.exception.message, INVALID_SSH_PUBLIC_KEY) + + @patch("osusers.forms.SshPublicKey.objects.parse_key_text") + def test_clean_publickeytext_valid(self, _): + self._setup_hostingpackage() + instance = MagicMock() + form = AddSshPublicKeyForm(instance, hostingpackage=self.hostingpackage) + form.cleaned_data = {"publickeytext": "good key comment"} + retval = form.clean_publickeytext() + self.assertEqual(retval, "good key comment") + + def test_clean_none(self): + self._setup_hostingpackage() + instance = MagicMock() + form = AddSshPublicKeyForm(instance, hostingpackage=self.hostingpackage) + form.cleaned_data = {"publickeytext": None} + form.clean() + self.assertIsNone(form.cleaned_data["publickeytext"]) + + @patch("osusers.forms.SshPublicKey.objects.parse_key_text") + def test_clean_fresh(self, parse_key_text): + self._setup_hostingpackage() + instance = MagicMock() + form = AddSshPublicKeyForm(instance, hostingpackage=self.hostingpackage) + sshpubkey = "good key comment" + form.cleaned_data = {"publickeytext": sshpubkey} + parse_key_text.return_value = sshpubkey.split(" ") + form.clean() + self.assertEqual(form.cleaned_data["publickeytext"], "good key comment") + + @patch("osusers.forms.SshPublicKey.objects.parse_key_text") + def test_clean_duplicate(self, parse_key_text): + self._setup_hostingpackage() + instance = MagicMock() + form = AddSshPublicKeyForm(instance, hostingpackage=self.hostingpackage) + SshPublicKey.objects.create( + user=self.hostingpackage.osuser, + algorithm="good", + data="key", + comment="comment", + ) + sshpubkey = "good key comment" + form.cleaned_data = {"publickeytext": sshpubkey} + parse_key_text.return_value = sshpubkey.split(" ") + form.clean() + self.assertIn("publickeytext", form.errors) + self.assertIn(DUPLICATE_SSH_PUBLIC_KEY_FOR_USER, form.errors["publickeytext"]) + + @patch("osusers.admin.SshPublicKey.objects.parse_key_text") + def test_save(self, parse_key_text): + self._setup_hostingpackage() + instance = MagicMock() + form = AddSshPublicKeyForm(instance, hostingpackage=self.hostingpackage) + sshpubkey = "good key comment" + form.cleaned_data = {"publickeytext": sshpubkey} + parse_key_text.return_value = sshpubkey.split(" ") + retval = form.save() + self.assertTrue(isinstance(retval, SshPublicKey)) + self.assertEqual(retval.algorithm, "good") + self.assertEqual(retval.data, "key") + self.assertEqual(retval.comment, "comment") + + +class ChangeOsUserPasswordFormTest(TestCase): + """ + Test for :py:class:`osusers.forms.ChangeOsUserPasswordForm`. + + """ + + def _setup_user(self): + customer = Customer.objects.create_user("test") + self.user = User.objects.create_user(customer=customer) + + def test_constructor(self): + self._setup_user() + form = ChangeOsUserPasswordForm(instance=self.user) + self.assertTrue(hasattr(form, "instance")) + self.assertEqual(form.instance, self.user) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse("set_osuser_password", kwargs={"slug": self.user.username}), + ) + self.assertEqual(form.helper.inputs[0].name, "submit") + + def test_save(self): + self._setup_user() + form = ChangeOsUserPasswordForm(instance=self.user) + form.cleaned_data = {"password1": "test"} + user = form.save() + self.assertTrue(sha512_crypt.verify("test", user.shadow.passwd)) + + +class EditSshPublicKeyCommentFormTest(TestCase): + """ + Test for :py:class:`osusers.forms.EditSshPublicKeyCommentForm`. + + """ + + def _setup_hostingpackage(self): + customer = Customer.objects.create_user("test") + user = User.objects.create_user(customer=customer) + self.hostingpackage = Mock(id=42, osuser=user) + + def test_constructor_needs_hostingpackage(self): + instance = MagicMock() + with self.assertRaises(KeyError) as ke: + EditSshPublicKeyCommentForm(instance) + self.assertEqual(ke.exception.args[0], "hostingpackage") + + def test_constructor(self): + self._setup_hostingpackage() + instance = MagicMock(id=1) + form = EditSshPublicKeyCommentForm( + instance=instance, hostingpackage=self.hostingpackage + ) + self.assertTrue(hasattr(form, "osuser")) + self.assertEqual(form.osuser, self.hostingpackage.osuser) + self.assertIn("comment", form.fields) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse( + "edit_ssh_key_comment", + kwargs={"package": self.hostingpackage.id, "pk": instance.id}, + ), + ) + self.assertEqual(form.helper.inputs[0].name, "submit") diff --git a/gnuviechadmin/osusers/tests/test_models.py b/gnuviechadmin/osusers/tests/test_models.py index 1897234..8e0439a 100644 --- a/gnuviechadmin/osusers/tests/test_models.py +++ b/gnuviechadmin/osusers/tests/test_models.py @@ -1,17 +1,17 @@ +# -*- coding: utf-8 -*- from datetime import date from django.conf import settings +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone -from django.contrib.auth import get_user_model - from passlib.hash import sha512_crypt from osusers.models import ( - CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL, AdditionalGroup, + CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL, Group, Shadow, SshPublicKey, @@ -19,7 +19,6 @@ from osusers.models import ( ) from taskresults.models import TaskResult - EXAMPLE_KEY_1_RFC4716 = """---- BEGIN SSH2 PUBLIC KEY ---- Comment: "1024-bit RSA, converted from OpenSSH by me@example.com" x-command: /home/me/bin/lock-in-guest.sh @@ -55,12 +54,14 @@ n24VYtYtsMu74qXviYjziVucWKjjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5 sY29ouezv4Xz2PuMch5VGPP+CDqzCM4loWgV ---- END SSH2 PUBLIC KEY ----""" -EXAMPLE_KEY_4_OPENSSH = "".join(( -"ssh-rsa ", -"AAAAB3NzaC1yc2EAAAABIwAAAIEA1on8gxCGJJWSRT4uOrR13mUaUk0hRf4RzxSZ1zRb", -"YYFw8pfGesIFoEuVth4HKyF8k1y4mRUnYHP1XNMNMJl1JcEArC2asV8sHf6zSPVffozZ", -"5TT4SfsUu/iKy9lUcCfXzwre4WWZSXXcPff+EHtWshahu3WzBdnGxm5Xoi89zcE=" -)) +EXAMPLE_KEY_4_OPENSSH = "".join( + ( + "ssh-rsa ", + "AAAAB3NzaC1yc2EAAAABIwAAAIEA1on8gxCGJJWSRT4uOrR13mUaUk0hRf4RzxSZ1zRb", + "YYFw8pfGesIFoEuVth4HKyF8k1y4mRUnYHP1XNMNMJl1JcEArC2asV8sHf6zSPVffozZ", + "5TT4SfsUu/iKy9lUcCfXzwre4WWZSXXcPff+EHtWshahu3WzBdnGxm5Xoi89zcE=", + ) +) EXAMPLE_KEY_5_RFC4716_MULTILINE = """---- BEGIN SSH2 PUBLIC KEY ---- Comment: DSA Public Key \\ @@ -91,13 +92,21 @@ n24VYtYtsMu74qXviYjziVucWKjjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5 sY29ouezv4Xz2PuMch5VGPP+CDqzCM4loWgV ---- END SSH2 PUBLIC KEY ----""" +EXAMPLE_KEY_7_NO_COMMENT = """---- BEGIN SSH2 PUBLIC KEY ---- +AAAAB3NzaC1yc2EAAAABIwAAAIEA1on8gxCGJJWSRT4uOrR13mUaUk0hRf4RzxSZ1zRb +YYFw8pfGesIFoEuVth4HKyF8k1y4mRUnYHP1XNMNMJl1JcEArC2asV8sHf6zSPVffozZ +5TT4SfsUu/iKy9lUcCfXzwre4WWZSXXcPff+EHtWshahu3WzBdnGxm5Xoi89zcE= +---- END SSH2 PUBLIC KEY ----""" + +EXAMPLE_KEY_8_OPENSSH_BROKEN = "".join(("ssh-rsa ", "AschrÖdderöd")) + +EXAMPLE_KEY_9_RFC4716_ONLY_HEADER = "---- BEGIN SSH2 PUBLIC KEY ----" + Customer = get_user_model() @override_settings( - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND='memory', - BROKER_BACKEND='memory' + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" ) class TestCaseWithCeleryTasks(TestCase): pass @@ -105,42 +114,65 @@ class TestCaseWithCeleryTasks(TestCase): class AdditionalGroupTest(TestCaseWithCeleryTasks): def setUp(self): - customer = Customer.objects.create(username='test') - self.group1 = Group.objects.create(groupname='test1', gid=1000) + customer = Customer.objects.create(username="test") + self.group1 = Group.objects.create(groupname="test1", gid=1000) self.user = User.objects.create( - customer=customer, username='test', uid=1000, group=self.group1, - homedir='/home/test', shell='/bin/bash') + customer=customer, + username="test", + uid=1000, + group=self.group1, + homedir="/home/test", + shell="/bin/bash", + ) def test_clean_primary_group(self): testsubj = AdditionalGroup(user=self.user, group=self.group1) with self.assertRaises(ValidationError) as cm: testsubj.clean() - self.assertEqual( - cm.exception.message, CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL) + self.assertEqual(cm.exception.message, CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL) def test_clean_other_group(self): - group2 = Group.objects.create(groupname='test2', gid=1001) + group2 = Group.objects.create(groupname="test2", gid=1001) testsubj = AdditionalGroup(user=self.user, group=group2) testsubj.clean() def test_save(self): - group2 = Group.objects.create(groupname='test2', gid=1001) + group2 = Group.objects.create(groupname="test2", gid=1001) addgroup = AdditionalGroup(user=self.user, group=group2) addgroup.save() taskres = TaskResult.objects.all() - self.assertTrue(len(taskres), 1) - self.assertEqual(taskres[0].task_name, 'setup_file_sftp_userdir') + self.assertTrue(len(taskres), 4) + creators = [r.creator for r in taskres] + for tcount, tcreator in [ + (2, "handle_group_created"), + (1, "handle_user_created"), + (1, "handle_user_added_to_group"), + ]: + self.assertEqual(creators.count(tcreator), tcount) + + def test_save_again(self): + group2 = Group.objects.create(groupname="test2", gid=1001) + TaskResult.objects.all().delete() + addgroup = AdditionalGroup(user=self.user, group=group2) + addgroup.save() + TaskResult.objects.all().delete() + addgroup.save() + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 0) def test_delete(self): - group2 = Group.objects.create(groupname='test2', gid=1001) + group2 = Group.objects.create(groupname="test2", gid=1001) + # noinspection PyUnresolvedReferences addgroup = AdditionalGroup.objects.create(user=self.user, group=group2) addgroup.delete() + # noinspection PyUnresolvedReferences self.assertEqual(len(AdditionalGroup.objects.all()), 0) def test___str__(self): - group2 = Group.objects.create(groupname='test2', gid=1001) + group2 = Group.objects.create(groupname="test2", gid=1001) + # noinspection PyUnresolvedReferences addgroup = AdditionalGroup.objects.create(user=self.user, group=group2) - self.assertEqual(str(addgroup), 'test (1000) in test2 (1001)') + self.assertEqual(str(addgroup), "test (1000) in test2 (1001)") @override_settings(OSUSER_MINGID=10000) @@ -149,43 +181,61 @@ class GroupManagerTest(TestCaseWithCeleryTasks): self.assertEqual(Group.objects.get_next_gid(), 10000) def test_get_next_gid_second(self): - Group.objects.create(gid=10010, groupname='test') + Group.objects.create(gid=10010, groupname="test") self.assertEqual(Group.objects.get_next_gid(), 10011) class GroupTest(TestCaseWithCeleryTasks): def test___str__(self): - group = Group.objects.create(gid=10000, groupname='test') - self.assertEqual(str(group), 'test (10000)') + group = Group.objects.create(gid=10000, groupname="test") + self.assertEqual(str(group), "test (10000)") def test_save(self): - group = Group(gid=10000, groupname='test') + group = Group(gid=10000, groupname="test") self.assertIs(group.save(), group) + taskres = TaskResult.objects.all() + self.assertTrue(len(taskres), 1) + creators = [r.creator for r in taskres] + for tcount, tcreator in [(1, "handle_group_created")]: + self.assertEqual(creators.count(tcreator), tcount) + + def test_save_again(self): + group = Group.objects.create(gid=10000, groupname="test") + TaskResult.objects.all().delete() + group.save() + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 0) def test_delete(self): - group = Group.objects.create(gid=10000, groupname='test') + group = Group.objects.create(gid=10000, groupname="test") self.assertEqual(len(Group.objects.all()), 1) group.delete() self.assertEqual(len(Group.objects.all()), 0) - self.assertEqual(len(TaskResult.objects.all()), 1) + self.assertEqual(len(TaskResult.objects.all()), 2) tr = TaskResult.objects.first() - self.assertEqual(tr.task_name, 'delete_ldap_group') + self.assertEqual(tr.creator, "handle_group_created") class ShadowManagerTest(TestCaseWithCeleryTasks): def setUp(self): - self.customer = Customer.objects.create(username='test') + self.customer = Customer.objects.create(username="test") super(ShadowManagerTest, self).setUp() def test_create_shadow(self): - user = User( - customer=self.customer, username='test', uid=1000, - group=Group(gid=1000, groupname='test'), homedir='/home/test', - shell='/bin/fooshell') - shadow = Shadow.objects.create_shadow(user, 'test') - self.assertTrue(sha512_crypt.verify('test', shadow.passwd)) - self.assertEqual(shadow.changedays, - (timezone.now().date() - date(1970, 1, 1)).days) + group = Group.objects.create(gid=1000, groupname="test") + user = User.objects.create( + customer=self.customer, + username="test", + uid=1000, + group=group, + homedir="/home/test", + shell="/bin/fooshell", + ) + shadow = Shadow.objects.create_shadow(user, "test") + self.assertTrue(sha512_crypt.verify("test", shadow.passwd)) + self.assertEqual( + shadow.changedays, (timezone.now().date() - date(1970, 1, 1)).days + ) self.assertEqual(shadow.user, user) self.assertEqual(shadow.minage, 0) self.assertIsNone(shadow.maxage) @@ -196,39 +246,50 @@ class ShadowManagerTest(TestCaseWithCeleryTasks): class ShadowTest(TestCaseWithCeleryTasks): def setUp(self): - self.customer = Customer.objects.create(username='test') + self.customer = Customer.objects.create(username="test") super(ShadowTest, self).setUp() def test___str__(self): - group = Group.objects.create( - groupname='test', gid=1000) + group = Group.objects.create(groupname="test", gid=1000) user = User.objects.create( - customer=self.customer, username='test', uid=1000, group=group, - homedir='/home/test', shell='/bin/bash') + customer=self.customer, + username="test", + uid=1000, + group=group, + homedir="/home/test", + shell="/bin/bash", + ) shadow = Shadow(user=user) - self.assertEqual(str(shadow), 'for user test (1000)') + self.assertEqual(str(shadow), "for user test (1000)") def test_set_password(self): - group = Group.objects.create( - groupname='test', gid=1000) + group = Group.objects.create(groupname="test", gid=1000) user = User.objects.create( - customer=self.customer, username='test', uid=1000, group=group, - homedir='/home/test', shell='/bin/bash') + customer=self.customer, + username="test", + uid=1000, + group=group, + homedir="/home/test", + shell="/bin/bash", + ) shadow = Shadow(user=user) - shadow.set_password('test') - self.assertTrue(sha512_crypt.verify('test', shadow.passwd)) + shadow.set_password("test") + self.assertTrue(sha512_crypt.verify("test", shadow.passwd)) @override_settings( - OSUSER_MINUID=10000, OSUSER_MINGID=10000, OSUSER_USERNAME_PREFIX='test', - OSUSER_HOME_BASEPATH='/home', OSUSER_DEFAULT_SHELL='/bin/fooshell' + OSUSER_MINUID=10000, + OSUSER_MINGID=10000, + OSUSER_USERNAME_PREFIX="test", + OSUSER_HOME_BASEPATH="/home", + OSUSER_DEFAULT_SHELL="/bin/fooshell", ) class UserManagerTest(TestCaseWithCeleryTasks): def _create_group(self): - return Group.objects.create(gid=10000, groupname='foo') + return Group.objects.create(gid=10000, groupname="foo") def setUp(self): - self.customer = Customer.objects.create(username='test') + self.customer = Customer.objects.create(username="test") super(UserManagerTest, self).setUp() def test_get_next_uid_first(self): @@ -236,49 +297,71 @@ class UserManagerTest(TestCaseWithCeleryTasks): def test_get_next_uid_second(self): User.objects.create( - customer=self.customer, uid=10010, username='foo', - group=self._create_group(), homedir='/home/foo', - shell='/bin/fooshell') + customer=self.customer, + uid=10010, + username="foo", + group=self._create_group(), + homedir="/home/foo", + shell="/bin/fooshell", + ) self.assertEqual(User.objects.get_next_uid(), 10011) def test_get_next_username_first(self): - self.assertEqual(User.objects.get_next_username(), 'test01') + self.assertEqual(User.objects.get_next_username(), "test01") def test_get_next_username_second(self): User.objects.create( - customer=self.customer, uid=10000, username='test01', - group=self._create_group(), homedir='/home/foo', - shell='/bin/fooshell') - self.assertEqual(User.objects.get_next_username(), 'test02') + customer=self.customer, + uid=10000, + username="test01", + group=self._create_group(), + homedir="/home/foo", + shell="/bin/fooshell", + ) + self.assertEqual(User.objects.get_next_username(), "test02") def test_get_next_username_gaps(self): group = self._create_group() User.objects.create( - customer=self.customer, uid=10000, username='test01', group=group, - homedir='/home/foo', shell='/bin/fooshell') + customer=self.customer, + uid=10000, + username="test01", + group=group, + homedir="/home/foo", + shell="/bin/fooshell", + ) User.objects.create( - customer=self.customer, uid=10002, username='test03', group=group, - homedir='/home/foo', shell='/bin/fooshell') - self.assertEqual(User.objects.get_next_username(), 'test02') + customer=self.customer, + uid=10002, + username="test03", + group=group, + homedir="/home/foo", + shell="/bin/fooshell", + ) + self.assertEqual(User.objects.get_next_username(), "test02") def test_create_user_first(self): user = User.objects.create_user(customer=self.customer) self.assertIsInstance(user, User) self.assertEqual(user.uid, 10000) self.assertEqual(user.group.gid, 10000) - self.assertEqual(user.group.groupname, 'test01') - self.assertEqual(user.username, 'test01') - self.assertEqual(user.homedir, '/home/test01') - self.assertEqual(user.shell, '/bin/fooshell') + self.assertEqual(user.group.groupname, "test01") + self.assertEqual(user.username, "test01") + self.assertEqual(user.homedir, "/home/test01") + self.assertEqual(user.shell, "/bin/fooshell") self.assertIsNotNone(user.shadow) def test_create_user_tasks(self): User.objects.create_user(customer=self.customer) taskres = TaskResult.objects.all() - self.assertEqual(len(taskres), 2) - tasknames = [r.task_name for r in taskres] - self.assertEqual(tasknames.count('setup_file_sftp_userdir'), 1) - self.assertEqual(tasknames.count('setup_file_mail_userdir'), 1) + self.assertEqual(len(taskres), 3) + creators = [r.creator for r in taskres] + for creator in [ + "handle_group_created", + "handle_user_created", + "handle_user_password_set", + ]: + self.assertIn(creator, creators) def test_create_user_second(self): User.objects.create_user(customer=self.customer) @@ -286,36 +369,34 @@ class UserManagerTest(TestCaseWithCeleryTasks): self.assertIsInstance(user, User) self.assertEqual(user.uid, 10001) self.assertEqual(user.group.gid, 10001) - self.assertEqual(user.group.groupname, 'test02') - self.assertEqual(user.username, 'test02') - self.assertEqual(user.homedir, '/home/test02') - self.assertEqual(user.shell, '/bin/fooshell') + self.assertEqual(user.group.groupname, "test02") + self.assertEqual(user.username, "test02") + self.assertEqual(user.homedir, "/home/test02") + self.assertEqual(user.shell, "/bin/fooshell") self.assertIsNotNone(user.shadow) self.assertEqual(len(User.objects.all()), 2) def test_create_user_known_password(self): - user = User.objects.create_user( - customer=self.customer, password='foobar') + user = User.objects.create_user(customer=self.customer, password="foobar") self.assertIsInstance(user, User) self.assertEqual(user.uid, 10000) self.assertEqual(user.group.gid, 10000) - self.assertEqual(user.group.groupname, 'test01') - self.assertEqual(user.username, 'test01') - self.assertEqual(user.homedir, '/home/test01') - self.assertEqual(user.shell, '/bin/fooshell') + self.assertEqual(user.group.groupname, "test01") + self.assertEqual(user.username, "test01") + self.assertEqual(user.homedir, "/home/test01") + self.assertEqual(user.shell, "/bin/fooshell") self.assertIsNotNone(user.shadow) - self.assertTrue(sha512_crypt.verify('foobar', user.shadow.passwd)) + self.assertTrue(sha512_crypt.verify("foobar", user.shadow.passwd)) def test_create_user_predefined_username(self): - user = User.objects.create_user( - customer=self.customer, username='tester') + user = User.objects.create_user(customer=self.customer, username="tester") self.assertIsInstance(user, User) self.assertEqual(user.uid, 10000) self.assertEqual(user.group.gid, 10000) - self.assertEqual(user.group.groupname, 'tester') - self.assertEqual(user.username, 'tester') - self.assertEqual(user.homedir, '/home/tester') - self.assertEqual(user.shell, '/bin/fooshell') + self.assertEqual(user.group.groupname, "tester") + self.assertEqual(user.username, "tester") + self.assertEqual(user.homedir, "/home/tester") + self.assertEqual(user.shell, "/bin/fooshell") self.assertIsNotNone(user.shadow) def test_create_user_commit(self): @@ -323,70 +404,87 @@ class UserManagerTest(TestCaseWithCeleryTasks): self.assertIsInstance(user, User) self.assertEqual(user.uid, 10000) self.assertEqual(user.group.gid, 10000) - self.assertEqual(user.group.groupname, 'test01') - self.assertEqual(user.username, 'test01') - self.assertEqual(user.homedir, '/home/test01') - self.assertEqual(user.shell, '/bin/fooshell') + self.assertEqual(user.group.groupname, "test01") + self.assertEqual(user.username, "test01") + self.assertEqual(user.homedir, "/home/test01") + self.assertEqual(user.shell, "/bin/fooshell") self.assertIsNotNone(user.shadow) @override_settings( - OSUSER_MINUID=10000, OSUSER_MINGID=10000, OSUSER_USERNAME_PREFIX='test', - OSUSER_HOME_BASEPATH='/home', OSUSER_DEFAULT_SHELL='/bin/fooshell' + OSUSER_MINUID=10000, + OSUSER_MINGID=10000, + OSUSER_USERNAME_PREFIX="test", + OSUSER_HOME_BASEPATH="/home", + OSUSER_DEFAULT_SHELL="/bin/fooshell", ) class UserTest(TestCaseWithCeleryTasks): def setUp(self): - self.customer = Customer.objects.create_user('test') + self.customer = Customer.objects.create_user("test") super(UserTest, self).setUp() def test___str__(self): user = User.objects.create_user(self.customer) - self.assertEqual(str(user), 'test01 (10000)') + self.assertEqual(str(user), "test01 (10000)") def test_set_password(self): user = User.objects.create_user(self.customer) - self.assertFalse(sha512_crypt.verify('test', user.shadow.passwd)) - user.set_password('test') - self.assertTrue(sha512_crypt.verify('test', user.shadow.passwd)) + self.assertFalse(sha512_crypt.verify("test", user.shadow.passwd)) + user.set_password("test") + self.assertTrue(sha512_crypt.verify("test", user.shadow.passwd)) def test_save(self): user = User.objects.create_user(self.customer) - TaskResult.objects.all().delete() user.save() taskres = TaskResult.objects.all() - self.assertEqual(len(taskres), 2) - task_names = [r.task_name for r in taskres] - self.assertIn('setup_file_sftp_userdir', task_names) - self.assertIn('setup_file_mail_userdir', task_names) + self.assertEqual(len(taskres), 3) + creators = [r.creator for r in taskres] + for task in [ + "handle_group_created", + "handle_user_created", + "handle_user_password_set", + ]: + self.assertIn(task, creators) def test_delete_only_user(self): user = User.objects.create_user(self.customer) - TaskResult.objects.all().delete() user.delete() taskres = TaskResult.objects.all() - self.assertEqual(len(taskres), 3) - tasknames = [r.task_name for r in taskres] - self.assertEqual(tasknames.count('delete_file_mail_userdir'), 1) - self.assertEqual(tasknames.count('delete_file_sftp_userdir'), 1) - self.assertEqual(tasknames.count('delete_ldap_group'), 1) + self.assertEqual(len(taskres), 6) + creators = [r.creator for r in taskres] + for task in [ + "handle_group_created", + "handle_user_created", + "handle_user_password_set", + "handle_user_deleted", + "handle_group_deleted", + "handle_user_deleted", + ]: + self.assertIn(task, creators) self.assertEqual(len(User.objects.all()), 0) def test_delete_additional_groups(self): - group1 = Group.objects.create(gid=2000, groupname='group1') - group2 = Group.objects.create(gid=2001, groupname='group2') + group1 = Group.objects.create(gid=2000, groupname="group1") + group2 = Group.objects.create(gid=2001, groupname="group2") user = User.objects.create_user(self.customer) for group in [group1, group2]: + # noinspection PyUnresolvedReferences user.additionalgroup_set.add( - AdditionalGroup.objects.create(user=user, group=group)) + AdditionalGroup.objects.create(user=user, group=group) + ) TaskResult.objects.all().delete() user.delete() taskres = TaskResult.objects.all() - self.assertEqual(len(taskres), 3) - tasknames = [t.task_name for t in taskres] - self.assertEqual(tasknames.count('delete_file_mail_userdir'), 1) - self.assertEqual(tasknames.count('delete_file_sftp_userdir'), 1) - self.assertEqual(tasknames.count('delete_ldap_group'), 1) + self.assertEqual(len(taskres), 5) + creators = [t.creator for t in taskres] + for tcount, tcreator in [ + (2, "handle_user_removed_from_group"), + (2, "handle_user_deleted"), + (1, "handle_group_deleted"), + ]: + self.assertEqual(creators.count(tcreator), tcount) self.assertEqual(len(User.objects.all()), 0) + # noinspection PyUnresolvedReferences self.assertEqual(len(AdditionalGroup.objects.all()), 0) def test_is_sftp_user(self): @@ -394,117 +492,140 @@ class UserTest(TestCaseWithCeleryTasks): self.assertFalse(user.is_sftp_user()) sftp_group = Group.objects.create( - gid=2000, groupname=settings.OSUSER_SFTP_GROUP) + gid=2000, groupname=settings.OSUSER_SFTP_GROUP + ) + # noinspection PyUnresolvedReferences user.additionalgroup_set.add( - AdditionalGroup.objects.create(user=user, group=sftp_group)) + AdditionalGroup.objects.create(user=user, group=sftp_group) + ) self.assertTrue(user.is_sftp_user()) class SshPublicKeyManagerTest(TestCaseWithCeleryTasks): def test_parse_keytext_rfc4716_1(self): - res = SshPublicKey.objects.parse_keytext(EXAMPLE_KEY_1_RFC4716) + res = SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_1_RFC4716) self.assertEqual(len(res), 3) self.assertGreater(len(res[1]), 40) - self.assertEqual(res[0], 'ssh-rsa') + self.assertEqual(res[0], "ssh-rsa") self.assertEqual( - res[2], '"1024-bit RSA, converted from OpenSSH by me@example.com"') + res[2], '"1024-bit RSA, converted from OpenSSH by me@example.com"' + ) def test_parse_keytext_rfc4716_2(self): - res = SshPublicKey.objects.parse_keytext(EXAMPLE_KEY_2_RFC4716) + res = SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_2_RFC4716) self.assertEqual(len(res), 3) - self.assertEqual(res[0], 'ssh-dss') + self.assertEqual(res[0], "ssh-dss") self.assertGreater(len(res[1]), 40) self.assertEqual( - res[2], - "This is my public key for use on servers which I don't like.") + res[2], "This is my public key for use on servers which I don't like." + ) def test_parse_keytext_rfc4716_3(self): - res = SshPublicKey.objects.parse_keytext(EXAMPLE_KEY_3_RFC4716) + res = SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_3_RFC4716) self.assertEqual(len(res), 3) - self.assertEqual(res[0], 'ssh-dss') + self.assertEqual(res[0], "ssh-dss") self.assertGreater(len(res[1]), 40) self.assertEqual(res[2], "DSA Public Key for use with MyIsp") def test_parse_keytext_openssh(self): - res = SshPublicKey.objects.parse_keytext(EXAMPLE_KEY_4_OPENSSH) + res = SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_4_OPENSSH) self.assertEquals(len(res), 3) - self.assertEqual(res[0], 'ssh-rsa') + self.assertEqual(res[0], "ssh-rsa") self.assertGreater(len(res[1]), 40) - self.assertEqual(res[2], '') + self.assertEqual(res[2], "") + + def test_parse_keytext_invalid_multiline(self): + with self.assertRaises(ValueError): + SshPublicKey.objects.parse_key_text("\r\n".join(["xx"] * 10)) + + def test_parse_keytext_empty_line(self): + res = SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_6_RFC4716_EMPTY_LINE) + self.assertEqual(len(res), 3) + self.assertEqual(res[0], "ssh-dss") + self.assertGreater(len(res[1]), 40) + self.assertEqual(res[2], "DSA Public Key for use with MyIsp") + + def test_parse_keytext_invalid_empty_rfc4716_header(self): + with self.assertRaises(ValueError): + SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_9_RFC4716_ONLY_HEADER) + + def test_parse_keytext_no_comment(self): + res = SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_7_NO_COMMENT) + self.assertEqual(len(res), 3) + self.assertEqual(res[0], "ssh-rsa") + self.assertGreater(len(res[1]), 40) + self.assertEqual(res[2], "") + + def test_parse_keytext_multiline_comment(self): + res = SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_5_RFC4716_MULTILINE) + self.assertEqual(len(res), 3) + self.assertEqual(res[0], "ssh-dss") + self.assertGreater(len(res[1]), 40) + self.assertEqual(res[2], "DSA Public Key for use with MyIsp") def test_parse_keytext_invalid(self): with self.assertRaises(ValueError): - SshPublicKey.objects.parse_keytext("\r\n".join(["xx"]*10)) + SshPublicKey.objects.parse_key_text("invalid") - def test_parse_keytext_empty_line(self): - res = SshPublicKey.objects.parse_keytext( - EXAMPLE_KEY_6_RFC4716_EMPTY_LINE) - self.assertEqual(len(res), 3) - self.assertEqual(res[0], 'ssh-dss') - self.assertGreater(len(res[1]), 40) - self.assertEqual(res[2], "DSA Public Key for use with MyIsp") - - def test_parse_keytext_multiline_comment(self): - res = SshPublicKey.objects.parse_keytext( - EXAMPLE_KEY_5_RFC4716_MULTILINE) - self.assertEqual(len(res), 3) - self.assertEqual(res[0], 'ssh-dss') - self.assertGreater(len(res[1]), 40) - self.assertEqual(res[2], "DSA Public Key for use with MyIsp") + def test_parse_keytext_invalid_openssh(self): + with self.assertRaises(ValueError): + SshPublicKey.objects.parse_key_text(EXAMPLE_KEY_8_OPENSSH_BROKEN) def test_create_ssh_public_key(self): - customer = Customer.objects.create_user('test') + customer = Customer.objects.create_user("test") user = User.objects.create_user(customer) - key = SshPublicKey.objects.create_ssh_public_key( - user, EXAMPLE_KEY_4_OPENSSH) + key = SshPublicKey.objects.create_ssh_public_key(user, EXAMPLE_KEY_4_OPENSSH) self.assertIsInstance(key, SshPublicKey) self.assertEqual(key.user, user) - self.assertEqual(key.algorithm, 'ssh-rsa') + self.assertEqual(key.algorithm, "ssh-rsa") self.assertEqual(key.data, EXAMPLE_KEY_4_OPENSSH.split()[1]) - self.assertEqual(key.comment, '') + self.assertEqual(key.comment, "") class SshPublicKeyTest(TestCaseWithCeleryTasks): def setUp(self): super(SshPublicKeyTest, self).setUp() - customer = Customer.objects.create_user('test') + customer = Customer.objects.create_user("test") self.user = User.objects.create_user(customer) TaskResult.objects.all().delete() def test__str__rfc4716(self): res = SshPublicKey.objects.create_ssh_public_key( - self.user, EXAMPLE_KEY_3_RFC4716) + self.user, EXAMPLE_KEY_3_RFC4716 + ) + self.maxDiff = None self.assertEqual( - str(res), 'ssh-dss AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxae' - 'hvx5wOJ0rzZdzoSOXxbETW6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7St' - 'xyltHnXF1YLfKD1G4T6JYrdHYI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3g' - 'Jq2e7Yisk/gF+1VAAAAFQDb8D5cvwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4' - 'KLYk3IwRbXblwXdkPggA4pfdtW9vGfJ0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/F' - 'XPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAAvioUPkmdMc0zuWoSOEsSNhVDtX3WdvVc' - 'GcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACBAN7CY+KKv1gHpRzFwdQm7HK9bb1LA' - 'o2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HSn24VYtYtsMu74qXviYjziVucWK' - 'jjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5sY29ouezv4Xz2PuMch5VGPP' - '+CDqzCM4loWgV DSA Public Key for use with MyIsp') + str(res), + "ssh-dss AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxae" + "hvx5wOJ0rzZdzoSOXxbETW6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7St" + "xyltHnXF1YLfKD1G4T6JYrdHYI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3g" + "Jq2e7Yisk/gF+1VAAAAFQDb8D5cvwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4" + "KLYk3IwRbXblwXdkPggA4pfdtW9vGfJ0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/F" + "XPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAAvioUPkmdMc0zuWoSOEsSNhVDtX3WdvVc" + "GcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACBAN7CY+KKv1gHpRzFwdQm7HK9bb1LA" + "o2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HSn24VYtYtsMu74qXviYjziVucWK" + "jjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5sY29ouezv4Xz2PuMch5VGPP" + "+CDqzCM4loWgV DSA Public Key for use with MyIsp", + ) def test__str__openssh(self): res = SshPublicKey.objects.create_ssh_public_key( - self.user, EXAMPLE_KEY_4_OPENSSH) + self.user, EXAMPLE_KEY_4_OPENSSH + ) self.assertEqual(str(res), EXAMPLE_KEY_4_OPENSSH) def test_call_tasks_on_save(self): - SshPublicKey.objects.create_ssh_public_key( - self.user, EXAMPLE_KEY_4_OPENSSH) + SshPublicKey.objects.create_ssh_public_key(self.user, EXAMPLE_KEY_4_OPENSSH) taskresults = TaskResult.objects.all() self.assertEqual(len(taskresults), 1) - self.assertEqual( - taskresults[0].task_name, 'set_file_ssh_authorized_keys') + self.assertEqual(taskresults[0].creator, "handle_ssh_keys_changed") def test_call_tasks_on_delete(self): key = SshPublicKey.objects.create_ssh_public_key( - self.user, EXAMPLE_KEY_4_OPENSSH) + self.user, EXAMPLE_KEY_4_OPENSSH + ) TaskResult.objects.all().delete() key.delete() taskresults = TaskResult.objects.all() self.assertEqual(len(taskresults), 1) - self.assertEqual( - taskresults[0].task_name, 'set_file_ssh_authorized_keys') + self.assertEqual(taskresults[0].creator, "handle_ssh_keys_changed") diff --git a/gnuviechadmin/osusers/tests/test_views.py b/gnuviechadmin/osusers/tests/test_views.py new file mode 100644 index 0000000..7c0d2bb --- /dev/null +++ b/gnuviechadmin/osusers/tests/test_views.py @@ -0,0 +1,381 @@ +""" +This module provides tests for :py:mod:`osusers.views`. + +""" + +from unittest.mock import patch, MagicMock + +from django.test import TestCase, TransactionTestCase +from django.contrib.auth import get_user_model +from django.urls import reverse + +from hostingpackages.models import CustomerHostingPackage, HostingPackageTemplate + +from osusers.models import SshPublicKey +from osusers.views import AddSshPublicKey, DeleteSshPublicKey, EditSshPublicKeyComment + + +User = get_user_model() + +TEST_USER = "test" +TEST_PASSWORD = "secret" +TEST_EMAIL = "test@example.org" +EXAMPLE_KEY = "".join( + ( + "ssh-rsa ", + "AAAAB3NzaC1yc2EAAAABIwAAAIEA1on8gxCGJJWSRT4uOrR13mUaUk0hRf4RzxSZ1zRb", + "YYFw8pfGesIFoEuVth4HKyF8k1y4mRUnYHP1XNMNMJl1JcEArC2asV8sHf6zSPVffozZ", + "5TT4SfsUu/iKy9lUcCfXzwre4WWZSXXcPff+EHtWshahu3WzBdnGxm5Xoi89zcE=", + ) +) + + +class HostingPackageAwareTestMixin(object): + + # noinspection PyMethodMayBeStatic + def _setup_hosting_package(self, customer): + template = HostingPackageTemplate.objects.create( + name="testpackagetemplate", mailboxcount=10, diskspace=1, diskspace_unit=0 + ) + package = CustomerHostingPackage.objects.create_from_template( + customer, template, "testpackage" + ) + with patch("hostingpackages.models.settings") as hmsettings: + hmsettings.OSUSER_DEFAULT_GROUPS = [] + package.save() + return package + + +class AddSshPublicKeyTest(HostingPackageAwareTestMixin, TestCase): + def setUp(self): + self.customer = User.objects.create_user( + username=TEST_USER, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer) + + def _get_url(self): + return reverse("add_ssh_key", kwargs={"package": self.package.id}) + + def test_get_anonymous(self): + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user("test2", password=TEST_PASSWORD) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertTemplateUsed(response, "osusers/sshpublickey_create.html") + + def test_get_form_kwargs(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = AddSshPublicKey( + request=MagicMock(), kwargs={"package": str(self.package.pk)} + ) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], self.package) + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], self.customer) + self.assertIn("osuser", response.context) + self.assertEqual(response.context["osuser"], self.package.osuser.username) + + def test_form_valid_redirect(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + self._get_url(), data={"publickeytext": EXAMPLE_KEY} + ) + self.assertRedirects(response, self.package.get_absolute_url()) + + def test_form_valid_message(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + self._get_url(), follow=True, data={"publickeytext": EXAMPLE_KEY} + ) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + "Successfully added new ssh-rsa SSH public key.".format( + username=self.package.osuser.username + ), + str(messages[0]), + ) + + +class DeleteSshPublicKeyTest(HostingPackageAwareTestMixin, TestCase): + def setUp(self): + self.customer = User.objects.create_user( + username=TEST_USER, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer) + self.sshkey = SshPublicKey.objects.create( + user=self.package.osuser, algorithm="good", data="key", comment="comment" + ) + + def _get_url(self): + return reverse( + "delete_ssh_key", kwargs={"package": self.package.id, "pk": self.sshkey.id} + ) + + def test_get_anonymous(self): + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user("test2", password=TEST_PASSWORD) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertTemplateUsed(response, "osusers/sshpublickey_confirm_delete.html") + + def test_get_queryset(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = DeleteSshPublicKey( + request=MagicMock(), + kwargs={"package": str(self.package.pk), "pk": str(self.sshkey.pk)}, + ) + queryset = view.get_queryset() + self.assertQuerysetEqual(queryset, [repr(self.sshkey)]) + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + for key in ("hostingpackage", "customer", "osuser"): + self.assertIn(key, response.context) + self.assertEqual(response.context["hostingpackage"], self.package) + self.assertEqual(response.context["customer"], self.customer) + self.assertEqual(response.context["osuser"], self.package.osuser.username) + + def test_get_success_url(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post(self._get_url(), data={"comment": "new comment"}) + self.assertRedirects( + response, reverse("list_ssh_keys", kwargs={"package": self.package.id}) + ) + + +class EditSshPublicKeyCommentTest(HostingPackageAwareTestMixin, TransactionTestCase): + def setUp(self): + self.customer = User.objects.create_user( + username=TEST_USER, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer) + self.sshkey = SshPublicKey.objects.create( + user=self.package.osuser, algorithm="good", data="key", comment="comment" + ) + + def _get_url(self): + return reverse( + "edit_ssh_key_comment", + kwargs={"package": self.package.id, "pk": self.sshkey.id}, + ) + + def test_get_anonymous(self): + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user("test2", password=TEST_PASSWORD) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertTemplateUsed(response, "osusers/sshpublickey_edit_comment.html") + + def test_get_queryset(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = EditSshPublicKeyComment( + request=MagicMock(), + kwargs={"package": str(self.package.pk), "pk": str(self.sshkey.pk)}, + ) + queryset = view.get_queryset() + self.assertQuerysetEqual(queryset, [repr(self.sshkey)]) + + def test_get_form_kwargs(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = EditSshPublicKeyComment( + request=MagicMock(), + kwargs={"package": str(self.package.pk), "pk": str(self.sshkey.pk)}, + ) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], self.package) + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + for key in ("hostingpackage", "customer", "osuser"): + self.assertIn(key, response.context) + self.assertEqual(response.context["hostingpackage"], self.package) + self.assertEqual(response.context["customer"], self.customer) + self.assertEqual(response.context["osuser"], self.package.osuser.username) + + def test_get_success_url(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post(self._get_url(), data={"comment": "new comment"}) + self.assertRedirects( + response, reverse("list_ssh_keys", kwargs={"package": self.package.id}) + ) + + +class ListSshPublicKeysTest(HostingPackageAwareTestMixin, TestCase): + def setUp(self): + self.customer = User.objects.create_user( + username=TEST_USER, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer) + + def _get_url(self): + return reverse("list_ssh_keys", kwargs={"package": self.package.id}) + + def test_get_anonymous(self): + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user("test2", password=TEST_PASSWORD) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertTemplateUsed(response, "osusers/sshpublickey_list.html") + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + for key in ("hostingpackage", "customer", "osuser"): + self.assertIn(key, response.context) + self.assertEqual(response.context["hostingpackage"], self.package) + self.assertEqual(response.context["customer"], self.customer) + self.assertEqual(response.context["osuser"], self.package.osuser.username) + + +class SetOsUserPasswordTest(HostingPackageAwareTestMixin, TestCase): + def setUp(self): + self.customer = User.objects.create_user( + username=TEST_USER, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer) + + def _get_url(self): + return reverse( + "set_osuser_password", kwargs={"slug": self.package.osuser.username} + ) + + def test_get_anonymous(self): + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user("test2", password=TEST_PASSWORD) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertTemplateUsed(response, "osusers/user_setpassword.html") + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], self.customer) + + def test_form_valid_redirect(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + self._get_url(), + data={"password1": TEST_PASSWORD, "password2": TEST_PASSWORD}, + ) + self.assertRedirects(response, self.package.get_absolute_url()) + + def test_form_valid_message(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + self._get_url(), + follow=True, + data={"password1": TEST_PASSWORD, "password2": TEST_PASSWORD}, + ) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + "New password for {username} has been set successfully.".format( + username=self.package.osuser.username + ), + str(messages[0]), + ) diff --git a/gnuviechadmin/osusers/urls.py b/gnuviechadmin/osusers/urls.py index 52c2657..086a2bf 100644 --- a/gnuviechadmin/osusers/urls.py +++ b/gnuviechadmin/osusers/urls.py @@ -4,7 +4,7 @@ This module defines the URL patterns for operating system user related views. """ from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( AddSshPublicKey, @@ -15,8 +15,7 @@ from .views import ( ) -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^(?P[\w0-9@.+-_]+)/setpassword$', SetOsUserPassword.as_view(), name='set_osuser_password'), url(r'^(?P\d+)/ssh-keys/$', ListSshPublicKeys.as_view(), @@ -27,4 +26,4 @@ urlpatterns = patterns( EditSshPublicKeyComment.as_view(), name='edit_ssh_key_comment'), url(r'^(?P\d+)/ssh-keys/(?P\d+)/delete$', DeleteSshPublicKey.as_view(), name='delete_ssh_key'), -) +] diff --git a/gnuviechadmin/osusers/views.py b/gnuviechadmin/osusers/views.py index 3c8c637..3a0a938 100644 --- a/gnuviechadmin/osusers/views.py +++ b/gnuviechadmin/osusers/views.py @@ -4,8 +4,8 @@ This module defines the views for gnuviechadmin operating system user handling. """ from __future__ import unicode_literals, absolute_import -from django.core.urlresolvers import reverse from django.shortcuts import redirect +from django.urls import reverse from django.views.generic import ( CreateView, DeleteView, @@ -89,7 +89,7 @@ class AddSshPublicKey( key = form.save() messages.success( self.request, - _('Successfully added new {algorithm} SSH public key').format( + _('Successfully added new {algorithm} SSH public key.').format( algorithm=key.algorithm) ) return redirect(self.get_hosting_package()) @@ -162,7 +162,6 @@ class EditSshPublicKeyComment( """ model = SshPublicKey context_object_name = 'key' - fields = ['comment'] template_name_suffix = '_edit_comment' form_class = EditSshPublicKeyCommentForm diff --git a/gnuviechadmin/setup.cfg b/gnuviechadmin/setup.cfg new file mode 100644 index 0000000..f2d9645 --- /dev/null +++ b/gnuviechadmin/setup.cfg @@ -0,0 +1,16 @@ +[pep8] +exclude = migrations + +[flake8] +exclude = migrations + +[coverage:run] +source = gnuviechadmin,contact_form,dashboard,domains,gvawebcore,managemails,osusers,taskresults,userdbs +branch = True + +[coverage:report] +omit = */migrations/*,*/tests/*.py,*/tests.py,gnuviechadmin/settings.py +show_missing = True + +[coverage:html] +directory = ../coverage-report diff --git a/gnuviechadmin/taskresults/admin.py b/gnuviechadmin/taskresults/admin.py index cd47dc8..3647426 100644 --- a/gnuviechadmin/taskresults/admin.py +++ b/gnuviechadmin/taskresults/admin.py @@ -9,4 +9,7 @@ from django.contrib import admin from .models import TaskResult -admin.site.register(TaskResult) +@admin.register(TaskResult) +class TaskResultAdmin(admin.ModelAdmin): + list_display = ('task_id', 'creator', 'signature', 'finished', 'state') + list_filter = ('finished',) diff --git a/gnuviechadmin/taskresults/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/taskresults/locale/de/LC_MESSAGES/django.po index 0decaab..104a2ad 100644 --- a/gnuviechadmin/taskresults/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/taskresults/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: gnuviechadmin taskresults\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-27 18:55+0100\n" -"PO-Revision-Date: 2015-01-17 16:00+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" +"PO-Revision-Date: 2016-01-29 11:07+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" "Language: de\n" @@ -16,33 +16,41 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 1.6.10\n" +"X-Generator: Poedit 1.8.6\n" "X-Poedit-SourceCharset: UTF-8\n" -#: taskresults/models.py:22 +#: taskresults/models.py:28 msgid "Task id" msgstr "Task-Id" -#: taskresults/models.py:23 -msgid "Task name" -msgstr "Taskname" +#: taskresults/models.py:29 +msgid "Task signature" +msgstr "Tasksignatur" -#: taskresults/models.py:24 taskresults/models.py:31 +#: taskresults/models.py:30 +msgid "Task creator" +msgstr "Taskersteller" + +#: taskresults/models.py:31 +msgid "Task notes" +msgstr "Tasknotizen" + +#: taskresults/models.py:32 taskresults/models.py:39 msgid "Task result" msgstr "Taskergebnis" -#: taskresults/models.py:26 +#: taskresults/models.py:34 msgid "Task state" msgstr "Taskstatus" -#: taskresults/models.py:32 +#: taskresults/models.py:40 msgid "Task results" msgstr "Taskergebnisse" -#: taskresults/models.py:38 +#: taskresults/models.py:47 msgid "yes" msgstr "ja" -#: taskresults/models.py:38 +#: taskresults/models.py:47 msgid "no" msgstr "nein" diff --git a/gnuviechadmin/taskresults/migrations/0002_auto_20151011_2248.py b/gnuviechadmin/taskresults/migrations/0002_auto_20151011_2248.py new file mode 100644 index 0000000..a13aee6 --- /dev/null +++ b/gnuviechadmin/taskresults/migrations/0002_auto_20151011_2248.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('taskresults', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='taskresult', + name='task_name', + ), + migrations.AddField( + model_name='taskresult', + name='creator', + field=models.TextField(default='migrated', verbose_name='Task creator'), + preserve_default=False, + ), + migrations.AddField( + model_name='taskresult', + name='notes', + field=models.TextField(default='', verbose_name='Task notes'), + preserve_default=False, + ), + migrations.AddField( + model_name='taskresult', + name='signature', + field=models.TextField(default='', verbose_name='Task signature'), + preserve_default=False, + ), + ] diff --git a/gnuviechadmin/taskresults/migrations/0003_auto_20160109_1524.py b/gnuviechadmin/taskresults/migrations/0003_auto_20160109_1524.py new file mode 100644 index 0000000..665c5f8 --- /dev/null +++ b/gnuviechadmin/taskresults/migrations/0003_auto_20160109_1524.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.1 on 2016-01-09 14:24 +from __future__ import unicode_literals + +from django.db import migrations +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('taskresults', '0002_auto_20151011_2248'), + ] + + operations = [ + migrations.AlterModelOptions( + name='taskresult', + options={'ordering': ['created'], 'verbose_name': 'Task result', 'verbose_name_plural': 'Task results'}, + ), + migrations.AddField( + model_name='taskresult', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), + ), + migrations.AddField( + model_name='taskresult', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), + ), + ] diff --git a/gnuviechadmin/taskresults/models.py b/gnuviechadmin/taskresults/models.py index 7178c84..64819eb 100644 --- a/gnuviechadmin/taskresults/models.py +++ b/gnuviechadmin/taskresults/models.py @@ -10,17 +10,25 @@ from django.utils.translation import ugettext as _ from gnuviechadmin.celery import app +from model_utils.models import TimeStampedModel + class TaskResultManager(models.Manager): - def create_task_result(self, asyncresult, name): - taskresult = self.create(task_id=asyncresult.id, task_name=name) + def create_task_result(self, creator, signature, notes=''): + sigstr = str(signature) + result = signature.apply_async() + taskresult = self.create( + task_id=result.task_id, creator=creator, signature=sigstr, + notes=notes) return taskresult @python_2_unicode_compatible -class TaskResult(models.Model): +class TaskResult(TimeStampedModel): task_id = models.CharField(_('Task id'), max_length=36) - task_name = models.CharField(_('Task name'), max_length=64) + signature = models.TextField(_('Task signature')) + creator = models.TextField(_('Task creator')) + notes = models.TextField(_('Task notes')) result = models.TextField(_('Task result')) finished = models.BooleanField(default=False) state = models.CharField(_('Task state'), max_length=16) @@ -30,10 +38,11 @@ class TaskResult(models.Model): class Meta: verbose_name = _('Task result') verbose_name_plural = _('Task results') + ordering = ['created'] def __str__(self): - return "{task_name} ({task_id}): {finished}".format( - task_name=self.task_name, + return "{creator} ({task_id}): {finished}".format( + creator=self.creator, task_id=self.task_id, finished=_('yes') if self.finished else _('no') ) @@ -41,7 +50,8 @@ class TaskResult(models.Model): def fetch_result(self): if not self.finished: ar = app.AsyncResult(self.task_id) - res = ar.get(no_ack=True, timeout=1) - self.result = str(res) self.state = ar.state - self.finished = True + if ar.ready(): + res = ar.get() + self.result = str(res) + self.finished = True diff --git a/gnuviechadmin/taskresults/tests/management/__init__.py b/gnuviechadmin/taskresults/tests/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/taskresults/tests/management/commands/__init__.py b/gnuviechadmin/taskresults/tests/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/taskresults/tests/management/commands/test_fetch_taskresults.py b/gnuviechadmin/taskresults/tests/management/commands/test_fetch_taskresults.py new file mode 100644 index 0000000..a7d10ff --- /dev/null +++ b/gnuviechadmin/taskresults/tests/management/commands/test_fetch_taskresults.py @@ -0,0 +1,66 @@ +""" +This module provides tests for the +:py:mod:`taskresults.management.commands.fetch_taskresults` Django management +command. + +""" + +from unittest.mock import MagicMock, patch + +from django.test import TestCase + +from taskresults.management.commands.fetch_taskresults import Command +from taskresults.models import TaskResult + +TEST_TASK_UUID = "3120f6a8-2665-4fa3-a785-79efd28bfe92" +TEST_TASK_NAME = "test.task" +TEST_TASK_RESULT = "4ll y0ur b453 4r3 b3l0ng t0 u5" + + +@patch("taskresults.models.app.AsyncResult") +class FetchTaskResultsCommandTest(TestCase): + def test_handle_unfinished(self, asyncresult): + resultmock = MagicMock(task_id=TEST_TASK_UUID) + sigmock = MagicMock() + sigmock.apply_async.return_value = resultmock + tr = TaskResult.objects.create_task_result(TEST_TASK_NAME, sigmock) + self.assertFalse(tr.finished) + self.assertEqual(tr.result, "") + self.assertEqual(tr.state, "") + + aresult = asyncresult.return_value + aresult.state = "PENDING" + aresult.ready.return_value = False + + Command().handle() + + tr = TaskResult.objects.get(task_id=TEST_TASK_UUID) + self.assertTrue(asyncresult.called_with(TEST_TASK_UUID)) + self.assertTrue(aresult.ready.called_with()) + self.assertFalse(tr.finished) + self.assertEqual(tr.result, "") + self.assertEqual(tr.state, "PENDING") + + def test_handle_finished(self, asyncresult): + resultmock = MagicMock(task_id=TEST_TASK_UUID) + sigmock = MagicMock() + sigmock.apply_async.return_value = resultmock + tr = TaskResult.objects.create_task_result(TEST_TASK_NAME, sigmock) + self.assertFalse(tr.finished) + self.assertEqual(tr.result, "") + self.assertEqual(tr.state, "") + + aresult = asyncresult.return_value + aresult.state = "SUCCESS" + aresult.ready.return_value = True + aresult.get.return_value = TEST_TASK_RESULT + + Command().handle() + + tr = TaskResult.objects.get(task_id=TEST_TASK_UUID) + self.assertTrue(asyncresult.called_with(TEST_TASK_UUID)) + self.assertTrue(aresult.ready.called_with()) + self.assertTrue(aresult.get.called_with()) + self.assertTrue(tr.finished) + self.assertEqual(tr.result, TEST_TASK_RESULT) + self.assertEqual(tr.state, "SUCCESS") diff --git a/gnuviechadmin/taskresults/tests/test_models.py b/gnuviechadmin/taskresults/tests/test_models.py index dcace59..fe6445d 100644 --- a/gnuviechadmin/taskresults/tests/test_models.py +++ b/gnuviechadmin/taskresults/tests/test_models.py @@ -1,42 +1,69 @@ -from __future__ import absolute_import, unicode_literals +""" +This module provides tests for :py:mod:`taskresults.models`. + +""" +from unittest.mock import MagicMock, patch + from django.test import TestCase -from mock import patch, MagicMock from taskresults.models import TaskResult - -TEST_TASK_UUID = '3120f6a8-2665-4fa3-a785-79efd28bfe92' -TEST_TASK_NAME = 'test.task' -TEST_TASK_RESULT = '4ll y0ur b453 4r3 b3l0ng t0 u5' +TEST_TASK_UUID = "3120f6a8-2665-4fa3-a785-79efd28bfe92" +TEST_TASK_NAME = "test.task" +TEST_TASK_RESULT = "4ll y0ur b453 4r3 b3l0ng t0 u5" class TaskResultTest(TestCase): - @patch('taskresults.models.app') - def test_update_taskstatus_unfinished(self, app): - mock = MagicMock(id=TEST_TASK_UUID, task_name=TEST_TASK_NAME) - mock.ready.return_value = False - tr = TaskResult.objects.create_task_result(mock, TEST_TASK_NAME) + @patch("taskresults.models.app.AsyncResult") + def test_update_taskstatus_unfinished(self, asyncresult): + resultmock = MagicMock(task_id=TEST_TASK_UUID) + sigmock = MagicMock() + sigmock.apply_async.return_value = resultmock + tr = TaskResult.objects.create_task_result(TEST_TASK_NAME, sigmock) self.assertFalse(tr.finished) - mymock = app.AsyncResult(TEST_TASK_UUID) - mymock.state = 'SUCCESS' - mymock.get.return_value = TEST_RESULT + mymock = asyncresult.return_value + mymock.state = "PENDING" + mymock.ready.return_value = False tr.fetch_result() - mymock.get.assert_called_with(no_ack=True, timeout=1) - self.assertTrue(tr.finished) + mymock.get.assert_not_called() + self.assertFalse(tr.finished) - @patch('taskresults.models.app') - def test_update_taskstatus_finished(self, app): - mock = MagicMock(id=TEST_TASK_UUID, task_name=TEST_TASK_NAME) - mock.ready.return_value = True - mock.state = 'SUCCESS' - mock.result = TEST_RESULT - tr = TaskResult.objects.create_task_result(mock, TEST_TASK_NAME) + @patch("taskresults.models.app.AsyncResult") + def test_update_taskstatus_finished(self, asyncresult): + resultmock = MagicMock(task_id=TEST_TASK_UUID) + sigmock = MagicMock() + sigmock.apply_async.return_value = resultmock + aresult = asyncresult.return_value + tr = TaskResult.objects.create_task_result(TEST_TASK_NAME, sigmock) + self.assertFalse(tr.finished) + aresult = asyncresult.return_value + aresult.state = "SUCCESS" + aresult.ready.return_value = True + aresult.get.return_value = TEST_TASK_RESULT tr.fetch_result() + self.assertTrue(aresult.get.called_with()) + self.assertEqual(aresult.get.call_count, 1) self.assertTrue(tr.finished) - mymock = app.AsyncResult(TEST_TASK_UUID) + self.assertEqual(tr.result, str(TEST_TASK_RESULT)) tr.fetch_result() - self.assertEqual(mymock.get.call_count, 1) + self.assertEqual(aresult.get.call_count, 1) self.assertTrue(tr.finished) + self.assertEqual(tr.result, str(TEST_TASK_RESULT)) + + def test___str__(self): + resultmock = MagicMock(task_id=TEST_TASK_UUID) + sigmock = MagicMock() + sigmock.apply_async.return_value = resultmock + tr = TaskResult.objects.create_task_result(TEST_TASK_NAME, sigmock) + self.assertEqual( + str(tr), + "{name} ({taskid}): no".format(name=TEST_TASK_NAME, taskid=TEST_TASK_UUID), + ) + tr.finished = True + self.assertEqual( + str(tr), + "{name} ({taskid}): yes".format(name=TEST_TASK_NAME, taskid=TEST_TASK_UUID), + ) TEST_RESULT = MagicMock() @@ -47,8 +74,10 @@ TEST_RESULT.ready.return_value = False class TaskResultManagerTest(TestCase): def test_create_task_result(self): - mock = MagicMock(id=TEST_TASK_UUID) - tr = TaskResult.objects.create_task_result(mock, TEST_TASK_NAME) + resultmock = MagicMock(task_id=TEST_TASK_UUID) + mock = MagicMock() + mock.apply_async.return_value = resultmock + tr = TaskResult.objects.create_task_result(TEST_TASK_NAME, mock) self.assertIsInstance(tr, TaskResult) self.assertEqual(tr.task_id, TEST_TASK_UUID) - self.assertEqual(tr.task_name, TEST_TASK_NAME) + self.assertEqual(tr.creator, TEST_TASK_NAME) diff --git a/gnuviechadmin/templates/account/login.html b/gnuviechadmin/templates/account/login.html index f3cb8f1..88862b6 100644 --- a/gnuviechadmin/templates/account/login.html +++ b/gnuviechadmin/templates/account/login.html @@ -1,11 +1,12 @@ {% extends "account/base.html" %} -{% load account crispy_forms_tags i18n %} +{% load account socialaccount crispy_forms_tags i18n %} {% block title %}{{ block.super }} - {% trans "Sign In" %}{% endblock title %} {% block page_title %}{% trans "Sign In" %}{% endblock page_title %} {% block content %} -{% if socialaccount.providers %} +{% get_providers as socialaccount_providers %} +{% if socialaccount_providers %}

{% blocktrans with site.name as site_name %}Please sign in with one of your existing third party accounts. Or, sign up for a {{site_name}} account and sign in below:{% endblocktrans %}

diff --git a/gnuviechadmin/templates/contact_form/contact_form.txt b/gnuviechadmin/templates/contact_form/contact_form.txt index 6adab33..b015009 100644 --- a/gnuviechadmin/templates/contact_form/contact_form.txt +++ b/gnuviechadmin/templates/contact_form/contact_form.txt @@ -1,4 +1,4 @@ -User {{ name }} <{{ email }}> from IP address {{ request.META.REMOTE_ADDR }} +User {{ name }} <{{ email }}> from IP address {{ remote_ip }} sent the following message via the contact form at {{ site }}{% url 'contact_form' %}: diff --git a/gnuviechadmin/templates/socialaccount/snippets/provider_list.html b/gnuviechadmin/templates/socialaccount/snippets/provider_list.html index 8bff939..3b7d9fb 100644 --- a/gnuviechadmin/templates/socialaccount/snippets/provider_list.html +++ b/gnuviechadmin/templates/socialaccount/snippets/provider_list.html @@ -1,6 +1,7 @@ {% load socialaccount %} -{% for provider in socialaccount.providers %} +{% get_providers as socialaccount_providers %} +{% for provider in socialaccount_providers %} {% if provider.id == "openid" %} {% for brand in provider.get_brands %}
  • diff --git a/gnuviechadmin/userdbs/admin.py b/gnuviechadmin/userdbs/admin.py index 7a498c8..9b807e2 100644 --- a/gnuviechadmin/userdbs/admin.py +++ b/gnuviechadmin/userdbs/admin.py @@ -170,7 +170,7 @@ class DatabaseUserAdmin(admin.ModelAdmin): """ actions = super(DatabaseUserAdmin, self).get_actions(request) - if 'delete_selected' in actions: + if 'delete_selected' in actions: # pragma: no cover del actions['delete_selected'] return actions @@ -250,8 +250,6 @@ class UserDatabaseAdmin(admin.ModelAdmin): databases """ - for dbuser in queryset.all(): - dbuser.delete() for database in queryset.all(): database.delete() perform_delete_selected.short_description = _( @@ -270,7 +268,7 @@ class UserDatabaseAdmin(admin.ModelAdmin): """ actions = super(UserDatabaseAdmin, self).get_actions(request) - if 'delete_selected' in actions: + if 'delete_selected' in actions: # pragma: no cover del actions['delete_selected'] return actions diff --git a/gnuviechadmin/userdbs/apps.py b/gnuviechadmin/userdbs/apps.py index 40fd5ec..304f4d2 100644 --- a/gnuviechadmin/userdbs/apps.py +++ b/gnuviechadmin/userdbs/apps.py @@ -16,3 +16,11 @@ class UserdbsAppConfig(AppConfig): """ name = 'userdbs' verbose_name = _('Database Users and their Databases') + + def ready(self): + """ + Takes care of importing the signal handlers of the :py:mod:`userdbs` + app. + + """ + import userdbs.signals # NOQA diff --git a/gnuviechadmin/userdbs/forms.py b/gnuviechadmin/userdbs/forms.py index 2c24743..bf9a099 100644 --- a/gnuviechadmin/userdbs/forms.py +++ b/gnuviechadmin/userdbs/forms.py @@ -5,7 +5,7 @@ This module defines form classes for user database editing. from __future__ import absolute_import, unicode_literals from django import forms -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from crispy_forms.helper import FormHelper diff --git a/gnuviechadmin/userdbs/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/userdbs/locale/de/LC_MESSAGES/django.po index 618f3cb..ca534d3 100644 --- a/gnuviechadmin/userdbs/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/userdbs/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: gnuviechadmin userdbs\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-27 18:55+0100\n" -"PO-Revision-Date: 2015-01-26 13:44+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" +"PO-Revision-Date: 2016-01-29 11:06+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" "Language: de\n" @@ -16,14 +16,14 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 1.6.10\n" +"X-Generator: Poedit 1.8.6\n" "X-Poedit-SourceCharset: UTF-8\n" #: userdbs/admin.py:158 msgid "Delete selected database users" msgstr "Ausgewählte Datenbanknutzer löschen" -#: userdbs/admin.py:258 +#: userdbs/admin.py:256 msgid "Delete selected user databases" msgstr "Ausgewählte Nutzerdatenbanken löschen" @@ -43,53 +43,56 @@ msgstr "Datenbank anlegen" msgid "Set password" msgstr "Passwort setzen" -#: userdbs/models.py:32 +#: userdbs/models.py:13 msgid "PostgreSQL" msgstr "PostgreSQL" -#: userdbs/models.py:33 +#: userdbs/models.py:14 msgid "MySQL" msgstr "MySQL" -#: userdbs/models.py:106 +#: userdbs/models.py:88 msgid "username" msgstr "Benutzername" -#: userdbs/models.py:108 +#: userdbs/models.py:90 msgid "database type" msgstr "Datenbanktyp" -#: userdbs/models.py:114 userdbs/models.py:250 +#: userdbs/models.py:96 userdbs/models.py:206 msgid "database user" msgstr "Datenbanknutzer" -#: userdbs/models.py:115 +#: userdbs/models.py:97 msgid "database users" msgstr "Datenbanknutzer" -#: userdbs/models.py:249 +#: userdbs/models.py:205 msgid "database name" msgstr "Datenbankname" -#: userdbs/models.py:256 +#: userdbs/models.py:212 msgid "user database" msgstr "Nutzerdatenbank" -#: userdbs/models.py:257 +#: userdbs/models.py:213 msgid "user specific database" msgstr "nutzerspezifische Datenbank" -#: userdbs/views.py:63 -#, python-brace-format -msgid "Successfully create new {type} database {dbname} for user {dbuser}" -msgstr "" -"Neue {type}-Datenbank {dbname} für Benutzer {dbuser} erfolgreich angelegt" +#: userdbs/views.py:54 +msgid "The hosting package has no database products assigned." +msgstr "Dem Hostingpaket sind keine Datenbankprodukte zugewiesen." -#: userdbs/views.py:100 +#: userdbs/views.py:67 #, python-brace-format -msgid "Successfully changed password of database user {dbuser}" +msgid "Successfully create new {type} database {dbname} for user {dbuser}." +msgstr "Neue {type}-Datenbank {dbname} für Benutzer {dbuser} erfolgreich angelegt." + +#: userdbs/views.py:104 +#, python-brace-format +msgid "Successfully changed password of database user {dbuser}." msgstr "Passwort des Datenbanknutzers {dbuser} wurde erfolgreich geändert." -#: userdbs/views.py:129 -msgid "Database deleted" -msgstr "Datenbank gelöscht" +#: userdbs/views.py:133 +msgid "Database deleted." +msgstr "Datenbank gelöscht." diff --git a/gnuviechadmin/userdbs/migrations/0001_initial.py b/gnuviechadmin/userdbs/migrations/0001_initial.py index 7a81c3c..54015a3 100644 --- a/gnuviechadmin/userdbs/migrations/0001_initial.py +++ b/gnuviechadmin/userdbs/migrations/0001_initial.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone import model_utils.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('osusers', '0004_auto_20150104_1751'), ] @@ -16,12 +15,22 @@ class Migration(migrations.Migration): migrations.CreateModel( name='DatabaseUser', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('name', models.CharField(max_length=63, verbose_name='username')), - ('db_type', models.PositiveSmallIntegerField(verbose_name='database type', choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), - ('osuser', models.ForeignKey(to='osusers.User')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('name', models.CharField( + max_length=63, verbose_name='username')), + ('db_type', models.PositiveSmallIntegerField( + verbose_name='database type', + choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), + ('osuser', models.ForeignKey( + to='osusers.User', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'database user', @@ -32,11 +41,20 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserDatabase', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('db_name', models.CharField(max_length=63, verbose_name='database name')), - ('db_user', models.ForeignKey(verbose_name='database user', to='userdbs.DatabaseUser')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('created', model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, verbose_name='created', + editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, verbose_name='modified', + editable=False)), + ('db_name', models.CharField( + max_length=63, verbose_name='database name')), + ('db_user', models.ForeignKey( + verbose_name='database user', to='userdbs.DatabaseUser', + on_delete=models.CASCADE)), ], options={ 'verbose_name': 'user database', @@ -46,10 +64,10 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='userdatabase', - unique_together=set([('db_name', 'db_user')]), + unique_together={('db_name', 'db_user')}, ), migrations.AlterUniqueTogether( name='databaseuser', - unique_together=set([('name', 'db_type')]), + unique_together={('name', 'db_type')}, ), ] diff --git a/gnuviechadmin/userdbs/models.py b/gnuviechadmin/userdbs/models.py index 4cc2b76..c9eafb5 100644 --- a/gnuviechadmin/userdbs/models.py +++ b/gnuviechadmin/userdbs/models.py @@ -1,33 +1,14 @@ from __future__ import unicode_literals -from django.db import models -from django.db import transaction +from django.db import models, transaction +from django.dispatch import Signal from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext as _ - from model_utils import Choices from model_utils.models import TimeStampedModel -from passlib.utils import generate_password - from osusers.models import User as OsUser -from mysqltasks.tasks import ( - create_mysql_database, - create_mysql_user, - delete_mysql_database, - delete_mysql_user, - set_mysql_userpassword, -) -from pgsqltasks.tasks import ( - create_pgsql_database, - create_pgsql_user, - delete_pgsql_database, - delete_pgsql_user, - set_pgsql_userpassword, -) - - DB_TYPES = Choices( (0, 'pgsql', _('PostgreSQL')), (1, 'mysql', _('MySQL')), @@ -37,6 +18,9 @@ Database type choice enumeration. """ +password_set = Signal(providing_args=['instance', 'password']) + + class DatabaseUserManager(models.Manager): """ Default Manager for :py:class:`userdbs.models.DatabaseUser`. @@ -93,15 +77,13 @@ class DatabaseUserManager(models.Manager): db_user = DatabaseUser( osuser=osuser, db_type=db_type, name=username) if commit: - db_user.create_in_database(password=password) db_user.save() return db_user - @python_2_unicode_compatible class DatabaseUser(TimeStampedModel, models.Model): - osuser = models.ForeignKey(OsUser) + osuser = models.ForeignKey(OsUser, on_delete=models.CASCADE) name = models.CharField( _('username'), max_length=63) db_type = models.PositiveSmallIntegerField( @@ -121,34 +103,15 @@ class DatabaseUser(TimeStampedModel, models.Model): 'osuser': self.osuser.username, } - def create_in_database(self, password=None): - """ - Create this user in the target database. - - :param str password: initial password for the database user - """ - if password is None: - password = generate_password() - # TODO: send GPG encrypted mail with this information - if self.db_type == DB_TYPES.pgsql: - create_pgsql_user.delay(self.name, password).get() - elif self.db_type == DB_TYPES.mysql: - create_mysql_user.delay(self.name, password).get() - else: - raise ValueError('Unknown database type %d' % self.db_type) - + @transaction.atomic def set_password(self, password): """ Set an existing user's password. :param str password: new password for the database user """ - if self.db_type == DB_TYPES.pgsql: - set_pgsql_userpassword.delay(self.name, password).get(timeout=5) - elif self.db_type == DB_TYPES.mysql: - set_mysql_userpassword.delay(self.name, password).get(timeout=5) - else: - raise ValueError('Unknown database type %d' % self.db_type) + password_set.send( + sender=self.__class__, password=password, instance=self) @transaction.atomic def delete(self, *args, **kwargs): @@ -164,12 +127,6 @@ class DatabaseUser(TimeStampedModel, models.Model): """ for database in self.userdatabase_set.all(): database.delete() - if self.db_type == DB_TYPES.pgsql: - delete_pgsql_user.delay(self.name).get(propagate=False, timeout=5) - elif self.db_type == DB_TYPES.mysql: - delete_mysql_user.delay(self.name).get(propagate=False, timeout=5) - else: - raise ValueError('Unknown database type %d' % self.db_type) super(DatabaseUser, self).delete(*args, **kwargs) @@ -204,7 +161,7 @@ class UserDatabaseManager(models.Manager): @transaction.atomic def create_userdatabase_with_user( - self, db_type, osuser, password=None, commit=True): + self, db_type, osuser, password=None, commit=True): """ Creates a new user database with a new user. @@ -237,7 +194,6 @@ class UserDatabaseManager(models.Manager): db_name = self._get_next_dbname(db_user) database = UserDatabase(db_user=db_user, db_name=db_name) if commit: - database.create_in_database() database.save() return database @@ -247,7 +203,9 @@ class UserDatabase(TimeStampedModel, models.Model): # MySQL limits to 64, PostgreSQL to 63 characters db_name = models.CharField( _('database name'), max_length=63) - db_user = models.ForeignKey(DatabaseUser, verbose_name=_('database user')) + db_user = models.ForeignKey( + DatabaseUser, verbose_name=_('database user'), + on_delete=models.CASCADE) objects = UserDatabaseManager() @@ -261,39 +219,3 @@ class UserDatabase(TimeStampedModel, models.Model): 'db_name': self.db_name, 'db_user': self.db_user, } - - def create_in_database(self): - """ - Create this database (schema) in the target database. - - """ - # TODO: send GPG encrypted mail with this information - if self.db_user.db_type == DB_TYPES.pgsql: - create_pgsql_database.delay(self.db_name, self.db_user.name).get() - elif self.db_user.db_type == DB_TYPES.mysql: - create_mysql_database.delay(self.db_name, self.db_user.name).get() - else: - raise ValueError('Unknown database type %d' % self.db_type) - - @transaction.atomic - def delete(self, *args, **kwargs): - """ - Delete the database (schema) from the target database and the Django - database. - - :param args: positional arguments for - :py:meth:`django.db.models.Model.delete` - :param kwargs: keyword arguments for - :py:meth:`django.db.models.Model.delete` - - """ - db_user = self.db_user - if db_user.db_type == DB_TYPES.pgsql: - delete_pgsql_database.delay(self.db_name).get() - elif db_user.db_type == DB_TYPES.mysql: - delete_mysql_database.delay(self.db_name, db_user.name).get() - else: - raise ValueError('Unknown database type %d' % self.db_type) - super(UserDatabase, self).delete(*args, **kwargs) - if not db_user.userdatabase_set.exists(): - db_user.delete() diff --git a/gnuviechadmin/userdbs/signals.py b/gnuviechadmin/userdbs/signals.py new file mode 100644 index 0000000..4a5bc32 --- /dev/null +++ b/gnuviechadmin/userdbs/signals.py @@ -0,0 +1,331 @@ +""" +This module contains the signal handlers of the :py:mod:`userdbs` app. + +The module starts Celery_ tasks. + +.. _Celery: http://www.celeryproject.org/ + +""" +from __future__ import unicode_literals + +import logging + +from django.db.models.signals import post_delete, post_save +from django.dispatch import receiver +from passlib.utils import generate_password + +from mysqltasks.tasks import (create_mysql_database, create_mysql_user, + delete_mysql_database, delete_mysql_user, + set_mysql_userpassword) +from pgsqltasks.tasks import (create_pgsql_database, create_pgsql_user, + delete_pgsql_database, delete_pgsql_user, + set_pgsql_userpassword) +from taskresults.models import TaskResult + +from .models import DB_TYPES, DatabaseUser, UserDatabase, password_set + +_LOGGER = logging.getLogger(__name__) + + +@receiver(password_set, sender=DatabaseUser) +def handle_dbuser_password_set(sender, instance, password, **kwargs): + """ + Signal handler triggered by password changes for + :py:class:`userdbs.models.DatabaseUser` instances. + + :param sender: the sender of the signal + :param instance: the Database user instance + :param str password: the new password for the database user + + This signal handler starts Celery_ tasks depending on the db_type value of + the database user. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + A -> C; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "set mysql userpassword", color = "PowderBlue", + description = ":py:func:`set_mysql_userpassword() + ` called with + database username and password" ]; + C [ label = "set pgsql userpassword", color = "DodgerBlue", + description = ":py:func:`set_pgsql_userpassword() + ` called with + database username and password" ]; + } + + """ + if instance.db_type == DB_TYPES.mysql: + taskresult = TaskResult.objects.create_task_result( + 'handle_dbuser_password_set', + set_mysql_userpassword.s(instance.name, password), + 'mysql password change') + _LOGGER.info( + 'MySQL password change has been requested in task %s', + taskresult.task_id) + elif instance.db_type == DB_TYPES.pgsql: + taskresult = TaskResult.objects.create_task_result( + 'handle_dbuser_password_set', + set_pgsql_userpassword.s(instance.name, password), + 'pgsql password change') + _LOGGER.info( + 'PostgreSQL password change has been requested in task %s', + taskresult.task_id) + else: + _LOGGER.warning( + 'Password change has been requested for unknown database %s' + ' the request has been ignored.', + instance.db_type) + + +@receiver(post_save, sender=DatabaseUser) +def handle_dbuser_created(sender, instance, created, **kwargs): + """ + Signal handler triggered after the creation of or updates to + :py:class:`userdbs.models.DatabaseUser` instances. + + :param sender: the sender of the signal + :param instance: the DatabaseUser instance + :param bool created: whether this signal handler is called for a newly + created instance + + This signal handler starts Celery_ tasks depending on the db_type value of + the database user. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + A -> C; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "create mysql user", color = "PowderBlue", + description = ":py:func:`create_mysql_user() + ` called with database + username and password" ]; + C [ label = "create pgsql user", color = "DodgerBlue", + description = ":py:func:`create_pgsql_user + ` called with database + username and password" ]; + } + + """ + if created: + password = kwargs.get('password', generate_password()) + # TODO: send GPG encrypted mail with this information + if instance.db_type == DB_TYPES.mysql: + taskresult = TaskResult.objects.create_task_result( + 'handle_dbuser_created', + create_mysql_user.s(instance.name, password), + 'mysql user creation') + _LOGGER.info( + 'A new MySQL user %s creation has been requested in task %s', + instance.name, taskresult.task_id) + elif instance.db_type == DB_TYPES.pgsql: + taskresult = TaskResult.objects.create_task_result( + 'handle_dbuser_created', + create_pgsql_user.s(instance.name, password), + 'pgsql user creation') + _LOGGER.info( + 'A new PostgreSQL user %s creation has been requested in task' + ' %s', + instance.name, taskresult.task_id) + else: + _LOGGER.warning( + 'created DatabaseUser for unknown database type %s', + instance.db_type) + _LOGGER.debug( + 'database user %s has been %s', + instance, created and "created" or "updated") + + +@receiver(post_delete, sender=DatabaseUser) +def handle_dbuser_deleted(sender, instance, **kwargs): + """ + Signal handler triggered after the deletion of + :py:class:`userdbs.models.DatabaseUser` instances. + + :param sender: the sender of the signal + :param instance: the DatabaseUser instance + + This signal handler starts Celery_ tasks depending on the db_type value of + the database user. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + A -> C; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "delete mysql user", color = "PowderBlue", + description = ":py:func:`delete_mysql_user() + ` called with username + from instance.name" ]; + C [ label = "delete pgsql user", color = "DodgerBlue", + description = ":py:func:`delete_pgsql_user() + ` called with username + from instance.name" ]; + } + """ + if instance.db_type == DB_TYPES.mysql: + taskresult = TaskResult.objects.create_task_result( + 'handle_dbuser_deleted', + delete_mysql_user.s(instance.name), + 'mysql user deletion') + _LOGGER.info( + 'MySQL user %s deletion has been requested in task %s', + instance.name, taskresult.task_id) + elif instance.db_type == DB_TYPES.pgsql: + taskresult = TaskResult.objects.create_task_result( + 'handle_dbuser_deleted', + delete_pgsql_user.s(instance.name), + 'pgsql user deletion') + _LOGGER.info( + 'PostgreSQL user %s deletion has been requested in task %s', + instance.name, taskresult.task_id) + else: + _LOGGER.warning( + 'deleted DatabaseUser %s for unknown database type %s', + instance.name, instance.db_type) + _LOGGER.debug( + 'database user %s has been deleted', instance) + + +@receiver(post_save, sender=UserDatabase) +def handle_userdb_created(sender, instance, created, **kwargs): + """ + Signal handler triggered after the creation of or updates to + :py:class:`userdbs.models.UserDatabase` instances. + + :param sender: the sender of the signal + :param instance: the UserDatabase instance + :param bool created: whether this signal handler has been called for a + newly created instance + + This signal handler starts Celery_ tasks depending on the db_type value of + the database user owning the UserDatabase instance. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + A -> C; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "create mysql database", color = "PowderBlue", + description = ":py:func:`create_mysql_database() + ` called with database + name and username" ]; + C [ label = "create pgsql database", color = "DodgerBlue", + description = ":py:func:`create_pgsql_database() + ` called with database + name and username" ]; + } + """ + if created: + if instance.db_user.db_type == DB_TYPES.mysql: + taskresult = TaskResult.objects.create_task_result( + 'handle_userdb_created', + create_mysql_database.s( + instance.db_name, instance.db_user.name), + 'mysql database creation') + _LOGGER.info( + 'The creation of a new MySQL database %s has been requested in' + ' task %s', + instance.db_name, taskresult.task_id) + elif instance.db_user.db_type == DB_TYPES.pgsql: + taskresult = TaskResult.objects.create_task_result( + 'handle_userdb_created', + create_pgsql_database.s( + instance.db_name, instance.db_user.name), + 'pgsql database creation') + _LOGGER.info( + 'The creation of a new PostgreSQL database %s has been' + ' requested in task %s', + instance.db_name, taskresult.task_id) + else: + _LOGGER.warning( + 'created UserDatabase for unknown database type %s', + instance.db_user.db_type) + _LOGGER.debug( + 'database %s has been %s', + instance, created and "created" or "updated") + + +@receiver(post_delete, sender=UserDatabase) +def handle_userdb_deleted(sender, instance, **kwargs): + """ + Signal handler triggered after the deletion of + :py:class:`userdbs.models.UserDatabase` instances. + + :param sender: the sender of the signal + :param instance: the UserDatabase instance + + This signal handler starts Celery_ tasks depending on the db_type value of + the database user owning the UserDatabase instance. + + .. blockdiag:: + :desctable: + + blockdiag { + node_width = 200; + + A -> B; + A -> C; + + A [ label = "", shape = beginpoint, + description = "this signal handler" ]; + B [ label = "delete mysql database", color = "PowderBlue", + description = ":py:func:`delete_mysql_user() + ` called with database + name and username" ]; + C [ label = "delete pgsql database", color = "DodgerBlue", + description = ":py:func:`delete_pgsql_user() + ` called with database + name" ]; + } + """ + if instance.db_user.db_type == DB_TYPES.mysql: + taskresult = TaskResult.objects.create_task_result( + 'handle_userdb_deleted', + delete_mysql_database.s(instance.db_name, instance.db_user.name), + 'mysql database deletion') + _LOGGER.info( + 'The deletion of MySQL database %s has been requested in task %s', + instance.db_name, taskresult.task_id) + elif instance.db_user.db_type == DB_TYPES.pgsql: + taskresult = TaskResult.objects.create_task_result( + 'handle_userdb_deleted', + delete_pgsql_database.s(instance.db_name), + 'pgsql database deletion') + _LOGGER.info( + 'The deletion of PostgreSQL database %s has been requested in ' + ' task %s', + instance.db_name, taskresult.task_id) + else: + _LOGGER.warning( + 'deleted UserDatabase %s of unknown type %s', + instance.db_name, instance.db_type) + pass + _LOGGER.debug( + 'database %s has been deleted', instance) diff --git a/gnuviechadmin/userdbs/tests/__init__.py b/gnuviechadmin/userdbs/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/userdbs/tests/templatetags/__init__.py b/gnuviechadmin/userdbs/tests/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/userdbs/tests/templatetags/test_userdb.py b/gnuviechadmin/userdbs/tests/templatetags/test_userdb.py new file mode 100644 index 0000000..120f18c --- /dev/null +++ b/gnuviechadmin/userdbs/tests/templatetags/test_userdb.py @@ -0,0 +1,45 @@ +""" +This module provides tests for the functions in +:py:mod:`userdbs.templatetags.userdb`. + +""" +from __future__ import unicode_literals + +from unittest import TestCase + +from django.utils.translation import gettext as _ + +from userdbs.models import DB_TYPES +from userdbs.templatetags.userdb import db_type_icon_class, db_type_name + + +class UserdbTemplateTagTests(TestCase): + """ + Test suite for :py:mod:`userdbs.templatetags.userdb` functions. + + """ + + def test_db_type_icon_class_unknown(self): + self.assertEqual( + db_type_icon_class({'db_type': 'unknown'}), + 'icon-database') + + def test_db_type_icon_class_mysql(self): + self.assertEqual( + db_type_icon_class({'db_type': DB_TYPES.mysql}), + 'icon-mysql') + + def test_db_type_icon_class_pgsql(self): + self.assertEqual( + db_type_icon_class({'db_type': DB_TYPES.pgsql}), + 'icon-postgres') + + def test_db_type_name_mysql(self): + self.assertEqual( + db_type_name({'db_type': DB_TYPES.mysql}), + _(DB_TYPES[DB_TYPES.mysql])) + + def test_db_type_name_pgsql(self): + self.assertEqual( + db_type_name({'db_type': DB_TYPES.pgsql}), + _(DB_TYPES[DB_TYPES.pgsql])) diff --git a/gnuviechadmin/userdbs/tests/test_admin.py b/gnuviechadmin/userdbs/tests/test_admin.py new file mode 100644 index 0000000..3479a30 --- /dev/null +++ b/gnuviechadmin/userdbs/tests/test_admin.py @@ -0,0 +1,148 @@ +""" +This module provides tests for :py:mod:`userdbs.admin`. + +""" +from unittest.mock import MagicMock, Mock, patch + +from django.contrib.admin import AdminSite +from django.test import TestCase + +from userdbs.admin import ( + DatabaseUserAdmin, + DatabaseUserCreationForm, + UserDatabaseAdmin, + UserDatabaseCreationForm, +) +from userdbs.models import DB_TYPES, DatabaseUser, UserDatabase + + +class DatabaseUserCreationFormTest(TestCase): + @patch("userdbs.admin.DatabaseUser.objects.create_database_user") + def test_save(self, create_database_user): + create_database_user.return_value = Mock() + form = DatabaseUserCreationForm() + mockuser = Mock(name="osuser") + form.cleaned_data = {"osuser": mockuser, "db_type": DB_TYPES.pgsql} + retval = form.save() + self.assertTrue( + create_database_user.called_with( + osuser=mockuser, db_type=DB_TYPES.pgsql, commit=True + ) + ) + self.assertEqual(retval, create_database_user.return_value) + + def test_save_m2m_returns_none(self): + form = DatabaseUserCreationForm() + self.assertIsNone(form.save_m2m()) + + +class UserDatabaseCreationFormTest(TestCase): + @patch("userdbs.admin.UserDatabase.objects.create_userdatabase") + def test_save(self, create_userdatabase): + create_userdatabase.return_value = Mock() + form = UserDatabaseCreationForm() + mockuser = Mock(name="mockuser") + form.cleaned_data = {"db_user": mockuser} + retval = form.save() + self.assertTrue(create_userdatabase.called_with(db_user=mockuser, commit=True)) + self.assertEqual(retval, create_userdatabase.return_value) + + def test_save_m2m_returns_none(self): + form = UserDatabaseCreationForm() + self.assertIsNone(form.save_m2m()) + + +class DatabaseUserAdminTest(TestCase): + def setUp(self): + site = AdminSite() + self.dbuadmin = DatabaseUserAdmin(DatabaseUser, site) + super(DatabaseUserAdminTest, self).setUp() + + def test_get_form_with_instance(self): + form = self.dbuadmin.get_form(Mock(name="request"), obj=Mock(name="dbuser")) + self.assertEqual(form.Meta.fields, ["osuser", "name", "db_type"]) + + def test_get_form_without_instance(self): + form = self.dbuadmin.get_form(Mock(name="request")) + self.assertEqual(form.Meta.fields, ["osuser", "db_type"]) + + def test_get_readonly_fields_with_instance(self): + fields = self.dbuadmin.get_readonly_fields( + Mock(name="request"), obj=Mock(name="dbuser") + ) + self.assertEqual(fields, ["osuser", "name", "db_type"]) + + def test_get_readonly_fields_without_instance(self): + fields = self.dbuadmin.get_readonly_fields(Mock(name="request")) + self.assertEqual(fields, []) + + def test_save_model_change(self): + objmock = Mock() + self.dbuadmin.save_model(Mock(name="request"), objmock, Mock(), True) + self.assertTrue(objmock.create_in_database.not_called()) + + def test_save_model_no_change(self): + objmock = Mock() + self.dbuadmin.save_model(Mock(name="request"), objmock, Mock(), False) + self.assertTrue(objmock.create_in_database.called_with()) + + def test_perform_delete_selected(self): + usermock = Mock() + selected = Mock() + selected.all.return_value = [usermock] + self.dbuadmin.perform_delete_selected(Mock(name="request"), selected) + self.assertTrue(selected.all.called_with()) + self.assertTrue(usermock.delete.called_with()) + + def test_get_actions(self): + requestmock = MagicMock(name="request") + self.assertNotIn("delete_selected", self.dbuadmin.get_actions(requestmock)) + self.assertIn("perform_delete_selected", self.dbuadmin.get_actions(requestmock)) + + +class UserDatabaseAdminTest(TestCase): + def setUp(self): + site = AdminSite() + self.udbadmin = UserDatabaseAdmin(UserDatabase, site) + super(UserDatabaseAdminTest, self).setUp() + + def test_get_form_with_instance(self): + form = self.udbadmin.get_form(Mock(name="request"), obj=Mock(name="userdb")) + self.assertEqual(form.Meta.fields, ["db_name", "db_user"]) + + def test_get_form_without_instance(self): + form = self.udbadmin.get_form(Mock(name="request")) + self.assertEqual(form.Meta.fields, ["db_user"]) + + def test_get_readonly_fields_with_instance(self): + fields = self.udbadmin.get_readonly_fields( + Mock(name="request"), obj=Mock(name="userdb") + ) + self.assertEqual(fields, ["db_name", "db_user"]) + + def test_get_readonly_fields_without_instance(self): + fields = self.udbadmin.get_readonly_fields(Mock(name="request")) + self.assertEqual(fields, []) + + def test_save_model_change(self): + objmock = Mock() + self.udbadmin.save_model(Mock(name="request"), objmock, Mock(), True) + self.assertTrue(objmock.create_in_database.not_called()) + + def test_save_model_no_change(self): + objmock = Mock() + self.udbadmin.save_model(Mock(name="request"), objmock, Mock(), False) + self.assertTrue(objmock.create_in_database.called_with()) + + def test_perform_delete_selected(self): + userdbmock = Mock() + selected = Mock() + selected.all.return_value = [userdbmock] + self.udbadmin.perform_delete_selected(Mock(name="request"), selected) + self.assertTrue(selected.all.called_with()) + self.assertTrue(userdbmock.delete.called_with()) + + def test_get_actions(self): + requestmock = MagicMock(name="request") + self.assertNotIn("delete_selected", self.udbadmin.get_actions(requestmock)) + self.assertIn("perform_delete_selected", self.udbadmin.get_actions(requestmock)) diff --git a/gnuviechadmin/userdbs/tests/test_forms.py b/gnuviechadmin/userdbs/tests/test_forms.py new file mode 100644 index 0000000..2aa1706 --- /dev/null +++ b/gnuviechadmin/userdbs/tests/test_forms.py @@ -0,0 +1,134 @@ +""" +This module provides tests for :py:mod:`userdbs.forms`. + +""" +from django import forms +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse + +from userdbs.forms import AddUserDatabaseForm, ChangeDatabaseUserPasswordForm +from userdbs.models import DB_TYPES + +from unittest.mock import MagicMock, Mock, patch + + +Customer = get_user_model() + + +class AddUserDatabaseFormTest(TestCase): + """ + Test class for :py:class:`userdbs.forms.AddUserDatabaseForm`. + + """ + + def _setup_hostingpackage(self): + self.hostingpackage = Mock(id=42) + + def test_constructor_needs_hostingpackage(self): + with self.assertRaises(KeyError) as ke: + AddUserDatabaseForm(instance=Mock()) + self.assertEqual(ke.exception.args[0], "hostingpackage") + + def test_constructor_needs_dbtypes(self): + with self.assertRaises(KeyError) as ke: + AddUserDatabaseForm(instance=Mock(), hostingpackage=Mock()) + self.assertEqual(ke.exception.args[0], "dbtypes") + + def test_constructor_one_dbtype(self): + self._setup_hostingpackage() + dbtypes = [(DB_TYPES.pgsql, DB_TYPES[DB_TYPES.pgsql])] + form = AddUserDatabaseForm( + instance=MagicMock(), hostingpackage=self.hostingpackage, dbtypes=dbtypes + ) + self.assertIn("db_type", form.fields) + self.assertEqual(form.fields["db_type"].choices, dbtypes) + self.assertTrue(isinstance(form.fields["db_type"].widget, forms.HiddenInput)) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse("add_userdatabase", kwargs={"package": self.hostingpackage.id}), + ) + self.assertEqual(form.helper.inputs[0].name, "submit") + + def test_constructor_multiple_dbtypes(self): + self._setup_hostingpackage() + dbtypes = [ + (DB_TYPES.pgsql, DB_TYPES[DB_TYPES.pgsql]), + (DB_TYPES.mysql, DB_TYPES[DB_TYPES.mysql]), + ] + form = AddUserDatabaseForm( + instance=MagicMock(), hostingpackage=self.hostingpackage, dbtypes=dbtypes + ) + self.assertIn("db_type", form.fields) + self.assertEqual(form.fields["db_type"].choices, dbtypes) + self.assertTrue(isinstance(form.fields["db_type"].widget, forms.RadioSelect)) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse("add_userdatabase", kwargs={"package": self.hostingpackage.id}), + ) + self.assertEqual(form.helper.inputs[0].name, "submit") + + @patch("userdbs.forms.UserDatabase.objects.create_userdatabase_with_user") + def test_save(self, create_userdatabase_with_user): + self._setup_hostingpackage() + dbtypes = [ + (DB_TYPES.pgsql, DB_TYPES[DB_TYPES.pgsql]), + (DB_TYPES.mysql, DB_TYPES[DB_TYPES.mysql]), + ] + form = AddUserDatabaseForm( + instance=MagicMock(), hostingpackage=self.hostingpackage, dbtypes=dbtypes + ) + form.cleaned_data = {"db_type": DB_TYPES.pgsql, "password1": "secret"} + form.save() + self.assertTrue( + create_userdatabase_with_user.called_with( + DB_TYPES.pgsql, + self.hostingpackage.osuser, + password="secret", + commit=True, + ) + ) + + +class ChangeDatabaseUserPasswordFormTest(TestCase): + """ + Test class for :py:class:`userdbs.forms.ChangeDatabaseUserPasswordForm`. + + """ + + def _setup_hostingpackage(self): + self.hostingpackage = Mock(id=42) + + def test_constructor_needs_hostingpackage(self): + with self.assertRaises(KeyError) as ke: + ChangeDatabaseUserPasswordForm(instance=Mock()) + self.assertEqual(ke.exception.args[0], "hostingpackage") + + def test_constructor(self): + self._setup_hostingpackage() + instance = MagicMock() + instance.name = "test" + form = ChangeDatabaseUserPasswordForm( + instance=instance, hostingpackage=self.hostingpackage + ) + self.assertIn("password1", form.fields) + self.assertIn("password2", form.fields) + self.assertTrue(hasattr(form, "helper")) + self.assertEqual( + form.helper.form_action, + reverse("change_dbuser_password", kwargs={"slug": "test", "package": 42}), + ) + self.assertEqual(form.helper.inputs[0].name, "submit") + + def test_save(self): + instance = MagicMock() + instance.name = "test" + self._setup_hostingpackage() + form = ChangeDatabaseUserPasswordForm( + instance=instance, hostingpackage=self.hostingpackage + ) + form.cleaned_data = {"password1": "secret"} + form.save() + self.assertTrue(instance.set_password.called_with("secret")) diff --git a/gnuviechadmin/userdbs/tests/test_models.py b/gnuviechadmin/userdbs/tests/test_models.py new file mode 100644 index 0000000..41306bd --- /dev/null +++ b/gnuviechadmin/userdbs/tests/test_models.py @@ -0,0 +1,286 @@ +""" +This module provides tests for :py:mod:`userdbs.models`. + +""" +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.test.utils import override_settings + +from osusers.models import User +from taskresults.models import TaskResult +from userdbs.models import DB_TYPES, DatabaseUser, UserDatabase + +Customer = get_user_model() + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class TestCaseWithCeleryTasks(TestCase): + pass + + +class DatabaseUserManagerTest(TestCaseWithCeleryTasks): + """ + Test case for :py:class:`userdbs.models.DatabaseUserManager`. + + """ + + def setUp(self): + self.customer = Customer.objects.create_user(username="testcustomer") + self.osuser = User.objects.create_user(customer=self.customer) + TaskResult.objects.all().delete() + + def test_create_database_user_with_name(self): + dbu = DatabaseUser.objects.create_database_user( + self.osuser, DB_TYPES.pgsql, "testname", "secret" + ) + self.assertEqual(dbu.name, "testname") + self.assertEqual(dbu.osuser, self.osuser) + self.assertEqual(dbu.db_type, DB_TYPES.pgsql) + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 1) + self.assertEqual(taskres[0].creator, "handle_dbuser_created") + self.assertEqual(taskres[0].notes, "pgsql user creation") + + def test_create_database_user_with_name_no_commit(self): + dbu = DatabaseUser.objects.create_database_user( + self.osuser, DB_TYPES.pgsql, "testname", "secret", False + ) + self.assertEqual(dbu.name, "testname") + self.assertEqual(dbu.osuser, self.osuser) + self.assertEqual(dbu.db_type, DB_TYPES.pgsql) + self.assertFalse(TaskResult.objects.exists()) + + def test_create_database_user_generate_name(self): + dbu = DatabaseUser.objects.create_database_user(self.osuser, DB_TYPES.pgsql) + self.assertEqual(dbu.name, "{user}db01".format(user=self.osuser.username)) + self.assertEqual(dbu.osuser, self.osuser) + self.assertEqual(dbu.db_type, DB_TYPES.pgsql) + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 1) + self.assertEqual(taskres[0].creator, "handle_dbuser_created") + self.assertEqual(taskres[0].notes, "pgsql user creation") + + def test_create_database_user_multiple_generate_name(self): + dbu = DatabaseUser.objects.create_database_user(self.osuser, DB_TYPES.mysql) + self.assertEqual(dbu.name, "{user}db01".format(user=self.osuser.username)) + self.assertEqual(dbu.osuser, self.osuser) + self.assertEqual(dbu.db_type, DB_TYPES.mysql) + dbu = DatabaseUser.objects.create_database_user(self.osuser, DB_TYPES.mysql) + self.assertEqual(dbu.name, "{user}db02".format(user=self.osuser.username)) + self.assertEqual(dbu.osuser, self.osuser) + self.assertEqual(dbu.db_type, DB_TYPES.mysql) + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 2) + self.assertEqual(taskres[0].creator, "handle_dbuser_created") + self.assertEqual(taskres[0].notes, "mysql user creation") + self.assertEqual(taskres[1].creator, "handle_dbuser_created") + self.assertEqual(taskres[1].notes, "mysql user creation") + + def test_create_database_user_multiple_gap_generate_name(self): + dbu = DatabaseUser.objects.create_database_user(self.osuser, DB_TYPES.mysql) + self.assertEqual(dbu.name, "{user}db01".format(user=self.osuser.username)) + self.assertEqual(dbu.osuser, self.osuser) + self.assertEqual(dbu.db_type, DB_TYPES.mysql) + dbu = DatabaseUser.objects.create_database_user(self.osuser, DB_TYPES.mysql) + self.assertEqual(dbu.name, "{user}db02".format(user=self.osuser.username)) + self.assertEqual(dbu.osuser, self.osuser) + self.assertEqual(dbu.db_type, DB_TYPES.mysql) + DatabaseUser.objects.get( + name="{user}db01".format(user=self.osuser.username) + ).delete() + dbu = DatabaseUser.objects.create_database_user(self.osuser, DB_TYPES.mysql) + self.assertEqual(dbu.name, "{user}db01".format(user=self.osuser.username)) + self.assertEqual(dbu.osuser, self.osuser) + self.assertEqual(dbu.db_type, DB_TYPES.mysql) + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 4) + self.assertEqual(taskres[0].creator, "handle_dbuser_created") + self.assertEqual(taskres[0].notes, "mysql user creation") + self.assertEqual(taskres[1].creator, "handle_dbuser_created") + self.assertEqual(taskres[1].notes, "mysql user creation") + self.assertEqual(taskres[2].creator, "handle_dbuser_deleted") + self.assertEqual(taskres[2].notes, "mysql user deletion") + self.assertEqual(taskres[3].creator, "handle_dbuser_created") + self.assertEqual(taskres[3].notes, "mysql user creation") + + +class DatabaseUserTest(TestCaseWithCeleryTasks): + """ + Test case for :py:class:`userdbs.models.DatabaseUser`. + + """ + + def setUp(self): + self.customer = Customer.objects.create_user(username="testcustomer") + self.osuser = User.objects.create_user(customer=self.customer) + self.dbu = DatabaseUser.objects.create_database_user( + self.osuser, DB_TYPES.pgsql + ) + TaskResult.objects.all().delete() + + def test___str__(self): + self.assertEqual( + str(self.dbu), + "{user}db01 (PostgreSQL for {user})".format(user=self.osuser.username), + ) + + def test_set_password_pgsql(self): + self.dbu.set_password("secret") + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 1) + self.assertEqual(taskres[0].creator, "handle_dbuser_password_set") + self.assertEqual(taskres[0].notes, "pgsql password change") + + def test_set_password_mysql(self): + self.dbu.db_type = DB_TYPES.mysql + self.dbu.save() + self.dbu.set_password("secret") + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 1) + self.assertEqual(taskres[0].creator, "handle_dbuser_password_set") + self.assertEqual(taskres[0].notes, "mysql password change") + + def test_delete_no_dbs(self): + self.dbu.delete() + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 1) + self.assertEqual(taskres[0].creator, "handle_dbuser_deleted") + self.assertEqual(taskres[0].notes, "pgsql user deletion") + + def test_delete_with_dbs(self): + db = UserDatabase.objects.create_userdatabase(self.dbu) + dbid = db.id + self.dbu.delete() + self.assertFalse(UserDatabase.objects.filter(id=dbid).exists()) + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 3) + self.assertEqual(taskres[0].creator, "handle_userdb_created") + self.assertEqual(taskres[0].notes, "pgsql database creation") + self.assertEqual(taskres[1].creator, "handle_userdb_deleted") + self.assertEqual(taskres[1].notes, "pgsql database deletion") + self.assertEqual(taskres[2].creator, "handle_dbuser_deleted") + self.assertEqual(taskres[2].notes, "pgsql user deletion") + + +class UserDatabaseManagerTest(TestCaseWithCeleryTasks): + """ + Test case for :py:class:`userdbs.models.UserDatabaseManager`. + + """ + + def setUp(self): + self.customer = Customer.objects.create_user(username="testcustomer") + self.osuser = User.objects.create_user(customer=self.customer) + TaskResult.objects.all().delete() + + def _create_database_user(self, dbtype): + self.dbu = DatabaseUser.objects.create_database_user(self.osuser, dbtype) + TaskResult.objects.all().delete() + + def test_create_userdatabase_with_user_mysql(self): + db = UserDatabase.objects.create_userdatabase_with_user( + DB_TYPES.mysql, self.osuser + ) + self.assertEqual(db.db_name, "{user}db01".format(user=self.osuser.username)) + self.assertEqual( + db.db_user.name, "{user}db01".format(user=self.osuser.username) + ) + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 2) + self.assertEqual(taskres[0].creator, "handle_dbuser_created") + self.assertEqual(taskres[0].notes, "mysql user creation") + self.assertEqual(taskres[1].creator, "handle_userdb_created") + self.assertEqual(taskres[1].notes, "mysql database creation") + + def test_create_userdatabase_with_user_pgsql(self): + db = UserDatabase.objects.create_userdatabase_with_user( + DB_TYPES.pgsql, self.osuser + ) + self.assertEqual(db.db_name, "{user}db01".format(user=self.osuser.username)) + self.assertEqual( + db.db_user.name, "{user}db01".format(user=self.osuser.username) + ) + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 2) + self.assertEqual(taskres[0].creator, "handle_dbuser_created") + self.assertEqual(taskres[0].notes, "pgsql user creation") + self.assertEqual(taskres[1].creator, "handle_userdb_created") + self.assertEqual(taskres[1].notes, "pgsql database creation") + + def test_create_userdatabase_given_name_no_new_write(self): + self._create_database_user(DB_TYPES.pgsql) + db = UserDatabase.objects.create_userdatabase(self.dbu, db_name="test") + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 1) + self.assertEqual(db.db_name, "test") + TaskResult.objects.all().delete() + db.save() + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 0) + + def test_create_userdatabase_given_name(self): + self._create_database_user(DB_TYPES.pgsql) + db = UserDatabase.objects.create_userdatabase(self.dbu, db_name="test") + self.assertEqual(db.db_name, "test") + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 1) + self.assertEqual(db.db_name, "test") + + def test_create_userdatabase_generate_name_no_commit(self): + self._create_database_user(DB_TYPES.pgsql) + db = UserDatabase.objects.create_userdatabase(self.dbu, commit=False) + self.assertEqual(db.db_name, self.dbu.name) + self.assertFalse(TaskResult.objects.all().exists()) + + def test_create_userdatabase_generate_name(self): + self._create_database_user(DB_TYPES.pgsql) + db = UserDatabase.objects.create_userdatabase(self.dbu) + self.assertEqual(db.db_name, self.dbu.name) + + def test_create_userdatabase_multiple_generate_name(self): + self._create_database_user(DB_TYPES.pgsql) + db = UserDatabase.objects.create_userdatabase(self.dbu) + self.assertEqual(db.db_name, self.dbu.name) + db = UserDatabase.objects.create_userdatabase(self.dbu) + self.assertEqual(db.db_name, "{user}_02".format(user=self.dbu.name)) + + def test_create_userdatabase_multiple_gap_generate_name(self): + self._create_database_user(DB_TYPES.pgsql) + db = UserDatabase.objects.create_userdatabase(self.dbu) + self.assertEqual(db.db_name, self.dbu.name) + dbx = UserDatabase.objects.create_userdatabase(self.dbu) + self.assertEqual(dbx.db_name, "{user}_02".format(user=self.dbu.name)) + db = UserDatabase.objects.create_userdatabase(self.dbu) + self.assertEqual(db.db_name, "{user}_03".format(user=self.dbu.name)) + dbx.delete() + db = UserDatabase.objects.create_userdatabase(self.dbu) + self.assertEqual(db.db_name, "{user}_02".format(user=self.dbu.name)) + + +class UserDatabaseTest(TestCaseWithCeleryTasks): + """ + Test case for :py:class:`userdbs.models.UserDabase`. + + """ + + def test___str__(self): + customer = Customer.objects.create_user(username="testcustomer") + osuser = User.objects.create_user(customer=customer) + db = UserDatabase.objects.create_userdatabase_with_user(DB_TYPES.pgsql, osuser) + self.assertEqual( + str(db), + "{user}db01 ({dbuser})".format(user=osuser.username, dbuser=db.db_user), + ) + + def test_delete_mysql_db(self): + customer = Customer.objects.create_user(username="testcustomer") + osuser = User.objects.create_user(customer=customer) + TaskResult.objects.all().delete() + db = UserDatabase.objects.create_userdatabase_with_user(DB_TYPES.mysql, osuser) + db.delete() + taskres = TaskResult.objects.all() + self.assertEqual(len(taskres), 3) + self.assertEqual(taskres[2].creator, "handle_userdb_deleted") + self.assertEqual(taskres[2].notes, "mysql database deletion") diff --git a/gnuviechadmin/userdbs/tests/test_signals.py b/gnuviechadmin/userdbs/tests/test_signals.py new file mode 100644 index 0000000..33541e3 --- /dev/null +++ b/gnuviechadmin/userdbs/tests/test_signals.py @@ -0,0 +1,63 @@ +""" +This module contains explicit tests for corner cases in +:py:mod:`userdbs.signals` that are not handled by the tests in +:py:mod:`userdbs.tests.test_models`. + +""" +from unittest.mock import Mock + +from django.test import TestCase +from django.test.utils import override_settings + +from taskresults.models import TaskResult +from userdbs.signals import ( + handle_dbuser_created, + handle_dbuser_deleted, + handle_dbuser_password_set, + handle_userdb_created, + handle_userdb_deleted, +) + + +@override_settings( + CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", BROKER_BACKEND="memory" +) +class TestCaseWithCeleryTasks(TestCase): + pass + + +class TestWithUnknownDBType(TestCaseWithCeleryTasks): + def test_handle_dbuser_password_set_unknown(self): + instance = Mock(data={"name": "test", "db_type": -1}) + handle_dbuser_password_set(Mock(name="sender"), instance, "secret") + self.assertFalse(TaskResult.objects.exists()) + + def test_handle_dbuser_create_unknown(self): + instance = Mock(data={"name": "test", "db_type": -1}) + handle_dbuser_created(Mock(name="sender"), instance, True, password="secret") + self.assertFalse(TaskResult.objects.exists()) + + def test_handle_dbuser_deleted_unknown(self): + instance = Mock(data={"name": "test", "db_type": -1}) + handle_dbuser_deleted(Mock(name="sender"), instance) + self.assertFalse(TaskResult.objects.exists()) + + def test_handle_userdb_created_unknown(self): + instance = Mock( + data={ + "db_name": "test", + "db_user": Mock(data={"name": "test", "db_type": -1}), + } + ) + handle_userdb_created(Mock(name="sender"), instance, True) + self.assertFalse(TaskResult.objects.exists()) + + def test_handle_userdb_deleted_unknown(self): + instance = Mock( + data={ + "db_name": "test", + "db_user": Mock(data={"name": "test", "db_type": -1}), + } + ) + handle_userdb_deleted(Mock(name="sender"), instance) + self.assertFalse(TaskResult.objects.exists()) diff --git a/gnuviechadmin/userdbs/tests/test_views.py b/gnuviechadmin/userdbs/tests/test_views.py new file mode 100644 index 0000000..b52cbdf --- /dev/null +++ b/gnuviechadmin/userdbs/tests/test_views.py @@ -0,0 +1,335 @@ +""" +This module provides tests for :py:mod:`userdbs.views`. + +""" +from unittest.mock import patch, MagicMock + +from django.test import TestCase +from django.contrib.auth import get_user_model +from django.urls import reverse + +from hostingpackages.models import ( + CustomerHostingPackage, + CustomerUserDatabaseOption, + HostingPackageTemplate, + UserDatabaseOption, +) + +from userdbs.models import DB_TYPES, UserDatabase +from userdbs.views import AddUserDatabase, ChangeDatabaseUserPassword + + +User = get_user_model() + +TEST_USER = "test" +TEST_PASSWORD = "secret" +TEST_EMAIL = "test@example.org" + + +class HostingPackageAwareTestMixin(object): + + # noinspection PyMethodMayBeStatic + def _setup_hosting_package(self, customer): + template = HostingPackageTemplate.objects.create( + name="testpackagetemplate", mailboxcount=10, diskspace=1, diskspace_unit=0 + ) + package = CustomerHostingPackage.objects.create_from_template( + customer, template, "testpackage" + ) + with patch("hostingpackages.models.settings") as hmsettings: + hmsettings.OSUSER_DEFAULT_GROUPS = [] + package.save() + return package + + +class CustomerUserDatabaseOptionAwareTestMixin(object): + def __init__(self, *args, **kwargs): + super(CustomerUserDatabaseOptionAwareTestMixin, self).__init__(*args, **kwargs) + self._templates = {} + + def _setup_userdatabaseoption(self, number, dbtype): + key = "{}_{}".format(dbtype, number) + if key not in self._templates: + self._templates[key] = UserDatabaseOption.objects.create( + number=number, db_type=dbtype + ) + return self._templates[key] + + def _create_userdatabase_option(self, number=1, dbtype=DB_TYPES.pgsql): + # noinspection PyUnresolvedReferences + return CustomerUserDatabaseOption.objects.create( + template=self._setup_userdatabaseoption(number, dbtype), + number=number, + db_type=dbtype, + hosting_package=self.package, + ) + + +class AddUserDatabaseTest( + HostingPackageAwareTestMixin, CustomerUserDatabaseOptionAwareTestMixin, TestCase +): + def setUp(self): + self.customer = User.objects.create_user( + username=TEST_USER, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer) + + def _get_url(self): + return reverse("add_userdatabase", kwargs={"package": self.package.id}) + + def test_get_anonymous(self): + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user_nodboption(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 400) + + def test_get_regular_user(self): + self._create_userdatabase_option() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user("test2", password=TEST_PASSWORD) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user_nodboption(self): + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 400) + + def test_get_staff_user(self): + self._create_userdatabase_option() + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 200) + + def test_get_regular_user_nofree_db(self): + db_option = self._create_userdatabase_option() + UserDatabase.objects.create_userdatabase_with_user( + db_option.db_type, self.package.osuser + ) + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertEqual(response.status_code, 400) + + def test_get_form_kwargs(self): + db_option = self._create_userdatabase_option() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = AddUserDatabase( + request=MagicMock(), kwargs={"package": str(self.package.pk)} + ) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], self.package) + self.assertIn("dbtypes", the_kwargs) + self.assertEqual( + the_kwargs["dbtypes"], [(db_option.db_type, DB_TYPES[db_option.db_type])] + ) + + def test_get_template(self): + self._create_userdatabase_option() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url()) + self.assertTemplateUsed(response, "userdbs/userdatabase_create.html") + + def test_form_valid_redirect(self): + db_option = self._create_userdatabase_option() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + self._get_url(), + data={ + "db_type": db_option.db_type, + "password1": TEST_PASSWORD, + "password2": TEST_PASSWORD, + }, + ) + self.assertRedirects(response, self.package.get_absolute_url()) + + def test_form_valid_message(self): + db_option = self._create_userdatabase_option() + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + self._get_url(), + follow=True, + data={ + "db_type": db_option.db_type, + "password1": TEST_PASSWORD, + "password2": TEST_PASSWORD, + }, + ) + db = UserDatabase.objects.filter(db_user__osuser=self.package.osuser).get() + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), + ( + "Successfully create new {type} database {dbname} for user " "{dbuser}." + ).format(type=db.db_user.db_type, dbname=db.db_name, dbuser=db.db_user), + ) + + +class ChangeDatabaseUserPasswordTest( + HostingPackageAwareTestMixin, CustomerUserDatabaseOptionAwareTestMixin, TestCase +): + def setUp(self): + self.customer = User.objects.create_user( + username=TEST_USER, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer) + template = self._create_userdatabase_option() + database = UserDatabase.objects.create_userdatabase_with_user( + template.db_type, self.package.osuser + ) + self.dbuser = database.db_user + + def _get_url(self, dbuser): + return reverse( + "change_dbuser_password", + kwargs={"package": self.package.id, "slug": dbuser.name}, + ) + + def test_get_anonymous(self): + response = self.client.get(self._get_url(self.dbuser)) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.dbuser)) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user("test2", password=TEST_PASSWORD) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.dbuser)) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.dbuser)) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.dbuser)) + self.assertTemplateUsed(response, "userdbs/databaseuser_setpassword.html") + + def test_get_form_kwargs(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + view = ChangeDatabaseUserPassword( + request=MagicMock(), + kwargs={"package": str(self.package.pk), "slug": self.dbuser.name}, + ) + the_kwargs = view.get_form_kwargs() + self.assertIn("hostingpackage", the_kwargs) + self.assertEqual(the_kwargs["hostingpackage"], self.package) + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.dbuser)) + self.assertIn("dbuser", response.context) + self.assertEqual(response.context["dbuser"], self.dbuser) + self.assertIn("hostingpackage", response.context) + self.assertEqual(response.context["hostingpackage"], self.package) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], self.customer) + + def test_form_valid_redirect(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + self._get_url(self.dbuser), + data={"password1": TEST_PASSWORD, "password2": TEST_PASSWORD}, + ) + self.assertRedirects(response, self.package.get_absolute_url()) + + def test_form_valid_message(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post( + self._get_url(self.dbuser), + follow=True, + data={"password1": TEST_PASSWORD, "password2": TEST_PASSWORD}, + ) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), + "Successfully changed password of database user {dbuser}.".format( + dbuser=self.dbuser.name + ), + ) + + +class DeleteUserDatabaseTest( + HostingPackageAwareTestMixin, CustomerUserDatabaseOptionAwareTestMixin, TestCase +): + def setUp(self): + self.customer = User.objects.create_user( + username=TEST_USER, password=TEST_PASSWORD + ) + self.package = self._setup_hosting_package(self.customer) + template = self._create_userdatabase_option() + self.database = UserDatabase.objects.create_userdatabase_with_user( + template.db_type, self.package.osuser + ) + + def _get_url(self, userdatabase): + return reverse( + "delete_userdatabase", + kwargs={"package": self.package.id, "slug": userdatabase.db_name}, + ) + + def test_get_anonymous(self): + response = self.client.get(self._get_url(self.database)) + self.assertEqual(response.status_code, 403) + + def test_get_regular_user(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.database)) + self.assertEqual(response.status_code, 200) + + def test_get_other_regular_user(self): + User.objects.create_user("test2", password=TEST_PASSWORD) + self.client.login(username="test2", password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.database)) + self.assertEqual(response.status_code, 403) + + def test_get_staff_user(self): + User.objects.create_superuser("admin", email=TEST_EMAIL, password=TEST_PASSWORD) + self.client.login(username="admin", password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.database)) + self.assertEqual(response.status_code, 200) + + def test_get_template(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.database)) + self.assertTemplateUsed(response, "userdbs/userdatabase_confirm_delete.html") + + def test_get_context_data(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.get(self._get_url(self.database)) + self.assertIn("database", response.context) + self.assertEqual(response.context["database"], self.database) + self.assertIn("hostingpackage", response.context) + self.assertEqual(response.context["hostingpackage"], self.package) + self.assertIn("customer", response.context) + self.assertEqual(response.context["customer"], self.customer) + + def test_form_valid_redirect(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post(self._get_url(self.database)) + self.assertRedirects(response, self.package.get_absolute_url()) + + def test_form_valid_message(self): + self.client.login(username=TEST_USER, password=TEST_PASSWORD) + response = self.client.post(self._get_url(self.database), follow=True) + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual(str(messages[0]), "Database deleted.") diff --git a/gnuviechadmin/userdbs/urls.py b/gnuviechadmin/userdbs/urls.py index c31cf4d..6aee543 100644 --- a/gnuviechadmin/userdbs/urls.py +++ b/gnuviechadmin/userdbs/urls.py @@ -4,7 +4,7 @@ This module defines the URL patterns for user database views. """ from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( AddUserDatabase, @@ -12,12 +12,11 @@ from .views import ( DeleteUserDatabase, ) -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^(?P\d+)/create$', AddUserDatabase.as_view(), name='add_userdatabase'), url(r'^(?P\d+)/(?P[\w0-9]+)/setpassword', ChangeDatabaseUserPassword.as_view(), name='change_dbuser_password'), url(r'^(?P\d+)/(?P[\w0-9]+)/delete', DeleteUserDatabase.as_view(), name='delete_userdatabase'), -) +] diff --git a/gnuviechadmin/userdbs/views.py b/gnuviechadmin/userdbs/views.py index e8794d6..d16eb5a 100644 --- a/gnuviechadmin/userdbs/views.py +++ b/gnuviechadmin/userdbs/views.py @@ -4,6 +4,7 @@ This module defines views for user database handling. """ from __future__ import absolute_import, unicode_literals +from django.core.exceptions import SuspiciousOperation from django.shortcuts import redirect from django.utils.translation import ugettext as _ from django.views.generic.edit import ( @@ -48,6 +49,9 @@ class AddUserDatabase( db_user__db_type=opt['db_type']).count() if dbs_of_type < opt['number']: retval.append((opt['db_type'], DB_TYPES[opt['db_type']])) + if len(retval) < 1: + raise SuspiciousOperation( + _("The hosting package has no database products assigned.")) return retval def get_form_kwargs(self): @@ -61,7 +65,7 @@ class AddUserDatabase( messages.success( self.request, _('Successfully create new {type} database {dbname} for user ' - '{dbuser}').format( + '{dbuser}.').format( type=userdatabase.db_user.db_type, dbname=userdatabase.db_name, dbuser=userdatabase.db_user) ) @@ -97,7 +101,7 @@ class ChangeDatabaseUserPassword( db_user = form.save() messages.success( self.request, - _('Successfully changed password of database user {dbuser}' + _('Successfully changed password of database user {dbuser}.' ).format(dbuser=db_user.name) ) return redirect(self.get_hosting_package()) @@ -126,6 +130,6 @@ class DeleteUserDatabase( def get_success_url(self): messages.success( self.request, - _('Database deleted'), + _('Database deleted.'), ) return self.get_hosting_package().get_absolute_url() diff --git a/gnuviechadmin/websites/forms.py b/gnuviechadmin/websites/forms.py index fa63538..94e52d1 100644 --- a/gnuviechadmin/websites/forms.py +++ b/gnuviechadmin/websites/forms.py @@ -5,7 +5,7 @@ This module defines form classes for website editing. from __future__ import absolute_import, unicode_literals from django import forms -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils.translation import ugettext as _ from crispy_forms.bootstrap import AppendedText diff --git a/gnuviechadmin/websites/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/websites/locale/de/LC_MESSAGES/django.po index c0c1851..ad09e6f 100644 --- a/gnuviechadmin/websites/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/websites/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: websites gnuviechadmin app\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-27 18:55+0100\n" +"POT-Creation-Date: 2016-01-29 11:04+0100\n" "PO-Revision-Date: 2015-01-27 19:00+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" diff --git a/gnuviechadmin/websites/migrations/0001_initial.py b/gnuviechadmin/websites/migrations/0001_initial.py index bfbb1a5..51dbee9 100644 --- a/gnuviechadmin/websites/migrations/0001_initial.py +++ b/gnuviechadmin/websites/migrations/0001_initial.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('osusers', '0004_auto_20150104_1751'), ('domains', '0002_auto_20150124_1909'), @@ -15,11 +14,19 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Website', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('subdomain', models.CharField(max_length=64, verbose_name='sub domain')), - ('wildcard', models.BooleanField(default=False, verbose_name='wildcard')), - ('domain', models.ForeignKey(verbose_name='domain', to='domains.HostingDomain')), - ('osuser', models.ForeignKey(verbose_name='operating system user', to='osusers.User')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('subdomain', models.CharField( + max_length=64, verbose_name='sub domain')), + ('wildcard', models.BooleanField( + default=False, verbose_name='wildcard')), + ('domain', models.ForeignKey( + verbose_name='domain', to='domains.HostingDomain', + on_delete=models.CASCADE)), + ('osuser', models.ForeignKey( + verbose_name='operating system user', to='osusers.User', + on_delete=models.CASCADE)), ], options={ 'verbose_name': 'website', @@ -29,6 +36,6 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='website', - unique_together=set([('domain', 'subdomain')]), + unique_together={('domain', 'subdomain')}, ), ] diff --git a/gnuviechadmin/websites/models.py b/gnuviechadmin/websites/models.py index c58e904..a0b2168 100644 --- a/gnuviechadmin/websites/models.py +++ b/gnuviechadmin/websites/models.py @@ -34,9 +34,10 @@ class Website(models.Model): subdomain = models.CharField( _('sub domain'), max_length=64) osuser = models.ForeignKey( - OsUser, verbose_name=_('operating system user')) + OsUser, verbose_name=_('operating system user'), + on_delete=models.CASCADE) domain = models.ForeignKey( - HostingDomain, verbose_name=_('domain')) + HostingDomain, models.CASCADE, verbose_name=_('domain')) wildcard = models.BooleanField(_('wildcard'), default=False) class Meta: diff --git a/gnuviechadmin/websites/urls.py b/gnuviechadmin/websites/urls.py index 4bcd5e0..1fba405 100644 --- a/gnuviechadmin/websites/urls.py +++ b/gnuviechadmin/websites/urls.py @@ -4,7 +4,7 @@ This module defines the URL patterns for website related views. """ from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import ( AddWebsite, @@ -12,10 +12,9 @@ from .views import ( ) -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^(?P\d+)/(?P[\w0-9.-]+)/create$', AddWebsite.as_view(), name='add_website'), url(r'^(?P\d+)/(?P[\w0-9.-]+)/(?P\d+)/delete$', DeleteWebsite.as_view(), name='delete_website'), -) +] diff --git a/gnuviechadmin/webtasks/tasks.py b/gnuviechadmin/webtasks/tasks.py index e7e1bf9..4abd4af 100644 --- a/gnuviechadmin/webtasks/tasks.py +++ b/gnuviechadmin/webtasks/tasks.py @@ -47,6 +47,7 @@ def enable_web_vhost(sitename): """ + @shared_task def delete_web_vhost_config(sitename): """ @@ -70,6 +71,7 @@ def create_web_php_fpm_pool_config(username): """ + @shared_task def delete_web_php_fpm_pool_config(username): """ diff --git a/gva.sh b/gva.sh new file mode 100755 index 0000000..4b0f3c2 --- /dev/null +++ b/gva.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +DB_HOST="${GVA_PGSQL_HOST:-db}" +DB_PORT="${GVA_PGSQL_PORT:-5432}" +DB_USER="${GVA_PGSQL_USER:-gnuviechadmin}" +DB_NAME="${GVA_PGSQL_DATABASE:-gnuviechadmin}" + +until pg_isready -q -h "${DB_HOST}" -p "${DB_PORT}" -U "${PG_USER}" -d "${DB_NAME}" +do + echo -n "." + sleep 1 +done + +echo " db is ready" + +. /home/gva/gva-venv/bin/activate +cd /srv/gva/gnuviechadmin +python3 manage.py compilemessages +python3 manage.py collectstatic --noinput +python3 manage.py migrate --noinput +python3 manage.py runserver 0.0.0.0:8000 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d119713..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -# This file is here because many Platforms as a Service look for -# requirements.txt in the root directory of a project. --r requirements/production.txt diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index 8d92d6b..0000000 --- a/requirements/base.txt +++ /dev/null @@ -1,18 +0,0 @@ -Django==1.7.4 -bpython==0.13.2 -django-braces==1.4.0 -django-model-utils==2.2 -django-crispy-forms==1.4.0 -logutils==0.3.3 -psycopg2==2.5.4 -passlib==1.6.2 -celery==3.1.17 -billiard==3.3.0.23 -kombu==3.0.37 -pytz==2019.1 -pyaml==14.12.10 -django-allauth==0.19.0 -oauthlib==0.7.2 -python-openid==2.2.5 -requests==2.5.1 -requests-oauthlib==0.4.2 diff --git a/requirements/local.txt b/requirements/local.txt deleted file mode 100644 index 97a9db7..0000000 --- a/requirements/local.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Local development dependencies go here --r base.txt -coverage==3.7.1 -mock==1.0.1 -django-debug-toolbar==1.2.2 -sqlparse==0.1.14 -Sphinx==1.2.3 -releases==0.7.0 diff --git a/requirements/production.txt b/requirements/production.txt deleted file mode 100644 index 696dc7c..0000000 --- a/requirements/production.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Pro-tip: Try not to put anything here. There should be no dependency in -# production that isn't in development. --r base.txt diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index 8bf1098..0000000 --- a/requirements/test.txt +++ /dev/null @@ -1,4 +0,0 @@ -# Test dependencies go here. --r base.txt -coverage==3.7.1 -mock==1.0.1