diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 590b8d7d..ceb4c4e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,3 +41,13 @@ Sites et doc cools ------------------ [Classy Class-Based Views](http://ccbv.co.uk/projects/Django/1.8/) + +Helpers: + +`./manage.py makemessages --ignore "env/*" -e py,jinja` + +`for f in $(find . -name "*.py" ! -path "*migration*" ! -path "./env/*" ! -path "./doc/*"); do cat ./doc/header "$f" > /tmp/temp && mv /tmp/temp "$f"; done` + + + + diff --git a/LICENSE b/LICENSE index 4e272d3f..94a9ed02 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,674 @@ -The MIT License (MIT) + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Copyright (c) 2016 Skia + 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. -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: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU General Public License is a free, copyleft license for +software and other kinds of works. -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. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/LICENSE.old b/LICENSE.old new file mode 100644 index 00000000..4e272d3f --- /dev/null +++ b/LICENSE.old @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Skia + +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/README.md b/README.md index 739141fa..d1cbea94 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,14 @@ generate a complete HTML documentation that will be available in the *./doc/html See requirements.txt You may need to install some dev libraries like `libmysqlclient-dev`, `libssl-dev`, `libjpeg-dev`, or `zlib1g-dev` to install all the -requiered dependancies with pip. You may also need `mysql-client`. +requiered dependancies with pip. You may also need `mysql-client`. Don't also forget `python3-dev` if you don't have it +already. + +You can check all of them with: + +``` +sudo apt install libmysqlclient-dev libssl-dev libjpeg-dev zlib1g-dev python3-dev libffi-dev +``` The development is done with sqlite, but it is advised to set a more robust DBMS for production (Postgresql for example) @@ -49,6 +56,12 @@ Finally, when building a class based view, which is highly advised, you just hav CanEditMixin, or CanViewMixin, which are located in core.views. Your view will then be protected using either the appropriate group fields, or the right method to check user permissions. +#### Counting the number of line of code + +``` +# apt install cloc +$ cloc --exclude-dir=doc,env . +``` diff --git a/accounting/__init__.py b/accounting/__init__.py index e69de29b..0a9419f8 100644 --- a/accounting/__init__.py +++ b/accounting/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/accounting/admin.py b/accounting/admin.py index 43fc1caa..67409ea7 100644 --- a/accounting/admin.py +++ b/accounting/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin from accounting.models import * diff --git a/accounting/migrations/0005_auto_20170324_0917.py b/accounting/migrations/0005_auto_20170324_0917.py new file mode 100644 index 00000000..d97d82f0 --- /dev/null +++ b/accounting/migrations/0005_auto_20170324_0917.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounting', '0004_auto_20161005_1505'), + ] + + operations = [ + migrations.AlterField( + model_name='operation', + name='remark', + field=models.CharField(null=True, max_length=128, blank=True, verbose_name='comment'), + ), + ] diff --git a/accounting/models.py b/accounting/models.py index 7338e907..d408ecfb 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.core.urlresolvers import reverse from django.core.exceptions import ValidationError from django.core import validators @@ -45,6 +69,32 @@ class Company(models.Model): class Meta: verbose_name = _("company") + def is_owned_by(self, user): + """ + Method to see if that object can be edited by the given user + """ + if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + return True + return False + + def can_be_edited_by(self, user): + """ + Method to see if that object can be edited by the given user + """ + for club in user.memberships.filter(end_date=None).all(): + if club and club.role == settings.SITH_CLUB_ROLES_ID['Treasurer']: + return True + return False + + def can_be_viewed_by(self, user): + """ + Method to see if that object can be viewed by the given user + """ + for club in user.memberships.filter(end_date=None).all(): + if club and club.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']: + return True + return False + def get_absolute_url(self): return reverse('accounting:co_edit', kwargs={'co_id': self.id}) @@ -71,7 +121,7 @@ class BankAccount(models.Model): if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True m = self.club.get_membership_for(user) - if m is not None and m.role >= 7: + if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']: return True return False @@ -103,7 +153,7 @@ class ClubAccount(models.Model): Method to see if that object can be edited by the given user """ m = self.club.get_membership_for(user) - if m and m.role == 7: + if m and m.role == settings.SITH_CLUB_ROLES_ID['Treasurer']: return True return False @@ -112,7 +162,7 @@ class ClubAccount(models.Model): Method to see if that object can be viewed by the given user """ m = self.club.get_membership_for(user) - if m and m.role >= 7: + if m and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']: return True return False @@ -161,6 +211,16 @@ class GeneralJournal(models.Model): return True return False + def can_be_edited_by(self, user): + """ + Method to see if that object can be edited by the given user + """ + if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + return True + if self.club_account.can_be_edited_by(user): + return True + return False + def can_be_viewed_by(self, user): return self.club_account.can_be_edited_by(user) @@ -192,7 +252,7 @@ class Operation(models.Model): journal = models.ForeignKey(GeneralJournal, related_name="operations", null=False, verbose_name=_("journal")) amount = CurrencyField(_('amount')) date = models.DateField(_('date')) - remark = models.CharField(_('comment'), max_length=128) + remark = models.CharField(_('comment'), max_length=128, null=True, blank=True) mode = models.CharField(_('payment method'), max_length=255, choices=settings.SITH_ACCOUNTING_PAYMENT_METHOD) cheque_number = models.CharField(_('cheque number'), max_length=32, default="", null=True, blank=True) invoice = models.ForeignKey(SithFile, related_name='operations', verbose_name=_("invoice"), null=True, blank=True) @@ -265,7 +325,7 @@ class Operation(models.Model): if self.journal.closed: return False m = self.journal.club_account.club.get_membership_for(user) - if m is not None and m.role >= 7: + if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']: return True return False @@ -273,7 +333,12 @@ class Operation(models.Model): """ Method to see if that object can be edited by the given user """ - if self.is_owned_by(user): + if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + return True + if self.journal.closed: + return False + m = self.journal.club_account.club.get_membership_for(user) + if m is not None and m.role == settings.SITH_CLUB_ROLES_ID['Treasurer']: return True return False diff --git a/accounting/templates/accounting/bank_account_details.jinja b/accounting/templates/accounting/bank_account_details.jinja index 076d2753..cd968322 100644 --- a/accounting/templates/accounting/bank_account_details.jinja +++ b/accounting/templates/accounting/bank_account_details.jinja @@ -11,7 +11,7 @@


{% trans %}Bank account: {% endtrans %}{{ object.name }}

- {% if user.is_root and not object.club_accounts.exists() %} + {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %} {% trans %}Delete{% endtrans %} {% endif %}

{% trans %}Infos{% endtrans %}

@@ -24,6 +24,9 @@ {% for c in object.club_accounts.all() %}
  • {{ c }} - {% trans %}Edit{% endtrans %} + {% if c.journals.count() == 0 %} + - {% trans %}Delete{% endtrans %} + {% endif %}
  • {% endfor %} diff --git a/accounting/templates/accounting/club_account_details.jinja b/accounting/templates/accounting/club_account_details.jinja index 333e3081..08f22c2d 100644 --- a/accounting/templates/accounting/club_account_details.jinja +++ b/accounting/templates/accounting/club_account_details.jinja @@ -15,7 +15,9 @@ {% if user.is_root and not object.journals.exists() %} {% trans %}Delete{% endtrans %} {% endif %} + {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}

    {% trans %}New label{% endtrans %}

    + {% endif %}

    {% trans %}Label list{% endtrans %}

    {% if not object.has_open_journal() %}

    {% trans %}New journal{% endtrans %}

    @@ -52,7 +54,11 @@ {% trans %}No{% endtrans %} {% endif %} {% trans %}View{% endtrans %} - {% trans %}Edit{% endtrans %} + {% trans %}Edit{% endtrans %} + {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %} + {% trans %}Delete{% endtrans %} + {% endif %} + {% endfor %} diff --git a/accounting/templates/accounting/co_list.jinja b/accounting/templates/accounting/co_list.jinja index 0cfbca70..e40ee2d8 100644 --- a/accounting/templates/accounting/co_list.jinja +++ b/accounting/templates/accounting/co_list.jinja @@ -5,7 +5,9 @@ {% endblock %} {% block content %} +{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) or user.is_root %}

    {% trans %}Create new company{% endtrans %}

    +{% endif %}
    diff --git a/accounting/templates/accounting/journal_details.jinja b/accounting/templates/accounting/journal_details.jinja index e38dfe7d..d2fb21cb 100644 --- a/accounting/templates/accounting/journal_details.jinja +++ b/accounting/templates/accounting/journal_details.jinja @@ -78,9 +78,11 @@ {% endif %} diff --git a/accounting/templates/accounting/label_list.jinja b/accounting/templates/accounting/label_list.jinja index 9d35701b..9841fba6 100644 --- a/accounting/templates/accounting/label_list.jinja +++ b/accounting/templates/accounting/label_list.jinja @@ -12,13 +12,18 @@


    {% trans %}Back to club account{% endtrans %}

    + {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}

    {% trans %}New label{% endtrans %}

    + {% endif %} {% if object.labels.all() %}

    {% trans %}Label list{% endtrans %}

    diff --git a/accounting/tests.py b/accounting/tests.py index 93fa9f79..d54efe34 100644 --- a/accounting/tests.py +++ b/accounting/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import Client, TestCase from django.core.urlresolvers import reverse from django.contrib.auth.models import Group diff --git a/accounting/urls.py b/accounting/urls.py index 3255cb9d..7085fc30 100644 --- a/accounting/urls.py +++ b/accounting/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from accounting.views import * @@ -26,6 +50,7 @@ urlpatterns = [ url(r'^journal/create$', JournalCreateView.as_view(), name='journal_new'), url(r'^journal/(?P[0-9]+)$', JournalDetailView.as_view(), name='journal_details'), url(r'^journal/(?P[0-9]+)/edit$', JournalEditView.as_view(), name='journal_edit'), + url(r'^journal/(?P[0-9]+)/delete$', JournalDeleteView.as_view(), name='journal_delete'), url(r'^journal/(?P[0-9]+)/statement/nature$', JournalNatureStatementView.as_view(), name='journal_nature_statement'), url(r'^journal/(?P[0-9]+)/statement/person$', JournalPersonStatementView.as_view(), name='journal_person_statement'), url(r'^journal/(?P[0-9]+)/statement/accounting$', JournalAccountingStatementView.as_view(), name='journal_accounting_statement'), diff --git a/accounting/views.py b/accounting/views.py index a561d614..c70de381 100644 --- a/accounting/views.py +++ b/accounting/views.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.views.generic import ListView, DetailView, RedirectView from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView from django.shortcuts import render @@ -230,6 +254,21 @@ class JournalEditView(CanEditMixin, UpdateView): fields = ['name', 'start_date', 'end_date', 'club_account', 'closed'] template_name = 'core/edit.jinja' +class JournalDeleteView(CanEditPropMixin, DeleteView): + """ + Delete a club account (for the admins) + """ + model = GeneralJournal + pk_url_kwarg = "j_id" + template_name = 'core/delete_confirm.jinja' + success_url = reverse_lazy('accounting:club_details') + + def dispatch(self, request, *args, **kwargs): + self.object = self.get_object() + if self.object.operations.count() == 0: + return super(JournalDeleteView, self).dispatch(request, *args, **kwargs) + else: + raise PermissionDenied # Operation views @@ -385,7 +424,7 @@ class OperationPDFView(CanViewMixin, DetailView): target = self.object.target.get_display_name() response = HttpResponse(content_type='application/pdf') - response['Content-Disposition'] = 'attachment; filename="op-%d(%s_on_%s).pdf"' %(num, ti, club_name) + response['Content-Disposition'] = 'filename="op-%d(%s_on_%s).pdf"' %(num, ti, club_name) p = canvas.Canvas(response) p.setFont('DejaVu', 12) @@ -401,8 +440,7 @@ class OperationPDFView(CanViewMixin, DetailView): label = Table(labelStr, colWidths=[150], rowHeights=[20]) label.setStyle(TableStyle([ - ('ALIGN',(0,0),(-1,-1),'CENTER'), - ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ('ALIGN',(0,0),(-1,-1),'RIGHT'), ])) w, h = label.wrapOn(label, 0, 0) label.drawOn(p, width-180, height) diff --git a/api/__init__.py b/api/__init__.py index e69de29b..0a9419f8 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/api/admin.py b/api/admin.py index 8c38f3f3..84bb227c 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin # Register your models here. diff --git a/api/models.py b/api/models.py index 71a83623..5877743e 100644 --- a/api/models.py +++ b/api/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.db import models # Create your models here. diff --git a/api/tests.py b/api/tests.py index 7ce503c2..b8fbbe1e 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase # Create your tests here. diff --git a/api/urls.py b/api/urls.py index 408cc562..299d04a6 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from api.views import * diff --git a/api/views/__init__.py b/api/views/__init__.py index 52da6d66..a0676bbe 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from rest_framework.response import Response from rest_framework import viewsets from django.core.exceptions import PermissionDenied diff --git a/api/views/api.py b/api/views/api.py index 37557587..a7d098e3 100644 --- a/api/views/api.py +++ b/api/views/api.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from rest_framework.response import Response from rest_framework.decorators import api_view, renderer_classes from rest_framework.renderers import StaticHTMLRenderer @@ -6,14 +30,14 @@ from rest_framework.views import APIView from core.templatetags.renderer import markdown -@api_view(['GET']) +@api_view(['POST']) @renderer_classes((StaticHTMLRenderer,)) def RenderMarkdown(request): """ Render Markdown """ try: - data = markdown(request.GET['text']) + data = markdown(request.POST['text']) except: data = 'Error' return Response(data) diff --git a/api/views/club.py b/api/views/club.py index 72ea1e32..fae04fc3 100644 --- a/api/views/club.py +++ b/api/views/club.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from rest_framework import serializers from club.models import Club diff --git a/api/views/counter.py b/api/views/counter.py index 69a98660..2df78e73 100644 --- a/api/views/counter.py +++ b/api/views/counter.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from rest_framework import serializers from rest_framework.response import Response from rest_framework.decorators import list_route diff --git a/api/views/group.py b/api/views/group.py index b9ff625c..ec46555e 100644 --- a/api/views/group.py +++ b/api/views/group.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from rest_framework import serializers from core.models import RealGroup diff --git a/api/views/launderette.py b/api/views/launderette.py index c3e586c0..fb4db382 100644 --- a/api/views/launderette.py +++ b/api/views/launderette.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from rest_framework import serializers from rest_framework.response import Response from rest_framework.decorators import list_route diff --git a/api/views/user.py b/api/views/user.py index f932a7f4..cb4d6a24 100644 --- a/api/views/user.py +++ b/api/views/user.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + import datetime from rest_framework import serializers diff --git a/club/__init__.py b/club/__init__.py index e69de29b..0a9419f8 100644 --- a/club/__init__.py +++ b/club/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/club/admin.py b/club/admin.py index 4d8a1bf5..c9ae46a4 100644 --- a/club/admin.py +++ b/club/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin from club.models import Club, Membership diff --git a/club/migrations/0007_auto_20170324_0917.py b/club/migrations/0007_auto_20170324_0917.py new file mode 100644 index 00000000..dd215472 --- /dev/null +++ b/club/migrations/0007_auto_20170324_0917.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('club', '0006_auto_20161229_0040'), + ] + + operations = [ + migrations.AlterModelOptions( + name='club', + options={'ordering': ['name', 'unix_name']}, + ), + ] diff --git a/club/models.py b/club/models.py index 3b02be0f..491d4e70 100644 --- a/club/models.py +++ b/club/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.db import models from django.core import validators from django.conf import settings @@ -38,6 +62,9 @@ class Club(models.Model): home = models.OneToOneField(SithFile, related_name='home_of_club', verbose_name=_("home"), null=True, blank=True, on_delete=models.SET_NULL) + class Meta: + ordering = ['name', 'unix_name'] + def check_loop(self): """Raise a validation error when a loop is found within the parent list""" objs = [] @@ -122,7 +149,7 @@ class Club(models.Model): sub = User.objects.filter(pk=user.pk).first() if sub is None: return False - return sub.is_subscribed() + return sub.is_subscribed def get_membership_for(self, user): """ @@ -151,7 +178,7 @@ class Membership(models.Model): def clean(self): sub = User.objects.filter(pk=self.user.pk).first() - if sub is None or not sub.is_subscribed(): + if sub is None or not sub.is_subscribed: raise ValidationError(_('User must be subscriber to take part to a club')) if Membership.objects.filter(user=self.user).filter(club=self.club).filter(end_date=None).exists(): raise ValidationError(_('User is already member of that club')) diff --git a/club/templates/club/club_sellings.jinja b/club/templates/club/club_sellings.jinja index 21c061d3..c5da0e61 100644 --- a/club/templates/club/club_sellings.jinja +++ b/club/templates/club/club_sellings.jinja @@ -11,7 +11,8 @@

    {% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}
    -{% trans %}Total: {% endtrans %}{{ total }} € +{% trans %}Total: {% endtrans %}{{ total }} €
    +{% trans %}Benefit: {% endtrans %}{{ benefit }} €

    - + {% if o.journal.club_account.bank_account.name != "AE TI" and o.journal.club_account.bank_account.name != "TI" or user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} {% if not o.journal.closed %} {% trans %}Edit{% endtrans %} {% endif %} + {% endif %} {% trans %}Generate{% endtrans %}
    diff --git a/club/templates/club/club_tools.jinja b/club/templates/club/club_tools.jinja index f18c3698..f28e4978 100644 --- a/club/templates/club/club_tools.jinja +++ b/club/templates/club/club_tools.jinja @@ -6,6 +6,7 @@

    {% trans %}Communication:{% endtrans %}

    {% trans %}Counters:{% endtrans %}

      diff --git a/club/tests.py b/club/tests.py index 8a3004f3..88020b14 100644 --- a/club/tests.py +++ b/club/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase from django.core.urlresolvers import reverse from django.core.management import call_command diff --git a/club/urls.py b/club/urls.py index bc0ccc3d..d141d0bb 100644 --- a/club/urls.py +++ b/club/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from club.views import * diff --git a/club/views.py b/club/views.py index 9e217fa0..8d985964 100644 --- a/club/views.py +++ b/club/views.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django import forms from django.shortcuts import render from django.views.generic import ListView, DetailView, TemplateView @@ -133,6 +157,8 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, UpdateView): form.instance = Membership.objects.filter(club=self.object).filter(user=form.data.get('user')).filter(end_date=None).first() if form.instance is None: # Instanciate a new membership form.instance = Membership(club=self.object, user=self.request.user) + if not self.request.user.is_root: + form.fields.pop('start_date', None) # form.initial = {'user': self.request.user} # form._user = self.request.user return form @@ -203,6 +229,7 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView): kwargs['result'] = qs.all().order_by('-id') kwargs['total'] = sum([s.quantity * s.unit_price for s in qs.all()]) kwargs['total_quantity'] = sum([s.quantity for s in qs.all()]) + kwargs['benefit'] = kwargs['total'] - sum([s.product.purchase_price for s in qs.exclude(product=None)]) else: kwargs['result'] = qs[:0] kwargs['form'] = form @@ -222,8 +249,11 @@ class ClubSellingCSVView(ClubSellingView): kwargs = self.get_context_data(**kwargs) writer = csv.writer(response, delimiter=";", lineterminator='\n', quoting=csv.QUOTE_ALL) + writer.writerow([_t('Quantity'), kwargs['total_quantity']]) + writer.writerow([_t('Total'), kwargs['total']]) + writer.writerow([_t('Benefit'), kwargs['benefit']]) writer.writerow([_t('Date'),_t('Counter'),_t('Barman'),_t('Customer'),_t('Label'), - _t('Quantity'), _t('Total'),_t('Payment method')]) + _t('Quantity'), _t('Total'),_t('Payment method'), _t('Selling price'), _t('Purchase price'), _t('Benefit')]) for o in kwargs['result']: row = [o.date, o.counter] if o.seller: @@ -234,6 +264,11 @@ class ClubSellingCSVView(ClubSellingView): else: row.append('') row = row +[o.label, o.quantity, o.quantity * o.unit_price, o.get_payment_method_display()] + if o.product: + row.append(o.product.selling_price) + row.append(o.product.purchase_price) + row.append(o.product.selling_price - o.product.purchase_price) + else: row = row + ['', '', ''] writer.writerow(row) return response diff --git a/com/__init__.py b/com/__init__.py index e69de29b..0a9419f8 100644 --- a/com/__init__.py +++ b/com/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/com/admin.py b/com/admin.py index b74b7a48..91e759db 100644 --- a/com/admin.py +++ b/com/admin.py @@ -1,9 +1,34 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin from com.models import * admin.site.register(Sith) admin.site.register(News) +admin.site.register(Weekmail) diff --git a/com/migrations/0003_auto_20170115_2300.py b/com/migrations/0003_auto_20170115_2300.py new file mode 100644 index 00000000..660c7635 --- /dev/null +++ b/com/migrations/0003_auto_20170115_2300.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('club', '0006_auto_20161229_0040'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('com', '0002_news_newsdate'), + ] + + operations = [ + migrations.CreateModel( + name='Weekmail', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), + ('title', models.CharField(max_length=64, verbose_name='title', blank=True)), + ('intro', models.TextField(verbose_name='intro', blank=True)), + ('joke', models.TextField(verbose_name='joke', blank=True)), + ('protip', models.TextField(verbose_name='protip', blank=True)), + ('conclusion', models.TextField(verbose_name='conclusion', blank=True)), + ('sent', models.BooleanField(verbose_name='sent', default=False)), + ], + options={ + 'ordering': ['-id'], + }, + ), + migrations.CreateModel( + name='WeekmailArticle', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), + ('title', models.CharField(max_length=64, verbose_name='title')), + ('content', models.TextField(verbose_name='content')), + ('rank', models.IntegerField(verbose_name='rank', default=-1)), + ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='author', related_name='owned_weekmail_articles')), + ('club', models.ForeignKey(to='club.Club', verbose_name='club', related_name='weekmail_articles')), + ('weekmail', models.ForeignKey(to='com.Weekmail', verbose_name='weekmail', related_name='articles', null=True)), + ], + ), + migrations.AddField( + model_name='sith', + name='weekmail_destinations', + field=models.TextField(verbose_name='weekmail destinations', default=''), + ), + ] diff --git a/com/models.py b/com/models.py index 6c48e332..74b30529 100644 --- a/com/models.py +++ b/com/models.py @@ -1,9 +1,36 @@ -from django.db import models +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.shortcuts import render +from django.db import models, transaction from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse_lazy, reverse from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.core.exceptions import ValidationError -from core.models import User +from core.models import User, Preferences from club.models import Club class Sith(models.Model): @@ -11,6 +38,7 @@ class Sith(models.Model): alert_msg = models.TextField(_("alert message"), default="", blank=True) info_msg = models.TextField(_("info message"), default="", blank=True) index_page = models.TextField(_("index page"), default="", blank=True) + weekmail_destinations = models.TextField(_("weekmail destinations"), default="") def is_owned_by(self, user): return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) @@ -64,3 +92,63 @@ class NewsDate(models.Model): def __str__(self): return "%s: %s - %s" % (self.news.title, self.start_date, self.end_date) + +class Weekmail(models.Model): + """ + The weekmail class + """ + title = models.CharField(_("title"), max_length=64, blank=True) + intro = models.TextField(_("intro"), blank=True) + joke = models.TextField(_("joke"), blank=True) + protip = models.TextField(_("protip"), blank=True) + conclusion = models.TextField(_("conclusion"), blank=True) + sent = models.BooleanField(_("sent"), default=False) + + class Meta: + ordering = ['-id'] + + def send(self): + dest = [i[0] for i in Preferences.objects.filter(receive_weekmail=True).values_list('user__email')] + with transaction.atomic(): + email = EmailMultiAlternatives( + subject=self.title, + body=self.render_text(), + from_email=settings.DEFAULT_FROM_EMAIL, + to=Sith.objects.first().weekmail_destinations.split(' '), + bcc=dest, + ) + email.attach_alternative(self.render_html(), "text/html") + email.send() + self.sent = True + self.save() + Weekmail().save() + + def render_text(self): + return render(None, "com/weekmail_renderer_text.jinja", context={ + 'weekmail': self, + }).content.decode('utf-8') + + def render_html(self): + return render(None, "com/weekmail_renderer_html.jinja", context={ + 'weekmail': self, + }).content.decode('utf-8') + + def __str__(self): + return "Weekmail %s (sent: %s) - %s" % (self.id, self.sent, self.title) + + def is_owned_by(self, user): + return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) + +class WeekmailArticle(models.Model): + weekmail = models.ForeignKey(Weekmail, related_name="articles", verbose_name=_("weekmail"), null=True) + title = models.CharField(_("title"), max_length=64) + content = models.TextField(_("content")) + author = models.ForeignKey(User, related_name="owned_weekmail_articles", verbose_name=_("author")) + club = models.ForeignKey(Club, related_name="weekmail_articles", verbose_name=_("club")) + rank = models.IntegerField(_('rank'), default=-1) + + def is_owned_by(self, user): + return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) + + def __str__(self): + return "%s - %s (%s)" % (self.title, self.author, self.club) diff --git a/com/templates/com/news_edit.jinja b/com/templates/com/news_edit.jinja index 1fa00414..ea4fb73a 100644 --- a/com/templates/com/news_edit.jinja +++ b/com/templates/com/news_edit.jinja @@ -11,16 +11,6 @@ {% block content %} {% if 'preview' in request.POST.keys() %} -
      -

      {{ form.instance.title }}

      -

      - {{ form.instance.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} - - {{ form.instance.dates.first().end_date|localtime|time(DATETIME_FORMAT) }} -

      -

      {{ form.instance.club or "Club" }}

      -

      {{ form.instance.summary|markdown }}

      -
      -

      {{ form.instance.title }}

      diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index af11e362..bd714fd3 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -54,8 +54,10 @@ section.news_event:nth-of-type(even) {

      {{ news.title }}

      - {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} - - {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }} + {{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} + {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} - + {{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} + {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}

      {{ news.club }}

      {{ news.summary|markdown }}

      diff --git a/com/templates/com/weekmail.jinja b/com/templates/com/weekmail.jinja new file mode 100644 index 00000000..f524fc3b --- /dev/null +++ b/com/templates/com/weekmail.jinja @@ -0,0 +1,79 @@ +{% extends "core/base.jinja" %} +{% from 'core/macros.jinja' import user_profile_link %} + +{% block title %} +{% trans %}Weekmail{% endtrans %} +{% endblock %} + +{% block content %} +

      {% trans %}Weekmail{% endtrans %} {{ object.id }}

      +

      {% trans %}Preview{% endtrans %}

      +

      {% trans %}Send{% endtrans %}

      +

      {% trans %}New article{% endtrans %}

      +

      {% trans %}Articles in no weekmail yet{% endtrans %}

      +
    + + + + + + + + + + + {% for a in orphans.all() %} + + + + + + + + {% endfor %} + +
    {% trans %}Author{% endtrans %}{% trans %}Club{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}Content{% endtrans %}{% trans %}Actions{% endtrans %}
    {{ user_profile_link(a.author) }}{{ a.club }}{{ a.title }}{{ a.content|markdown }} + {% trans %}Edit{% endtrans %} | + {% trans %}Delete{% endtrans %} | + {% trans %}Add to weekmail{% endtrans %} | + {% trans %}Up{% endtrans %} | + {% trans %}Down{% endtrans %} +
    +

    {% trans %}Articles included the next weekmail{% endtrans %}

    + + + + + + + + + + + + {% for a in object.articles.order_by('rank') %} + + + + + + + + {% endfor %} + +
    {% trans %}Author{% endtrans %}{% trans %}Club{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}Content{% endtrans %}{% trans %}Actions{% endtrans %}
    {{ user_profile_link(a.author) }}{{ a.club }}{{ a.title }}{{ a.content|markdown }} + {% trans %}Edit{% endtrans %} | + {% trans %}Delete{% endtrans %} | + {% trans %}Delete from weekmail{% endtrans %} | + {% trans %}Up{% endtrans %} | + {% trans %}Down{% endtrans %} +
    +
    + {% csrf_token %} + {{ form.as_p() }} +

    +
    +{% endblock %} + + + diff --git a/com/templates/com/weekmail_preview.jinja b/com/templates/com/weekmail_preview.jinja new file mode 100644 index 00000000..0f160419 --- /dev/null +++ b/com/templates/com/weekmail_preview.jinja @@ -0,0 +1,25 @@ +{% extends "core/base.jinja" %} +{% from 'core/macros.jinja' import user_profile_link %} + +{% block title %} +{{ weekmail.title }} +{% endblock %} + +{% block content %} +{% trans %}Back{% endtrans %} +{% if request.GET['send'] %} +

    {% trans %}Are you sure you want to send this weekmail?{% endtrans %}

    +{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %} +

    {% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}

    +{% endif %} +
    + {% csrf_token %} + +
    +{% endif %} +
    +{{ weekmail_rendered|safe }} +{% endblock %} + + + diff --git a/com/templates/com/weekmail_renderer_html.jinja b/com/templates/com/weekmail_renderer_html.jinja new file mode 100644 index 00000000..37a9bc50 --- /dev/null +++ b/com/templates/com/weekmail_renderer_html.jinja @@ -0,0 +1,46 @@ + +
    +
    +

    {{ weekmail.title }}

    + + {% if weekmail.intro %} +

    {% trans %}Intro{% endtrans %}

    + {{ weekmail.intro|markdown }} + {% endif %} + +

    {% trans %}Table of content{% endtrans %}

    +
      + {% for a in weekmail.articles.all() %} +
    • [{{ a.club }}] {{ a.title }}
    • + {%- endfor %} +
    + + {%- for a in weekmail.articles.all() %} +

    [{{ a.club }}] {{ a.title }}

    + {{ a.content|markdown }} + {%- endfor -%} + + {%- if weekmail.joke %} +

    {% trans %}Joke{% endtrans %}

    + {{ weekmail.joke|markdown }} + {% endif -%} + + {%- if weekmail.protip %} +

    {% trans %}Pro tip{% endtrans %}

    + {{ weekmail.protip|markdown }} + {% endif -%} + + {%- if weekmail.conclusion %} +

    {% trans %}Final word{% endtrans %}

    + {{ weekmail.conclusion|markdown }} + {% endif -%} + +
    + +
    + diff --git a/com/templates/com/weekmail_renderer_text.jinja b/com/templates/com/weekmail_renderer_text.jinja new file mode 100644 index 00000000..82c6b813 --- /dev/null +++ b/com/templates/com/weekmail_renderer_text.jinja @@ -0,0 +1,32 @@ +# {{ weekmail.title }} + +{%- if weekmail.intro %} +## {% trans %}Intro{% endtrans %} +{{ weekmail.intro }} +{% endif %} + +## {% trans %}Table of content{% endtrans %} +{% for a in weekmail.articles.all() %} + * [{{ a.club }}] {{ a.title }} +{% endfor -%} + +{% for a in weekmail.articles.all() %} +## [{{ a.club }}] {{ a.title }} +{{ a.content }} +{% endfor -%} + +{%- if weekmail.joke %} +## {% trans %}Joke{% endtrans %} +{{ weekmail.joke }} +{% endif -%} + +{%- if weekmail.protip %} +## {% trans %}Pro tip{% endtrans %} +{{ weekmail.protip }} +{% endif -%} + +{%- if weekmail.conclusion %} +## {% trans %}Final word{% endtrans %} +{{ weekmail.conclusion }} +{% endif -%} + diff --git a/com/tests.py b/com/tests.py index 4badf49f..3c1638ee 100644 --- a/com/tests.py +++ b/com/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase from django.conf import settings from django.core.urlresolvers import reverse diff --git a/com/urls.py b/com/urls.py index bf842472..092ecbe9 100644 --- a/com/urls.py +++ b/com/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from com.views import * @@ -6,6 +30,12 @@ urlpatterns = [ url(r'^sith/edit/alert$', AlertMsgEditView.as_view(), name='alert_edit'), url(r'^sith/edit/info$', InfoMsgEditView.as_view(), name='info_edit'), url(r'^sith/edit/index$', IndexEditView.as_view(), name='index_edit'), + url(r'^sith/edit/weekmail_destinations$', WeekmailDestinationEditView.as_view(), name='weekmail_destinations'), + url(r'^weekmail$', WeekmailEditView.as_view(), name='weekmail'), + url(r'^weekmail/preview$', WeekmailPreviewView.as_view(), name='weekmail_preview'), + url(r'^weekmail/new_article$', WeekmailArticleCreateView.as_view(), name='weekmail_article'), + url(r'^weekmail/article/(?P[0-9]+)/delete$', WeekmailArticleDeleteView.as_view(), name='weekmail_article_delete'), + url(r'^weekmail/article/(?P[0-9]+)/edit$', WeekmailArticleEditView.as_view(), name='weekmail_article_edit'), url(r'^news$', NewsListView.as_view(), name='news_list'), url(r'^news/admin$', NewsAdminListView.as_view(), name='news_admin_list'), url(r'^news/create$', NewsCreateView.as_view(), name='news_new'), diff --git a/com/views.py b/com/views.py index 14a6efab..b4b473d4 100644 --- a/com/views.py +++ b/com/views.py @@ -1,18 +1,45 @@ -from django.shortcuts import render, redirect +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import HttpResponseRedirect from django.views.generic import ListView, DetailView, RedirectView -from django.views.generic.edit import UpdateView, CreateView +from django.views.generic.edit import UpdateView, CreateView, DeleteView from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse, reverse_lazy from django.core.exceptions import ValidationError from django.utils import timezone from django.conf import settings +from django.db.models import Max +from django.forms.models import modelform_factory from django import forms from datetime import timedelta -from com.models import Sith, News, NewsDate -from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, TabedViewMixin, CanCreateMixin +from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle +from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, TabedViewMixin, CanCreateMixin, QuickNotifMixin from core.views.forms import SelectDateTime from core.models import Notification, RealGroup from club.models import Club @@ -28,6 +55,16 @@ class ComTabsMixin(TabedViewMixin): def get_list_of_tabs(self): tab_list = [] + tab_list.append({ + 'url': reverse('com:weekmail'), + 'slug': 'weekmail', + 'name': _("Weekmail"), + }) + tab_list.append({ + 'url': reverse('com:weekmail_destinations'), + 'slug': 'weekmail_destinations', + 'name': _("Weekmail destinations"), + }) tab_list.append({ 'url': reverse('com:index_edit'), 'slug': 'index', @@ -67,6 +104,11 @@ class IndexEditView(ComEditView): current_tab = "index" success_url = reverse_lazy('com:index_edit') +class WeekmailDestinationEditView(ComEditView): + fields = ['weekmail_destinations'] + current_tab = "weekmail_destinations" + success_url = reverse_lazy('com:weekmail_destinations') + # News class NewsForm(forms.ModelForm): @@ -89,6 +131,8 @@ class NewsForm(forms.ModelForm): self.add_error('start_date', ValidationError(_("This field is required."))) if not self.cleaned_data['end_date']: self.add_error('end_date', ValidationError(_("This field is required."))) + if self.cleaned_data['start_date'] > self.cleaned_data['end_date']: + self.add_error('end_date', ValidationError(_("You crazy? You can not finish an event before starting it."))) if self.cleaned_data['type'] == "WEEKLY" and not self.cleaned_data['until']: self.add_error('until', ValidationError(_("This field is required."))) return self.cleaned_data @@ -148,7 +192,7 @@ class NewsEditView(CanEditMixin, UpdateView): Notification(user=u, url=reverse("com:news_detail", kwargs={'news_id': self.object.id}), type="NEWS_MODERATION").save() return super(NewsEditView, self).form_valid(form) -class NewsCreateView(CanCreateMixin, CreateView): +class NewsCreateView(CanCreateMixin, CreateView): #XXX no can_be_created_by function in News model model = News form_class = NewsForm template_name = 'com/news_edit.jinja' @@ -213,4 +257,126 @@ class NewsDetailView(CanViewMixin, DetailView): template_name = 'com/news_detail.jinja' pk_url_kwarg = 'news_id' +# Weekmail + +class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView): + model = Weekmail + template_name = 'com/weekmail_preview.jinja' + success_url = reverse_lazy('com:weekmail') + current_tab = "weekmail" + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + try: + if request.POST['send'] == "validate": + self.object.send() + return HttpResponseRedirect(reverse('com:weekmail') + "?qn_weekmail_send_success") + except: pass + return super(WeekmailEditView, self).get(request, *args, **kwargs) + + def get_object(self, queryset=None): + return self.model.objects.filter(sent=False).order_by('-id').first() + + def get_context_data(self, **kwargs): + """Add rendered weekmail""" + kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs) + kwargs['weekmail_rendered'] = self.object.render_html() + return kwargs + +class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView): + model = Weekmail + template_name = 'com/weekmail.jinja' + form_class = modelform_factory(Weekmail, fields=['title', 'intro', 'joke', 'protip', 'conclusion'], + help_texts={'title': _("Delete and save to regenerate")}) + success_url = reverse_lazy('com:weekmail') + current_tab = "weekmail" + + def get_object(self, queryset=None): + weekmail = self.model.objects.filter(sent=False).order_by('-id').first() + if not weekmail.title: + now = timezone.now() + weekmail.title = _("Weekmail of the ") + (now + timedelta(days=6 - now.weekday())).strftime('%d/%m/%Y') + weekmail.save() + return weekmail + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + if 'up_article' in request.GET.keys(): + art = get_object_or_404(WeekmailArticle, id=request.GET['up_article'], weekmail=self.object) + prev_art = self.object.articles.order_by('rank').filter(rank__lt=art.rank).last() + if prev_art: + art.rank, prev_art.rank = prev_art.rank, art.rank + art.save() + prev_art.save() + self.quick_notif_list += ['qn_success'] + if 'down_article' in request.GET.keys(): + art = get_object_or_404(WeekmailArticle, id=request.GET['down_article'], weekmail=self.object) + next_art = self.object.articles.order_by('rank').filter(rank__gt=art.rank).first() + if next_art: + art.rank, next_art.rank = next_art.rank, art.rank + art.save() + next_art.save() + self.quick_notif_list += ['qn_success'] + if 'add_article' in request.GET.keys(): + art = get_object_or_404(WeekmailArticle, id=request.GET['add_article'], weekmail=None) + art.weekmail = self.object + art.rank = self.object.articles.aggregate(Max('rank'))['rank__max'] or 0 + art.rank += 1 + art.save() + self.quick_notif_list += ['qn_success'] + if 'del_article' in request.GET.keys(): + art = get_object_or_404(WeekmailArticle, id=request.GET['del_article'], weekmail=self.object) + art.weekmail = None + art.rank = -1 + art.save() + self.quick_notif_list += ['qn_success'] + return super(WeekmailEditView, self).get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + """Add orphan articles """ + kwargs = super(WeekmailEditView, self).get_context_data(**kwargs) + kwargs['orphans'] = WeekmailArticle.objects.filter(weekmail=None) + return kwargs + +class WeekmailArticleEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView): + """Edit an article""" + model = WeekmailArticle + fields = ['title', 'club', 'content'] + pk_url_kwarg = "article_id" + template_name = 'core/edit.jinja' + success_url = reverse_lazy('com:weekmail') + quick_notif_url_arg = "qn_weekmail_article_edit" + current_tab = "weekmail" + +class WeekmailArticleCreateView(QuickNotifMixin, CreateView): #XXX need to protect this view + """Post an article""" + model = WeekmailArticle + fields = ['title', 'club', 'content'] + template_name = 'core/create.jinja' + success_url = reverse_lazy('core:user_tools') + quick_notif_url_arg = "qn_weekmail_new_article" + + def get_initial(self): + init = {} + try: + init['club'] = Club.objects.filter(id=self.request.GET['club']).first() + except: pass + return init + + def form_valid(self, form): + # club = get_object_or_404(Club, id=self.kwargs['club_id']) + # form.instance.club = club + form.instance.author = self.request.user + return super(WeekmailArticleCreateView, self).form_valid(form) + +class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView): + """Delete an article""" + model = WeekmailArticle + template_name = 'core/delete_confirm.jinja' + success_url = reverse_lazy('com:weekmail') + pk_url_kwarg = "article_id" + + + + diff --git a/core/__init__.py b/core/__init__.py index e69de29b..0a9419f8 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/core/admin.py b/core/admin.py index 5742d460..6b60ff9c 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin from ajax_select import make_ajax_form from core.models import User, Page, RealGroup, SithFile diff --git a/core/lookups.py b/core/lookups.py index 20dd7113..b8fd7c44 100644 --- a/core/lookups.py +++ b/core/lookups.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.core.exceptions import PermissionDenied from ajax_select import register, LookupChannel @@ -14,7 +38,7 @@ def check_token(request): class RightManagedLookupChannel(LookupChannel): def check_auth(self, request): - if not request.user.subscribed and not check_token(request): + if not request.user.was_subscribed and not check_token(request): raise PermissionDenied @register('users') diff --git a/core/management/__init__.py b/core/management/__init__.py index e69de29b..0a9419f8 100644 --- a/core/management/__init__.py +++ b/core/management/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py index e69de29b..0a9419f8 100644 --- a/core/management/commands/__init__.py +++ b/core/management/commands/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 4f57c253..bc9d8c32 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + import os from datetime import date, datetime from io import StringIO, BytesIO @@ -16,8 +40,9 @@ from core.utils import resize_image from club.models import Club, Membership from subscription.models import Subscription from counter.models import Customer, ProductType, Product, Counter -from com.models import Sith +from com.models import Sith, Weekmail from election.models import Election, Role, Candidature, ElectionList +from forum.models import Forum, ForumMessage, ForumTopic class Command(BaseCommand): @@ -37,7 +62,9 @@ class Command(BaseCommand): Site(id=4000, domain=settings.SITH_URL, name=settings.SITH_NAME).save() root_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) Group(name="Root").save() - Group(name="Not registered users").save() + Group(name="Public").save() + Group(name="Subscribers").save() + Group(name="Old subscribers").save() Group(name="Accounting admin").save() Group(name="Communication admin").save() Group(name="Counter admin").save() @@ -45,6 +72,7 @@ class Command(BaseCommand): Group(name="Banned from counters").save() Group(name="Banned to subscribe").save() Group(name="SAS admin").save() + Group(name="Forum admin").save() self.reset_index("core", "auth") root = User(id=0, username='root', last_name="", first_name="Bibou", email="ae.info@utbm.fr", @@ -86,7 +114,8 @@ class Command(BaseCommand): home_root.save() club_root.save() - Sith().save() + Sith(weekmail_destinations="etudiants@git.an personnel@git.an").save() + Weekmail().save() p = Page(name='Index') p.set_lock(root) @@ -428,3 +457,15 @@ Welcome to the wiki page! cand = Candidature(role=pres, user=sli, election_list=listeT, program="En fait j'aime pas l'info, je voulais faire GMC") cand.save() + # Forum + room = Forum(name="Salon de discussions", description="Pour causer de tout", is_category=True) + room.save() + Forum(name="AE", description="Réservé au bureau AE", parent=room).save() + Forum(name="BdF", description="Réservé au bureau BdF", parent=room).save() + hall = Forum(name="Hall de discussions", description="Pour toutes les discussions", parent=room) + hall.save() + various = Forum(name="Divers", description="Pour causer de rien", is_category=True) + various.save() + Forum(name="Promos", description="Réservé aux Promos", parent=various).save() + ForumTopic(forum=hall) + diff --git a/core/management/commands/setup.py b/core/management/commands/setup.py old mode 100755 new mode 100644 index 6f4a3e13..eed57896 --- a/core/management/commands/setup.py +++ b/core/management/commands/setup.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + import os from django.core.management.base import BaseCommand, CommandError from django.core.management import call_command @@ -14,9 +38,14 @@ class Command(BaseCommand): root_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) try: os.mkdir(os.path.join(root_path)+'/data') + print("Data dir created") except Exception as e: - print(e) - call_command('flush') + repr(e) + try: + os.remove(os.path.join(root_path, 'db.sqlite3')) + print("db.sqlite3 deleted") + except Exception as e: + repr(e) call_command('migrate') if options['prod']: call_command('populate', '--prod') diff --git a/core/markdown.py b/core/markdown.py index 11bda419..00355f28 100644 --- a/core/markdown.py +++ b/core/markdown.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + import re from mistune import Renderer, InlineGrammar, InlineLexer, Markdown from django.core.urlresolvers import reverse_lazy, reverse @@ -7,9 +31,102 @@ class SithRenderer(Renderer): def file_link(self, id, suffix): return reverse('core:file_detail', kwargs={'file_id': id}) + suffix + def exposant(self, text): + return """%s""" % text + + def indice(self, text): + return """%s""" % text + + def underline(self, text): + return """%s""" % text + +class SithInlineGrammar(InlineGrammar): + double_emphasis = re.compile( + r'^\*{2}([\s\S]+?)\*{2}(?!\*)' # **word** + ) + emphasis = re.compile( + r'^\*((?:\*\*|[^\*])+?)\*(?!\*)' # *word* + ) + underline = re.compile( + r'^_{2}([\s\S]+?)_{2}(?!_)' # __word__ + ) + exposant = re.compile( # FIXME Does not work for now + r'^\^([\s\S]+?)\^' # ^text^ + # r'|' # FIXME doesn't properly works like this + # r'^\^(\S+)' # ^word + ) + indice = re.compile( + r'^_([\s\S]+?)_' # _text_ (^` hack, because no other solution were found :/ this sadly prevent code in indices) + # r'|' # FIXME doesn't properly works like this + # r'^_(\S+)' # _word + ) + class SithInlineLexer(InlineLexer): + grammar_class = SithInlineGrammar + + default_rules = [ + 'escape', + 'inline_html', + 'autolink', + 'url', + 'footnote', + 'link', + 'reflink', + 'nolink', + 'exposant', + 'double_emphasis', + 'emphasis', + 'underline', + 'indice', + 'code', + 'linebreak', + 'strikethrough', + 'text', + ] + inline_html_rules = [ + 'escape', + 'autolink', + 'url', + 'link', + 'reflink', + 'nolink', + 'exposant', + 'double_emphasis', + 'emphasis', + 'underline', + 'indice', + 'code', + 'linebreak', + 'strikethrough', + 'text', + ] + + def output_underline(self, m): + text = m.group(1) + return self.renderer.underline(text) + + def output_exposant(self, m): + text = m.group(1) + return self.renderer.exposant(text) + + def output_indice(self, m): + text = m.group(1) + return self.renderer.indice(text) + + # Double emphasis rule changed + def output_double_emphasis(self, m): + text = m.group(1) + text = self.output(text) + return self.renderer.double_emphasis(text) + + # Emphasis rule changed + def output_emphasis(self, m): + text = m.group(1) + text = self.output(text) + return self.renderer.emphasis(text) + def _process_link(self, m, link, title=None): - try: + try: # Add page:// support for links page = re.compile( r'^page://(\S*)' # page://nom_de_ma_page ) @@ -17,7 +134,7 @@ class SithInlineLexer(InlineLexer): page = match.group(1) or "" link = reverse('core:page', kwargs={'page_name': page}) except: pass - try: + try: # Add file:// support for links file_link = re.compile( r'^file://(\d*)/?(\S*)?' # file://4000/download ) @@ -28,30 +145,48 @@ class SithInlineLexer(InlineLexer): except: pass return super(SithInlineLexer, self)._process_link(m, link, title) - # def enable_file_link(self): - # # add file_link rules - # self.rules.file_link = re.compile( - # r'dfile://(\d*)/?(\S*)?' # dfile://4000/download - # ) - # # Add file_link parser to default rules - # # you can insert it some place you like - # # but place matters, maybe 2 is not good - # self.default_rules.insert(0, 'file_link') - - # def output_file_link(self, m): - # id = m.group(1) - # suffix = m.group(2) or "" - # # you can create an custom render - # # you can also return the html if you like - # # return directly html like this: - # # return reverse('core:file_detail', kwargs={'file_id': id}) + suffix - # return self.renderer.file_link(id, suffix) - -renderer = SithRenderer() +renderer = SithRenderer(escape=True) inline = SithInlineLexer(renderer) -# enable the features -# inline.enable_file_link() markdown = Markdown(renderer, inline=inline) +if __name__ == "__main__": + print(markdown.inline.default_rules) + print(markdown.inline.inline_html_rules) + text = """ +## Basique + +* Mettre le texte en **gras** : `**texte**` + +* Mettre le texte en *italique* : `*texte*` + +* __Souligner__ le texte : `__texte__` + +* ~~Barrer du texte~~ : `~~texte~~` + +* Mettre ^du texte^ en ^exposant^ : `^mot` ou `^texte^` + +* _Mettre du texte_ en _indice_ : `_mot` ou `_texte_` + +* Pied de page [^en pied de page] + +## Blocs de citations + +Un bloc de citation se crée ainsi : +``` +> Ceci est +> un bloc de +> citation +``` + +> Ceci est +> un bloc de +> citation + +Il est possible d'intégrer de la syntaxe Markdown-AE dans un tel bloc. + +Petit *test* _sur_ ^une^ **seule** ^ligne pour voir^ + +""" + print(markdown(text)) diff --git a/core/middleware.py b/core/middleware.py index c26543f5..09cb9b0a 100644 --- a/core/middleware.py +++ b/core/middleware.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + import importlib from django.conf import settings from django.utils.functional import SimpleLazyObject diff --git a/core/migrations/0019_preferences_receive_weekmail.py b/core/migrations/0019_preferences_receive_weekmail.py new file mode 100644 index 00000000..7f7b117a --- /dev/null +++ b/core/migrations/0019_preferences_receive_weekmail.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0018_auto_20161224_0211'), + ] + + operations = [ + migrations.AddField( + model_name='preferences', + name='receive_weekmail', + field=models.BooleanField(default=False, verbose_name='do you want to receive the weekmail'), + ), + ] diff --git a/core/migrations/0020_auto_20170324_0917.py b/core/migrations/0020_auto_20170324_0917.py new file mode 100644 index 00000000..8867e399 --- /dev/null +++ b/core/migrations/0020_auto_20170324_0917.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.core.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0019_preferences_receive_weekmail'), + ] + + operations = [ + migrations.AlterModelOptions( + name='group', + options={'ordering': ['name']}, + ), + migrations.AlterField( + model_name='page', + name='name', + field=models.CharField(validators=[django.core.validators.RegexValidator('^[A-z.+-]+$', 'Enter a valid page name. This value may contain only unaccented letters, numbers and ./+/-/_ characters.')], max_length=30, verbose_name='page unix name'), + ), + ] diff --git a/core/models.py b/core/models.py index 1ddf9cd9..809224a8 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.db import models from django.core.mail import send_mail from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager, Group as AuthGroup, GroupManager as AuthGroupManager, AnonymousUser as AuthAnonymousUser @@ -10,6 +34,10 @@ from django.conf import settings from django.db import transaction from django.contrib.staticfiles.storage import staticfiles_storage from django.utils.html import escape +from django.utils.functional import cached_property + +import os + from phonenumber_field.modelfields import PhoneNumberField from datetime import datetime, timedelta, date @@ -31,6 +59,10 @@ class Group(AuthGroup): help_text=_('Whether a group is a meta group or not'), ) description = models.CharField(_('description'), max_length=60) + + class Meta: + ordering = ['name'] + def get_absolute_url(self): """ This is needed for black magic powered UpdateView's children @@ -182,9 +214,11 @@ class User(AbstractBaseUser): def to_dict(self): return self.__dict__ + @cached_property def was_subscribed(self): return self.subscriptions.exists() + @cached_property def is_subscribed(self): s = self.subscriptions.last() return s.is_valid_now() if s is not None else False @@ -204,8 +238,12 @@ class User(AbstractBaseUser): return False if group_id == settings.SITH_GROUP_PUBLIC_ID: return True + if group_id == settings.SITH_GROUP_SUBSCRIBERS_ID: + return self.is_subscribed + if group_id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID: + return self.was_subscribed if group_name == settings.SITH_MAIN_MEMBERS_GROUP: # We check the subscription if asked - return self.is_subscribed() + return self.is_subscribed if group_name[-len(settings.SITH_BOARD_SUFFIX):] == settings.SITH_BOARD_SUFFIX: from club.models import Club name = group_name[:-len(settings.SITH_BOARD_SUFFIX)] @@ -226,25 +264,25 @@ class User(AbstractBaseUser): return True return self.groups.filter(name=group_name).exists() - @property + @cached_property def is_root(self): return self.is_superuser or self.groups.filter(id=settings.SITH_GROUP_ROOT_ID).exists() - @property + @cached_property def is_board_member(self): from club.models import Club return Club.objects.filter(unix_name=settings.SITH_MAIN_CLUB['unix_name']).first().get_membership_for(self) - @property + @cached_property def is_launderette_manager(self): from club.models import Club return Club.objects.filter(unix_name=settings.SITH_LAUNDERETTE_MANAGER['unix_name']).first().get_membership_for(self) - @property + @cached_property def is_banned_alcohol(self): return self.is_in_group(settings.SITH_GROUP_BANNED_ALCOHOL_ID) - @property + @cached_property def is_banned_counter(self): return self.is_in_group(settings.SITH_GROUP_BANNED_COUNTER_ID) @@ -402,7 +440,7 @@ class User(AbstractBaseUser): return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root def can_be_viewed_by(self, user): - return (user.is_in_group(settings.SITH_MAIN_MEMBERS_GROUP) and self.is_subscriber_viewable) or user.is_root + return (user.was_subscribed and self.is_subscriber_viewable) or user.is_root def get_mini_item(self): return """ @@ -418,14 +456,25 @@ class User(AbstractBaseUser): escape(self.get_display_name()), ) - @property + @cached_property def subscribed(self): return self.is_in_group(settings.SITH_MAIN_MEMBERS_GROUP) + @cached_property + def forum_infos(self): + try: + return self._forum_infos + except: + from forum.models import ForumUserInfo + infos = ForumUserInfo(user=self) + infos.save() + return infos + class AnonymousUser(AuthAnonymousUser): def __init__(self, request): super(AnonymousUser, self).__init__() + @property def was_subscribed(self): return False @@ -487,12 +536,23 @@ class AnonymousUser(AuthAnonymousUser): class Preferences(models.Model): user = models.OneToOneField(User, related_name="preferences") + receive_weekmail = models.BooleanField( + _('do you want to receive the weekmail'), + default=False, + # help_text=_('Do you want to receive the weekmail?'), + ) show_my_stats = models.BooleanField( _('define if we show a users stats'), default=False, help_text=_('Show your account statistics to others'), ) + def get_display_name(self): + return self.user.get_display_name() + + def get_absolute_url(self): + return self.user.get_absolute_url() + def get_directory(instance, filename): return '.{0}/{1}'.format(instance.get_parent_path(), filename) @@ -630,10 +690,10 @@ class SithFile(models.Model): if self.is_folder: for c in self.children.all(): c.move_to(self) - shutil.rmtree(settings.MEDIA_ROOT + old_file_name) + shutil.rmtree(os.path.join(settings.MEDIA_ROOT, old_file_name)) else: self.file.save(name=self.name, content=self.file) - os.remove(settings.MEDIA_ROOT + old_file_name) + os.remove(os.path.join(settings.MEDIA_ROOT, old_file_name)) def __getattribute__(self, attr): if attr == "is_file": @@ -641,12 +701,12 @@ class SithFile(models.Model): else: return super(SithFile, self).__getattribute__(attr) - @property + @cached_property def as_picture(self): from sas.models import Picture return Picture.objects.filter(id=self.id).first() - @property + @cached_property def as_album(self): from sas.models import Album return Album.objects.filter(id=self.id).first() @@ -703,7 +763,15 @@ class Page(models.Model): Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast query, but don't rely on it when playing with a Page object, use get_full_name() instead! """ - name = models.CharField(_('page name'), max_length=30, blank=False) + name = models.CharField(_('page unix name'), max_length=30, + validators=[ + validators.RegexValidator( + r'^[A-z.+-]+$', + _('Enter a valid page name. This value may contain only ' + 'unaccented letters, numbers ' 'and ./+/-/_ characters.') + ), + ], + blank=False) parent = models.ForeignKey('self', related_name="children", verbose_name=_("parent"), null=True, blank=True, on_delete=models.SET_NULL) # Attention: this field may not be valid until you call save(). It's made for fast query, but don't rely on it when # playing with a Page object, use get_full_name() instead! @@ -808,6 +876,14 @@ class Page(models.Model): p.set_lock_recursive(user) self.set_lock(user) + def unset_lock_recursive(self): + """ + Unlocks recursively all the child pages + """ + for p in self.children.all(): + p.unset_lock_recursive() + self.unset_lock() + def unset_lock(self): """Always try to unlock, even if there is no lock""" self.lock_user = None @@ -848,6 +924,16 @@ class Page(models.Model): except: return self.name + def delete(self): + self.unset_lock_recursive() + self.set_lock_recursive(User.objects.get(id=0)) + for child in self.children.all(): + child.parent = self.parent + child.save() + child.unset_lock_recursive() + super(Page, self).delete() + + class PageRev(models.Model): """ This is the true content of the page. diff --git a/core/search_indexes.py b/core/search_indexes.py index 03ececa4..916cf909 100644 --- a/core/search_indexes.py +++ b/core/search_indexes.py @@ -1,4 +1,30 @@ -from haystack import indexes +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.db import models + +from haystack import indexes, signals from core.models import User @@ -15,3 +41,15 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable): def get_updated_field(self): return "last_update" + + +class UserOnlySignalProcessor(signals.BaseSignalProcessor): + def setup(self): + # Listen only to the ``User`` model. + models.signals.post_save.connect(self.handle_save, sender=User) + models.signals.post_delete.connect(self.handle_delete, sender=User) + + def teardown(self): + # Disconnect only for the ``User`` model. + models.signals.post_save.disconnect(self.handle_save, sender=User) + models.signals.post_delete.disconnect(self.handle_delete, sender=User) diff --git a/core/static/core/img/partners.png b/core/static/core/img/partners.png new file mode 100644 index 00000000..93f9a0f6 Binary files /dev/null and b/core/static/core/img/partners.png differ diff --git a/core/static/core/js/script.js b/core/static/core/js/script.js index 714e2d12..c30264bf 100644 --- a/core/static/core/js/script.js +++ b/core/static/core/js/script.js @@ -35,6 +35,9 @@ $( function() { popup.html('
    '); popup.dialog({title: $(this).text()}).dialog( "open" ); }); + $("#quick_notif li").click(function () { + $(this).hide(); + }) } ); function display_notif() { diff --git a/core/static/core/style.css b/core/static/core/style.css index c1a753f5..8d841061 100644 --- a/core/static/core/style.css +++ b/core/static/core/style.css @@ -11,6 +11,20 @@ a { } a:hover { color: #7FDBFF; } a:active { color: #007BE6; } +.ib { + display: inline-block; + padding: 2px; + margin: 2px; +} +.w_big { + width: 75%; +} +.w_medium { + width: 45%; +} +.w_small { + width: 20%; +} /*--------------------------------HEADER-------------------------------*/ #logo { margin-left: 5%; @@ -122,6 +136,15 @@ nav a:hover { } /*--------------------------------CONTENT------------------------------*/ +#quick_notif { + width: 90%; + margin: 0px auto; + list-style-type: none; + background: lightblue; +} +#quick_notif li { + padding: 10px; +} #content { width: 88%; margin: 0px auto; @@ -180,7 +203,11 @@ ul, ol { code { font-family: monospace; } - +blockquote { + margin: 10px; + padding: 5px; + border: solid 1px black; +} .edit-bar { display: block; margin: 4px; @@ -220,10 +247,16 @@ tbody>tr:hover { background: darkgrey; width: 100%; } +em { + font-style: italic; +} .highlight { background: orange; font-weight: bold; } +.underline { + text-decoration: underline; +} .tool-bar { overflow: auto; padding: 4px; @@ -363,6 +396,80 @@ textarea { display: inline; } +/*------------------------------FORUM----------------------------------*/ +.topic a, .forum a, .category a { + color: black; +} +.topic a:hover, .forum a:hover, .category a:hover { + color: #424242; + text-decoration: underline; +} +.topic { + border: solid skyblue 1px; + padding: 2px; + margin: 2px; +} +.forum { + background: lightblue; + padding: 2px; + margin: 2px; +} +.category { + background: skyblue; +} +.message { + padding: 2px; + margin: 2px; + background: #eff7ff; +} +.message:nth-child(odd) { + background: #fff; +} +.message h5 { + font-size: 100%; +} +.message.unread { + background: #d8e7f3; +} +.msg_author.deleted { + background: #ffcfcf; +} +.msg_content.deleted { + background: #ffefef; +} +.msg_content { + display: inline-block; + width: 80%; + vertical-align: top; +} +.msg_author { + display: inline-block; + width: 19%; + text-align: center; + background: #d8e7f3; +} +.msg_author img { + max-width: 70%; + margin: 0px auto; +} +.msg_meta { + font-size: small; + list-style-type: none; +} +.msg_meta li { + padding: 2px; + margin: 2px; +} +.forum_signature { + color: #C0C0C0; + border-top: 1px solid #C0C0C0; +} +.forum_signature a { + color: #C0C0C0; +} +.forum_signature a:hover { + text-decoration: underline; +} /*------------------------------SAS------------------------------------*/ .album { display: inline-block; diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index ae77cd85..7dd1c37d 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -54,8 +54,8 @@
      {% for n in user.notifications.filter(viewed=False).order_by('-id') %}
    • - {{ n.date|date(DATE_FORMAT) }} {{ - n.date|time(DATETIME_FORMAT) }}
      + {{ n.date|localtime|date(DATE_FORMAT) }} {{ + n.date|localtime|time(DATETIME_FORMAT) }}
      {{ n }}
    • {% endfor %}
    • {% trans %}View more{% endtrans %} @@ -91,7 +91,7 @@ {% trans %}Matmatronch{% endtrans %} {% trans %}Wiki{% endtrans %} {% trans %}SAS{% endtrans %} - {% trans %}Forum{% endtrans %} + {% trans %}Forum{% endtrans %} {% trans %}Services{% endtrans %} {% trans %}Files{% endtrans %} {% trans %}Sponsors{% endtrans %} @@ -100,6 +100,12 @@ {% endif %} {% endblock %} +
        + {% for n in quick_notifs %} +
      • {{ n }}
      • + {% endfor %} +
      +
      {% if list_of_tabs %}
      diff --git a/core/templates/core/notification_list.jinja b/core/templates/core/notification_list.jinja index 7de1836b..412d79f4 100644 --- a/core/templates/core/notification_list.jinja +++ b/core/templates/core/notification_list.jinja @@ -14,8 +14,8 @@
    • {% endif %} - {{ n.date|date(DATE_FORMAT) }} {{ - n.date|time(DATETIME_FORMAT) }}
      + {{ n.date|localtime|date(DATE_FORMAT) }} {{ + n.date|localtime|time(DATETIME_FORMAT) }}
      {{ n }}
    • {% endfor %} diff --git a/core/templates/core/page_list.jinja b/core/templates/core/page_list.jinja index 0e382ecb..39e9b69f 100644 --- a/core/templates/core/page_list.jinja +++ b/core/templates/core/page_list.jinja @@ -9,7 +9,7 @@

      {% trans %}Page list{% endtrans %}

      {% else %} diff --git a/core/templates/core/pagerev_edit.jinja b/core/templates/core/pagerev_edit.jinja index ca9b41df..97a3f2dc 100644 --- a/core/templates/core/pagerev_edit.jinja +++ b/core/templates/core/pagerev_edit.jinja @@ -4,10 +4,12 @@ {{ super() }} {% endblock %} diff --git a/counter/templates/counter/invoices_call.jinja b/counter/templates/counter/invoices_call.jinja index 823b6f3d..cac7d7ae 100644 --- a/counter/templates/counter/invoices_call.jinja +++ b/counter/templates/counter/invoices_call.jinja @@ -15,6 +15,9 @@ +
      +

      {% trans %}CB Payments{% endtrans %} : {{ sum_cb }} €

      +
      diff --git a/counter/tests.py b/counter/tests.py index 7ce503c2..b8fbbe1e 100644 --- a/counter/tests.py +++ b/counter/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase # Create your tests here. diff --git a/counter/urls.py b/counter/urls.py index 72b53406..dbb9c027 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from counter.views import * diff --git a/counter/views.py b/counter/views.py index 22f4e7fb..d69db0f8 100644 --- a/counter/views.py +++ b/counter/views.py @@ -1,6 +1,32 @@ -from django.shortcuts import render +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.shortcuts import render, get_object_or_404 +from django.http import Http404 from django.core.exceptions import PermissionDenied from django.views.generic import ListView, DetailView, RedirectView, TemplateView +from django.views.generic.base import View from django.views.generic.edit import UpdateView, CreateView, DeleteView, ProcessFormView, FormMixin from django.forms.models import modelform_factory from django.forms import CheckboxSelectMultiple @@ -27,6 +53,33 @@ from counter.models import Counter, Customer, Product, Selling, Refilling, Produ CashRegisterSummary, CashRegisterSummaryItem, Eticket, Permanency from accounting.models import CurrencyField +class CounterAdminMixin(View): + """ + This view is made to protect counter admin section + """ + edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] + edit_club = [] + + def _test_group(self, user): + for g in self.edit_group: + if user.is_in_group(g): + return True + return False + + def _test_club(self, user): + for c in self.edit_club: + if c.can_be_edited_by(user): + return True + return False + + + def dispatch(self, request, *args, **kwargs): + res = super(CounterAdminMixin, self).dispatch(request, *args, **kwargs) + if not (request.user.is_root or self._test_group(request.user) + or self._test_club(request.user)): + raise PermissionDenied + return res + class GetUserForm(forms.Form): """ The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view, @@ -49,9 +102,7 @@ class GetUserForm(forms.Form): cus = Customer.objects.filter(account_id__iexact=cleaned_data['code']).first() elif cleaned_data['id'] is not None: cus = Customer.objects.filter(user=cleaned_data['id']).first() - sub = cus.user if cus is not None else None - if (cus is None or sub is None or not sub.subscriptions.last() or - (date.today() - sub.subscriptions.last().subscription_end) > timedelta(days=90)): + if (cus is None or not cus.can_buy): raise forms.ValidationError(_("User not found")) cleaned_data['user_id'] = cus.user.id cleaned_data['user'] = cus.user @@ -60,12 +111,10 @@ class GetUserForm(forms.Form): class RefillForm(forms.ModelForm): error_css_class = 'error' required_css_class = 'required' + amount = forms.FloatField(min_value=0, widget=forms.NumberInput(attrs={'class':'focus'})) class Meta: model = Refilling fields = ['amount', 'payment_method', 'bank'] - widgets = { - 'amount': forms.NumberInput(attrs={'class':'focus'},) - } class CounterTabsMixin(TabedViewMixin): def get_tabs_title(self): @@ -171,9 +220,22 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): pk_url_kwarg = "counter_id" current_tab = "counter" + def dispatch(self, request, *args, **kwargs): + self.customer = get_object_or_404(Customer, user__id=self.kwargs['user_id']) + obj = self.get_object() + if not self.customer.can_buy: + raise Http404 + if obj.type == "BAR": + if not ('counter_token' in request.session.keys() and + request.session['counter_token'] == obj.token) or len(obj.get_barmen_list())<1: + raise PermissionDenied + else: + if not request.user.is_authenticated(): + raise PermissionDenied + return super(CounterClick, self).dispatch(request, *args, **kwargs) + def get(self, request, *args, **kwargs): """Simple get view""" - self.customer = Customer.objects.filter(user__id=self.kwargs['user_id']).first() if 'basket' not in request.session.keys(): # Init the basket session entry request.session['basket'] = {} request.session['basket_total'] = 0 @@ -192,7 +254,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): def post(self, request, *args, **kwargs): """ Handle the many possibilities of the post request """ self.object = self.get_object() - self.customer = Customer.objects.filter(user__id=self.kwargs['user_id']).first() self.refill_form = None if ((self.object.type != "BAR" and not request.user.is_authenticated()) or (self.object.type == "BAR" and @@ -287,7 +348,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6 bq = int((total_qty_mod_6 + q) / 6) # Integer division q -= bq - if self.customer.amount < (total + q*float(price)): # Check for enough money + if self.customer.amount < (total + round(q*float(price),2)): # Check for enough money request.session['not_enough'] = True return False if product.limit_age >= 18 and not self.customer.user.date_of_birth: @@ -387,14 +448,17 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): def refill(self, request): """Refill the customer's account""" - form = RefillForm(request.POST) - if form.is_valid(): - form.instance.counter = self.object - form.instance.operator = self.operator - form.instance.customer = self.customer - form.instance.save() + if self.get_object().type == 'BAR': + form = RefillForm(request.POST) + if form.is_valid(): + form.instance.counter = self.object + form.instance.operator = self.operator + form.instance.customer = self.customer + form.instance.save() + else: + self.refill_form = form else: - self.refill_form = form + raise PermissionDenied def get_context_data(self, **kwargs): """ Add customer to the context """ @@ -512,7 +576,7 @@ class CounterEditForm(forms.ModelForm): sellers = make_ajax_field(Counter, 'sellers', 'users', help_text="") products = make_ajax_field(Counter, 'products', 'products', help_text="") -class CounterEditView(CounterAdminTabsMixin, CanEditMixin, UpdateView): +class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): """ Edit a counter's main informations (for the counter's manager) """ @@ -522,10 +586,15 @@ class CounterEditView(CounterAdminTabsMixin, CanEditMixin, UpdateView): template_name = 'core/edit.jinja' current_tab = "counters" + def dispatch(self, request, *args, **kwargs): + obj = self.get_object() + self.edit_club.append(obj.club) + return super(CounterEditView, self).dispatch(request, *args, **kwargs) + def get_success_url(self): return reverse_lazy('counter:admin', kwargs={'counter_id': self.object.id}) -class CounterEditPropView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): +class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): """ Edit a counter's main informations (for the counter's admin) """ @@ -535,7 +604,7 @@ class CounterEditPropView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): template_name = 'core/edit.jinja' current_tab = "counters" -class CounterCreateView(CounterAdminTabsMixin, CanEditMixin, CreateView): +class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): """ Create a counter (for the admins) """ @@ -545,7 +614,7 @@ class CounterCreateView(CounterAdminTabsMixin, CanEditMixin, CreateView): template_name = 'core/create.jinja' current_tab = "counters" -class CounterDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView): +class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): """ Delete a counter (for the admins) """ @@ -557,7 +626,7 @@ class CounterDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView): # Product management -class ProductTypeListView(CounterAdminTabsMixin, CanEditPropMixin, ListView): +class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): """ A list view for the admins """ @@ -565,7 +634,7 @@ class ProductTypeListView(CounterAdminTabsMixin, CanEditPropMixin, ListView): template_name = 'counter/producttype_list.jinja' current_tab = "product_types" -class ProductTypeCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): +class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): """ A create view for the admins """ @@ -574,7 +643,7 @@ class ProductTypeCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): template_name = 'core/create.jinja' current_tab = "products" -class ProductTypeEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): +class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): """ An edit view for the admins """ @@ -584,7 +653,7 @@ class ProductTypeEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): pk_url_kwarg = "type_id" current_tab = "products" -class ProductArchivedListView(CounterAdminTabsMixin, CanEditPropMixin, ListView): +class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): """ A list view for the admins """ @@ -594,7 +663,7 @@ class ProductArchivedListView(CounterAdminTabsMixin, CanEditPropMixin, ListView) ordering = ['name'] current_tab = "archive" -class ProductListView(CounterAdminTabsMixin, CanEditPropMixin, ListView): +class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): """ A list view for the admins """ @@ -632,7 +701,7 @@ class ProductEditForm(forms.ModelForm): c.save() return ret -class ProductCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): +class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): """ A create view for the admins """ @@ -641,7 +710,7 @@ class ProductCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): template_name = 'core/create.jinja' current_tab = "products" -class ProductEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): +class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): """ An edit view for the admins """ @@ -705,26 +774,26 @@ class CashRegisterSummaryForm(forms.Form): """ Provide the cash summary form """ - ten_cents = forms.IntegerField(label=_("10 cents"), required=False) - twenty_cents = forms.IntegerField(label=_("20 cents"), required=False) - fifty_cents = forms.IntegerField(label=_("50 cents"), required=False) - one_euro = forms.IntegerField(label=_("1 euro"), required=False) - two_euros = forms.IntegerField(label=_("2 euros"), required=False) - five_euros = forms.IntegerField(label=_("5 euros"), required=False) - ten_euros = forms.IntegerField(label=_("10 euros"), required=False) - twenty_euros = forms.IntegerField(label=_("20 euros"), required=False) - fifty_euros = forms.IntegerField(label=_("50 euros"), required=False) - hundred_euros = forms.IntegerField(label=_("100 euros"), required=False) - check_1_value = forms.DecimalField(label=_("Check amount"), required=False) - check_1_quantity = forms.IntegerField(label=_("Check quantity"), required=False) - check_2_value = forms.DecimalField(label=_("Check amount"), required=False) - check_2_quantity = forms.IntegerField(label=_("Check quantity"), required=False) - check_3_value = forms.DecimalField(label=_("Check amount"), required=False) - check_3_quantity = forms.IntegerField(label=_("Check quantity"), required=False) - check_4_value = forms.DecimalField(label=_("Check amount"), required=False) - check_4_quantity = forms.IntegerField(label=_("Check quantity"), required=False) - check_5_value = forms.DecimalField(label=_("Check amount"), required=False) - check_5_quantity = forms.IntegerField(label=_("Check quantity"), required=False) + ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0) + twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0) + fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0) + one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0) + two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0) + five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0) + ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0) + twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0) + fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0) + hundred_euros = forms.IntegerField(label=_("100 euros"), required=False, min_value=0) + check_1_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0) + check_1_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0) + check_2_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0) + check_2_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0) + check_3_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0) + check_3_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0) + check_4_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0) + check_4_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0) + check_5_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0) + check_5_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0) comment = forms.CharField(label=_("Comment"), required=False) emptied = forms.BooleanField(label=_("Emptied"), required=False) @@ -817,8 +886,8 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): """Add form to the context """ kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs) threshold = timezone.now() - timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT) - kwargs['last_refillings'] = self.object.refillings.filter(date__gte=threshold).all() - kwargs['last_sellings'] = self.object.sellings.filter(date__gte=threshold).all() + kwargs['last_refillings'] = self.object.refillings.filter(date__gte=threshold).order_by('-id')[:20] + kwargs['last_sellings'] = self.object.sellings.filter(date__gte=threshold).order_by('-id')[:20] return kwargs class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): @@ -871,7 +940,7 @@ class CounterActivityView(DetailView): pk_url_kwarg = "counter_id" template_name = 'counter/activity.jinja' -class CounterStatView(DetailView, CanEditMixin): +class CounterStatView(DetailView, CounterAdminMixin): """ Show the bar stats """ @@ -933,7 +1002,7 @@ class CounterStatView(DetailView, CanEditMixin): return super(CanEditMixin, self).dispatch(request, *args, **kwargs) raise PermissionDenied -class CashSummaryEditView(CanEditPropMixin, CounterAdminTabsMixin, UpdateView): +class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): """Edit cash summaries""" model = CashRegisterSummary template_name = 'counter/cash_register_summary.jinja' @@ -949,12 +1018,14 @@ class CashSummaryFormBase(forms.Form): begin_date = forms.DateTimeField(['%Y-%m-%d %H:%M:%S'], label=_("Begin date"), required=False, widget=SelectDateTime) end_date = forms.DateTimeField(['%Y-%m-%d %H:%M:%S'], label=_("End date"), required=False, widget=SelectDateTime) -class CashSummaryListView(CanEditPropMixin, CounterAdminTabsMixin, ListView): +class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): """Display a list of cash summaries""" model = CashRegisterSummary template_name = 'counter/cash_summary_list.jinja' context_object_name = "cashsummary_list" current_tab = "cash_summary" + queryset = CashRegisterSummary.objects.all().order_by('-date') + paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH def get_context_data(self, **kwargs): """ Add sums to the context """ @@ -984,7 +1055,7 @@ class CashSummaryListView(CanEditPropMixin, CounterAdminTabsMixin, ListView): kwargs['refilling_sums'][c.name] = sum([s.amount for s in refillings.all()]) return kwargs -class InvoiceCallView(CounterAdminTabsMixin, TemplateView): +class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): template_name = 'counter/invoices_call.jinja' current_tab = 'invoices_call' @@ -1001,6 +1072,10 @@ class InvoiceCallView(CounterAdminTabsMixin, TemplateView): start_date = start_date.replace(tzinfo=pytz.UTC) end_date = (start_date + timedelta(days=32)).replace(day=1, hour=0, minute=0, microsecond=0) from django.db.models import Sum, Case, When, F, DecimalField + kwargs['sum_cb'] = sum([r.amount for r in Refilling.objects.filter(payment_method='CARD', is_validated=True, + date__gte=start_date, date__lte=end_date)]) + kwargs['sum_cb'] += sum([s.quantity*s.unit_price for s in Selling.objects.filter(payment_method='CARD', is_validated=True, + date__gte=start_date, date__lte=end_date)]) kwargs['start_date'] = start_date kwargs['sums'] = Selling.objects.values('club__name').annotate(selling_sum=Sum( Case(When(date__gte=start_date, @@ -1011,7 +1086,7 @@ class InvoiceCallView(CounterAdminTabsMixin, TemplateView): )).exclude(selling_sum=None).order_by('-selling_sum') return kwargs -class EticketListView(CounterAdminTabsMixin, CanEditPropMixin, ListView): +class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): """ A list view for the admins """ @@ -1029,7 +1104,7 @@ class EticketForm(forms.ModelForm): } product = AutoCompleteSelectField('products', show_help_text=False, label=_("Product"), required=True) -class EticketCreateView(CounterAdminTabsMixin, CanEditPropMixin, CreateView): +class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): """ Create an eticket """ @@ -1038,7 +1113,7 @@ class EticketCreateView(CounterAdminTabsMixin, CanEditPropMixin, CreateView): form_class = EticketForm current_tab = "etickets" -class EticketEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): +class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): """ Edit an eticket """ @@ -1109,6 +1184,13 @@ class EticketPDFView(CanViewMixin, DetailView): renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm) p.drawCentredString(10.5 * cm, 6 * cm, code) + partners = ImageReader("core/static/core/img/partners.png") + width, height = partners.getSize() + size = max(width, height) + width = width * 2 / 3 + height = height * 2 / 3 + p.drawImage(partners, 0 * cm, 0 * cm, width, height) + p.showPage() p.save() return response diff --git a/doc/Comptes-rendus/CR 16.01.05.pdf b/doc/Comptes-rendus/CR 16.01.05.pdf new file mode 100644 index 00000000..e4b3e733 Binary files /dev/null and b/doc/Comptes-rendus/CR 16.01.05.pdf differ diff --git a/doc/Comptes-rendus/CR 16.11.10.pdf b/doc/Comptes-rendus/CR 16.11.10.pdf new file mode 100644 index 00000000..90f9088f Binary files /dev/null and b/doc/Comptes-rendus/CR 16.11.10.pdf differ diff --git a/doc/Comptes-rendus/CR 16.11.16.pdf b/doc/Comptes-rendus/CR 16.11.16.pdf new file mode 100644 index 00000000..276df059 Binary files /dev/null and b/doc/Comptes-rendus/CR 16.11.16.pdf differ diff --git a/doc/Comptes-rendus/CR 16.12.1.pdf b/doc/Comptes-rendus/CR 16.12.1.pdf new file mode 100644 index 00000000..6646da00 Binary files /dev/null and b/doc/Comptes-rendus/CR 16.12.1.pdf differ diff --git a/doc/Comptes-rendus/CR 16.12.15.pdf b/doc/Comptes-rendus/CR 16.12.15.pdf new file mode 100644 index 00000000..06b12767 Binary files /dev/null and b/doc/Comptes-rendus/CR 16.12.15.pdf differ diff --git a/doc/Comptes-rendus/CR 16.12.8.pdf b/doc/Comptes-rendus/CR 16.12.8.pdf new file mode 100644 index 00000000..2fb7fd6a Binary files /dev/null and b/doc/Comptes-rendus/CR 16.12.8.pdf differ diff --git a/doc/Etransaction/Manuel_Integration_E-transactions_Internet_V6.6_FR.pdf b/doc/Etransaction/Manuel_Integration_E-transactions_Internet_V6.6_FR.pdf new file mode 100644 index 00000000..c9612fee Binary files /dev/null and b/doc/Etransaction/Manuel_Integration_E-transactions_Internet_V6.6_FR.pdf differ diff --git a/doc/TO_Skia_LoJ/.gitignore b/doc/TO_Skia_LoJ/.gitignore new file mode 100644 index 00000000..cbe4c5a9 --- /dev/null +++ b/doc/TO_Skia_LoJ/.gitignore @@ -0,0 +1,2 @@ +Rapport.pdf +_minted-Rapport/ diff --git a/doc/TO_Skia_LoJ/Couvertures.odg b/doc/TO_Skia_LoJ/Couvertures.odg new file mode 100644 index 00000000..afcfff80 Binary files /dev/null and b/doc/TO_Skia_LoJ/Couvertures.odg differ diff --git a/doc/TO_Skia_LoJ/Couvertures.pdf b/doc/TO_Skia_LoJ/Couvertures.pdf new file mode 100644 index 00000000..012f067b Binary files /dev/null and b/doc/TO_Skia_LoJ/Couvertures.pdf differ diff --git a/doc/TO_Skia_LoJ/Makefile b/doc/TO_Skia_LoJ/Makefile new file mode 100644 index 00000000..4d77f84b --- /dev/null +++ b/doc/TO_Skia_LoJ/Makefile @@ -0,0 +1,13 @@ +CC=pdflatex + +all: rapport clean + +rapport: Rapport.tex + @echo "Compiling "$< + $(CC) -shell-escape $< + $(CC) -shell-escape $< + +clean: + @echo "Cleaning folder" + rm *.aux; rm *.log; rm *.out; rm *.toc; rm *.snm; rm *.nav; rm *.lof + diff --git a/doc/TO_Skia_LoJ/Rapport.pdf b/doc/TO_Skia_LoJ/Rapport.pdf new file mode 100644 index 00000000..2dfe27dc Binary files /dev/null and b/doc/TO_Skia_LoJ/Rapport.pdf differ diff --git a/doc/TO_Skia_LoJ/Rapport.tex b/doc/TO_Skia_LoJ/Rapport.tex new file mode 100644 index 00000000..e247cfa0 --- /dev/null +++ b/doc/TO_Skia_LoJ/Rapport.tex @@ -0,0 +1,490 @@ +%% +% +% Skia +% skia@libskia.so +% +%% + +\documentclass[a4paper]{report} + +%packages +\usepackage[utf8]{inputenc} +\usepackage[francais]{babel} +\usepackage{graphicx}\graphicspath{{pix/}} +\usepackage{float} +\usepackage{scrextend} +\usepackage[T1]{fontenc} +\usepackage{color} +\usepackage{fancyhdr} +%Options: Sonny, Lenny, Glenn, Conny, Rejne, Bjarne, Bjornstrup +\usepackage[Bjornstrup]{fncychap} +\usepackage{minted} +\usepackage[colorlinks=true,linkcolor=black]{hyperref} +\usepackage{pdfpages} +%\usepackage{titlesec, blindtext, color} + +%pdf metadata +\hypersetup{ + unicode=true, + colorlinks=true, + citecolor=black, + filecolor=black, + linkcolor=black, + urlcolor=black, + pdfauthor={Skia }, + pdftitle={}, + pdfcreator={pdftex}, + pdfsubject={}, + pdfkeywords={}, +} + + +\definecolor{keywords}{RGB}{200,0,90} +\definecolor{comments}{RGB}{50,50,253} +\definecolor{red}{RGB}{160,0,0} +\definecolor{brown}{RGB}{160,100,100} +\definecolor{green}{RGB}{0,200,0} +\definecolor{darkgreen}{RGB}{0,130,0} +\definecolor{gray}{RGB}{100,100,100} + + +%inner meta +\title{Sith: Développement de nouvelles applications} +\author{Florent \textsc{Jacquet}\\ +Guillaume \textsc{Renaud}} +\date{Dernière version: \today} + +\begin{document} + +% \maketitle +\includepdf[pages={1}]{Couvertures.pdf} + +\tableofcontents + +\chapter*{Remerciements} +\section*{Guillaume \textsc{Renaud}} +\par Je remercie tout d'abord Monsieur Frédéric \textsc{Lassabe} qui nous a permis d'effectuer cette TO52 lors de notre cursus à +l'UTBM, nous permettant ainsi de mêler nôtre travail scolaire à nôtre envie de participer à l'amélioration de la vie +associative de l'UTBM. + +\par Je tiens aussi à remercier Florent \textsc{Jacquet} qui m'a aidé tout au long de ce travail et à qui j'ai pu poser mes +différentes questions pour apprendre et comprendre plus rapidement que si j'avais été seul. + +\section*{Florent \textsc{Jacquet}} +\par Je remercie également Frédéric \textsc{Lassabe} , non seulement pour la TO, mais également pour la précédente TW. Sans ces +deux UV hors emploi du temps, jamais un projet comme ce site de l'AE n'aurait pu voir le jour. Cela a demandé beaucoup +d'investissement, et il est plus qu'appréciable de pouvoir obtenir quelques crédits en retour. + +\par Je tiens également à remercier l'ensemble de l'équipe info de l'AE qui s'est motivée ce semestre à organiser des +réunions hebdomadaires afin de reprendre le projet du mieux possible. C'est maintenant à eux que va être confié le +projet, et il est agréable de constater qu'ils n'ont pas attendu le dernier moment pour se pencher sur la question. + + +\chapter*{Introduction} +\addcontentsline{toc}{chapter}{Introduction} +\par Après le développement de la base du nouveau site de l'AE, le projet \emph{Sith}, au Printemps 2016, la mise en +production a pu avoir lieu avec succès fin Août 2016. + +\par Mais le site était encore très incomplet, et il était nécessaire d'y ajouter un grand nombre de fonctionnalités +moins critiques, celles-ci n'ayant pas de rapport avec l'argent, mais tout de même très utiles pour le fonctionnement +de l'AE. + +\par Parmis elles, se trouvait notamment une application de gestion des stocks, qui a été confiée à Guillaume, puisqu'elle +concernait en premier lieu le \emph{Bureau des Festivités} et qu'il en était le président. Il était donc parmis les +mieux placé pour évaluer le besoin et développer l'outil, d'autant qu'il fallait le concevoir depuis le début, ces +fonctions n'étant pas du tout présentes dans l'ancien site. + +\par Du reste, Florent a eu la responsabilité de développer les autres applications, ou bien de gérer leur développement +lors qu'il était fait par quelqu'un d'autre. + +\chapter{Eboutic} +\label{sec:eboutic} +\par Développeur principal: Florent + +\section{But} +\label{sub:but} +\par Fournir une boutique en ligne, avec paiement sécurisé, compatible avec l'API de paiement du Crédit Agricole. +\begin{itemize} + \item Gérer les cotisations + \item Gérer les rechargements de compte AE + \item Gérer différents groupes de vente +\end{itemize} + +\section{Principaux problèmes} +\label{sec:principaux_problemes} + +\subsection{Interaction avec l'API} +\label{sub:interaction_avec_l_api} +\par C'est la principale contrainte de cette application. On doit interagir avec les serveurs du Crédit Agricole, et +pour cela, ces derniers n'aident pas beaucoup. + +\par Ils fournissent un PDF peu clair\footnote{disponible dans le dossier \url{doc/Etransaction/} des sources du site} +expliquant l'implémentation d'un site marchand, en plus des nombreux autres PDF de documentation disponibles à l'adresse +\url{https://e-transactions.avem-groupe.com/pages/global.php?page=telechargement}. + +\par Une implémentation de référence uniquement en PHP, et contenant que peut de fonctionnalités par rapport à ce +que dit le PDF peut aussi être obtenue, mais n'est guère utile excepté pour la vérification cryptographique de la +signature de la réponse. Mais encore, il faut arriver à traduire les fonctions propres à PHP, et ce n'est pas toujours +une mince affaire, mais fort heureusement, les algorithmes sont encore assez standards et l'on trouve vite de l'aide +quant à ces fonctions. + +\par De plus, certaines informations concernants les numéros d'identification de marchand son incohérents +d'une documentation à l'autre, et le plus simple à ce niveau est encore de contacter le support. + +\subsection{Accès concurrentiels} +\label{sub:acces_concurrentiels} +\par En production, le projet Sith tourne à l'aide d'\textbf{uWSGI}, qui s'occupe lui de gérer les différents processus du +logiciel. Cela se traduit par des accès concurrentiels à la base de donnée lors de l'appel de deux pages simultanément +qui ont besoin d'accèder aux mêmes ressources. + +\par Le problème n'en est la plupart du temps pas un, mais il devient très critique lorsque la page appelée permet par +exemple de recharger un compte AE. Il ne faut alors surtout faire l'opération en double. + +\par Pour protéger ces accès en double, on peut alors utiliser des transactions, et \textbf{Django} fournit une +abstraction très pratique: \verb-with transaction.atomic():-. + +\par L'Eboutic, avec sa réponse de la banque, est très sujette à ces accès concurrents, et cela a posé quelques +problèmes dans les débuts. La plupart ont été résolu, mais il arrive encore dans les comptoirs d'avoir une vente en +double, sans pour autant avoir le débit du compte qui soit doublé. Cela ne pose pas foncièrement de problèmes, puisque +le solde du compte est tout de même valide, et c'est un problème très compliqué à debugger, puisqu'il survient très +rarement, mais il faudrait tout de même arriver à le résoudre un jour. + + +\chapter{Le SAS} +\label{sec:le_sas} +\par Développeur principal: Florent + +\section{But} +\label{sub:but} +\par Fournir un système de galerie de photo: +\begin{itemize} + \item Upload en ligne via un formulaire pour tous les cotisants. + \item Modération pour l'équipe du SAS. + \item Système d'identification des membres pour retrouver rapidement ses photos. + \item Affichage des photos dans les différents album et sur la page "photo" du profil d'un utilisateur. +\end{itemize} + +\section{Principaux problèmes} +\label{sec:principaux_problemes} + +\subsection{Gestion des fichiers} +\label{sub:gestion_des_fichiers} +\par L'envoie en grande quantité de photos nécéssite une gestion des fichiers solide, en même temps qu'un formulaire +d'envoie efficace, capable d'envoyer plusieurs dizaines de photos en une seule action de l'utilisateur. + +\par L'envoie est donc fait à l'aide de requêtes AJAX pour envoyer les photos une par une et éviter alors le timeout. + +\par Concernant les fichiers une fois envoyé, ils sont en réalité traités par la classe \verb#SithFile# qui gère tous +les fichiers du site. Cela ne fait qu'une seule classe à développer, de même qu'un seul système de fichier avec une +seule arborescence, ce qui est beaucoup plus robuste. + +\par La difficulté a aussi été de permettre le déplacement des fichiers, par couper-coller, tout en faisant de même dans +le système de fichier réel, afin d'avoir une arborescence cohérente même en cas de perte de la base de données. + +\subsection{Optimisation des pages} +\label{sub:optimisation_des_pages} +\par La génération d'un grand nombre de requêtes SQL est un des principaux problèmes de ralentissement d'un site. Le +SAS, avec ses très nombreuses photos, qui requierent une validation des droits, a posé un gros problème à ce niveau. + +\par Certaines pages ont pu mettre jusqu'à plus de 10 secondes à générer, ce qui est inconcevable pour une galerie de +photos, mais ce temps à pu être réduit à moins de 3 secondes. + +\par L'astuce à été d'utiliser des actions utilisateurs, comme l'upload de nouvelles photos, pour faire plus de +traitement que nécessaire, afin de mettre en "cache" une grande partie des actions, comme par exemple la génération des +miniatures des albums. + +\par Une autre technique pour gagner du temps est de mettre en cache certaines requêtes en forcant les \emph{QuerySet} +à s'évaluer dans une \emph{list} \textbf{Python} que l'on stocke afin d'obtenir sa longueur, au lieu de lancer d'abord +un \emph{count}, puis une itération des résultats, qui en utilisant directement l'ORM, conduit à réaliser deux +requêtes SQL. + +\par Enfin, le passage à \textbf{HTTP/2} permettrait d'améliorer encore les performances côté utilisateur puisqu'il n'y +aurait plus qu'un seul \emph{socket} d'ouvert pour transférer toutes les photos d'une page par exemple, sans avoir +pour autant à toucher au code. + + +\chapter{Les élections} +\label{sec:les_elections} +\par Développeur principal: Antoine + +\section{But} +\label{sub:but} +\par Fournir un système d'élections: +\begin{itemize} + \item Gestion des différentes élections comprenants à chaque fois une liste de postes pour lesquels les gens + candidatent, ainsi qu'une gestion des listes, pour pouvoir classifier et répartir les candidatures. + \item Gestion d'une page de vote, permettant aux gens autorisés de pouvoir voter. + \item Affichage des résultats une fois le vote terminé. + \item Pas compatible avec la législation française: trop contraignant et pas utile, puisque validation officiel en + AG. +\end{itemize} + +\section{Principaux problèmes} +\label{sec:principaux_problemes} + +\subsection{Automatisation d'un widget particulier pour les formulaires} +\label{sub:automatisation_d_un_widget_particulier_pour_les_formulaires} +\par La demande est venue du \textbf{BdF} qui a voulu autoriser pour certains poste un nombre de vote supérieur à 1. +Cela signifie que l'on passe d'un choix simple, type \verb#radio# à un choix multiple, type \verb#checkbox#, tout cela +étant paramètrable dans l'élection. + +\par Ce genre de choix n'étant pas disponible dans \textbf{Django} de base, il a fallut développer le \emph{widget} à +utiliser dans le formulaire qui permette cette configuration tout en validant bien les données reçues par rapport au +modèle, et éviter ainsi de pouvoir "tricher" en envoyant des requêtes erronées. + +\subsection{Revue du code d'un autre développeur} +\label{sub:revue_du_code_d_un_autre_developpeur} +\par \emph{Antoine} étant le principal développeur de cette application, un gros travail de revue de code a dû être +effectuer afin de garantir une certaine cohérence avec le reste du projet. + +\par C'est un travail très long et fastidieux, car il faut bien revérifier chaque ligne, sur chaque fichier, tout en +faisant des commentaire lorsque quelque chose ne va pas. \textbf{Gitlab} a, à ce niveau, grandement facilité la tâche, à +l'aide de ses outils de \emph{merge request} assez avancés. + +\par En plus du code en lui-même, il a fallut porter une attention particulière aux migrations. Ces fichiers générés +automatiquement par \textbf{Django} sont responsables du maintient d'une base de donnée cohérente malgré les évolutions +des modèles. Même si le code parait donc valide, il est impératif de surveiller que la chaîne de dépendance des dites +migrations ne soit pas cassée, au risque de problèmes potentiels au moment de la mise en production, ce qui entraînent à +coup sûr un \emph{downtime}. + +\chapter{Les stocks} +\label{sub:les_stocks} +\par Développeur principal: Guillaume +\vskip 2em + +\par Cette application s'occupe de la gestion des stocks des comptoirs de type « BAR ». Elle permet de suivre les +quantités restantes afin de pouvoir déterminer de manière automatisée quels sont les produits qu'il faut acheter et en +quelle quantité. + +\section{Liste des modèles} +\label{sec:liste_des_modeles} + +\subsection{Stock} +\par Un Stock possède un nom et est lié à la classe Counter. Ainsi, chaque Comptoir peut avoir son propre Stock. + +\subsection{StockItem} +\par Un StockItem possède un nom, une quantité unitaire, une quantité effective, une quantité minimale et est lié à la +classe Stock ainsi qu'à la classe ProductType. De cette manière, chaque élément appartient à un Stock et il est +catégorisé de la même manière que les Products (qui sont les objets utilisés pour la vente dans les comptoirs). + +\subsection{ShoppingList} +\par Une ShoppingList possède un nom, une date, un booléen (fait ou à faire), un commentaire et est liée à un Stock. +Chaque ShoppingList est donc liée à un Stock ce qui permet d'avoir des listes de courses spécifique à chaque comptoir. + +\subsection{ShoppingListItem} +\par Un ShoppingListItem possède un nom, une quantité demandée, une quantité achetée et est lié à la classe StockItem, à +la classe ShoppingList et à la classe ProductType. Cela permet de pouvoir faire plusieurs listes de courses différentes +en même temps et d'en garder un historique. + +\section{Fonctionnement} +\label{sec:fonctionnement} + +\par Au départ, si le comptoir de type « BAR » n'a pas de stock, la seule chose qu'il est possible de faire est d'en +créer un. Ensuite, il va falloir créer les objets StockItem en indiquant pour chacun les quantités qu'il y a dans le +stock. + +\par De plus, la personne (généralement le Responsable du lieu de vie) qui aura la responsabilité d'informatiser les +stocks devra aussi définir la quantité unitaire, effective et minimale. +\par Par exemple, les Cheeseburger vendus aux différents comptoirs sont achetés par boite de 6, la quantité unitaire +sera donc 6, la quantité effective correspondra au nombre de boites restantes dans le stock (c'est à dire dans la +réserve du lieu de vie, une boite sortie du stock est considérée comme consommée) et enfin, la quantité minimale servira +de valeur seuil. +\par Une fois l'état de la réserve retranscrit dans le site, il reste encore gérer les stocks de manière quotidienne. +Pour ce faire, l'application se décompose en 3 parties : +\begin{itemize} + \item Création automatique des listes de courses + \item Approvisionnement du stock + \item Prise d'éléments dans le stock +\end{itemize} + +\par Lorsque l'on accède à la partie qui s'occupe de la gestion des listes de courses, il y a un bouton permettant de +créer une liste de courses en fonction de l'état des stocks à cet instant, puis un premier tableau contenant les listes +de courses qu'il faut faire et enfin un second tableau servant d'historique des listes de courses déjà effectuées. +\par Pour chaque liste de course ainsi créée, qu'elle soit « faite » ou « à faire », il est possible de cliquer sur son +nom pour voir le détail de ce qu'elle comprend. + +\subsection{Création automatique des listes de courses} +\par En cliquant sur le bouton permettant de créer une nouvelle liste de courses, il faut remplir un formulaire. Les +informations à donner dans ce formulaire sont le nom de la liste de course (par exemple, une liste spéciale pour +Leclerc), ensuite, apparaissent tous les StockItem ayant une quantité effective inférieure au seuil fixé par leur +quantité minimale. Il faut donc donner pour chacun de ces éléments une quantité à acheter. Enfin, un dernier champ de +commentaire peut être compléter, il sert à demander l'achat d'éléments qui n'apparaissent pas dans le Stock, par +exemple, des couteaux, fourchettes ou encore tasses... +\par Lors de la validation de ce formulaire, la liste de courses est créée et est ajoutée au tableau contenant les +listes de courses à faire. + +\subsection{Approvisionnement du stock} +\par Au retour des courses, il faut ranger les produits achetés dans la réserve. À ce moment-là, il faut aussi mettre à +jour le stock. Une opération « Mettre à jour le stock » est disponible pour chaque liste de courses du tableau « À +faire ». +\par En effectuant cette action, il va falloir indiquer les quantités effectivement achetées. En effet, les quantités +demandées ne sont pas forcément celles achetées, c'est donc les quantités effectives qu'il faut ajouter au stock. Une +fois ce formulaire validé, la liste de couses passera de l'état « à faire » à l'état « faite ». + +\subsection{Prise d'éléments dans le stock} +\par La réserve étant accessible aux barmen afin qu'ils puissent réapprovisionner les réfrigérateurs à tout moment, +l'interface permettant de prendre des éléments dans le stock a été ajoutée dans les onglets de l'interface des ventes +(là où le barman inscrit le code du compte du client qui souhaite commander quelque chose). Ainsi, en revenant de la +réserve, le barman doit indiquer le nombre de chaque produit qu'il a rapporté. +\par Dans un souci de simplicité pour le gérant du lieu de vie, ce formulaire de prise des éléments dans le stock est +aussi accessible depuis son interface de gestion. + + +\section{Améliorations à apporter} +\label{sec:amelioration_a_apporter} + +\begin{itemize} + \item Il n'est pas encore possible de modifier les quantités demandées pour un ou plusieurs des produits d'une liste + de course. Il faudrait rendre cela possible car actuellement, il faut supprimer la liste de courses et la + refaire en changeant les quantités souhaitées. + \item Il faudrait améliorer la manière dont on ajoute les éléments non définis en tant que StockItem dans la liste + de course. Un objet ShoppingListItem avec une référence « Null » vers la classe StockItem pourrait être créé à + la place de compléter le champ de commentaires. + \item Dans les améliorations sur le long terme, il faudrait que la décrémentation des quantités de chaque élément + dans le stock soit automatique. En repensant une partie de l'architecture de l'application, on pourrait faire en + sorte que chaque vente faite au comptoir face diminuer les quantités restantes (cela remplacerait le formulaire + de « Prise d'éléments dans le stock », mais cela ne serait pas applicable à tous les produits mis en vente. Par + exemple, pour les cacahuètes que nous vendons au bol et non par paquet) + \item Avec le système de notifications qui a été mis en place sur le site, on pourrait faire en sorte que le ou les + responsables des lieux de vie reçoivent une notification lorsque la liste de courses contient plus de 5 éléments +\end{itemize} + + +\chapter{La laverie} +\label{sec:la_laverie} +\par Développeur principal: Florent + +\section{But} +\label{sub:but} +\par Cette application doit fournir un système de gestion de laverie. Cela comprend: +\begin{itemize} + \item Un système de planning et de réservation de créneaux + \item Un système de vente de jetons de laverie, lié aux comptoirs et au compte AE, permettant aux permanenciers de + cliquer les jetons en même temps qu'ils vérifient l'état de la cotisation. + \item Un système d'inventaire, pour gérer les différentes machines dans les différents lieux, et gérer également le + retour des jetons après utilisation. +\end{itemize} + +\section{Principaux problèmes} +\label{sec:principaux_problemes} + +\subsection{Génération de plannings} +\label{sub:generation_de_plannings} +\par Il y a là beaucoup de cas à prendre en compte. Lorsque que quelqu'un veut réserver directement un "Lavage + +Séchage", un simple "Lavage", ou un simple "Séchage", il faut toujours vérifier la disponibilité des créneaux en +fonction du nombre de machine de chaque type présent dans la laverie en question \footnote{Belfort ou Sevenans, en +l'occurrence}, et cela représente vite un grand nombre de combinaisons à vérifier. + +\par De plus, la réservation doit rester ergonomique, et s'afficher dans un format le plus lisible possible pour un +humain. Là dessus, un tableau est le plus approprié, avec chaque jour représenté par une colonne, et chaque créneau par +une ligne.\\ +Mais cela ne représente malheureusement pas la temporalité, et la génération du tableau devient alors plutôt compliquée, +et d'autant plus si l'on veut qu'il soit sémantiquement correct en HTML. + +\subsection{Gestion des timezones} +\label{sub:gestion_des_timezones} +\par La gestion et le stockage des crénaux implique l'utilisation de champs de type \verb#DateTime#. \textbf{Django} les +gère très bien, particulièrement au niveau des \emph{timezones}, où ce dernier n'hésite pas à lancer un warning lorsque +l'objet \emph{date} passé ne contient pas d'information de fuseau horaire. + +\par Mais avec notre décalage d'une heure par rapport au temps UTC, tous les horaires se retrouvent décalés, et gérer +cela convenablement sans sortir d'avertissement a été plutôt compliqué. La solution a été de forcer un peu partout la +\emph{timezone} à UTC, afin de ne pas créer de décalage, mais en conservant tout de même l'information de fuseau +horaire, et sans tout casser lors du passage à l'heure d'hiver. + + +\chapter{La communication} +\label{sec:la_communication} +\par Développeur principal: Florent + +\section{But} +\label{sub:but} +\par Cette application a plusieurs but: +\begin{itemize} + \item Donner la possibilité au responsable communication d'éditer les différents textes, messages, et pages + statiques du site. + \item Fournir un système de news. + \item Fournir un système de newsletter: le Weekmail. +\end{itemize} + +\section{Principaux problèmes} +\label{sec:principaux_problemes} + +\subsection{Envoie de mails} +\label{sub:envoie_de_mails} +\par Un outil de \emph{newsletter} nécessite l'envoie de mail. C'est là quelque chose de relativement compliqué à +tester, d'autant plus lorsqu'il s'agit de mailing-list contenant l'intégralité des étudiants de l'UTBM. + +\par \textbf{Django} fournit toutefois un outil très pratique: il contient plusieurs \emph{backend} d'emails, dont entre +autre un \emph{SMTP}, et un \emph{console}. Le \emph{SMTP} est bien évidemment utilisé en production pour envoyer +effectivement les mails, mais il est compliqué à utiliser en développement, car il suppose que le développeur a à sa +disposition un serveur de ce type. On utilise alors le backend \emph{console}, qui affiche simplement dans le thread +d'execution de \textbf{Django} une version texte de l'email envoyé, avec d'une part les entêtes, d'autre part le corps +de message. + +\par Mais autant pour tester l'envoie d'un mail unique à une adresse unique, cela fonctionne parfaitement bien, ce n'est +toutefois pas suffisant pour tester un envoie massif à plusieurs mailings, avec en plus encore d'autres adresses en +\emph{Bcc} pour les gens ne faisant pas partie des mailings "classiques", mais souhaitant quand même recevoir le +\textbf{Weekmail}. + +\subsection{Amélioration de l'outil de recherche} +\label{sub:amelioration_de_l_outil_de_recherche} +\par Pour la gestion de l'AE, il est nécessaire de pouvoir rechercher et trouver efficacement n'importe quel membre, en +tapant au choix son nom, prénom, ou surnom, voire une combinaison de ces trois champs. + +\par Mais une fonction de recherche aussi complexe est très difficile a mettre en place efficacement sans un traitement +préalable, d'où la nécessité d'indexer les entrées à chercher. Un indexeur étant très complexe, mais également très +courant, il n'a pas été difficile de trouver une application déjà existante fournissant ces fonctionnalités. + +\par Le choix s'est porté sur \textbf{Haystack}, en l'utilisant avec l'indexeur \textbf{Whoosh}, plutôt efficace pour +des bases raisonnables, et surtout écrit en pure \textbf{Python}, donc ne nécessitant pas d'installation compliquée en +parallèle du site. + +\par Le résultat est plutôt satisfaisant, mais il faudrait encore améliorer les résultats en utilisant les fonctions de +\emph{boost} pour certains champs. De plus, une certaine lenteur se fait encore sentir avec certaines recherches trop +communes ou générales. + + +\chapter{Conclusions personnelles} +\section{Florent} +\label{sec:skia} +\par Développer de nouvelles application m'a permis d'apréhender d'autres problématiques, comme la gestion des fichiers +dans le SAS, ou bien des contraintes de concurrence et d'atomicité sur l'Eboutic. + +\par Mais la plus grosse partie de mon travail ce semestre a surtout été de superviser une équipe de développement +naissante, de relire les "Merge request", et de m'assurer de la cohérence du code des contributeurs avec le reste du +projet. + +\par J'ai églament pu approfondir mon utilisation de Gitlab à travers ses outils de gestion de projet, de revue de code, +et de gestion des permissions sur les différentes branches. + +\section{Guillaume} +\label{sec:lo_j} +\par Je suis très heureux d'avoir pu participer à ce projet de TO52 sur le développement de modules sur le site de +l'Association des Étudiants. J'ai pu apprendre à travailler avec un nouvel environnement informatique tout en +contribuant au développement d'outils pour l'association dont je suis Président, le Bureau des Festivités. + +\subsection{Django} +\par Ayant déjà travaillé avec le framework Spring et Java durant mon stage ST40, j'ai pu m'appuyer sur des notions +générales afin d'apprendre et de comprendre le fonctionnement du Python et de Django que je ne connaissais pas du tout. + +\par Mon apprentissage a été assez long au départ, car il y avait beaucoup d'informations à intégrer. Django est un +framework très pratique qui permet d'effectuer de nombreuses tâches assez rébarbatives de manière automatique certes, +mais encore faut-il comprendre ce qu'il se passe en arrière-plan. C'est cet apprentissage qui m'a pris le plus de temps. + +\par Mon deuxième point de difficulté a été les formulaires. Là encore, Django est très pratique dès lors qu'il s'agit +de faire un formulaire avec tous les champs d'un même Model. Cependant, il m'a fallu de l'aide et du temps pour +comprendre comment faire pour ajouter d'autres champs en plus de ceux du Model au formulaire et pour comprendre comment +récupérer les valeurs associées à chacun d'eux. + +\subsection{Git} +\par J'ai eu aussi à apprendre le fonctionnement de Git que j'avais déjà pu manipuler quelque peu mais il me manquait +quand même beaucoup d'éléments. + +\par Aujourd'hui, je pense pouvoir dire que j'ai progressé dans ce domaine mais il me reste encore bien des choses à +apprendre pour être capable de l'utiliser de manière efficace. + +\includepdf[pages={2}]{Couvertures.pdf} + +\end{document} + diff --git a/doc/TO_Skia_LoJ/slides/Makefile b/doc/TO_Skia_LoJ/slides/Makefile new file mode 100644 index 00000000..139c7ad0 --- /dev/null +++ b/doc/TO_Skia_LoJ/slides/Makefile @@ -0,0 +1,18 @@ +LATEX := pdflatex +TARGET := slide.pdf + +.PHONY: all clean distclean + +all: $(TARGET) clean + +%.pdf: %.tex + echo "Building pdf" + $(LATEX) --shell-escape $< + rm -f $@ + $(LATEX) --shell-escape $< + +clean: + rm -f *.log *.nav *.snm *.aux *.out *.toc *.pyg + +distclean: clean + rm -f $(TARGET) diff --git a/doc/TO_Skia_LoJ/slides/beamercolorthememetropolis.sty b/doc/TO_Skia_LoJ/slides/beamercolorthememetropolis.sty new file mode 100644 index 00000000..f7495179 --- /dev/null +++ b/doc/TO_Skia_LoJ/slides/beamercolorthememetropolis.sty @@ -0,0 +1,133 @@ +%% +%% This is file `beamercolorthememetropolis.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% beamercolorthememetropolis.dtx (with options: `package') +%% --------------------------------------------------------------------------- +%% Copyright 2015 Matthias Vogelgesang and the LaTeX community. A full list of +%% contributors can be found at +%% +%% https://github.com/matze/mtheme/graphs/contributors +%% +%% and the original template was based on the HSRM theme by Benjamin Weiss. +%% +%% This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 +%% International License (https://creativecommons.org/licenses/by-sa/4.0/). +%% --------------------------------------------------------------------------- +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{beamercolorthememetropolis}[2016/02/21 Metropolis color theme] +\RequirePackage{pgfopts} +\pgfkeys{ + /metropolis/color/block/.cd, + .is choice, + transparent/.code=\metropolis@block@transparent, + fill/.code=\metropolis@block@fill, +} +\pgfkeys{ + /metropolis/color/background/.cd, + .is choice, + dark/.code=\metropolis@colors@dark, + light/.code=\metropolis@colors@light, +} +\newcommand{\metropolis@color@setdefaults}{ + \pgfkeys{/metropolis/color/.cd, + background=light, + block=transparent, + } +} +\definecolor{mDarkBrown}{HTML}{604c38} +\definecolor{mDarkTeal}{HTML}{23373b} +\definecolor{mLightBrown}{HTML}{EB811B} +\definecolor{mLightGreen}{HTML}{14B03D} +\newcommand{\metropolis@colors@dark}{ + \setbeamercolor{normal text}{% + fg=black!2, + bg=mDarkTeal + } +} +\newcommand{\metropolis@colors@light}{ + \setbeamercolor{normal text}{% + fg=mDarkTeal, + bg=black!2 + } +} +\setbeamercolor{alerted text}{% + fg=mLightBrown +} +\setbeamercolor{example text}{% + fg=mLightGreen +} +\setbeamercolor{titlelike}{use=normal text, parent=normal text} +\setbeamercolor{author}{use=normal text, parent=normal text} +\setbeamercolor{date}{use=normal text, parent=normal text} +\setbeamercolor{institute}{use=normal text, parent=normal text} +\setbeamercolor{structure}{use=normal text, fg=normal text.fg} +\setbeamercolor{palette primary}{% + use=normal text, + fg=normal text.bg, + bg=normal text.fg +} +\setbeamercolor{frametitle}{% + use=palette primary, + parent=palette primary +} +\setbeamercolor{progress bar}{% + use=alerted text, + fg=alerted text.fg, + bg=alerted text.fg!50!black!30 +} +\setbeamercolor{title separator}{ + use=progress bar, + parent=progress bar +} +\setbeamercolor{progress bar in head/foot}{% + use=progress bar, + parent=progress bar +} +\setbeamercolor{progress bar in section page}{ + use=progress bar, + parent=progress bar +} +\newcommand{\metropolis@block@transparent}{ + \setbeamercolor{block title}{% + use=normal text, + fg=normal text.fg, + bg= + } + \setbeamercolor{block body}{ + bg= + } +} +\newcommand{\metropolis@block@fill}{ + \setbeamercolor{block title}{% + use=normal text, + fg=normal text.fg, + bg=normal text.bg!80!fg + } + \setbeamercolor{block body}{ + use={block title, normal text}, + bg=block title.bg!50!normal text.bg + } +} +\setbeamercolor{block title alerted}{% + use={block title, alerted text}, + bg=block title.bg, + fg=alerted text.fg +} +\setbeamercolor{block title example}{% + use={block title, example text}, + bg=block title.bg, + fg=example text.fg +} +\setbeamercolor{block body alerted}{use=block body, parent=block body} +\setbeamercolor{block body example}{use=block body, parent=block body} +\setbeamercolor{footnote}{fg=normal text.fg!90} +\setbeamercolor{footnote mark}{fg=.} +\metropolis@color@setdefaults +\ProcessPgfPackageOptions{/metropolis/color} +\mode +\endinput +%% +%% End of file `beamercolorthememetropolis.sty'. diff --git a/doc/TO_Skia_LoJ/slides/beamerfontthememetropolis.sty b/doc/TO_Skia_LoJ/slides/beamerfontthememetropolis.sty new file mode 100644 index 00000000..bb0b11f6 --- /dev/null +++ b/doc/TO_Skia_LoJ/slides/beamerfontthememetropolis.sty @@ -0,0 +1,283 @@ +%% +%% This is file `beamerfontthememetropolis.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% beamerfontthememetropolis.dtx (with options: `package') +%% --------------------------------------------------------------------------- +%% Copyright 2015 Matthias Vogelgesang and the LaTeX community. A full list of +%% contributors can be found at +%% +%% https://github.com/matze/mtheme/graphs/contributors +%% +%% and the original template was based on the HSRM theme by Benjamin Weiss. +%% +%% This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 +%% International License (https://creativecommons.org/licenses/by-sa/4.0/). +%% --------------------------------------------------------------------------- +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{beamerfontthememetropolis}[2016/02/21 Metropolis font theme] +\RequirePackage{etoolbox} +\RequirePackage{ifxetex} +\RequirePackage{ifluatex} +\RequirePackage{pgfopts} +\ifboolexpr{bool {xetex} or bool {luatex}}{ + \RequirePackage[no-math]{fontspec} + \newcounter{fontsnotfound} + \newcommand{\checkfont}[1]{% + \suppressfontnotfounderror=1% + \font\x = "#1" at 10pt + \selectfont + \ifx\x\nullfont% + \stepcounter{fontsnotfound}% + \fi% + \suppressfontnotfounderror=0% + } + + \newcommand{\iffontsavailable}[3]{% + \setcounter{fontsnotfound}{0}% + \expandafter\forcsvlist\expandafter% + \checkfont\expandafter{#1}% + \ifnum\value{fontsnotfound}=0% + #2% + \else% + #3% + \fi% + } + \iffontsavailable{Fira Sans Light,% + Fira Sans Light Italic,% + Fira Sans,% + Fira Sans Italic}% + {% + \setsansfont[ItalicFont={Fira Sans Light Italic},% + BoldFont={Fira Sans},% + BoldItalicFont={Fira Sans Italic}]% + {Fira Sans Light}% + }{% + \iffontsavailable{Fira Sans Light OT,% + Fira Sans Light Italic OT,% + Fira Sans OT,% + Fira Sans Italic OT}% + {% + \setsansfont[ItalicFont={Fira Sans Light Italic OT},% + BoldFont={Fira Sans OT},% + BoldItalicFont={Fira Sans Italic OT}]% + {Fira Sans Light OT}% + }{% + \PackageWarning{beamerthememetropolis}{% + Could not find Fira Sans fonts% + } + } + } + \iffontsavailable{Fira Mono, Fira Mono Bold}{% + \setmonofont[BoldFont={Fira Mono Medium}]{Fira Mono}% + }{% + \iffontsavailable{Fira Mono OT, Fira Mono Bold OT}{% + \setmonofont[BoldFont={Fira Mono Medium OT}]{Fira Mono OT}% + }{% + \PackageWarning{beamerthememetropolis}{% + Could not find Fira Mono fonts% + } + } + } + \AtBeginEnvironment{tabular}{% + \addfontfeature{Numbers={Monospaced}}% + } +}{% + \PackageWarning{beamerthememetropolis}{% + You need to compile with XeLaTeX or LuaLaTeX to use the Fira fonts% + } +} +\setbeamerfont{title}{size=\Large,% + series=\bfseries} +\setbeamerfont{author}{size=\small} +\setbeamerfont{date}{size=\small} +\setbeamerfont{section title}{size=\Large,% + series=\bfseries} +\setbeamerfont{block title}{size=\normalsize,% + series=\bfseries} +\setbeamerfont{block title alerted}{size=\normalsize,% + series=\bfseries} +\setbeamerfont*{subtitle}{size=\large} +\setbeamerfont{frametitle}{size=\large,% + series=\bfseries} +\setbeamerfont{caption}{size=\small} +\setbeamerfont{caption name}{series=\bfseries} +\setbeamerfont{description item}{series=\bfseries} +\setbeamerfont{page number in head/foot}{size=\scriptsize} +\setbeamerfont{bibliography entry author}{size=\normalsize,% + series=\normalfont} +\setbeamerfont{bibliography entry title}{size=\normalsize,% + series=\bfseries} +\setbeamerfont{bibliography entry location}{size=\normalsize,% + series=\normalfont} +\setbeamerfont{bibliography entry note}{size=\small,% + series=\normalfont} +\setbeamerfont{standout}{size=\Large,% + series=\bfseries} +\pgfkeys{ + /metropolis/font/titleformat title/.cd, + .is choice, + regular/.code={% + \let\metropolis@titleformat\@empty% + \setbeamerfont{title}{shape=\normalfont}% + }, + smallcaps/.code={% + \let\metropolis@titleformat\@empty% + \setbeamerfont{title}{shape=\scshape}% + }, + allsmallcaps/.code={% + \let\metropolis@titleformat\lowercase% + \setbeamerfont{title}{shape=\scshape}% + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat title=allsmallcaps can lead to problems% + } + }, + allcaps/.code={% + \let\metropolis@titleformat\uppercase% + \setbeamerfont{title}{shape=\normalfont} + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat title=allcaps can lead to problems% + } + }, +} +\pgfkeys{ + /metropolis/font/titleformat subtitle/.cd, + .is choice, + regular/.code={% + \let\metropolis@subtitleformat\@empty% + \setbeamerfont{subtitle}{shape=\normalfont}% + }, + smallcaps/.code={% + \let\metropolis@subtitleformat\@empty% + \setbeamerfont{subtitle}{shape=\scshape}% + }, + allsmallcaps/.code={% + \let\metropolis@subtitleformat\lowercase% + \setbeamerfont{subtitle}{shape=\scshape}% + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat subtitle=allsmallcaps can lead to problems% + } + }, + allcaps/.code={% + \let\metropolis@subtitleformat\uppercase% + \setbeamerfont{subtitle}{shape=\normalfont}% + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat subtitle=allcaps can lead to problems% + } + }, +} +\pgfkeys{ + /metropolis/font/titleformat section/.cd, + .is choice, + regular/.code={% + \let\metropolis@sectiontitleformat\@empty% + \setbeamerfont{section title}{shape=\normalfont}% + }, + smallcaps/.code={% + \let\metropolis@sectiontitleformat\@empty% + \setbeamerfont{section title}{shape=\scshape}% + }, + allsmallcaps/.code={% + \let\metropolis@sectiontitleformat\MakeLowercase% + \setbeamerfont{section title}{shape=\scshape}% + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat section=allsmallcaps can lead to problems% + } + }, + allcaps/.code={% + \let\metropolis@sectiontitleformat\MakeUppercase% + \setbeamerfont{section title}{shape=\normalfont}% + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat section=allcaps can lead to problems% + } + }, +} +\pgfkeys{ + /metropolis/font/titleformat frame/.cd, + .is choice, + regular/.code={% + \let\metropolis@frametitleformat\@empty% + \setbeamerfont{frametitle}{shape=\normalfont}% + }, + smallcaps/.code={% + \let\metropolis@frametitleformat\@empty% + \setbeamerfont{frametitle}{shape=\scshape}% + }, + allsmallcaps/.code={% + \let\metropolis@frametitleformat\MakeLowercase% + \setbeamerfont{frametitle}{shape=\scshape}% + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat frame=allsmallcaps can lead to problems% + } + }, + allcaps/.code={% + \let\metropolis@frametitleformat\MakeUppercase% + \setbeamerfont{frametitle}{shape=\normalfont} + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat frame=allcaps can lead to problems% + } + }, +} +\pgfkeys{ + /metropolis/font/.cd, + titleformattitle/.code=\pgfkeysalso{titleformat title=#1}, + titleformatsubtitle/.code=\pgfkeysalso{titleformat subtitle=#1}, + titleformatsection/.code=\pgfkeysalso{titleformat section=#1}, + titleformatframe/.code=\pgfkeysalso{titleformat frame=#1}, +} +\newcommand{\metropolis@font@setdefaults}{ + \pgfkeys{/metropolis/font/.cd, + titleformat title=regular, + titleformat subtitle=regular, + titleformat section=regular, + titleformat frame=regular, + } +} +\def\metropolis@titleformat#1{#1} +\def\metropolis@subtitleformat#1{#1} +\def\metropolis@sectiontitleformat#1{#1} +\def\metropolis@frametitleformat#1{#1} +\patchcmd{\beamer@title}% + {\def\inserttitle{#2}}% + {\def\inserttitle{\metropolis@titleformat{#2}}}% + {}% + {\PackageError{beamerfontthememetropolis}{Patching title failed}} +\patchcmd{\beamer@subtitle}% + {\def\insertsubtitle{#2}}% + {\def\insertsubtitle{\metropolis@subtitleformat{#2}}}% + {}% + {\PackageError{beamerfontthememetropolis}{Patching subtitle failed}} +\patchcmd{\sectionentry} + {\def\insertsectionhead{#2}} + {\def\insertsectionhead{\metropolis@sectiontitleformat{#2}}} + {} + {\PackageError{beamerfontthememetropolis}{Patching section title failed}} +\patchcmd{\beamer@section} + {\def\insertsectionhead{\hyperlink{Navigation\the\c@page}{#1}}} + {\def\insertsectionhead{\hyperlink{Navigation\the\c@page}{% + \metropolis@sectiontitleformat{#1}}}} + {} + {\PackageError{beamerfontthememetropolis}{Patching section title failed}} +\patchcmd{\beamer@@frametitle} + {\beamer@ifempty{#2}{}{% + \gdef\insertframetitle{{#2\ifnum\beamer@autobreakcount>0\relax{}\space% + \usebeamertemplate*{frametitle continuation}\fi}}% + \gdef\beamer@frametitle{#2}% + \gdef\beamer@shortframetitle{#1}% + }} + {\beamer@ifempty{#2}{}{% + \gdef\insertframetitle{{\metropolis@frametitleformat{#2}\ifnum% + \beamer@autobreakcount>0\relax{}\space% + \usebeamertemplate*{frametitle continuation}\fi}}% + \gdef\beamer@frametitle{#2}% + \gdef\beamer@shortframetitle{#1}% + }} + {} + {\PackageError{beamerfontthememetropolis}{Patching frame title failed}} +\metropolis@font@setdefaults +\ProcessPgfPackageOptions{/metropolis/font} +\endinput +%% +%% End of file `beamerfontthememetropolis.sty'. diff --git a/doc/TO_Skia_LoJ/slides/beamerinnerthememetropolis.sty b/doc/TO_Skia_LoJ/slides/beamerinnerthememetropolis.sty new file mode 100644 index 00000000..2842ccf0 --- /dev/null +++ b/doc/TO_Skia_LoJ/slides/beamerinnerthememetropolis.sty @@ -0,0 +1,281 @@ +%% +%% This is file `beamerinnerthememetropolis.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% beamerinnerthememetropolis.dtx (with options: `package') +%% --------------------------------------------------------------------------- +%% Copyright 2015 Matthias Vogelgesang and the LaTeX community. A full list of +%% contributors can be found at +%% +%% https://github.com/matze/mtheme/graphs/contributors +%% +%% and the original template was based on the HSRM theme by Benjamin Weiss. +%% +%% This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 +%% International License (https://creativecommons.org/licenses/by-sa/4.0/). +%% --------------------------------------------------------------------------- +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{beamerinnerthememetropolis}[2016/02/21 Metropolis inner theme] +\RequirePackage{etoolbox} +\RequirePackage{keyval} +\RequirePackage{calc} +\RequirePackage{pgfopts} +\RequirePackage{tikz} +\pgfkeys{ + /metropolis/inner/sectionpage/.cd, + .is choice, + none/.code=\metropolis@disablesectionpage, + simple/.code={\metropolis@enablesectionpage + \setbeamertemplate{section page}[simple]}, + progressbar/.code={\metropolis@enablesectionpage + \setbeamertemplate{section page}[progressbar]}, +} +\pgfkeys{ + /metropolis/inner/subsectionpage/.cd, + .is choice, + none/.code=\metropolis@disablesubsectionpage, + simple/.code={\metropolis@enablesubsectionpage + \setbeamertemplate{section page}[simple]}, + progressbar/.code={\metropolis@enablesubsectionpage + \setbeamertemplate{section page}[progressbar]}, +} +\newcommand{\metropolis@inner@setdefaults}{ + \pgfkeys{/metropolis/inner/.cd, + sectionpage=progressbar, + subsectionpage=none + } +} +\setbeamertemplate{title page}{ + \begin{minipage}[b][\paperheight]{\textwidth} + \ifx\inserttitlegraphic\@empty\else\usebeamertemplate*{title graphic}\fi + \vfill% + \ifx\inserttitle\@empty\else\usebeamertemplate*{title}\fi + \ifx\insertsubtitle\@empty\else\usebeamertemplate*{subtitle}\fi + \usebeamertemplate*{title separator} + \ifx\beamer@shortauthor\@empty\else\usebeamertemplate*{author}\fi + \ifx\insertdate\@empty\else\usebeamertemplate*{date}\fi + \ifx\insertinstitute\@empty\else\usebeamertemplate*{institute}\fi + \vfill + \vspace*{1mm} + \end{minipage} +} +\def\maketitle{% + \ifbeamer@inframe + \titlepage + \else + \frame[plain,noframenumbering]{\titlepage} + \fi +} +\def\titlepage{% + \usebeamertemplate{title page} +} +\setbeamertemplate{title graphic}{ + \vbox to 0pt { + \vspace*{2em} + \inserttitlegraphic% + }% + \nointerlineskip% +} +\setbeamertemplate{title}{ + \raggedright% + \linespread{1.0}% + \inserttitle% + \par% + \vspace*{0.5em} +} +\setbeamertemplate{subtitle}{ + \insertsubtitle% + \par% + \vspace*{0.5em} +} +\setbeamertemplate{title separator}{ + \begin{tikzpicture} + \draw[fg, fill=fg] (0,0) rectangle (\textwidth, 0.4pt); + \end{tikzpicture}% + \par% +} +\setbeamertemplate{author}{ + \vspace*{2em} + \insertauthor% + \par% + \vspace*{0.25em} +} +\setbeamertemplate{date}{ + \insertdate% + \par% +} +\setbeamertemplate{institute}{ + \vspace*{3mm} + \insertinstitute% + \par% +} +\defbeamertemplate{section page}{simple}{ + \begin{center} + \usebeamercolor[fg]{section title} + \usebeamerfont{section title} + \insertsectionhead\par + \ifx\insertsubsection\@empty\else + \usebeamercolor[fg]{subsection title} + \usebeamerfont{subsection title} + \insertsubsection + \fi + \end{center} +} +\defbeamertemplate{section page}{progressbar}{ + \centering + \begin{minipage}{22em} + \raggedright + \usebeamercolor[fg]{section title} + \usebeamerfont{section title} + \insertsectionhead\\[-1ex] + \usebeamertemplate*{progress bar in section page} + \par + \ifx\insertsubsection\@empty\else% + \usebeamercolor[fg]{subsection title}% + \usebeamerfont{subsection title}% + \insertsubsection + \fi + \end{minipage} + \par + \vspace{\baselineskip} +} +\newcommand{\metropolis@disablesectionpage}{ + \AtBeginSection{ + % intentionally empty + } +} +\newcommand{\metropolis@enablesectionpage}{ + \AtBeginSection{ + \ifbeamer@inframe + \sectionpage + \else + \frame[plain,c,noframenumbering]{\sectionpage} + \fi + } +} +\setbeamertemplate{subsection page}{% + \usebeamertemplate*{section page} +} +\newcommand{\metropolis@disablesubsectionpage}{ + \AtBeginSubsection{ + % intentionally empty + } +} +\newcommand{\metropolis@enablesubsectionpage}{ + \AtBeginSubsection{ + \ifbeamer@inframe + \subsectionpage + \else + \frame[plain,c,noframenumbering]{\subsectionpage} + \fi + } +} +\newlength{\metropolis@progressonsectionpage} +\setbeamertemplate{progress bar in section page}{ + \setlength{\metropolis@progressonsectionpage}{% + \textwidth * \ratio{\insertframenumber pt}{\inserttotalframenumber pt}% + }% + \begin{tikzpicture} + \draw[bg, fill=bg] (0,0) rectangle (\textwidth, 0.4pt); + \draw[fg, fill=fg] (0,0) rectangle (\metropolis@progressonsectionpage, 0.4pt); + \end{tikzpicture}% +} +\def\inserttotalframenumber{100} +\newlength{\metropolis@blocksep} +\newlength{\metropolis@blockadjust} +\setlength{\metropolis@blocksep}{0.75ex} +\setlength{\metropolis@blockadjust}{0.25ex} +\providecommand{\metropolis@strut}{% + \vphantom{ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz()}% +} +\newcommand{\metropolis@block}[1]{ + \par\vskip\medskipamount% + \setlength{\parskip}{0pt} + \ifbeamercolorempty[bg]{block title#1}{% + \begin{beamercolorbox}[rightskip=0pt plus 4em]{block title#1}}{% + \ifbeamercolorempty[bg]{block title}{% + \begin{beamercolorbox}[rightskip=0pt plus 4em]{block title#1}% + }% + {% + \begin{beamercolorbox}[ + sep=\dimexpr\metropolis@blocksep-\metropolis@blockadjust\relax, + leftskip=\metropolis@blockadjust, + rightskip=\dimexpr\metropolis@blockadjust plus 4em\relax + ]{block title#1}% + }}% + \usebeamerfont*{block title#1}% + \metropolis@strut% + \insertblocktitle% + \metropolis@strut% + \end{beamercolorbox}% + \nointerlineskip% + \ifbeamercolorempty[bg]{block body#1}{% + \begin{beamercolorbox}[vmode]{block body#1}}{ + \ifbeamercolorempty[bg]{block body}{% + \begin{beamercolorbox}[vmode]{block body#1}% + }{% + \begin{beamercolorbox}[sep=\metropolis@blocksep, vmode]{block body#1}% + \vspace{-\metropolis@parskip} + }}% + \usebeamerfont{block body#1}% + \setlength{\parskip}{\metropolis@parskip}% +} +\setbeamertemplate{block begin}{\metropolis@block{}} +\setbeamertemplate{block alerted begin}{\metropolis@block{ alerted}} +\setbeamertemplate{block example begin}{\metropolis@block{ example}} +\setbeamertemplate{block end}{\end{beamercolorbox}\vspace*{0.2ex}} +\setbeamertemplate{block alerted end}{\end{beamercolorbox}\vspace*{0.2ex}} +\setbeamertemplate{block example end}{\end{beamercolorbox}\vspace*{0.2ex}} +\setbeamertemplate{itemize items}{\textbullet} +\setbeamertemplate{caption label separator}{: } +\setbeamertemplate{caption}[numbered] +\setbeamertemplate{footnote}{% + \parindent 0em\noindent% + \raggedright + \usebeamercolor{footnote}\hbox to 0.8em{\hfil\insertfootnotemark}\insertfootnotetext\par% +} +\newlength{\metropolis@parskip} +\setlength{\metropolis@parskip}{0.5em} +\setlength{\parskip}{\metropolis@parskip} +\linespread{1.15} +\define@key{beamerframe}{c}[true]{% centered + \beamer@frametopskip=0pt plus 1fill\relax% + \beamer@framebottomskip=0pt plus 1fill\relax% + \beamer@frametopskipautobreak=0pt plus .4\paperheight\relax% + \beamer@framebottomskipautobreak=0pt plus .6\paperheight\relax% + \def\beamer@initfirstlineunskip{}% +} +\providebool{metropolis@standout} +\define@key{beamerframe}{standout}[true]{% + \booltrue{metropolis@standout} + \begingroup + \setkeys{beamerframe}{c} + \setkeys{beamerframe}{noframenumbering} + \ifbeamercolorempty[bg]{palette primary}{ + \setbeamercolor{background canvas}{ + use=palette primary, + bg=-palette primary.fg + } + }{ + \setbeamercolor{background canvas}{ + use=palette primary, + bg=palette primary.bg + } + } + \centering + \usebeamercolor[fg]{palette primary} + \usebeamerfont{standout} +} + \apptocmd{\beamer@reseteecodes}{% + \ifbool{metropolis@standout}{ + \endgroup + \boolfalse{metropolis@standout} + }{} + }{}{} +\metropolis@inner@setdefaults +\ProcessPgfPackageOptions{/metropolis/inner} +\endinput +%% +%% End of file `beamerinnerthememetropolis.sty'. diff --git a/doc/TO_Skia_LoJ/slides/beamerouterthememetropolis.sty b/doc/TO_Skia_LoJ/slides/beamerouterthememetropolis.sty new file mode 100644 index 00000000..a168d14f --- /dev/null +++ b/doc/TO_Skia_LoJ/slides/beamerouterthememetropolis.sty @@ -0,0 +1,126 @@ +%% +%% This is file `beamerouterthememetropolis.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% beamerouterthememetropolis.dtx (with options: `package') +%% --------------------------------------------------------------------------- +%% Copyright 2015 Matthias Vogelgesang and the LaTeX community. A full list of +%% contributors can be found at +%% +%% https://github.com/matze/mtheme/graphs/contributors +%% +%% and the original template was based on the HSRM theme by Benjamin Weiss. +%% +%% This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 +%% International License (https://creativecommons.org/licenses/by-sa/4.0/). +%% --------------------------------------------------------------------------- +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{beamerouterthememetropolis}[2016/02/21 Metropolis outer theme] +\RequirePackage{etoolbox} +\RequirePackage{calc} +\RequirePackage{pgfopts} +\pgfkeys{ + /metropolis/outer/numbering/.cd, + .is choice, + none/.code=\setbeamertemplate{frame numbering}[none], + counter/.code=\setbeamertemplate{frame numbering}[counter], + fraction/.code=\setbeamertemplate{frame numbering}[fraction], +} +\pgfkeys{ + /metropolis/outer/progressbar/.cd, + .is choice, + none/.code={% + \setbeamertemplate{headline}[plain] + \setbeamertemplate{frametitle}[plain] + \setbeamertemplate{footline}[plain] + }, + head/.code={\pgfkeys{/metropolis/outer/progressbar=none} + \addtobeamertemplate{headline}{}{% + \usebeamertemplate*{progress bar in head/foot} + } + }, + frametitle/.code={\pgfkeys{/metropolis/outer/progressbar=none} + \addtobeamertemplate{frametitle}{}{% + \usebeamertemplate*{progress bar in head/foot} + } + }, + foot/.code={\pgfkeys{/metropolis/outer/progressbar=none} + \addtobeamertemplate{footline}{}{% + \usebeamertemplate*{progress bar in head/foot}% + } + }, +} +\newcommand{\metropolis@outer@setdefaults}{ + \pgfkeys{/metropolis/outer/.cd, + numbering=counter, + progressbar=none, + } +} +\setbeamertemplate{navigation symbols}{} +\defbeamertemplate{frame footer}{none}{} +\defbeamertemplate{frame footer}{custom}[1]{ #1 } +\defbeamertemplate{frame numbering}{none}{} +\defbeamertemplate{frame numbering}{counter}{\insertframenumber} +\defbeamertemplate{frame numbering}{fraction}{ + \insertframenumber/\inserttotalframenumber +} +\defbeamertemplate{headline}{plain}{} +\defbeamertemplate{footline}{plain}{% + \begin{beamercolorbox}[wd=\textwidth, sep=3ex]{footline}% + \usebeamerfont{page number in head/foot}% + \usebeamertemplate*{frame footer} + \hfill% + \usebeamertemplate*{frame numbering} + \end{beamercolorbox}% +} +\newlength{\metropolis@frametitle@padding} +\setlength{\metropolis@frametitle@padding}{2.2ex} +\newcommand{\metropolis@frametitlestrut@start}{ + \rule{0pt}{\metropolis@frametitle@padding +% + \totalheightof{% + \ifcsdef{metropolis@frametitleformat}{\metropolis@frametitleformat X}{X}% + }% + }% +} +\newcommand{\metropolis@frametitlestrut@end}{ + \rule[-\metropolis@frametitle@padding]{0pt}{\metropolis@frametitle@padding} +} +\defbeamertemplate{frametitle}{plain}{% + \nointerlineskip% + \begin{beamercolorbox}[% + wd=\paperwidth,% + sep=0pt,% + leftskip=\metropolis@frametitle@padding,% + rightskip=\metropolis@frametitle@padding,% + ]{frametitle}% + \metropolis@frametitlestrut@start\insertframetitle\metropolis@frametitlestrut@end% + \end{beamercolorbox}% +} +\newlength{\metropolis@progressinheadfoot} +\setbeamertemplate{progress bar in head/foot}{ + \nointerlineskip + \setlength{\metropolis@progressinheadfoot}{% + \paperwidth * \ratio{\insertframenumber pt}{\inserttotalframenumber pt}% + }% + \begin{beamercolorbox}[wd=\paperwidth]{progress bar in head/foot} + \begin{tikzpicture} + \draw[bg, fill=bg] (0,0) rectangle (\paperwidth, 0.4pt); + \draw[fg, fill=fg] (0,0) rectangle (\metropolis@progressinheadfoot, 0.4pt); + \end{tikzpicture}% + \end{beamercolorbox} +} +\AtBeginDocument{% + \apptocmd{\appendix}{% + \pgfkeys{% + /metropolis/outer/.cd, + numbering=none, + progressbar=none} + }{}{} +} +\metropolis@outer@setdefaults +\ProcessPgfPackageOptions{/metropolis/outer} +\endinput +%% +%% End of file `beamerouterthememetropolis.sty'. diff --git a/doc/TO_Skia_LoJ/slides/beamerthememetropolis.sty b/doc/TO_Skia_LoJ/slides/beamerthememetropolis.sty new file mode 100644 index 00000000..d5e7b80d --- /dev/null +++ b/doc/TO_Skia_LoJ/slides/beamerthememetropolis.sty @@ -0,0 +1,105 @@ +%% +%% This is file `beamerthememetropolis.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% beamerthememetropolis.dtx (with options: `package') +%% --------------------------------------------------------------------------- +%% Copyright 2015 Matthias Vogelgesang and the LaTeX community. A full list of +%% contributors can be found at +%% +%% https://github.com/matze/mtheme/graphs/contributors +%% +%% and the original template was based on the HSRM theme by Benjamin Weiss. +%% +%% This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 +%% International License (https://creativecommons.org/licenses/by-sa/4.0/). +%% --------------------------------------------------------------------------- +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{beamerthememetropolis} + [2016/02/21 v1.1 Metropolis Beamer theme] +\RequirePackage{etoolbox} +\RequirePackage{pgfopts} +\pgfkeys{/metropolis/.cd, + .search also={ + /metropolis/inner, + /metropolis/outer, + /metropolis/color, + /metropolis/font, + } +} +\pgfkeys{ + /metropolis/titleformat plain/.cd, + .is choice, + regular/.code={% + \let\metropolis@plaintitleformat\@empty% + \setbeamerfont{standout}{shape=\normalfont}% + }, + smallcaps/.code={% + \let\metropolis@plaintitleformat\@empty% + \setbeamerfont{standout}{shape=\scshape}% + }, + allsmallcaps/.code={% + \let\metropolis@plaintitleformat\MakeLowercase% + \setbeamerfont{standout}{shape=\scshape}% + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat plain=allsmallcaps can lead to problems% + } + }, + allcaps/.code={% + \let\metropolis@plaintitleformat\MakeUppercase% + \setbeamerfont{standout}{shape=\normalfont}% + \PackageWarning{beamerthememetropolis}{% + Be aware that titleformat plain=allcaps can lead to problems% + } + }, +} +\pgfkeys{ + /metropolis/titleformat/.code=\pgfkeysalso{ + font/titleformat title=#1, + font/titleformat subtitle=#1, + font/titleformat section=#1, + font/titleformat frame=#1, + titleformat plain=#1, + } +} +\pgfkeys{/metropolis/.cd, + usetitleprogressbar/.code=\pgfkeysalso{outer/progressbar=frametitle}, + noslidenumbers/.code=\pgfkeysalso{outer/numbering=none}, + usetotalslideindicator/.code=\pgfkeysalso{outer/numbering=fraction}, + nosectionslide/.code=\pgfkeysalso{inner/sectionpage=none}, + darkcolors/.code=\pgfkeysalso{color/background=dark}, + blockbg/.code=\pgfkeysalso{color/block=fill, inner/block=fill}, +} +\newcommand{\metropolis@setdefaults}{ + \pgfkeys{/metropolis/.cd, + titleformat plain=regular, + } +} +\useinnertheme{metropolis} +\useoutertheme{metropolis} +\usecolortheme{metropolis} +\usefonttheme{metropolis} +\AtEndPreamble{% + \@ifpackageloaded{pgfplots}{% + \RequirePackage{pgfplotsthemetol} + }{} +} +\newcommand{\metroset}[1]{\pgfkeys{/metropolis/.cd,#1}} +\def\metropolis@plaintitleformat#1{#1} +\newcommand{\plain}[2][]{% + \PackageWarning{beamerthememetropolis}{% + The syntax `\plain' may be deprecated in a future version of Metropolis. + Please use a frame with [standout] instead. + } + \begin{frame}[standout]{#1} + \metropolis@plaintitleformat{#2} + \end{frame} +} +\newcommand{\mreducelistspacing}{\vspace{-\topsep}} +\metropolis@setdefaults +\ProcessPgfOptions{/metropolis} +\endinput +%% +%% End of file `beamerthememetropolis.sty'. diff --git a/doc/TO_Skia_LoJ/slides/pgfplotsthemetol.sty b/doc/TO_Skia_LoJ/slides/pgfplotsthemetol.sty new file mode 100644 index 00000000..05f4383d --- /dev/null +++ b/doc/TO_Skia_LoJ/slides/pgfplotsthemetol.sty @@ -0,0 +1,123 @@ +%% +%% This is file `pgfplotsthemetol.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% pgfplotsthemetol.dtx (with options: `package') +%% --------------------------------------------------------------------------- +%% Copyright 2015 Matthias Vogelgesang and the LaTeX community. A full list of +%% contributors can be found at +%% +%% https://github.com/matze/mtheme/graphs/contributors +%% +%% and the original template was based on the HSRM theme by Benjamin Weiss. +%% +%% This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 +%% International License (https://creativecommons.org/licenses/by-sa/4.0/). +%% --------------------------------------------------------------------------- +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{pgfplotsthemetol} + [2015/06/16 PGFplots colors based on Paul Tol's SRON technical note] +\definecolor{TolDarkPurple}{HTML}{332288} +\definecolor{TolDarkBlue}{HTML}{6699CC} +\definecolor{TolLightBlue}{HTML}{88CCEE} +\definecolor{TolLightGreen}{HTML}{44AA99} +\definecolor{TolDarkGreen}{HTML}{117733} +\definecolor{TolDarkBrown}{HTML}{999933} +\definecolor{TolLightBrown}{HTML}{DDCC77} +\definecolor{TolDarkRed}{HTML}{661100} +\definecolor{TolLightRed}{HTML}{CC6677} +\definecolor{TolLightPink}{HTML}{AA4466} +\definecolor{TolDarkPink}{HTML}{882255} +\definecolor{TolLightPurple}{HTML}{AA4499} +\pgfplotscreateplotcyclelist{mbarplot cycle}{% + {draw=TolDarkBlue, fill=TolDarkBlue!70}, + {draw=TolLightBrown, fill=TolLightBrown!70}, + {draw=TolLightGreen, fill=TolLightGreen!70}, + {draw=TolDarkPink, fill=TolDarkPink!70}, + {draw=TolDarkPurple, fill=TolDarkPurple!70}, + {draw=TolDarkRed, fill=TolDarkRed!70}, + {draw=TolDarkBrown, fill=TolDarkBrown!70}, + {draw=TolLightRed, fill=TolLightRed!70}, + {draw=TolLightPink, fill=TolLightPink!70}, + {draw=TolLightPurple, fill=TolLightPurple!70}, + {draw=TolLightBlue, fill=TolLightBlue!70}, + {draw=TolDarkGreen, fill=TolDarkGreen!70}, +} +\pgfplotscreateplotcyclelist{mlineplot cycle}{% + {TolDarkBlue, mark=*, mark size=1.5pt}, + {TolLightBrown, mark=square*, mark size=1.3pt}, + {TolLightGreen, mark=triangle*, mark size=1.5pt}, + {TolDarkBrown, mark=diamond*, mark size=1.5pt}, +} +\pgfplotsset{ + compat=1.9, + mlineplot/.style={ + mbaseplot, + xmajorgrids=true, + ymajorgrids=true, + major grid style={dotted}, + axis x line=bottom, + axis y line=left, + legend style={ + cells={anchor=west}, + draw=none + }, + cycle list name=mlineplot cycle, + }, + mbarplot base/.style={ + mbaseplot, + bar width=6pt, + axis y line*=none, + }, + mbarplot/.style={ + mbarplot base, + ybar, + xmajorgrids=false, + ymajorgrids=true, + area legend, + legend image code/.code={% + \draw[#1] (0cm,-0.1cm) rectangle (0.15cm,0.1cm); + }, + cycle list name=mbarplot cycle, + }, + horizontal mbarplot/.style={ + mbarplot base, + xmajorgrids=true, + ymajorgrids=false, + xbar stacked, + area legend, + legend image code/.code={% + \draw[#1] (0cm,-0.1cm) rectangle (0.15cm,0.1cm); + }, + cycle list name=mbarplot cycle, + }, + mbaseplot/.style={ + legend style={ + draw=none, + fill=none, + cells={anchor=west}, + }, + x tick label style={ + font=\footnotesize + }, + y tick label style={ + font=\footnotesize + }, + legend style={ + font=\footnotesize + }, + major grid style={ + dotted, + }, + axis x line*=bottom, + }, + disable thousands separator/.style={ + /pgf/number format/.cd, + 1000 sep={} + }, +} +\endinput +%% +%% End of file `pgfplotsthemetol.sty'. diff --git a/doc/TO_Skia_LoJ/slides/slide.pdf b/doc/TO_Skia_LoJ/slides/slide.pdf new file mode 100644 index 00000000..6e8bd56d Binary files /dev/null and b/doc/TO_Skia_LoJ/slides/slide.pdf differ diff --git a/doc/TO_Skia_LoJ/slides/slide.tex b/doc/TO_Skia_LoJ/slides/slide.tex new file mode 100644 index 00000000..81e7cce1 --- /dev/null +++ b/doc/TO_Skia_LoJ/slides/slide.tex @@ -0,0 +1,158 @@ +\documentclass[10pt]{beamer} +\beamertemplatenavigationsymbolsempty + +\usepackage[utf8]{inputenc} +\usepackage{default} + +\usepackage{graphicx} +\graphicspath{{pictures/}} + +\usepackage[french]{babel} +\usepackage[T1]{fontenc} + +\usetheme{metropolis} +%\usecolortheme{dove} + +\begin{document} + +\begin{frame} + \frametitle{Université de Technologie de Belfort-Montbéliard\\ + Département informatique} + \vskip 4em + \begin{center} + {\LARGE Développement de nouveaux modules sur le projet Sith}\\ + \end{center} + \vskip 4em + Florent \textsc{Jacquet}\\ + Guillaume \textsc{Renaud}\\ + {\scriptsize TO52 - A16} +\end{frame} + +\begin{frame} + \frametitle{Sommaire} + \tableofcontents +\end{frame} + +\section{Les nouvelles applications} +\subsection{Eboutic} +\begin{frame}[fragile]\frametitle{Eboutic} + \begin{itemize} + \item Fournir une boutique + \item Paiement en ligne en lien avec l'API du Credit Agricole + \item Gestion des cotisations et rechargements + \item Attention aux accès concurrentiels: pas visibles pendant le développement, car mono-thread, mais problèmes + à la mise en production + \end{itemize} +\end{frame} + +\subsection{Le SAS} +\begin{frame}[fragile]\frametitle{Le SAS - Stock à Souvenirs} + \begin{itemize} + \item Galerie de photos + \item Upload simple pour tout le monde, même pour plusieurs dizaines de photos + \item Modération et gestion des droits basée sur la gestion des fichiers, ce qui a permis d'améliorer ces + derniers + \item Problèmes d'optimisation de certaines pages qui mettaient plus de 9 secondes à générer (plus que 2s + maintenant) + \end{itemize} +\end{frame} + +\subsection{Les élections} +\begin{frame}[fragile]\frametitle{Les élections} + \begin{itemize} + \item Grosse partie "gestion": c'est Sli qui a principalement développé l'application + \item Revue des \textsc{merges request} et choix de design + \item Problèmatique de législation vite ignorées puisque validation officielle en AG + \end{itemize} +\end{frame} + +\subsection{La laverie} +\begin{frame}[fragile]\frametitle{La laverie} + \begin{itemize} + \item Gestion d'un planning de reservation en prenant bien en compte les différents états (hors-service, ...) de + chaque machine + \item Génération de formulaires dynamiques en fonction des réservations (factory design pattern) + \end{itemize} +\end{frame} + +\subsection{La communication} +\begin{frame}[fragile]\frametitle{La communication} + \begin{itemize} + \item Dynamise le site avec tous les textes paramètrables + \item Fourni un système de news + \item Fourni une newsletter + \end{itemize} + \begin{itemize} + \item Envoie de mails en masse + \item Beaucoup de templates + \end{itemize} +\end{frame} + +\section{La gestion des stocks} + +\subsection{Fonctionnement} +\begin{frame}[fragile]{Fonctionnement} + \begin{itemize} + \item Création automatique des listes de courses + \item Approvisionnement des stocks + \item Prise d'éléments dans le stock + \end{itemize} +\end{frame} + +\subsection{Améliorations et difficultés} +\begin{frame}[fragile]\frametitle{Améliorations et difficultés} + \begin{itemize} + \item Mise à jour quantité liste de courses + \item Mise à jour automatique du stock selon les ventes + \item Ajout au système de notifications + \end{itemize} + \textbf{Difficultés} + \begin{itemize} + \item Découverte du design pattern "factory" pour les formulaires dynamiques + \item Apprentissage de Python, en plus du framework + \end{itemize} +\end{frame} + +\section{Le rôle de mainteneur} + +\subsection{Réviser les merge requests} +\begin{frame}[fragile]\frametitle{Réviser les merge requests} + \begin{itemize} + \item Long et fastidieux + \item Nécessaire pour maintenir une base de code cohérente + \item Permet de retrouver les bugs des nouveaux contributeurs + \item Oriente les contributeurs sur la bonne voie et la marche à suivre avec Django/Jinja2/etc... + \end{itemize} +\end{frame} + +\subsection{Gestion des bugs, des tickets, de la mise en production...} +\begin{frame}[fragile]\frametitle{Gestion des bugs, des tickets, de la mise en production...} + \begin{itemize} + \item Ouverture/fermeture des tickets + \item Mailing list/IRC + \item Mise en production, gestion des migrations + \item Restauration de la base de tests régulièrement + \end{itemize} + \par Organisation de la passation +\end{frame} + +\section{Conclusion} +\begin{frame}[fragile]\frametitle{Conclusion} + \begin{itemize} + \item Apprentissage Django/Git + \item Nouvelle mise en pratique des concepts de base de données relationnelles + \item Utilisation poussée de Gitlab + \item Formation de nouveaux contributeurs + \end{itemize} +\end{frame} + + +\begin{frame}[fragile] + \begin{center} + \textbf{Merci de votre attention}\\ + Questions?\\ + Remarques?\\ + \end{center} +\end{frame} + +\end{document} diff --git a/doc/header b/doc/header new file mode 100644 index 00000000..0a9419f8 --- /dev/null +++ b/doc/header @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/eboutic/__init__.py b/eboutic/__init__.py index e69de29b..0a9419f8 100644 --- a/eboutic/__init__.py +++ b/eboutic/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/eboutic/admin.py b/eboutic/admin.py index 1e56ae1c..324a0175 100644 --- a/eboutic/admin.py +++ b/eboutic/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin from eboutic.models import * diff --git a/eboutic/models.py b/eboutic/models.py index 3549e103..63c2969e 100644 --- a/eboutic/models.py +++ b/eboutic/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.db import models, DataError from django.utils.translation import ugettext_lazy as _ from django.conf import settings diff --git a/eboutic/tests.py b/eboutic/tests.py index 7ce503c2..b8fbbe1e 100644 --- a/eboutic/tests.py +++ b/eboutic/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase # Create your tests here. diff --git a/eboutic/urls.py b/eboutic/urls.py index 56ff8975..bb964d1e 100644 --- a/eboutic/urls.py +++ b/eboutic/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from eboutic.views import * diff --git a/eboutic/views.py b/eboutic/views.py index 87b4ce00..58e518cb 100644 --- a/eboutic/views.py +++ b/eboutic/views.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from collections import OrderedDict from datetime import datetime import pytz @@ -80,7 +104,7 @@ class EbouticMain(TemplateView): kwargs['basket'] = self.basket kwargs['eboutic'] = Counter.objects.filter(type="EBOUTIC").first() kwargs['categories'] = ProductType.objects.all() - if not self.request.user.was_subscribed(): + if not self.request.user.was_subscribed: kwargs['categories'] = kwargs['categories'].exclude(id=settings.SITH_PRODUCTTYPE_SUBSCRIPTION) return kwargs diff --git a/election/models.py b/election/models.py index 635ec7e5..b2659424 100644 --- a/election/models.py +++ b/election/models.py @@ -85,18 +85,22 @@ class Role(models.Model): def results(self, total_vote): results = {} total_vote *= self.max_choice - if total_vote == 0: - return None non_blank = 0 for candidature in self.candidatures.all(): cand_results = {} cand_results['vote'] = self.votes.filter(candidature=candidature).count() - cand_results['percent'] = cand_results['vote'] * 100 / total_vote + if total_vote == 0: + cand_results['percent'] = 0 + else: + cand_results['percent'] = cand_results['vote'] * 100 / total_vote non_blank += cand_results['vote'] results[candidature.user.username] = cand_results results['total vote'] = total_vote - results['blank vote'] = {'vote': total_vote - non_blank, - 'percent': (total_vote - non_blank) * 100 / total_vote} + if total_vote == 0: + results['blank vote'] = {'vote': 0, 'percent': 0} + else: + results['blank vote'] = {'vote': total_vote - non_blank, + 'percent': (total_vote - non_blank) * 100 / total_vote} return results @property diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 777c00d8..5f2b4e57 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -301,7 +301,7 @@ th { {%- if election.is_vote_finished %} {%- set results = election_results[role.title]['blank vote'] %}
      - {{ results.vote }} {% trans %}votes{% endtrans %} ({{ results.percent }} %) + {{ results.vote }} {% trans %}votes{% endtrans %} ({{ "%.2f" % results.percent }} %)
      {%- endif %} @@ -318,12 +318,14 @@ th {
      {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} + {%- if not election.is_vote_finished %} {{ candidature.program or '' }} + {%- endif %} {%- if user.can_edit(candidature) -%} {% if election.is_vote_editable %} {% trans %}Edit{% endtrans %} {% endif %} - {% if election.can_candidate -%} + {% if election.is_vote_editable -%} {% trans %}Delete{% endtrans %} {%- endif -%} {%- endif -%} @@ -339,7 +341,7 @@ th { {%- if election.is_vote_finished %} {%- set results = election_results[role.title][candidature.user.username] %}
      - {{ results.vote }} {% trans %}votes{% endtrans %} ({{ results.percent }} %) + {{ results.vote }} {% trans %}votes{% endtrans %} ({{ "%.2f" % results.percent }} %)
      {%- endif %} @@ -362,7 +364,9 @@ th { {%- if (election.can_candidate(user) and election.is_candidature_active) or (user.can_edit(election) and election.is_vote_editable) %} {% trans %}Candidate{% endtrans %} {%- endif %} + {%- if election.is_vote_editable %} {% trans %}Add a new list{% endtrans %} + {%- endif %} {%- if user.can_edit(election) %} {% if election.is_vote_editable %} {% trans %}Add a new role{% endtrans %} diff --git a/election/views.py b/election/views.py index 11f933c3..2c4a8821 100644 --- a/election/views.py +++ b/election/views.py @@ -271,7 +271,7 @@ class ElectionCreateView(CanCreateMixin, CreateView): template_name = 'core/create.jinja' def dispatch(self, request, *args, **kwargs): - if not request.user.is_subscribed(): + if not request.user.is_subscribed: raise PermissionDenied return super(ElectionCreateView, self).dispatch(request, *args, **kwargs) @@ -472,7 +472,7 @@ class CandidatureDeleteView(CanEditMixin, DeleteView): def dispatch(self, request, *arg, **kwargs): self.object = self.get_object() self.election = self.object.role.election - if not self.election.can_candidate: + if not self.election.can_candidate or not self.election.is_vote_editable: raise PermissionDenied return super(CandidatureDeleteView, self).dispatch(request, *arg, **kwargs) diff --git a/forum/__init__.py b/forum/__init__.py new file mode 100644 index 00000000..0a9419f8 --- /dev/null +++ b/forum/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/forum/admin.py b/forum/admin.py new file mode 100644 index 00000000..5db34b10 --- /dev/null +++ b/forum/admin.py @@ -0,0 +1,31 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.contrib import admin + +from forum.models import * + +admin.site.register(Forum) +admin.site.register(ForumTopic) +admin.site.register(ForumMessage) diff --git a/forum/migrations/0001_initial.py b/forum/migrations/0001_initial.py new file mode 100644 index 00000000..5e5a499c --- /dev/null +++ b/forum/migrations/0001_initial.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.utils.timezone import utc +from django.conf import settings +import django.utils.timezone +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('club', '0006_auto_20161229_0040'), + ('core', '0019_preferences_receive_weekmail'), + ] + + operations = [ + migrations.CreateModel( + name='Forum', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('name', models.CharField(max_length=64, verbose_name='name')), + ('description', models.CharField(max_length=256, verbose_name='description', default='')), + ('is_category', models.BooleanField(verbose_name='is a category', default=False)), + ('edit_groups', models.ManyToManyField(related_name='editable_forums', to='core.Group', blank=True, default=[4])), + ('owner_club', models.ForeignKey(to='club.Club', verbose_name='owner club', related_name='owned_forums', default=1)), + ('parent', models.ForeignKey(to='forum.Forum', null=True, related_name='children', blank=True)), + ('view_groups', models.ManyToManyField(related_name='viewable_forums', to='core.Group', blank=True, default=[2])), + ], + ), + migrations.CreateModel( + name='ForumMessage', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('title', models.CharField(max_length=64, blank=True, verbose_name='title', default='')), + ('message', models.TextField(verbose_name='message', default='')), + ('date', models.DateTimeField(verbose_name='date', default=django.utils.timezone.now)), + ('author', models.ForeignKey(related_name='forum_messages', to=settings.AUTH_USER_MODEL)), + ('readers', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='readers', related_name='read_messages')), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='ForumMessageMeta', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('date', models.DateTimeField(verbose_name='date', default=django.utils.timezone.now)), + ('action', models.CharField(max_length=16, choices=[('EDIT', 'Message edited by'), ('DELETE', 'Message deleted by'), ('UNDELETE', 'Message undeleted by')], verbose_name='action')), + ('message', models.ForeignKey(related_name='metas', to='forum.ForumMessage')), + ('user', models.ForeignKey(related_name='forum_message_metas', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ForumTopic', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('description', models.CharField(max_length=256, verbose_name='description', default='')), + ('author', models.ForeignKey(related_name='forum_topics', to=settings.AUTH_USER_MODEL)), + ('forum', models.ForeignKey(related_name='topics', to='forum.Forum')), + ], + options={ + 'ordering': ['-id'], + }, + ), + migrations.CreateModel( + name='ForumUserInfo', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('last_read_date', models.DateTimeField(verbose_name='last read date', default=datetime.datetime(1999, 1, 1, 0, 0, tzinfo=utc))), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='_forum_infos')), + ], + ), + migrations.AddField( + model_name='forummessage', + name='topic', + field=models.ForeignKey(related_name='messages', to='forum.ForumTopic'), + ), + ] diff --git a/forum/migrations/0002_auto_20170312_1753.py b/forum/migrations/0002_auto_20170312_1753.py new file mode 100644 index 00000000..fdd0a431 --- /dev/null +++ b/forum/migrations/0002_auto_20170312_1753.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('forum', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='forum', + options={'ordering': ['number']}, + ), + migrations.AddField( + model_name='forum', + name='number', + field=models.IntegerField(verbose_name='number to choose a specific forum ordering', default=1), + ), + migrations.AlterField( + model_name='forum', + name='edit_groups', + field=models.ManyToManyField(related_name='editable_forums', blank=True, to='core.Group', default=[331]), + ), + ] diff --git a/forum/migrations/__init__.py b/forum/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/forum/models.py b/forum/models.py new file mode 100644 index 00000000..0f2c3a8d --- /dev/null +++ b/forum/models.py @@ -0,0 +1,261 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.db import models +from django.core import validators +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ValidationError +from django.db import IntegrityError, transaction +from django.core.urlresolvers import reverse +from django.utils import timezone +from django.utils.functional import cached_property + +from datetime import datetime +import pytz + +from core.models import User, MetaGroup, Group, SithFile +from club.models import Club + +class Forum(models.Model): + """ + The Forum class, made as a tree to allow nice tidy organization + + owner_club allows club members to moderate there own topics + edit_groups allows to put any group as a forum admin + view_groups allows some groups to view a forum + """ + name = models.CharField(_('name'), max_length=64) + description = models.CharField(_('description'), max_length=256, default="") + is_category = models.BooleanField(_('is a category'), default=False) + parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True) + owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"), + default=settings.SITH_MAIN_CLUB_ID) + edit_groups = models.ManyToManyField(Group, related_name="editable_forums", blank=True, + default=[settings.SITH_GROUP_OLD_SUBSCRIBERS_ID]) + view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True, + default=[settings.SITH_GROUP_PUBLIC_ID]) + number = models.IntegerField(_("number to choose a specific forum ordering"), default=1) + + class Meta: + ordering = ['number'] + + def clean(self): + self.check_loop() + + def save(self, *args, **kwargs): + copy_rights = False + if self.id is None: + copy_rights = True + super(Forum, self).save(*args, **kwargs) + if copy_rights: + self.copy_rights() + + def apply_rights_recursively(self): + children = self.children.all() + for c in children: + c.copy_rights() + c.apply_rights_recursively() + + def copy_rights(self): + """Copy, if possible, the rights of the parent folder""" + if self.parent is not None: + self.owner_club = self.parent.owner_club + self.edit_groups = self.parent.edit_groups.all() + self.view_groups = self.parent.view_groups.all() + self.save() + + def is_owned_by(self, user): + if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID): + return True + m = self.owner_club.get_membership_for(user) + if m: + return m.role > settings.SITH_MAXIMUM_FREE_ROLE + return False + + def check_loop(self): + """Raise a validation error when a loop is found within the parent list""" + objs = [] + cur = self + while cur.parent is not None: + if cur in objs: + raise ValidationError(_('You can not make loops in forums')) + objs.append(cur) + cur = cur.parent + + def __str__(self): + return "%s" % (self.name) + + def get_absolute_url(self): + return reverse('forum:view_forum', kwargs={'forum_id': self.id}) + + @cached_property + def parent_list(self): + return self.get_parent_list() + + def get_parent_list(self): + l = [] + p = self.parent + while p is not None: + l.append(p) + p = p.parent + return l + + @cached_property + def topic_number(self): + return self.get_topic_number() + + def get_topic_number(self): + number = self.topics.all().count() + for c in self.children.all(): + number += c.topic_number + return number + + @cached_property + def last_message(self): + return self.get_last_message() + + def get_last_message(self): + last_msg = None + for m in ForumMessage.objects.select_related('topic__forum', 'author').order_by('-id'): + forum = m.topic.forum + if self in (forum.parent_list + [forum]) and not m.deleted: + return m + +class ForumTopic(models.Model): + forum = models.ForeignKey(Forum, related_name='topics') + author = models.ForeignKey(User, related_name='forum_topics') + description = models.CharField(_('description'), max_length=256, default="") + + class Meta: + ordering = ['-id'] # TODO: add date message ordering + + def is_owned_by(self, user): + return self.forum.is_owned_by(user) + + def can_be_edited_by(self, user): + return user.can_edit(self.forum) + + def can_be_viewed_by(self, user): + return user.can_view(self.forum) + + def __str__(self): + return "%s" % (self.title) + + def get_absolute_url(self): + return reverse('forum:view_topic', kwargs={'topic_id': self.id}) + + def get_first_unread_message(self, user): + try: + msg = self.messages.exclude(readers=user).filter(date__gte=user.forum_infos.last_read_date).order_by('id').first() + return msg + except: + return None + + @cached_property + def last_message(self): + for msg in self.messages.order_by('id').select_related('author').order_by('-id').all(): + if not msg.deleted: + return msg + + @property + def title(self): + return self.messages.order_by('date').first().title + +class ForumMessage(models.Model): + """ + "A ForumMessage object represents a message in the forum" -- Cpt. Obvious + """ + topic = models.ForeignKey(ForumTopic, related_name='messages') + author = models.ForeignKey(User, related_name='forum_messages') + title = models.CharField(_("title"), default="", max_length=64, blank=True) + message = models.TextField(_("message"), default="") + date = models.DateTimeField(_('date'), default=timezone.now) + readers = models.ManyToManyField(User, related_name="read_messages", verbose_name=_("readers")) + + class Meta: + ordering = ['id'] + + def __str__(self): + return "%s - %s" % (self.id, self.title) + + def is_owned_by(self, user): # Anyone can create a topic: it's better to + # check the rights at the forum level, since it's more controlled + return self.topic.forum.is_owned_by(user) or user.id == self.author.id + + def can_be_edited_by(self, user): + return user.can_edit(self.topic.forum) + + def can_be_viewed_by(self, user): + return (not self.deleted and user.can_view(self.topic)) + + def can_be_moderated_by(self, user): + return self.topic.forum.is_owned_by(user) or user.id == self.author.id + + def get_absolute_url(self): + return self.topic.get_absolute_url() + "?page=" + str(self.get_page()) + "#msg_" + str(self.id) + + def get_page(self): + return int(self.topic.messages.filter(id__lt=self.id).count() / settings.SITH_FORUM_PAGE_LENGTH) + 1 + + def mark_as_read(self, user): + try: # Need the try/except because of AnonymousUser + self.readers.add(user) + except: pass + + def is_read(self, user): + return (self.date < user.forum_infos.last_read_date) or (user in self.readers.all()) + + @cached_property + def deleted(self): + return self.is_deleted() + + def is_deleted(self): + meta = self.metas.exclude(action="EDIT").order_by('-date').first() + if meta: + return meta.action == "DELETE" + return False + +MESSAGE_META_ACTIONS = [ + ('EDIT', _("Message edited by")), + ('DELETE', _("Message deleted by")), + ('UNDELETE', _("Message undeleted by")), + ] + +class ForumMessageMeta(models.Model): + user = models.ForeignKey(User, related_name="forum_message_metas") + message = models.ForeignKey(ForumMessage, related_name="metas") + date = models.DateTimeField(_('date'), default=timezone.now) + action = models.CharField(_("action"), choices=MESSAGE_META_ACTIONS, max_length=16) + +class ForumUserInfo(models.Model): + """ + This currently stores only the last date a user clicked "Mark all as read". + However, this can be extended with lot of user preferences dedicated to a + user, such as the favourite topics, the signature, and so on... + """ + user = models.OneToOneField(User, related_name="_forum_infos") # TODO: see to move that to the User class in order to reduce the number of db queries + last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR, + month=1, day=1, tzinfo=pytz.UTC)) + diff --git a/forum/templates/forum/forum.jinja b/forum/templates/forum/forum.jinja new file mode 100644 index 00000000..f6e4e1ae --- /dev/null +++ b/forum/templates/forum/forum.jinja @@ -0,0 +1,65 @@ +{% extends "core/base.jinja" %} +{% from 'forum/macros.jinja' import display_forum, display_topic %} + +{% block title %} +{{ forum }} +{% endblock %} + +{% block content %} +
      + {% trans %}Forum{% endtrans %} + {% for f in forum.get_parent_list()|reverse %} + > {{ f }} + {% endfor %} + > {{ forum }} +
      +

      {{ forum.name }}

      +

      + {% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %} + {% trans %}New forum{% endtrans %}
      + {% endif %} + {% trans %}New topic{% endtrans %} +

      + {% if forum.children.exists() %} +
      +
      + {% trans %}Title{% endtrans %} +
      +
      +
      + {% trans %}Topics{% endtrans %} +
      +
      + {% trans %}Last message{% endtrans %} +
      +
      +
      + {% for f in forum.children.all() %} + {{ display_forum(f, user) }} + {% endfor %} + {% endif %} + {% if topics %} +
      +
      + {% trans %}Title{% endtrans %} +
      +
      +
      + {% trans %}Author{% endtrans %} +
      +
      + {% trans %}Messages{% endtrans %} +
      +
      + {% trans %}Last message{% endtrans %} +
      +
      +
      + {% for t in topics %} + {{ display_topic(t, user) }} + {% endfor %} + {% endif %} +{% endblock %} + + + diff --git a/forum/templates/forum/last_unread.jinja b/forum/templates/forum/last_unread.jinja new file mode 100644 index 00000000..51fb1b7c --- /dev/null +++ b/forum/templates/forum/last_unread.jinja @@ -0,0 +1,24 @@ +{% extends "core/base.jinja" %} +{% from 'forum/macros.jinja' import display_topic %} + +{% block title %} +{% trans %}Last unread messages{% endtrans %} +{% endblock %} + +{% block content %} +

      + Forum > +

      +

      {% trans %}Forum{% endtrans %}

      +

      {% trans %}Last unread messages{% endtrans %}

      +

      + {% trans %}Mark all as read{% endtrans %} + {% trans %}Refresh{% endtrans %} +

      + {% for t in forumtopic_list %} + {{ display_topic(t, user, True) }} + {% endfor %} +{% endblock %} + + + diff --git a/forum/templates/forum/macros.jinja b/forum/templates/forum/macros.jinja new file mode 100644 index 00000000..d8b529f7 --- /dev/null +++ b/forum/templates/forum/macros.jinja @@ -0,0 +1,145 @@ +{% from 'core/macros.jinja' import user_profile_link %} + +{% macro display_forum(forum, user) %} +
      +
      + {% if not forum.is_category %} + + {% else %} + + {% endif %} + {% if user.is_owner(forum) %} + {% trans %}Edit{% endtrans %} + {% trans %}Delete{% endtrans %} + {% endif %} +
      + {% if not forum.is_category %} +
      +
      + {{ forum.topic_number }} +
      + +
      + {% endif %} +
      +{% endmacro %} + +{% macro display_topic(topic, user, first_unread=False) %} +
      +
      + {% if first_unread %} + + {% else %} + + {% endif %} +
      {{ topic.title }}
      +

      {{ topic.description }}

      +
      + {% if user.can_edit(topic) %} + + {% endif %} +
      +
      +
      +
      + {{ user_profile_link(topic.author) }} +
      +
      + {{ topic.messages.count() }} +
      +
      +
      + {% set last_msg = topic.last_message %} + {% if last_msg %} + {{ user_profile_link(last_msg.author) }}
      + + {{ last_msg.date|date(DATETIME_FORMAT) }} {{ last_msg.date|time(DATETIME_FORMAT) }} + + {% endif %} +
      +
      +
      +{% endmacro %} + +{% macro display_message(m, user, unread=False) %} +{% if user.can_view(m) %} +
      +
      + {% if m.author.avatar_pict %} + {% trans %}Profile{% endtrans %} + {% else %} + {% trans %}Profile{% endtrans %} + {% endif %} +
      + {{ m.author.get_short_name() }} +
      +
      +
      + {% if m.title %} +
      {{ m.title }}
      + {% endif %} +
      +
      + + {% trans %}Reply as quote{% endtrans %} + {% if user.can_edit(m) %} + {% trans %}Edit{% endtrans %} + {% endif %} + {% if m.can_be_moderated_by(user) %} + {% if m.deleted %} + {% trans %}Undelete{% endtrans %} + {% else %} + {% trans %}Delete{% endtrans %} + {% endif %} + {% endif %} +
      + {{ m.date|localtime|date(DATETIME_FORMAT) }} {{ m.date|localtime|time(DATETIME_FORMAT) }} +
      +
      +
      + {{ m.message|markdown }} +
      + {% if m.can_be_moderated_by(user) %} +
        + {% for meta in m.metas.select_related('user').order_by('id') %} +
      • + {{ meta.get_action_display() }} {{ meta.user.get_display_name() }} + {% trans %} at {% endtrans %}{{ meta.date|localtime|time(DATETIME_FORMAT) }} + {% trans %} the {% endtrans %}{{ meta.date|localtime|date(DATETIME_FORMAT)}}
      • + {% endfor %} +
      + {% endif %} +
      {{ m.author.forum_signature|markdown }}
      +
      +
      +{% else %} +
      +
      +
      +
      +

      {% trans %}Deleted or unreadable message.{% endtrans %}

      +

      {{ m.date|localtime|date(DATETIME_FORMAT) }} {{ m.date|localtime|time(DATETIME_FORMAT) }}

      +
      +
      +{% endif %} +{{ m.mark_as_read(user) or "" }} +{% endmacro %} + diff --git a/forum/templates/forum/main.jinja b/forum/templates/forum/main.jinja new file mode 100644 index 00000000..243cbb12 --- /dev/null +++ b/forum/templates/forum/main.jinja @@ -0,0 +1,33 @@ +{% extends "core/base.jinja" %} +{% from 'core/macros.jinja' import user_profile_link %} +{% from 'forum/macros.jinja' import display_forum %} + +{% block title %} +{% trans %}Forum{% endtrans %} +{% endblock %} + +{% block content %} +

      + {% trans %}Forum{% endtrans %} > +

      +

      {% trans %}Forum{% endtrans %}

      +

      + {% trans %}View last unread messages{% endtrans %} +

      + {% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %} +

      + {% trans %}New forum{% endtrans %} +

      + {% endif %} + {% for f in forum_list %} +
      + {{ display_forum(f, user) }} + {% for c in f.children.all() %} + {{ display_forum(c, user) }} + {% endfor %} +
      + {% endfor %} +{% endblock %} + + + diff --git a/forum/templates/forum/reply.jinja b/forum/templates/forum/reply.jinja new file mode 100644 index 00000000..c7bcfe66 --- /dev/null +++ b/forum/templates/forum/reply.jinja @@ -0,0 +1,81 @@ +{% extends "core/base.jinja" %} +{% from 'forum/macros.jinja' import display_message %} + +{% block title %} +{% if topic %} +{% trans %}Reply{% endtrans %} +{% else %} +{% trans %}New topic{% endtrans %} +{% endif %} +{% endblock %} + +{% block content %} +{% if topic %} +

      +{% trans %}Forum{% endtrans %} +{% for f in topic.forum.get_parent_list() %} +> {{ f }} +{% endfor %} +> {{ topic.forum }} +> {{ topic }} +

      +

      {{ topic.title }}

      +

      {% trans %}Reply{% endtrans %}

      +{% else %} +

      {% trans %}New topic{% endtrans %}

      +{% endif %} +
      + {% csrf_token %} + {{ form.as_p() }} +

      +

      + + + +
      + +{% if topic %} +{% for m in topic.messages.select_related('author__avatar_pict').order_by('-id')[:10] %} + {{ display_message(m, user) }} +{% endfor %} +{% endif %} + +{% endblock %} + + +{% block script %} +{{ super() }} + +{% endblock %} + + + diff --git a/forum/templates/forum/topic.jinja b/forum/templates/forum/topic.jinja new file mode 100644 index 00000000..80c246b1 --- /dev/null +++ b/forum/templates/forum/topic.jinja @@ -0,0 +1,68 @@ +{% extends "core/base.jinja" %} +{% from 'core/macros.jinja' import user_profile_link %} +{% from 'forum/macros.jinja' import display_message %} + +{% block title %} +{{ topic }} +{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block content %} +

      + {% trans %}Forum{% endtrans %} + {% for f in topic.forum.get_parent_list()|reverse %} + > {{ f }} + {% endfor %} + > {{ topic.forum }} + > {{ topic }} +

      +

      {{ topic.title }}

      +

      {{ topic.description }}

      +

      {% trans %}Reply{% endtrans %}

      + +

      + {% for p in msgs.paginator.page_range %} + {{ p }} + {% endfor %} +

      + + {% for m in msgs %} + {% if m.id == first_unread_message_id %} + + {% endif %} + {% if m.id >= first_unread_message_id %} + {{ display_message(m, user, True) }} + {% else %} + {{ display_message(m, user, False) }} + {% endif %} + {% endfor %} + +

      {% trans %}Reply{% endtrans %}

      + +

      + {% for p in msgs.paginator.page_range %} + {{ p }} + {% endfor %} +

      +{% endblock %} + + + diff --git a/forum/tests.py b/forum/tests.py new file mode 100644 index 00000000..b8fbbe1e --- /dev/null +++ b/forum/tests.py @@ -0,0 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.test import TestCase + +# Create your tests here. diff --git a/forum/urls.py b/forum/urls.py new file mode 100644 index 00000000..7fabc688 --- /dev/null +++ b/forum/urls.py @@ -0,0 +1,45 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.conf.urls import url, include + +from forum.views import * + +urlpatterns = [ + url(r'^$', ForumMainView.as_view(), name='main'), + url(r'^new_forum$', ForumCreateView.as_view(), name='new_forum'), + url(r'^mark_all_as_read$', ForumMarkAllAsRead.as_view(), name='mark_all_as_read'), + url(r'^last_unread$', ForumLastUnread.as_view(), name='last_unread'), + url(r'^(?P[0-9]+)$', ForumDetailView.as_view(), name='view_forum'), + url(r'^(?P[0-9]+)/edit$', ForumEditView.as_view(), name='edit_forum'), + url(r'^(?P[0-9]+)/delete$', ForumDeleteView.as_view(), name='delete_forum'), + url(r'^(?P[0-9]+)/new_topic$', ForumTopicCreateView.as_view(), name='new_topic'), + url(r'^topic/(?P[0-9]+)$', ForumTopicDetailView.as_view(), name='view_topic'), + url(r'^topic/(?P[0-9]+)/edit$', ForumTopicEditView.as_view(), name='edit_topic'), + url(r'^topic/(?P[0-9]+)/new_message$', ForumMessageCreateView.as_view(), name='new_message'), + url(r'^message/(?P[0-9]+)/edit$', ForumMessageEditView.as_view(), name='edit_message'), + url(r'^message/(?P[0-9]+)/delete$', ForumMessageDeleteView.as_view(), name='delete_message'), + url(r'^message/(?P[0-9]+)/undelete$', ForumMessageUndeleteView.as_view(), name='undelete_message'), +] + diff --git a/forum/views.py b/forum/views.py new file mode 100644 index 00000000..5ef4ff5b --- /dev/null +++ b/forum/views.py @@ -0,0 +1,248 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.shortcuts import render, get_object_or_404 +from django.views.generic import ListView, DetailView, RedirectView +from django.views.generic.edit import UpdateView, CreateView, DeleteView +from django.views.generic.detail import SingleObjectMixin +from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse, reverse_lazy +from django.utils import timezone +from django.conf import settings +from django import forms +from django.db import models +from django.core.exceptions import PermissionDenied +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger + +from ajax_select import make_ajax_form, make_ajax_field + +from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin, TabedViewMixin +from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta + +class ForumMainView(ListView): + queryset = Forum.objects.filter(parent=None) + template_name = "forum/main.jinja" + +class ForumMarkAllAsRead(RedirectView): + permanent = False + url = reverse_lazy('forum:last_unread') + + def get(self, request, *args, **kwargs): + try: + fi = request.user.forum_infos + fi.last_read_date = timezone.now() + fi.save() + except: pass + return super(ForumMarkAllAsRead, self).get(request, *args, **kwargs) + +class ForumLastUnread(ListView): + model = ForumTopic + template_name = "forum/last_unread.jinja" + + def get_queryset(self): + l = ForumMessage.objects.exclude(readers=self.request.user).filter( + date__gt=self.request.user.forum_infos.last_read_date).values_list('topic') # TODO try to do better + return self.model.objects.filter(id__in=l).annotate(models.Max('messages__date')).order_by('-messages__date__max').select_related('author') + +class ForumForm(forms.ModelForm): + class Meta: + model = Forum + fields = ['name', 'parent', 'number', 'owner_club', 'is_category', 'edit_groups', 'view_groups'] + edit_groups = make_ajax_field(Forum, 'edit_groups', 'groups', help_text="") + view_groups = make_ajax_field(Forum, 'view_groups', 'groups', help_text="") + +class ForumCreateView(CanCreateMixin, CreateView): + model = Forum + form_class = ForumForm + template_name = "core/create.jinja" + + def get_initial(self): + init = super(ForumCreateView, self).get_initial() + try: + parent = Forum.objects.filter(id=self.request.GET['parent']).first() + init['parent'] = parent + init['owner_club'] = parent.owner_club + init['edit_groups'] = parent.edit_groups.all() + init['view_groups'] = parent.view_groups.all() + except: pass + return init + +class ForumEditForm(ForumForm): + recursive = forms.BooleanField(label=_("Apply rights and club owner recursively"), required=False) + +class ForumEditView(CanEditPropMixin, UpdateView): + model = Forum + pk_url_kwarg = "forum_id" + form_class = ForumEditForm + template_name = "core/edit.jinja" + success_url = reverse_lazy('forum:main') + + def form_valid(self, form): + ret = super(ForumEditView, self).form_valid(form) + if form.cleaned_data['recursive']: + self.object.apply_rights_recursively() + return ret + +class ForumDeleteView(CanEditPropMixin, DeleteView): + model = Forum + pk_url_kwarg = "forum_id" + template_name = "core/delete_confirm.jinja" + success_url = reverse_lazy('forum:main') + +class ForumDetailView(CanViewMixin, DetailView): + model = Forum + template_name = "forum/forum.jinja" + pk_url_kwarg = "forum_id" + + def get_context_data(self, **kwargs): + kwargs = super(ForumDetailView, self).get_context_data(**kwargs) + kwargs['topics'] = self.object.topics.annotate(models.Max('messages__date')).order_by('-messages__date__max') + return kwargs + +class TopicForm(forms.ModelForm): + class Meta: + model = ForumMessage + fields = ['title', 'message'] + title = forms.CharField(required=True, label=_("Title")) + +class ForumTopicCreateView(CanCreateMixin, CreateView): + model = ForumMessage + form_class = TopicForm + template_name = "forum/reply.jinja" + + def dispatch(self, request, *args, **kwargs): + self.forum = get_object_or_404(Forum, id=self.kwargs['forum_id'], is_category=False) + if not request.user.can_view(self.forum): + raise PermissionDenied + return super(ForumTopicCreateView, self).dispatch(request, *args, **kwargs) + + def form_valid(self, form): + topic = ForumTopic(title=form.instance.title, author=self.request.user, forum=self.forum) + topic.save() + form.instance.topic = topic + form.instance.author = self.request.user + return super(ForumTopicCreateView, self).form_valid(form) + +class ForumTopicEditView(CanEditMixin, UpdateView): + model = ForumTopic + fields = ['forum'] + pk_url_kwarg = "topic_id" + template_name = "core/edit.jinja" + +class ForumTopicDetailView(CanViewMixin, DetailView): + model = ForumTopic + pk_url_kwarg = "topic_id" + template_name = "forum/topic.jinja" + context_object_name = "topic" + queryset = ForumTopic.objects.select_related('forum__parent') + + def get_context_data(self, **kwargs): + kwargs = super(ForumTopicDetailView, self).get_context_data(**kwargs) + try: + msg = self.object.get_first_unread_message(user) + kwargs['first_unread_message_id'] = msg.id + except: + kwargs['first_unread_message_id'] = float("inf") + paginator = Paginator(self.object.messages.select_related('author__avatar_pict').all(), + settings.SITH_FORUM_PAGE_LENGTH) + page = self.request.GET.get('page') + try: + kwargs["msgs"] = paginator.page(page) + except PageNotAnInteger: + kwargs["msgs"] = paginator.page(1) + except EmptyPage: + kwargs["msgs"] = paginator.page(paginator.num_pages) + return kwargs + +class ForumMessageEditView(CanEditMixin, UpdateView): + model = ForumMessage + fields = ['title', 'message'] + template_name = "forum/reply.jinja" + pk_url_kwarg = "message_id" + + def form_valid(self, form): + ForumMessageMeta(message=self.object, user=self.request.user, action="EDIT").save() + return super(ForumMessageEditView, self).form_valid(form) + + def get_context_data(self, **kwargs): + kwargs = super(ForumMessageEditView, self).get_context_data(**kwargs) + kwargs['topic'] = self.object.topic + return kwargs + +class ForumMessageDeleteView(SingleObjectMixin, RedirectView): + model = ForumMessage + pk_url_kwarg = "message_id" + permanent = False + + def get_redirect_url(self, *args, **kwargs): + self.object = self.get_object() + if self.object.can_be_moderated_by(self.request.user): + ForumMessageMeta(message=self.object, user=self.request.user, action="DELETE").save() + return self.object.get_absolute_url() + +class ForumMessageUndeleteView(SingleObjectMixin, RedirectView): + model = ForumMessage + pk_url_kwarg = "message_id" + permanent = False + + def get_redirect_url(self, *args, **kwargs): + self.object = self.get_object() + if self.object.can_be_moderated_by(self.request.user): + ForumMessageMeta(message=self.object, user=self.request.user, action="UNDELETE").save() + return self.object.get_absolute_url() + +class ForumMessageCreateView(CanCreateMixin, CreateView): + model = ForumMessage + fields = ['title', 'message'] + template_name = "forum/reply.jinja" + + def dispatch(self, request, *args, **kwargs): + self.topic = get_object_or_404(ForumTopic, id=self.kwargs['topic_id']) + if not request.user.can_view(self.topic): + raise PermissionDenied + return super(ForumMessageCreateView, self).dispatch(request, *args, **kwargs) + + def get_initial(self): + init = super(ForumMessageCreateView, self).get_initial() + try: + message = ForumMessage.objects.select_related('author').filter(id=self.request.GET['quote_id']).first() + init['message'] = "> ##### %s\n" % (_("%(author)s said") % {'author': message.author.get_short_name()}) + init['message'] += "\n".join([ + "> " + line for line in message.message.split('\n') + ]) + init['message'] += "\n\n" + except Exception as e: + print(repr(e)) + return init + + def form_valid(self, form): + form.instance.topic = self.topic + form.instance.author = self.request.user + return super(ForumMessageCreateView, self).form_valid(form) + + def get_context_data(self, **kwargs): + kwargs = super(ForumMessageCreateView, self).get_context_data(**kwargs) + kwargs['topic'] = self.topic + return kwargs + diff --git a/launderette/__init__.py b/launderette/__init__.py index e69de29b..0a9419f8 100644 --- a/launderette/__init__.py +++ b/launderette/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/launderette/admin.py b/launderette/admin.py index 3f7c0d27..954a74a8 100644 --- a/launderette/admin.py +++ b/launderette/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin from launderette.models import * diff --git a/launderette/models.py b/launderette/models.py index 75aa5610..9ad0f35e 100644 --- a/launderette/models.py +++ b/launderette/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.db import models, DataError from django.utils.translation import ugettext_lazy as _ from django.conf import settings diff --git a/launderette/tests.py b/launderette/tests.py index 7ce503c2..b8fbbe1e 100644 --- a/launderette/tests.py +++ b/launderette/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase # Create your tests here. diff --git a/launderette/urls.py b/launderette/urls.py index 2ee14e9d..ccd09415 100644 --- a/launderette/urls.py +++ b/launderette/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from launderette.views import * diff --git a/launderette/views.py b/launderette/views.py index 4c4b7102..340f6607 100644 --- a/launderette/views.py +++ b/launderette/views.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from datetime import datetime, timedelta from collections import OrderedDict import pytz @@ -60,7 +84,7 @@ class LaunderetteBookView(CanViewMixin, DetailView): self.slot_type = request.POST['slot_type'] if 'slot' in request.POST.keys() and request.user.is_authenticated(): self.subscriber = request.user - if self.subscriber.is_subscribed(): + if self.subscriber.is_subscribed: self.date = dateparse.parse_datetime(request.POST['slot']).replace(tzinfo=pytz.UTC) if self.slot_type == "WASHING": if self.check_slot(self.slot_type): diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 41a2407a..bb317e05 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-01-12 15:49+0100\n" +"POT-Creation-Date: 2017-04-10 23:25+0200\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Skia \n" "Language-Team: AE info \n" @@ -16,11 +16,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: accounting/models.py:36 accounting/models.py:58 accounting/models.py:85 -#: accounting/models.py:144 club/models.py:18 counter/models.py:70 -#: counter/models.py:95 counter/models.py:130 launderette/models.py:13 -#: launderette/models.py:58 launderette/models.py:83 stock/models.py:13 -#: stock/models.py:29 stock/models.py:52 stock/models.py:72 +#: accounting/models.py:36 accounting/models.py:84 accounting/models.py:111 +#: accounting/models.py:170 club/models.py:18 counter/models.py:75 +#: counter/models.py:100 counter/models.py:135 forum/models.py:25 +#: launderette/models.py:13 launderette/models.py:58 launderette/models.py:83 msgid "name" msgstr "nom" @@ -40,7 +39,7 @@ msgstr "code postal" msgid "country" msgstr "pays" -#: accounting/models.py:41 core/models.py:155 +#: accounting/models.py:41 core/models.py:163 msgid "phone" msgstr "téléphone" @@ -56,121 +55,122 @@ msgstr "site internet" msgid "company" msgstr "entreprise" -#: accounting/models.py:59 +#: accounting/models.py:85 msgid "iban" msgstr "IBAN" -#: accounting/models.py:60 +#: accounting/models.py:86 msgid "account number" msgstr "numero de compte" -#: accounting/models.py:61 accounting/models.py:86 club/models.py:145 -#: com/models.py:34 counter/models.py:104 counter/models.py:131 +#: accounting/models.py:87 accounting/models.py:112 club/models.py:148 +#: com/models.py:37 com/models.py:122 counter/models.py:109 +#: counter/models.py:136 msgid "club" msgstr "club" -#: accounting/models.py:64 +#: accounting/models.py:90 msgid "Bank account" msgstr "Compte en banque" -#: accounting/models.py:87 +#: accounting/models.py:113 msgid "bank account" msgstr "compte en banque" -#: accounting/models.py:90 +#: accounting/models.py:116 msgid "Club account" msgstr "Compte club" -#: accounting/models.py:135 +#: accounting/models.py:161 #, python-format msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:142 club/models.py:146 counter/models.py:399 +#: accounting/models.py:168 club/models.py:149 counter/models.py:404 #: election/models.py:18 launderette/models.py:120 msgid "start date" msgstr "date de début" -#: accounting/models.py:143 club/models.py:147 counter/models.py:400 +#: accounting/models.py:169 club/models.py:150 counter/models.py:405 #: election/models.py:19 msgid "end date" msgstr "date de fin" -#: accounting/models.py:145 +#: accounting/models.py:171 msgid "is closed" msgstr "est fermé" -#: accounting/models.py:146 accounting/models.py:349 +#: accounting/models.py:172 accounting/models.py:390 msgid "club account" msgstr "compte club" -#: accounting/models.py:147 accounting/models.py:193 counter/models.py:28 -#: counter/models.py:239 +#: accounting/models.py:173 accounting/models.py:229 counter/models.py:28 +#: counter/models.py:244 msgid "amount" msgstr "montant" -#: accounting/models.py:148 +#: accounting/models.py:174 msgid "effective_amount" msgstr "montant effectif" -#: accounting/models.py:151 +#: accounting/models.py:177 msgid "General journal" msgstr "Classeur" -#: accounting/models.py:191 +#: accounting/models.py:227 msgid "number" msgstr "numéro" -#: accounting/models.py:192 +#: accounting/models.py:228 msgid "journal" msgstr "classeur" -#: accounting/models.py:194 core/models.py:517 core/models.py:862 -#: core/models.py:902 counter/models.py:242 counter/models.py:290 -#: counter/models.py:416 eboutic/models.py:15 eboutic/models.py:48 -#: stock/models.py:51 +#: accounting/models.py:230 core/models.py:553 core/models.py:924 +#: core/models.py:964 counter/models.py:247 counter/models.py:295 +#: counter/models.py:421 eboutic/models.py:15 eboutic/models.py:48 +#: forum/models.py:170 forum/models.py:225 msgid "date" msgstr "date" -#: accounting/models.py:195 counter/models.py:417 stock/models.py:54 +#: accounting/models.py:231 counter/models.py:422 msgid "comment" msgstr "commentaire" -#: accounting/models.py:196 counter/models.py:243 counter/models.py:291 +#: accounting/models.py:232 counter/models.py:248 counter/models.py:296 #: subscription/models.py:29 msgid "payment method" msgstr "méthode de paiement" -#: accounting/models.py:197 +#: accounting/models.py:233 msgid "cheque number" msgstr "numéro de chèque" -#: accounting/models.py:198 eboutic/models.py:116 +#: accounting/models.py:234 eboutic/models.py:116 msgid "invoice" msgstr "facture" -#: accounting/models.py:199 +#: accounting/models.py:235 msgid "is done" msgstr "est fait" -#: accounting/models.py:201 +#: accounting/models.py:237 msgid "simple type" msgstr "type simplifié" -#: accounting/models.py:203 accounting/models.py:304 +#: accounting/models.py:239 accounting/models.py:345 msgid "accounting type" msgstr "type comptable" -#: accounting/models.py:205 accounting/models.py:299 accounting/models.py:325 -#: accounting/models.py:348 counter/models.py:282 +#: accounting/models.py:241 accounting/models.py:340 accounting/models.py:366 +#: accounting/models.py:389 counter/models.py:287 msgid "label" msgstr "étiquette" -#: accounting/models.py:206 +#: accounting/models.py:242 msgid "target type" msgstr "type de cible" -#: accounting/models.py:207 club/templates/club/club_members.jinja:8 +#: accounting/models.py:243 club/templates/club/club_members.jinja:8 #: club/templates/club/club_old_members.jinja:8 #: core/templates/core/user_clubs.jinja:15 #: core/templates/core/user_clubs.jinja:41 @@ -182,39 +182,39 @@ msgstr "type de cible" msgid "User" msgstr "Utilisateur" -#: accounting/models.py:207 club/templates/club/club_detail.jinja:5 +#: accounting/models.py:243 club/templates/club/club_detail.jinja:5 #: com/templates/com/news_admin_list.jinja:17 #: com/templates/com/news_admin_list.jinja:51 -#: counter/templates/counter/invoices_call.jinja:20 +#: com/templates/com/weekmail.jinja:18 com/templates/com/weekmail.jinja:47 +#: counter/templates/counter/invoices_call.jinja:23 msgid "Club" msgstr "Club" -#: accounting/models.py:207 core/views/user.py:179 +#: accounting/models.py:243 core/views/user.py:184 msgid "Account" msgstr "Compte" -#: accounting/models.py:207 +#: accounting/models.py:243 msgid "Company" msgstr "Entreprise" -#: accounting/models.py:207 sith/settings.py:314 -#: stock/templates/stock/shopping_list_items.jinja:37 +#: accounting/models.py:243 sith/settings.py:325 msgid "Other" msgstr "Autre" -#: accounting/models.py:208 +#: accounting/models.py:244 msgid "target id" msgstr "id de la cible" -#: accounting/models.py:209 +#: accounting/models.py:245 msgid "target label" msgstr "nom de la cible" -#: accounting/models.py:210 +#: accounting/models.py:246 msgid "linked operation" msgstr "opération liée" -#: accounting/models.py:226 +#: accounting/models.py:262 #, python-format msgid "" "The date can not be before the start date of the journal, which is\n" @@ -223,16 +223,16 @@ msgstr "" "La date ne peut pas être avant la date de début du journal, qui est\n" "%(start_date)s." -#: accounting/models.py:229 +#: accounting/models.py:265 msgid "Target does not exists" msgstr "La cible n'existe pas." -#: accounting/models.py:231 +#: accounting/models.py:267 msgid "Please add a target label if you set no existing target" msgstr "" "Merci d'ajouter un nom de cible si vous ne spécifiez pas de cible existante" -#: accounting/models.py:233 +#: accounting/models.py:269 msgid "" "You need to provide ether a simplified accounting type or a standard " "accounting type" @@ -240,41 +240,41 @@ msgstr "" "Vous devez fournir soit un type comptable simplifié ou un type comptable " "standard" -#: accounting/models.py:294 counter/models.py:99 +#: accounting/models.py:335 counter/models.py:104 msgid "code" msgstr "code" -#: accounting/models.py:296 +#: accounting/models.py:337 msgid "An accounting type code contains only numbers" msgstr "Un code comptable ne contient que des numéros" -#: accounting/models.py:300 +#: accounting/models.py:341 msgid "movement type" msgstr "type de mouvement" -#: accounting/models.py:300 +#: accounting/models.py:341 #: accounting/templates/accounting/journal_statement_nature.jinja:8 #: accounting/templates/accounting/journal_statement_person.jinja:11 -#: accounting/views.py:417 +#: accounting/views.py:431 msgid "Credit" msgstr "Crédit" -#: accounting/models.py:300 +#: accounting/models.py:341 #: accounting/templates/accounting/journal_statement_nature.jinja:27 #: accounting/templates/accounting/journal_statement_person.jinja:39 -#: accounting/views.py:417 +#: accounting/views.py:431 msgid "Debit" msgstr "Débit" -#: accounting/models.py:301 +#: accounting/models.py:342 msgid "Neutral" msgstr "Neutre" -#: accounting/models.py:327 +#: accounting/models.py:368 msgid "simplified accounting types" msgstr "type simplifié" -#: accounting/models.py:330 +#: accounting/models.py:371 msgid "simplified type" msgstr "type simplifié" @@ -315,31 +315,35 @@ msgid "Bank account: " msgstr "Compte en banque : " #: accounting/templates/accounting/bank_account_details.jinja:15 +#: accounting/templates/accounting/bank_account_details.jinja:28 #: accounting/templates/accounting/club_account_details.jinja:16 -#: accounting/templates/accounting/label_list.jinja:21 -#: club/templates/club/club_sellings.jinja:49 +#: accounting/templates/accounting/club_account_details.jinja:59 +#: accounting/templates/accounting/label_list.jinja:25 +#: club/templates/club/club_sellings.jinja:50 +#: com/templates/com/weekmail.jinja:33 com/templates/com/weekmail.jinja:62 #: core/templates/core/file_detail.jinja:25 #: core/templates/core/file_detail.jinja:62 #: core/templates/core/file_moderation.jinja:24 #: core/templates/core/group_list.jinja:13 core/templates/core/macros.jinja:49 #: core/templates/core/macros.jinja:68 +#: core/templates/core/pagerev_edit.jinja:26 #: core/templates/core/user_account_detail.jinja:38 #: core/templates/core/user_edit.jinja:19 #: counter/templates/counter/last_ops.jinja:29 #: counter/templates/counter/last_ops.jinja:59 #: election/templates/election/election_detail.jinja:280 -#: election/templates/election/election_detail.jinja:327 +#: election/templates/election/election_detail.jinja:329 +#: forum/templates/forum/macros.jinja:20 +#: forum/templates/forum/macros.jinja:110 #: launderette/templates/launderette/launderette_admin.jinja:16 #: launderette/views.py:154 sas/templates/sas/album.jinja:26 -#: sas/templates/sas/moderation.jinja:18 sas/templates/sas/picture.jinja:66 -#: sas/templates/sas/picture.jinja.py:116 -#: stock/templates/stock/stock_shopping_list.jinja:43 -#: stock/templates/stock/stock_shopping_list.jinja:69 +#: sas/templates/sas/moderation.jinja:18 sas/templates/sas/picture.jinja:74 +#: sas/templates/sas/picture.jinja:124 msgid "Delete" msgstr "Supprimer" #: accounting/templates/accounting/bank_account_details.jinja:17 -#: club/views.py:33 core/views/user.py:130 sas/templates/sas/picture.jinja:78 +#: club/views.py:33 core/views/user.py:130 sas/templates/sas/picture.jinja:86 msgid "Infos" msgstr "Infos" @@ -357,20 +361,24 @@ msgstr "Nouveau compte club" #: accounting/templates/accounting/bank_account_details.jinja:26 #: accounting/templates/accounting/bank_account_list.jinja:21 -#: accounting/templates/accounting/club_account_details.jinja:55 -#: accounting/templates/accounting/journal_details.jinja:82 club/views.py:55 +#: accounting/templates/accounting/club_account_details.jinja:57 +#: accounting/templates/accounting/journal_details.jinja:83 club/views.py:55 #: com/templates/com/news_admin_list.jinja:39 -#: com/templates/com/news_admin_list.jinja:71 core/templates/core/file.jinja:38 -#: core/templates/core/page.jinja:31 core/templates/core/user_tools.jinja:39 -#: core/views/user.py:152 counter/templates/counter/cash_summary_list.jinja:53 +#: com/templates/com/news_admin_list.jinja:71 +#: com/templates/com/weekmail.jinja:32 com/templates/com/weekmail.jinja:61 +#: core/templates/core/file.jinja:38 core/templates/core/page.jinja:31 +#: core/templates/core/user_tools.jinja:38 core/views/user.py:152 +#: counter/templates/counter/cash_summary_list.jinja:53 #: counter/templates/counter/counter_list.jinja:17 #: counter/templates/counter/counter_list.jinja:32 #: counter/templates/counter/counter_list.jinja:53 #: election/templates/election/election_detail.jinja:279 -#: election/templates/election/election_detail.jinja:324 -#: election/templates/election/election_detail.jinja:370 +#: election/templates/election/election_detail.jinja:326 +#: election/templates/election/election_detail.jinja:374 +#: forum/templates/forum/macros.jinja:19 forum/templates/forum/macros.jinja:56 +#: forum/templates/forum/macros.jinja:104 #: launderette/templates/launderette/launderette_list.jinja:16 -#: sas/templates/sas/album.jinja:18 sas/templates/sas/picture.jinja:92 +#: sas/templates/sas/album.jinja:18 sas/templates/sas/picture.jinja:100 msgid "Edit" msgstr "Éditer" @@ -400,28 +408,28 @@ msgstr "Il n'y a pas de comptes dans ce site web." msgid "Club account:" msgstr "Compte club : " -#: accounting/templates/accounting/club_account_details.jinja:18 +#: accounting/templates/accounting/club_account_details.jinja:19 #: accounting/templates/accounting/journal_details.jinja:16 -#: accounting/templates/accounting/label_list.jinja:15 +#: accounting/templates/accounting/label_list.jinja:16 msgid "New label" msgstr "Nouvelle étiquette" -#: accounting/templates/accounting/club_account_details.jinja:19 +#: accounting/templates/accounting/club_account_details.jinja:21 #: accounting/templates/accounting/journal_details.jinja:17 #: accounting/templates/accounting/label_list.jinja:4 -#: accounting/templates/accounting/label_list.jinja:17 +#: accounting/templates/accounting/label_list.jinja:19 msgid "Label list" msgstr "Liste des étiquettes" -#: accounting/templates/accounting/club_account_details.jinja:21 +#: accounting/templates/accounting/club_account_details.jinja:23 msgid "New journal" msgstr "Nouveau classeur" -#: accounting/templates/accounting/club_account_details.jinja:23 +#: accounting/templates/accounting/club_account_details.jinja:25 msgid "You can not create new journal while you still have one opened" msgstr "Vous ne pouvez pas créer de journal tant qu'il y en a un d'ouvert" -#: accounting/templates/accounting/club_account_details.jinja:28 +#: accounting/templates/accounting/club_account_details.jinja:30 #: launderette/templates/launderette/launderette_admin.jinja:43 #: stock/templates/stock/shopping_list_items.jinja:20 #: stock/templates/stock/stock_shopping_list.jinja:26 @@ -429,19 +437,19 @@ msgstr "Vous ne pouvez pas créer de journal tant qu'il y en a un d'ouvert" msgid "Name" msgstr "Nom" -#: accounting/templates/accounting/club_account_details.jinja:29 +#: accounting/templates/accounting/club_account_details.jinja:31 #: com/templates/com/news_admin_list.jinja:20 #: com/templates/com/news_admin_list.jinja:53 msgid "Start" msgstr "Début" -#: accounting/templates/accounting/club_account_details.jinja:30 +#: accounting/templates/accounting/club_account_details.jinja:32 #: com/templates/com/news_admin_list.jinja:21 #: com/templates/com/news_admin_list.jinja:54 msgid "End" msgstr "Fin" -#: accounting/templates/accounting/club_account_details.jinja:31 +#: accounting/templates/accounting/club_account_details.jinja:33 #: accounting/templates/accounting/journal_details.jinja:33 #: core/templates/core/user_account_detail.jinja:53 #: core/templates/core/user_account_detail.jinja:80 @@ -449,32 +457,33 @@ msgstr "Fin" msgid "Amount" msgstr "Montant" -#: accounting/templates/accounting/club_account_details.jinja:32 +#: accounting/templates/accounting/club_account_details.jinja:34 msgid "Effective amount" msgstr "Montant effectif" -#: accounting/templates/accounting/club_account_details.jinja:33 +#: accounting/templates/accounting/club_account_details.jinja:35 msgid "Closed" msgstr "Fermé" -#: accounting/templates/accounting/club_account_details.jinja:34 +#: accounting/templates/accounting/club_account_details.jinja:36 #: accounting/templates/accounting/journal_details.jinja:41 #: com/templates/com/news_admin_list.jinja:22 #: com/templates/com/news_admin_list.jinja:55 +#: com/templates/com/weekmail.jinja:21 com/templates/com/weekmail.jinja:50 msgid "Actions" msgstr "Actions" -#: accounting/templates/accounting/club_account_details.jinja:50 +#: accounting/templates/accounting/club_account_details.jinja:52 #: accounting/templates/accounting/journal_details.jinja:61 msgid "Yes" msgstr "Oui" -#: accounting/templates/accounting/club_account_details.jinja:52 +#: accounting/templates/accounting/club_account_details.jinja:54 #: accounting/templates/accounting/journal_details.jinja:63 msgid "No" msgstr "Non" -#: accounting/templates/accounting/club_account_details.jinja:54 +#: accounting/templates/accounting/club_account_details.jinja:56 #: com/templates/com/news_admin_list.jinja:38 #: com/templates/com/news_admin_list.jinja:70 core/templates/core/file.jinja:36 #: core/templates/core/page.jinja:28 @@ -487,11 +496,11 @@ msgstr "Voir" msgid "Company list" msgstr "Liste des entreprises" -#: accounting/templates/accounting/co_list.jinja:8 +#: accounting/templates/accounting/co_list.jinja:9 msgid "Create new company" msgstr "Nouvelle entreprise" -#: accounting/templates/accounting/co_list.jinja:14 +#: accounting/templates/accounting/co_list.jinja:16 msgid "Companies" msgstr "Entreprises" @@ -532,20 +541,18 @@ msgid "Nb" msgstr "No" #: accounting/templates/accounting/journal_details.jinja:31 -#: club/templates/club/club_sellings.jinja:19 +#: club/templates/club/club_sellings.jinja:20 #: core/templates/core/user_account_detail.jinja:17 #: core/templates/core/user_account_detail.jinja:50 #: core/templates/core/user_account_detail.jinja:78 #: counter/templates/counter/cash_summary_list.jinja:34 #: counter/templates/counter/last_ops.jinja:14 -#: counter/templates/counter/last_ops.jinja:39 -#: stock/templates/stock/stock_shopping_list.jinja:25 -#: stock/templates/stock/stock_shopping_list.jinja:54 +#: counter/templates/counter/last_ops.jinja:39 sas/views.py:257 msgid "Date" msgstr "Date" #: accounting/templates/accounting/journal_details.jinja:32 -#: club/templates/club/club_sellings.jinja:23 +#: club/templates/club/club_sellings.jinja:24 #: core/templates/core/user_account_detail.jinja:20 #: counter/templates/counter/last_ops.jinja:42 msgid "Label" @@ -573,7 +580,7 @@ msgid "Done" msgstr "Effectuées" #: accounting/templates/accounting/journal_details.jinja:39 -#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:728 +#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:756 msgid "Comment" msgstr "Commentaire" @@ -602,7 +609,7 @@ msgstr "" "Ouvrez un classeur dans ce compte club, puis sauver " "cette opération à nouveau pour créer l'opération liée." -#: accounting/templates/accounting/journal_details.jinja:85 +#: accounting/templates/accounting/journal_details.jinja:87 msgid "Generate" msgstr "Générer" @@ -619,7 +626,7 @@ msgstr "Type d'opération" #: accounting/templates/accounting/journal_statement_nature.jinja:32 #: accounting/templates/accounting/journal_statement_person.jinja:17 #: accounting/templates/accounting/journal_statement_person.jinja:45 -#: counter/templates/counter/invoices_call.jinja:21 +#: counter/templates/counter/invoices_call.jinja:24 msgid "Sum" msgstr "Somme" @@ -654,7 +661,7 @@ msgstr "Cible de l'opération" msgid "Back to club account" msgstr "Retour au compte club" -#: accounting/templates/accounting/label_list.jinja:26 +#: accounting/templates/accounting/label_list.jinja:31 msgid "There is no label in this club account." msgstr "Il n'y a pas d'étiquette dans ce compte club." @@ -669,20 +676,21 @@ msgid "Linked operation:" msgstr "Opération liée : " #: accounting/templates/accounting/operation_edit.jinja:51 -#: com/templates/com/news_edit.jinja:66 core/templates/core/create.jinja:12 -#: core/templates/core/edit.jinja:7 core/templates/core/edit.jinja.py:15 -#: core/templates/core/edit.jinja:20 core/templates/core/file_edit.jinja:8 -#: core/templates/core/page_prop.jinja:8 +#: com/templates/com/news_edit.jinja:66 com/templates/com/weekmail.jinja:74 +#: core/templates/core/create.jinja:12 core/templates/core/edit.jinja:7 +#: core/templates/core/edit.jinja.py:15 core/templates/core/edit.jinja:20 +#: core/templates/core/file_edit.jinja:8 core/templates/core/page_prop.jinja:8 #: core/templates/core/pagerev_edit.jinja:24 #: core/templates/core/user_godfathers.jinja:35 #: counter/templates/counter/cash_register_summary.jinja:22 +#: forum/templates/forum/reply.jinja:22 #: subscription/templates/subscription/subscription.jinja:23 msgid "Save" msgstr "Sauver" #: accounting/templates/accounting/refound_account.jinja:4 #: accounting/templates/accounting/refound_account.jinja:8 -#: accounting/views.py:680 +#: accounting/views.py:694 msgid "Refound account" msgstr "Remboursement de compte" @@ -703,7 +711,7 @@ msgstr "Types simplifiés" msgid "New simplified type" msgstr "Nouveau type simplifié" -#: accounting/views.py:172 accounting/views.py:179 accounting/views.py:399 +#: accounting/views.py:172 accounting/views.py:179 accounting/views.py:414 msgid "Journal" msgstr "Classeur" @@ -719,59 +727,59 @@ msgstr "Bilan par personne" msgid "Accounting statement" msgstr "Bilan comptable" -#: accounting/views.py:393 accounting/views.py:399 +#: accounting/views.py:408 accounting/views.py:414 msgid "Operation" msgstr "Opération" -#: accounting/views.py:410 +#: accounting/views.py:424 msgid "Financial proof: " msgstr "Justificatif de libellé : " -#: accounting/views.py:411 +#: accounting/views.py:425 #, python-format msgid "Club: %(club_name)s" msgstr "Club : %(club_name)s" -#: accounting/views.py:412 +#: accounting/views.py:426 #, python-format msgid "Label: %(op_label)s" msgstr "Libellé : %(op_label)s" -#: accounting/views.py:413 +#: accounting/views.py:427 #, python-format msgid "Date: %(date)s" msgstr "Date : %(date)s" -#: accounting/views.py:419 +#: accounting/views.py:433 #, python-format msgid "Amount: %(amount).2f €" msgstr "Montant : %(amount).2f €" -#: accounting/views.py:431 +#: accounting/views.py:445 msgid "Debtor" msgstr "Débiteur" -#: accounting/views.py:431 +#: accounting/views.py:445 msgid "Creditor" msgstr "Créditeur" -#: accounting/views.py:433 +#: accounting/views.py:447 msgid "Comment:" msgstr "Commentaire :" -#: accounting/views.py:452 +#: accounting/views.py:466 msgid "Signature:" msgstr "Signature :" -#: accounting/views.py:506 +#: accounting/views.py:520 msgid "General statement" msgstr "Bilan général" -#: accounting/views.py:509 +#: accounting/views.py:523 msgid "No label operations" msgstr "Opérations sans étiquette" -#: accounting/views.py:642 +#: accounting/views.py:656 msgid "Refound this account" msgstr "Rembourser ce compte" @@ -791,48 +799,48 @@ msgstr "" msgid "A club with that unix name already exists." msgstr "Un club avec ce nom UNIX existe déjà." -#: club/models.py:32 core/models.py:157 +#: club/models.py:32 core/models.py:165 msgid "address" msgstr "Adresse" -#: club/models.py:38 core/models.py:118 +#: club/models.py:38 core/models.py:126 msgid "home" msgstr "home" -#: club/models.py:47 +#: club/models.py:50 msgid "You can not make loops in clubs" msgstr "Vous ne pouvez pas faire de boucles dans les clubs" -#: club/models.py:61 +#: club/models.py:64 msgid "A club with that unix_name already exists" msgstr "Un club avec ce nom UNIX existe déjà." -#: club/models.py:144 counter/models.py:397 counter/models.py:414 -#: eboutic/models.py:14 eboutic/models.py:47 election/models.py:126 -#: launderette/models.py:87 launderette/models.py:124 sas/models.py:131 +#: club/models.py:147 counter/models.py:402 counter/models.py:419 +#: eboutic/models.py:14 eboutic/models.py:47 election/models.py:130 +#: launderette/models.py:87 launderette/models.py:124 sas/models.py:132 msgid "user" msgstr "nom d'utilisateur" -#: club/models.py:148 core/models.py:137 election/models.py:125 -#: election/models.py:141 +#: club/models.py:151 core/models.py:145 election/models.py:129 +#: election/models.py:145 msgid "role" msgstr "rôle" -#: club/models.py:150 core/models.py:33 counter/models.py:71 -#: counter/models.py:96 election/models.py:15 election/models.py:82 -#: election/models.py:127 +#: club/models.py:153 core/models.py:37 counter/models.py:76 +#: counter/models.py:101 election/models.py:15 election/models.py:82 +#: election/models.py:131 forum/models.py:26 forum/models.py:125 msgid "description" msgstr "description" -#: club/models.py:155 +#: club/models.py:158 msgid "User must be subscriber to take part to a club" msgstr "L'utilisateur doit être cotisant pour faire partie d'un club" -#: club/models.py:157 +#: club/models.py:160 msgid "User is already member of that club" msgstr "L'utilisateur est déjà membre de ce club" -#: club/models.py:161 +#: club/models.py:164 msgid "past member" msgstr "Anciens membres" @@ -879,7 +887,7 @@ msgid "Mark as old" msgstr "Marquer comme ancien" #: club/templates/club/club_members.jinja:30 -#: core/templates/core/file_detail.jinja:19 core/views/forms.py:203 +#: core/templates/core/file_detail.jinja:19 core/views/forms.py:205 #: launderette/views.py:154 msgid "Add" msgstr "Ajouter" @@ -898,13 +906,14 @@ msgstr "Du" msgid "To" msgstr "Au" -#: club/templates/club/club_sellings.jinja:5 club/views.py:60 club/views.py:220 -#: counter/templates/counter/counter_main.jinja:19 +#: club/templates/club/club_sellings.jinja:5 club/views.py:60 +#: club/views.py:223 counter/templates/counter/counter_main.jinja:19 #: counter/templates/counter/last_ops.jinja:35 msgid "Sellings" msgstr "Ventes" -#: club/templates/club/club_sellings.jinja:9 club/templates/club/stats.jinja:19 +#: club/templates/club/club_sellings.jinja:9 +#: club/templates/club/stats.jinja:19 #: counter/templates/counter/cash_summary_list.jinja:15 msgid "Show" msgstr "Montrer" @@ -921,14 +930,18 @@ msgstr "Quantité : " msgid "units" msgstr "unités" -#: club/templates/club/club_sellings.jinja:20 club/views.py:171 +#: club/templates/club/club_sellings.jinja:15 +msgid "Benefit: " +msgstr "Bénéfice : " + +#: club/templates/club/club_sellings.jinja:21 club/views.py:173 #: core/templates/core/user_account_detail.jinja:18 #: core/templates/core/user_account_detail.jinja:51 -#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:82 +#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:103 msgid "Counter" msgstr "Comptoir" -#: club/templates/club/club_sellings.jinja:21 +#: club/templates/club/club_sellings.jinja:22 #: core/templates/core/user_account_detail.jinja:19 #: core/templates/core/user_account_detail.jinja:52 #: counter/templates/counter/last_ops.jinja:15 @@ -936,21 +949,21 @@ msgstr "Comptoir" msgid "Barman" msgstr "Barman" -#: club/templates/club/club_sellings.jinja:22 +#: club/templates/club/club_sellings.jinja:23 #: counter/templates/counter/counter_click.jinja:29 #: counter/templates/counter/last_ops.jinja:16 #: counter/templates/counter/last_ops.jinja:41 msgid "Customer" msgstr "Client" -#: club/templates/club/club_sellings.jinja:24 +#: club/templates/club/club_sellings.jinja:25 #: core/templates/core/user_account_detail.jinja:21 #: core/templates/core/user_stats.jinja:28 #: counter/templates/counter/last_ops.jinja:43 msgid "Quantity" msgstr "Quantité" -#: club/templates/club/club_sellings.jinja:25 +#: club/templates/club/club_sellings.jinja:26 #: core/templates/core/user_account.jinja:10 #: core/templates/core/user_account_detail.jinja:22 #: counter/templates/counter/cash_summary_list.jinja:35 @@ -959,7 +972,7 @@ msgstr "Quantité" msgid "Total" msgstr "Total" -#: club/templates/club/club_sellings.jinja:26 +#: club/templates/club/club_sellings.jinja:27 #: core/templates/core/user_account_detail.jinja:23 #: core/templates/core/user_account_detail.jinja:54 #: counter/templates/counter/last_ops.jinja:18 @@ -968,7 +981,7 @@ msgid "Payment method" msgstr "Méthode de paiement" #: club/templates/club/club_tools.jinja:4 -#: core/templates/core/user_tools.jinja:91 +#: core/templates/core/user_tools.jinja:86 msgid "Club tools" msgstr "Outils club" @@ -980,15 +993,19 @@ msgstr "Communication : " msgid "Create a news" msgstr "Créer une nouvelle" -#: club/templates/club/club_tools.jinja:10 +#: club/templates/club/club_tools.jinja:9 +msgid "Post in the Weekmail" +msgstr "Poster dans le Weekmail" + +#: club/templates/club/club_tools.jinja:11 msgid "Counters:" msgstr "Comptoirs : " -#: club/templates/club/club_tools.jinja:26 +#: club/templates/club/club_tools.jinja:27 msgid "Accouting: " msgstr "Comptabilité : " -#: club/templates/club/club_tools.jinja:34 +#: club/templates/club/club_tools.jinja:35 msgid "Manage launderettes" msgstr "Gestion des laveries" @@ -1005,108 +1022,140 @@ msgid "Old members" msgstr "Anciens membres" #: club/views.py:50 core/templates/core/base.jinja:64 core/views/user.py:146 -#: sas/templates/sas/picture.jinja:87 +#: sas/templates/sas/picture.jinja:95 msgid "Tools" msgstr "Outils" #: club/views.py:66 counter/templates/counter/counter_list.jinja:21 -#: counter/templates/counter/counter_list.jinja:42 -#: counter/templates/counter/counter_list.jinja:57 +#: counter/templates/counter/counter_list.jinja:36 +#: counter/templates/counter/counter_list.jinja:51 msgid "Props" msgstr "Propriétés" -#: club/views.py:107 core/views/forms.py:204 counter/views.py:39 +#: club/views.py:107 core/views/forms.py:206 counter/views.py:68 msgid "Select user" msgstr "Choisir un utilisateur" -#: club/views.py:154 sas/views.py:82 sas/views.py:132 sas/views.py:201 +#: club/views.py:156 sas/views.py:82 sas/views.py:133 sas/views.py:204 msgid "You do not have the permission to do that" msgstr "Vous n'avez pas la permission de faire cela" -#: club/views.py:169 counter/views.py:949 +#: club/views.py:171 counter/views.py:977 msgid "Begin date" msgstr "Date de début" -#: club/views.py:170 com/views.py:81 counter/views.py:950 election/views.py:131 +#: club/views.py:172 com/views.py:99 counter/views.py:978 +#: election/views.py:131 msgid "End date" msgstr "Date de fin" -#: club/views.py:184 core/templates/core/user_stats.jinja:27 -#: counter/views.py:1030 +#: club/views.py:186 core/templates/core/user_stats.jinja:27 +#: counter/views.py:1064 msgid "Product" msgstr "Produit" -#: com/models.py:11 +#: com/models.py:13 msgid "alert message" msgstr "message d'alerte" -#: com/models.py:12 +#: com/models.py:14 msgid "info message" msgstr "message d'info" -#: com/models.py:13 +#: com/models.py:15 msgid "index page" msgstr "page d'accueil" -#: com/models.py:22 +#: com/models.py:16 +msgid "weekmail destinations" +msgstr "destinataires du weekmail" + +#: com/models.py:25 msgid "Notice" msgstr "Information" -#: com/models.py:23 +#: com/models.py:26 msgid "Event" msgstr "Événement" -#: com/models.py:24 com/templates/com/news_list.jinja:79 +#: com/models.py:27 com/templates/com/news_list.jinja:79 msgid "Weekly" msgstr "Hebdomadaire" -#: com/models.py:25 +#: com/models.py:28 msgid "Call" msgstr "Appel" -#: com/models.py:30 election/models.py:14 election/models.py:81 -#: election/models.py:114 +#: com/models.py:33 com/models.py:75 com/models.py:119 election/models.py:14 +#: election/models.py:81 election/models.py:118 forum/models.py:168 msgid "title" msgstr "titre" -#: com/models.py:31 +#: com/models.py:34 msgid "summary" msgstr "résumé" -#: com/models.py:32 +#: com/models.py:35 com/models.py:120 msgid "content" msgstr "contenu de la nouvelle" -#: com/models.py:33 core/models.py:901 launderette/models.py:60 -#: launderette/models.py:85 launderette/models.py:121 stock/models.py:34 -#: stock/models.py:73 +#: com/models.py:36 core/models.py:963 launderette/models.py:60 +#: launderette/models.py:85 launderette/models.py:121 msgid "type" msgstr "type" -#: com/models.py:35 +#: com/models.py:38 com/models.py:121 msgid "author" msgstr "auteur" -#: com/models.py:36 core/models.py:518 +#: com/models.py:39 core/models.py:554 msgid "is moderated" msgstr "est modéré" -#: com/models.py:37 +#: com/models.py:40 msgid "moderator" msgstr "modérateur" -#: com/models.py:61 +#: com/models.py:64 msgid "news_date" msgstr "date de la nouvelle" -#: com/models.py:62 +#: com/models.py:65 msgid "start_date" msgstr "date de début" -#: com/models.py:63 +#: com/models.py:66 msgid "end_date" msgstr "date de fin" +#: com/models.py:76 +msgid "intro" +msgstr "intro" + +#: com/models.py:77 +msgid "joke" +msgstr "blague" + +#: com/models.py:78 +msgid "protip" +msgstr "astuce" + +#: com/models.py:79 +msgid "conclusion" +msgstr "conclusion" + +#: com/models.py:80 +msgid "sent" +msgstr "envoyé" + +#: com/models.py:118 +msgid "weekmail" +msgstr "weekmail" + +#: com/models.py:123 +msgid "rank" +msgstr "rang" + #: com/templates/com/news_admin_list.jinja:5 msgid "News admin" msgstr "Administration des nouvelles" @@ -1130,6 +1179,8 @@ msgstr "Type" #: com/templates/com/news_admin_list.jinja:15 #: com/templates/com/news_admin_list.jinja:49 +#: com/templates/com/weekmail.jinja:19 com/templates/com/weekmail.jinja:48 +#: forum/templates/forum/forum.jinja:26 forum/templates/forum/forum.jinja:44 msgid "Title" msgstr "Titre" @@ -1140,6 +1191,8 @@ msgstr "Résumé" #: com/templates/com/news_admin_list.jinja:18 #: com/templates/com/news_admin_list.jinja:52 +#: com/templates/com/weekmail.jinja:17 com/templates/com/weekmail.jinja:46 +#: forum/templates/forum/forum.jinja:48 msgid "Author" msgstr "Auteur" @@ -1155,7 +1208,7 @@ msgstr "Nouvelles à modérer" #: com/templates/com/news_detail.jinja:26 #: core/templates/core/file_detail.jinja:65 #: core/templates/core/file_moderation.jinja:23 -#: sas/templates/sas/moderation.jinja:17 sas/templates/sas/picture.jinja:114 +#: sas/templates/sas/moderation.jinja:17 sas/templates/sas/picture.jinja:122 msgid "Moderate" msgstr "Modérer" @@ -1167,7 +1220,7 @@ msgstr "Retour aux nouvelles" msgid "Author: " msgstr "Auteur : " -#: com/templates/com/news_detail.jinja:24 sas/templates/sas/picture.jinja:82 +#: com/templates/com/news_detail.jinja:24 sas/templates/sas/picture.jinja:90 msgid "Moderator: " msgstr "Modérateur : " @@ -1180,6 +1233,7 @@ msgid "Edit news" msgstr "Éditer la nouvelle" #: com/templates/com/news_edit.jinja:8 com/templates/com/news_edit.jinja:40 +#: core/templates/core/user_tools.jinja:72 msgid "Create news" msgstr "Créer nouvelle" @@ -1206,7 +1260,7 @@ msgstr "" "Appel : événement de longue durée, associé à une longue date (candidature, " "concours, ...)" -#: com/templates/com/news_edit.jinja:65 +#: com/templates/com/news_edit.jinja:65 com/templates/com/weekmail.jinja:10 #: core/templates/core/pagerev_edit.jinja:23 msgid "Preview" msgstr "Prévisualiser" @@ -1219,62 +1273,159 @@ msgstr "Événement aujourd'hui et dans les prochains jours" msgid "Coming soon... don't miss!" msgstr "Prochainement... à ne pas rater!" -#: com/views.py:27 +#: com/templates/com/weekmail.jinja:5 com/templates/com/weekmail.jinja.py:9 +#: com/views.py:37 core/templates/core/user_tools.jinja:70 +msgid "Weekmail" +msgstr "Weekmail" + +#: com/templates/com/weekmail.jinja:11 +#: com/templates/com/weekmail_preview.jinja:17 +msgid "Send" +msgstr "Envoyer" + +#: com/templates/com/weekmail.jinja:12 +msgid "New article" +msgstr "Nouvel article" + +#: com/templates/com/weekmail.jinja:13 +msgid "Articles in no weekmail yet" +msgstr "Articles dans aucun weekmail" + +#: com/templates/com/weekmail.jinja:20 com/templates/com/weekmail.jinja:49 +msgid "Content" +msgstr "Contenu" + +#: com/templates/com/weekmail.jinja:34 +msgid "Add to weekmail" +msgstr "Ajouter au Weekmail" + +#: com/templates/com/weekmail.jinja:35 com/templates/com/weekmail.jinja:64 +msgid "Up" +msgstr "Monter" + +#: com/templates/com/weekmail.jinja:36 com/templates/com/weekmail.jinja:65 +msgid "Down" +msgstr "Descendre" + +#: com/templates/com/weekmail.jinja:42 +msgid "Articles included the next weekmail" +msgstr "Article inclus dans le prochain Weekmail" + +#: com/templates/com/weekmail.jinja:63 +msgid "Delete from weekmail" +msgstr "Supprimer du Weekmail" + +#: com/templates/com/weekmail_preview.jinja:9 +#: core/templates/core/user_account_detail.jinja:11 +#: core/templates/core/user_account_detail.jinja:104 launderette/views.py:154 +msgid "Back" +msgstr "Retour" + +#: com/templates/com/weekmail_preview.jinja:11 +msgid "Are you sure you want to send this weekmail?" +msgstr "Êtes-vous sûr de vouloir envoyer ce Weekmail ?" + +#: com/templates/com/weekmail_preview.jinja:13 +msgid "" +"Warning: you are sending the weekmail in another language than the default " +"one!" +msgstr "" +"Attention : vous allez envoyer le Weekmail dans un langage différent de " +"celui par défaut !" + +#: com/templates/com/weekmail_renderer_html.jinja:12 +#: com/templates/com/weekmail_renderer_text.jinja:4 +msgid "Intro" +msgstr "Intro" + +#: com/templates/com/weekmail_renderer_html.jinja:16 +#: com/templates/com/weekmail_renderer_text.jinja:8 +msgid "Table of content" +msgstr "Sommaire" + +#: com/templates/com/weekmail_renderer_html.jinja:29 +#: com/templates/com/weekmail_renderer_text.jinja:19 +msgid "Joke" +msgstr "Blague" + +#: com/templates/com/weekmail_renderer_html.jinja:34 +#: com/templates/com/weekmail_renderer_text.jinja:24 +msgid "Pro tip" +msgstr "Astuce" + +#: com/templates/com/weekmail_renderer_html.jinja:39 +#: com/templates/com/weekmail_renderer_text.jinja:29 +msgid "Final word" +msgstr "Le mot de la fin" + +#: com/views.py:30 msgid "Communication administration" msgstr "Administration de la communication" -#: com/views.py:34 +#: com/views.py:42 core/templates/core/user_tools.jinja:71 +msgid "Weekmail destinations" +msgstr "Destinataires du Weekmail" + +#: com/views.py:47 msgid "Index page" msgstr "Page d'accueil" -#: com/views.py:39 +#: com/views.py:52 msgid "Info message" msgstr "Message d'info" -#: com/views.py:44 +#: com/views.py:57 msgid "Alert message" msgstr "Message d'alerte" -#: com/views.py:80 election/views.py:130 +#: com/views.py:98 election/views.py:130 msgid "Start date" msgstr "Date de début" -#: com/views.py:82 +#: com/views.py:100 msgid "Until" msgstr "Jusqu'à" -#: com/views.py:83 +#: com/views.py:101 msgid "Automoderation" msgstr "Automodération" -#: com/views.py:89 com/views.py:91 com/views.py:93 +#: com/views.py:107 com/views.py:109 com/views.py:111 msgid "This field is required." msgstr "Ce champ est obligatoire." -#: core/models.py:29 +#: com/views.py:264 +msgid "Delete and save to regenerate" +msgstr "Supprimer et sauver pour regénérer" + +#: com/views.py:272 +msgid "Weekmail of the " +msgstr "Weekmail du " + +#: core/models.py:33 msgid "meta group status" msgstr "status du meta-groupe" -#: core/models.py:31 +#: core/models.py:35 msgid "Whether a group is a meta group or not" msgstr "Si un groupe est un meta-groupe ou pas" -#: core/models.py:59 +#: core/models.py:67 #, python-format msgid "%(value)s is not a valid promo (between 0 and %(end)s)" msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)" -#: core/models.py:75 +#: core/models.py:83 msgid "username" msgstr "nom d'utilisateur" -#: core/models.py:78 +#: core/models.py:86 msgid "Required. 254 characters or fewer. Letters, digits and ./+/-/_ only." msgstr "" "Requis. Pas plus de 254 caractères. Uniquement des lettres, numéros, et ./" "+/-/_" -#: core/models.py:82 +#: core/models.py:90 msgid "" "Enter a valid username. This value may contain only letters, numbers and ./" "+/-/_ characters." @@ -1282,43 +1433,43 @@ msgstr "" "Entrez un nom d'utilisateur correct. Uniquement des lettres, numéros, et ./" "+/-/_" -#: core/models.py:87 +#: core/models.py:95 msgid "A user with that username already exists." msgstr "Un utilisateur de ce nom existe déjà" -#: core/models.py:90 +#: core/models.py:98 msgid "first name" msgstr "Prénom" -#: core/models.py:91 +#: core/models.py:99 msgid "last name" msgstr "Nom" -#: core/models.py:92 +#: core/models.py:100 msgid "email address" msgstr "adresse email" -#: core/models.py:93 +#: core/models.py:101 msgid "date of birth" msgstr "date de naissance" -#: core/models.py:94 +#: core/models.py:102 msgid "nick name" msgstr "surnom" -#: core/models.py:96 +#: core/models.py:104 msgid "staff status" msgstr "status \"staff\"" -#: core/models.py:98 +#: core/models.py:106 msgid "Designates whether the user can log into this admin site." msgstr "Est-ce que l'utilisateur peut se logger à la partie admin du site." -#: core/models.py:101 +#: core/models.py:109 msgid "active" msgstr "actif" -#: core/models.py:104 +#: core/models.py:112 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -1326,305 +1477,322 @@ msgstr "" "Est-ce que l'utilisateur doit être traité comme actif. Déselectionnez au " "lieu de supprimer les comptes." -#: core/models.py:108 +#: core/models.py:116 msgid "date joined" msgstr "date d'inscription" -#: core/models.py:109 +#: core/models.py:117 msgid "last update" msgstr "dernière mise à jour" -#: core/models.py:111 +#: core/models.py:119 msgid "superuser" msgstr "super-utilisateur" -#: core/models.py:114 +#: core/models.py:122 msgid "Designates whether this user is a superuser. " msgstr "Est-ce que l'utilisateur est super-utilisateur." -#: core/models.py:120 +#: core/models.py:128 msgid "profile" msgstr "profil" -#: core/models.py:122 +#: core/models.py:130 msgid "avatar" msgstr "avatar" -#: core/models.py:124 +#: core/models.py:132 msgid "scrub" msgstr "blouse" -#: core/models.py:126 +#: core/models.py:134 msgid "sex" msgstr "sexe" -#: core/models.py:126 +#: core/models.py:134 msgid "Man" msgstr "Homme" -#: core/models.py:126 +#: core/models.py:134 msgid "Woman" msgstr "Femme" -#: core/models.py:127 +#: core/models.py:135 msgid "tshirt size" msgstr "taille de tshirt" -#: core/models.py:128 +#: core/models.py:136 msgid "-" msgstr "-" -#: core/models.py:129 +#: core/models.py:137 msgid "XS" msgstr "XS" -#: core/models.py:130 +#: core/models.py:138 msgid "S" msgstr "S" -#: core/models.py:131 +#: core/models.py:139 msgid "M" msgstr "M" -#: core/models.py:132 +#: core/models.py:140 msgid "L" msgstr "L" -#: core/models.py:133 +#: core/models.py:141 msgid "XL" msgstr "XL" -#: core/models.py:134 +#: core/models.py:142 msgid "XXL" msgstr "XXL" -#: core/models.py:135 +#: core/models.py:143 msgid "XXXL" msgstr "XXXL" -#: core/models.py:138 +#: core/models.py:146 msgid "Student" msgstr "Étudiant" -#: core/models.py:139 +#: core/models.py:147 msgid "Administrative agent" msgstr "Personnel administratif" -#: core/models.py:140 +#: core/models.py:148 msgid "Teacher" msgstr "Enseignant" -#: core/models.py:141 +#: core/models.py:149 msgid "Agent" msgstr "Personnel" -#: core/models.py:142 +#: core/models.py:150 msgid "Doctor" msgstr "Doctorant" -#: core/models.py:143 +#: core/models.py:151 msgid "Former student" msgstr "Ancien étudiant" -#: core/models.py:144 +#: core/models.py:152 msgid "Service" msgstr "Service" -#: core/models.py:146 +#: core/models.py:154 msgid "department" msgstr "département" -#: core/models.py:148 +#: core/models.py:156 msgid "dpt option" msgstr "Filière" -#: core/models.py:149 +#: core/models.py:157 msgid "semester" msgstr "semestre" -#: core/models.py:150 +#: core/models.py:158 msgid "quote" msgstr "citation" -#: core/models.py:151 +#: core/models.py:159 msgid "school" msgstr "école" -#: core/models.py:152 +#: core/models.py:160 msgid "promo" msgstr "promo" -#: core/models.py:153 +#: core/models.py:161 msgid "forum signature" msgstr "signature du forum" -#: core/models.py:154 +#: core/models.py:162 msgid "second email address" msgstr "adresse email secondaire" -#: core/models.py:156 +#: core/models.py:164 msgid "parent phone" msgstr "téléphone des parents" -#: core/models.py:158 +#: core/models.py:166 msgid "parent address" msgstr "adresse des parents" -#: core/models.py:159 +#: core/models.py:167 msgid "is subscriber viewable" msgstr "profil visible par les cotisants" -#: core/models.py:292 +#: core/models.py:306 msgid "A user with that username already exists" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" -#: core/models.py:417 core/templates/core/macros.jinja:17 +#: core/models.py:431 core/templates/core/macros.jinja:17 #: core/templates/core/user_detail.jinja:14 #: core/templates/core/user_detail.jinja:16 #: core/templates/core/user_edit.jinja:17 #: election/templates/election/election_detail.jinja:316 +#: forum/templates/forum/macros.jinja:87 forum/templates/forum/macros.jinja:89 msgid "Profile" msgstr "Profil" -#: core/models.py:486 +#: core/models.py:511 msgid "Visitor" msgstr "Visiteur" -#: core/models.py:491 +#: core/models.py:516 +msgid "do you want to receive the weekmail" +msgstr "voulez-vous recevoir le Weekmail" + +#: core/models.py:521 msgid "define if we show a users stats" msgstr "Definit si l'on montre les statistiques de l'utilisateur" -#: core/models.py:493 +#: core/models.py:523 msgid "Show your account statistics to others" msgstr "Montrez vos statistiques de compte aux autres" -#: core/models.py:506 +#: core/models.py:542 msgid "file name" msgstr "nom du fichier" -#: core/models.py:507 core/models.py:707 +#: core/models.py:543 core/models.py:751 msgid "parent" msgstr "parent" -#: core/models.py:508 core/models.py:524 +#: core/models.py:544 core/models.py:560 msgid "file" msgstr "fichier" -#: core/models.py:509 +#: core/models.py:545 msgid "compressed file" msgstr "version allégée" -#: core/models.py:510 +#: core/models.py:546 msgid "thumbnail" msgstr "miniature" -#: core/models.py:511 core/models.py:519 +#: core/models.py:547 core/models.py:555 msgid "owner" msgstr "propriétaire" -#: core/models.py:512 core/models.py:713 +#: core/models.py:548 core/models.py:757 core/views/files.py:122 msgid "edit group" msgstr "groupe d'édition" -#: core/models.py:513 core/models.py:714 +#: core/models.py:549 core/models.py:758 core/views/files.py:123 msgid "view group" msgstr "groupe de vue" -#: core/models.py:514 +#: core/models.py:550 msgid "is folder" msgstr "est un dossier" -#: core/models.py:515 +#: core/models.py:551 msgid "mime type" msgstr "type mime" -#: core/models.py:516 +#: core/models.py:552 msgid "size" msgstr "taille" -#: core/models.py:520 +#: core/models.py:556 msgid "asked for removal" msgstr "retrait demandé" -#: core/models.py:521 +#: core/models.py:557 msgid "is in the SAS" msgstr "est dans le SAS" -#: core/models.py:560 +#: core/models.py:596 msgid "Character '/' not authorized in name" msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier" -#: core/models.py:563 core/models.py:568 +#: core/models.py:599 core/models.py:604 msgid "Loop in folder tree" msgstr "Boucle dans l'arborescence des dossiers" -#: core/models.py:572 +#: core/models.py:608 msgid "You can not make a file be a children of a non folder file" msgstr "" "Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas " "un dossier" -#: core/models.py:576 +#: core/models.py:612 msgid "Duplicate file" msgstr "Un fichier de ce nom existe déjà" -#: core/models.py:590 +#: core/models.py:626 msgid "You must provide a file" msgstr "Vous devez fournir un fichier" -#: core/models.py:656 +#: core/models.py:692 msgid "Folder: " msgstr "Dossier : " -#: core/models.py:658 +#: core/models.py:694 msgid "File: " msgstr "Fichier : " -#: core/models.py:706 core/models.py:710 +#: core/models.py:742 +msgid "page unix name" +msgstr "nom unix de la page" + +#: core/models.py:746 +msgid "" +"Enter a valid page name. This value may contain only unaccented letters, " +"numbers and ./+/-/_ characters." +msgstr "" +"Entrez un nom de page correct. Uniquement des lettres non accentuées, " +"numéros, et ./+/-/_" + +#: core/models.py:754 msgid "page name" msgstr "nom de la page" -#: core/models.py:711 +#: core/models.py:755 msgid "owner group" msgstr "groupe propriétaire" -#: core/models.py:715 +#: core/models.py:759 msgid "lock user" msgstr "utilisateur bloquant" -#: core/models.py:716 +#: core/models.py:760 msgid "lock_timeout" msgstr "décompte du déblocage" -#: core/models.py:743 +#: core/models.py:787 msgid "Duplicate page" msgstr "Une page de ce nom existe déjà" -#: core/models.py:749 +#: core/models.py:793 msgid "Loop in page tree" msgstr "Boucle dans l'arborescence des pages" -#: core/models.py:859 +#: core/models.py:921 msgid "revision" msgstr "révision" -#: core/models.py:860 +#: core/models.py:922 msgid "page title" msgstr "titre de la page" -#: core/models.py:861 +#: core/models.py:923 msgid "page content" msgstr "contenu de la page" -#: core/models.py:899 +#: core/models.py:961 msgid "url" msgstr "url" -#: core/models.py:900 +#: core/models.py:962 msgid "param" msgstr "param" -#: core/models.py:903 +#: core/models.py:965 msgid "viewed" msgstr "vue" @@ -1658,6 +1826,7 @@ msgid "View more" msgstr "Voir plus" #: core/templates/core/base.jinja:62 +#: forum/templates/forum/last_unread.jinja:15 msgid "Mark all as read" msgstr "Marquer tout commme lu" @@ -1682,12 +1851,16 @@ msgid "Wiki" msgstr "Wiki" #: core/templates/core/base.jinja:93 sas/templates/sas/album.jinja:4 -#: sas/templates/sas/main.jinja:4 sas/templates/sas/main.jinja.py:8 -#: sas/templates/sas/picture.jinja:26 +#: sas/templates/sas/main.jinja:4 sas/templates/sas/main.jinja.py:32 +#: sas/templates/sas/picture.jinja:34 msgid "SAS" msgstr "SAS" -#: core/templates/core/base.jinja:94 +#: core/templates/core/base.jinja:94 forum/templates/forum/forum.jinja:10 +#: forum/templates/forum/last_unread.jinja:12 +#: forum/templates/forum/main.jinja:6 forum/templates/forum/main.jinja:11 +#: forum/templates/forum/main.jinja:13 forum/templates/forum/reply.jinja:10 +#: forum/templates/forum/topic.jinja:30 msgid "Forum" msgstr "Forum" @@ -1696,7 +1869,7 @@ msgid "Services" msgstr "Services" #: core/templates/core/base.jinja:96 core/templates/core/file.jinja:20 -#: core/views/files.py:50 +#: core/views/files.py:51 msgid "Files" msgstr "Fichiers" @@ -1708,27 +1881,27 @@ msgstr "Partenaires" msgid "Help" msgstr "Aide" -#: core/templates/core/base.jinja:131 +#: core/templates/core/base.jinja:137 msgid "Contacts" msgstr "Contacts" -#: core/templates/core/base.jinja:132 +#: core/templates/core/base.jinja:138 msgid "Legal notices" msgstr "Mentions légales" -#: core/templates/core/base.jinja:133 +#: core/templates/core/base.jinja:139 msgid "Intellectual property" msgstr "Propriété intellectuelle" -#: core/templates/core/base.jinja:134 +#: core/templates/core/base.jinja:140 msgid "Help & Documentation" msgstr "Aide & Documentation" -#: core/templates/core/base.jinja:135 +#: core/templates/core/base.jinja:141 msgid "R&D" msgstr "R&D" -#: core/templates/core/base.jinja:137 +#: core/templates/core/base.jinja:143 msgid "Site made by good people" msgstr "Site réalisé par des gens bons" @@ -1790,7 +1963,7 @@ msgstr "Propriétés" #: core/templates/core/file_detail.jinja:13 #: core/templates/core/file_moderation.jinja:20 -#: sas/templates/sas/picture.jinja:80 +#: sas/templates/sas/picture.jinja:88 msgid "Owner: " msgstr "Propriétaire : " @@ -1816,7 +1989,7 @@ msgstr "Nom réel : " #: core/templates/core/file_detail.jinja:54 #: core/templates/core/file_moderation.jinja:21 -#: sas/templates/sas/picture.jinja:79 +#: sas/templates/sas/picture.jinja:87 msgid "Date: " msgstr "Date : " @@ -2124,7 +2297,7 @@ msgstr "Résultat de la recherche" msgid "Users" msgstr "Utilisateurs" -#: core/templates/core/search.jinja:18 core/views/user.py:158 +#: core/templates/core/search.jinja:18 core/views/user.py:163 #: counter/templates/counter/stats.jinja:17 msgid "Clubs" msgstr "Clubs" @@ -2166,7 +2339,7 @@ msgid "Eboutic invoices" msgstr "Facture eboutic" #: core/templates/core/user_account.jinja:53 -#: core/templates/core/user_tools.jinja:33 counter/views.py:496 +#: core/templates/core/user_tools.jinja:33 counter/views.py:519 msgid "Etickets" msgstr "Etickets" @@ -2175,12 +2348,6 @@ msgstr "Etickets" msgid "User has no account" msgstr "L'utilisateur n'a pas de compte" -#: core/templates/core/user_account_detail.jinja:11 -#: core/templates/core/user_account_detail.jinja:104 launderette/views.py:154 -#: stock/templates/stock/shopping_list_items.jinja:9 -msgid "Back" -msgstr "Retour" - #: core/templates/core/user_account_detail.jinja:79 msgid "Items" msgstr "Articles" @@ -2330,7 +2497,7 @@ msgstr "Outils utilisateurs" msgid "Sith management" msgstr "Gestion de Sith" -#: core/templates/core/user_tools.jinja:14 core/views/user.py:164 +#: core/templates/core/user_tools.jinja:14 core/views/user.py:169 msgid "Groups" msgstr "Groupes" @@ -2343,8 +2510,8 @@ msgstr "Fusionner deux utilisateurs" msgid "Subscriptions" msgstr "Cotisations" -#: core/templates/core/user_tools.jinja:24 counter/views.py:466 -#: counter/views.py:615 +#: core/templates/core/user_tools.jinja:24 counter/views.py:489 +#: counter/views.py:643 msgid "Counters" msgstr "Comptoirs" @@ -2365,16 +2532,16 @@ msgid "Product types management" msgstr "Gestion des types de produit" #: core/templates/core/user_tools.jinja:31 -#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:486 +#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:509 msgid "Cash register summaries" msgstr "Relevés de caisse" #: core/templates/core/user_tools.jinja:32 -#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:491 +#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:514 msgid "Invoices call" msgstr "Appels à facture" -#: core/templates/core/user_tools.jinja:40 core/views/user.py:174 +#: core/templates/core/user_tools.jinja:39 core/views/user.py:179 #: counter/templates/counter/counter_list.jinja:18 #: counter/templates/counter/counter_list.jinja:33 #: counter/templates/counter/counter_list.jinja:54 @@ -2409,70 +2576,78 @@ msgstr "Compte club : " msgid "Communication" msgstr "Communication" -#: core/templates/core/user_tools.jinja:78 +#: core/templates/core/user_tools.jinja:69 +msgid "Create weekmail article" +msgstr "Rédiger un nouvel article dans le Weekmail" + +#: core/templates/core/user_tools.jinja:73 msgid "Moderate news" msgstr "Modérer les nouvelles" -#: core/templates/core/user_tools.jinja:79 +#: core/templates/core/user_tools.jinja:74 msgid "Edit index page" msgstr "Éditer la page d'accueil" -#: core/templates/core/user_tools.jinja:80 +#: core/templates/core/user_tools.jinja:75 msgid "Edit alert message" msgstr "Éditer le message d'alerte" -#: core/templates/core/user_tools.jinja:81 +#: core/templates/core/user_tools.jinja:76 msgid "Edit information message" msgstr "Éditer le message d'informations" -#: core/templates/core/user_tools.jinja:82 +#: core/templates/core/user_tools.jinja:77 msgid "Moderate files" msgstr "Modérer les fichiers" -#: core/templates/core/user_tools.jinja:85 +#: core/templates/core/user_tools.jinja:80 msgid "Moderate pictures" msgstr "Modérer les photos" -#: core/templates/core/user_tools.jinja:98 +#: core/templates/core/user_tools.jinja:93 msgid "Elections" msgstr "Élections" -#: core/templates/core/user_tools.jinja:100 +#: core/templates/core/user_tools.jinja:95 msgid "See available elections" msgstr "Voir les élections disponibles" -#: core/templates/core/user_tools.jinja:102 +#: core/templates/core/user_tools.jinja:97 msgid "Create a new election" msgstr "Créer une nouvelle élection" -#: core/views/files.py:49 +#: core/views/files.py:50 msgid "Add a new folder" msgstr "Ajouter un nouveau dossier" -#: core/views/files.py:62 +#: core/views/files.py:63 #, python-format msgid "Error creating folder %(folder_name)s: %(msg)s" msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s" -#: core/views/files.py:72 core/views/forms.py:181 core/views/forms.py:185 +#: core/views/files.py:73 core/views/forms.py:183 core/views/forms.py:187 #: sas/views.py:53 #, python-format msgid "Error uploading file %(file_name)s: %(msg)s" msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s" -#: core/views/forms.py:59 core/views/forms.py:62 +#: core/views/files.py:124 sas/views.py:260 +msgid "Apply rights recursively" +msgstr "Appliquer les droits récursivement" + +#: core/views/forms.py:60 core/views/forms.py:63 msgid "Choose file" msgstr "Choisir un fichier" -#: core/views/forms.py:73 core/views/forms.py:76 +#: core/views/forms.py:74 core/views/forms.py:77 msgid "Choose user" msgstr "Choisir un utilisateur" -#: core/views/forms.py:98 +#: core/views/forms.py:99 msgid "Username, email, or account number" msgstr "Nom d'utilisateur, email, ou numéro de compte AE" -#: core/views/forms.py:140 +#: core/views/forms.py:141 msgid "" "Profile: you need to be visible on the picture, in order to be recognized (e." "g. by the barmen)" @@ -2480,23 +2655,23 @@ msgstr "" "Photo de profil: vous devez être visible sur la photo afin d'être reconnu " "(par exemple par les barmen)" -#: core/views/forms.py:141 +#: core/views/forms.py:142 msgid "Avatar: used on the forum" msgstr "Avatar : utilisé sur le forum" -#: core/views/forms.py:142 +#: core/views/forms.py:143 msgid "Scrub: let other know how your scrub looks like!" msgstr "Blouse : montrez aux autres à quoi ressemble votre blouse !" -#: core/views/forms.py:186 +#: core/views/forms.py:188 msgid "Bad image format, only jpeg, png, and gif are accepted" msgstr "Mauvais format d'image, seuls les jpeg, png, et gif sont acceptés" -#: core/views/forms.py:203 +#: core/views/forms.py:205 msgid "Godfather" msgstr "Parrain" -#: core/views/forms.py:203 +#: core/views/forms.py:205 msgid "Godchild" msgstr "Fillot" @@ -2504,7 +2679,11 @@ msgstr "Fillot" msgid "Pictures" msgstr "Photos" -#: core/views/user.py:312 +#: core/views/user.py:157 +msgid "Preferences" +msgstr "Préférences" + +#: core/views/user.py:331 msgid "User already has a profile picture" msgstr "L'utilisateur a déjà une photo de profil" @@ -2520,136 +2699,136 @@ msgstr "client" msgid "customers" msgstr "clients" -#: counter/models.py:47 counter/templates/counter/counter_click.jinja:48 +#: counter/models.py:52 counter/templates/counter/counter_click.jinja:48 #: counter/templates/counter/counter_click.jinja:82 msgid "Not enough money" msgstr "Solde insuffisant" -#: counter/models.py:75 counter/models.py:97 +#: counter/models.py:80 counter/models.py:102 msgid "product type" msgstr "type du produit" -#: counter/models.py:100 +#: counter/models.py:105 msgid "purchase price" msgstr "prix d'achat" -#: counter/models.py:101 +#: counter/models.py:106 msgid "selling price" msgstr "prix de vente" -#: counter/models.py:102 +#: counter/models.py:107 msgid "special selling price" msgstr "prix de vente spécial" -#: counter/models.py:103 +#: counter/models.py:108 msgid "icon" msgstr "icône" -#: counter/models.py:105 +#: counter/models.py:110 msgid "limit age" msgstr "âge limite" -#: counter/models.py:106 +#: counter/models.py:111 msgid "tray price" msgstr "prix plateau" -#: counter/models.py:107 +#: counter/models.py:112 msgid "parent product" msgstr "produit parent" -#: counter/models.py:109 +#: counter/models.py:114 msgid "buying groups" msgstr "groupe d'achat" -#: counter/models.py:110 +#: counter/models.py:115 msgid "archived" msgstr "archivé" -#: counter/models.py:113 counter/models.py:497 +#: counter/models.py:118 counter/models.py:502 msgid "product" msgstr "produit" -#: counter/models.py:132 +#: counter/models.py:137 msgid "products" msgstr "produits" -#: counter/models.py:133 +#: counter/models.py:138 msgid "counter type" msgstr "type de comptoir" -#: counter/models.py:135 +#: counter/models.py:140 msgid "Bar" msgstr "Bar" -#: counter/models.py:135 +#: counter/models.py:140 msgid "Office" msgstr "Bureau" -#: counter/models.py:135 counter/templates/counter/counter_list.jinja:11 +#: counter/models.py:140 counter/templates/counter/counter_list.jinja:11 #: eboutic/templates/eboutic/eboutic_main.jinja:4 #: eboutic/templates/eboutic/eboutic_main.jinja:24 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:8 #: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 -#: sith/settings.py:313 sith/settings.py:321 +#: sith/settings.py:324 sith/settings.py:332 msgid "Eboutic" msgstr "Eboutic" -#: counter/models.py:136 +#: counter/models.py:141 msgid "sellers" msgstr "vendeurs" -#: counter/models.py:139 launderette/models.py:123 +#: counter/models.py:144 launderette/models.py:123 msgid "token" msgstr "jeton" -#: counter/models.py:142 counter/models.py:398 counter/models.py:415 -#: launderette/models.py:14 stock/models.py:14 +#: counter/models.py:147 counter/models.py:403 counter/models.py:420 +#: launderette/models.py:14 msgid "counter" msgstr "comptoir" -#: counter/models.py:245 +#: counter/models.py:250 msgid "bank" msgstr "banque" -#: counter/models.py:247 counter/models.py:293 +#: counter/models.py:252 counter/models.py:298 msgid "is validated" msgstr "est validé" -#: counter/models.py:250 +#: counter/models.py:255 msgid "refilling" msgstr "rechargement" -#: counter/models.py:286 eboutic/models.py:103 +#: counter/models.py:291 eboutic/models.py:103 msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:287 counter/models.py:487 eboutic/models.py:104 +#: counter/models.py:292 counter/models.py:492 eboutic/models.py:104 msgid "quantity" msgstr "quantité" -#: counter/models.py:292 +#: counter/models.py:297 msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:292 sith/settings.py:306 sith/settings.py:311 -#: sith/settings.py:333 +#: counter/models.py:297 sith/settings.py:317 sith/settings.py:322 +#: sith/settings.py:344 msgid "Credit card" msgstr "Carte bancaire" -#: counter/models.py:296 +#: counter/models.py:301 msgid "selling" msgstr "vente" -#: counter/models.py:315 +#: counter/models.py:320 msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:316 +#: counter/models.py:321 #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:318 counter/models.py:330 +#: counter/models.py:323 counter/models.py:335 #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -2658,51 +2837,51 @@ msgstr "" "Vous avez acheté un Eticket pour l'événement %(event)s.\n" "Vous pouvez le télécharger sur cette page: %(url)s" -#: counter/models.py:401 +#: counter/models.py:406 msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:404 +#: counter/models.py:409 msgid "permanency" msgstr "permanence" -#: counter/models.py:418 +#: counter/models.py:423 msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:421 +#: counter/models.py:426 msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:485 +#: counter/models.py:490 msgid "cash summary" msgstr "relevé" -#: counter/models.py:486 +#: counter/models.py:491 msgid "value" msgstr "valeur" -#: counter/models.py:488 +#: counter/models.py:493 msgid "check" msgstr "chèque" -#: counter/models.py:491 +#: counter/models.py:496 msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:498 +#: counter/models.py:503 msgid "banner" msgstr "bannière" -#: counter/models.py:499 +#: counter/models.py:504 msgid "event date" msgstr "date de l'événement" -#: counter/models.py:500 +#: counter/models.py:505 msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:501 +#: counter/models.py:506 msgid "secret" msgstr "secret" @@ -2750,7 +2929,7 @@ msgstr "Liste des relevés de caisse" msgid "Theoric sums" msgstr "Sommes théoriques" -#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:729 +#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:757 msgid "Emptied" msgstr "Coffre vidé" @@ -2758,7 +2937,21 @@ msgstr "Coffre vidé" msgid "yes" msgstr "oui" -#: counter/templates/counter/cash_summary_list.jinja:59 +#: counter/templates/counter/cash_summary_list.jinja:61 +#: counter/templates/counter/cash_summary_list.jinja:63 +msgid "Previous" +msgstr "Précédent" + +#: counter/templates/counter/cash_summary_list.jinja:67 +msgid "current" +msgstr "actuel" + +#: counter/templates/counter/cash_summary_list.jinja:73 +#: counter/templates/counter/cash_summary_list.jinja:75 +msgid "Next" +msgstr "Suivant" + +#: counter/templates/counter/cash_summary_list.jinja:79 msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." @@ -2787,7 +2980,7 @@ msgstr "Pas de date de naissance renseignée" #: counter/templates/counter/invoices_call.jinja:16 #: launderette/templates/launderette/launderette_admin.jinja:35 #: launderette/templates/launderette/launderette_click.jinja:13 -#: sas/templates/sas/picture.jinja:74 +#: sas/templates/sas/picture.jinja:82 msgid "Go" msgstr "Valider" @@ -2883,6 +3076,10 @@ msgstr "Appels à facture pour %(date)s" msgid "Choose another month: " msgstr "Choisir un autre mois : " +#: counter/templates/counter/invoices_call.jinja:19 +msgid "CB Payments" +msgstr "Payements en Carte Bancaire" + #: counter/templates/counter/last_ops.jinja:5 #: counter/templates/counter/last_ops.jinja:9 #, python-format @@ -2946,132 +3143,124 @@ msgstr "Top 100 barman %(counter_name)s" #: counter/templates/counter/stats.jinja:53 #: counter/templates/counter/stats.jinja:78 msgid "Time" -msgstr "Heure" +msgstr "Temps" #: counter/templates/counter/stats.jinja:72 #, python-format msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" -#: counter/views.py:55 +#: counter/views.py:82 msgid "User not found" msgstr "Utilisateur non trouvé" -#: counter/views.py:89 +#: counter/views.py:109 msgid "Cash summary" msgstr "Relevé de caisse" -#: counter/views.py:95 +#: counter/views.py:114 msgid "Last operations" msgstr "Dernières opérations" -#: counter/views.py:101 -msgid "Take items from stock" -msgstr "Prendre des éléments du stock" - -#: counter/views.py:135 +#: counter/views.py:148 msgid "Bad credentials" msgstr "Mauvais identifiants" -#: counter/views.py:137 +#: counter/views.py:150 msgid "User is not barman" msgstr "L'utilisateur n'est pas barman." -#: counter/views.py:141 +#: counter/views.py:154 msgid "Bad location, someone is already logged in somewhere else" msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" -#: counter/views.py:331 +#: counter/views.py:356 msgid "END" msgstr "FIN" -#: counter/views.py:333 +#: counter/views.py:358 msgid "CAN" msgstr "ANN" -#: counter/views.py:363 +#: counter/views.py:388 msgid "You have not enough money to buy all the basket" msgstr "Vous n'avez pas assez d'argent pour acheter le panier" -#: counter/views.py:456 +#: counter/views.py:484 msgid "Counter administration" msgstr "Administration des comptoirs" -#: counter/views.py:461 -msgid "Stocks" -msgstr "Stocks" - -#: counter/views.py:471 +#: counter/views.py:494 msgid "Products" msgstr "Produits" -#: counter/views.py:476 +#: counter/views.py:499 msgid "Archived products" msgstr "Produits archivés" -#: counter/views.py:481 +#: counter/views.py:504 msgid "Product types" msgstr "Types de produit" -#: counter/views.py:612 +#: counter/views.py:640 msgid "Parent product" msgstr "Produit parent" -#: counter/views.py:613 +#: counter/views.py:641 msgid "Buying groups" msgstr "Groupes d'achat" -#: counter/views.py:708 +#: counter/views.py:736 msgid "10 cents" msgstr "10 centimes" -#: counter/views.py:709 +#: counter/views.py:737 msgid "20 cents" msgstr "20 centimes" -#: counter/views.py:710 +#: counter/views.py:738 msgid "50 cents" msgstr "50 centimes" -#: counter/views.py:711 +#: counter/views.py:739 msgid "1 euro" msgstr "1 €" -#: counter/views.py:712 +#: counter/views.py:740 msgid "2 euros" msgstr "2 €" -#: counter/views.py:713 +#: counter/views.py:741 msgid "5 euros" msgstr "5 €" -#: counter/views.py:714 +#: counter/views.py:742 msgid "10 euros" msgstr "10 €" -#: counter/views.py:715 +#: counter/views.py:743 msgid "20 euros" msgstr "20 €" -#: counter/views.py:716 +#: counter/views.py:744 msgid "50 euros" msgstr "50 €" -#: counter/views.py:717 +#: counter/views.py:745 msgid "100 euros" msgstr "100 €" -#: counter/views.py:718 counter/views.py:720 counter/views.py:722 -#: counter/views.py:724 counter/views.py:726 +#: counter/views.py:746 counter/views.py:748 counter/views.py:750 +#: counter/views.py:752 counter/views.py:754 msgid "Check amount" msgstr "Montant du chèque" -#: counter/views.py:719 counter/views.py:721 counter/views.py:723 -#: counter/views.py:725 counter/views.py:727 +#: counter/views.py:747 counter/views.py:749 counter/views.py:751 +#: counter/views.py:753 counter/views.py:755 msgid "Check quantity" msgstr "Nombre de chèque" -#: counter/views.py:1101 +#: counter/views.py:1135 msgid "people(s)" msgstr "personne(s)" @@ -3162,7 +3351,7 @@ msgstr "groupe de vote" msgid "candidature groups" msgstr "groupe de candidature" -#: election/models.py:80 election/models.py:115 +#: election/models.py:80 election/models.py:119 msgid "election" msgstr "élection" @@ -3170,17 +3359,17 @@ msgstr "élection" msgid "max choice" msgstr "nombre de choix maxi" -#: election/models.py:128 +#: election/models.py:132 msgid "election list" msgstr "liste électorale" -#: election/models.py:142 +#: election/models.py:146 msgid "candidature" msgstr "candidature" #: election/templates/election/candidate_form.jinja:4 #: election/templates/election/candidate_form.jinja:13 -#: election/templates/election/election_detail.jinja:363 +#: election/templates/election/election_detail.jinja:365 msgid "Candidate" msgstr "Candidater" @@ -3206,6 +3395,7 @@ msgstr "Les votes ouvriront " #: election/templates/election/election_list.jinja:34 #: election/templates/election/election_list.jinja:39 #: election/templates/election/election_list.jinja:42 +#: forum/templates/forum/macros.jinja:125 msgid " at " msgstr " à " @@ -3238,24 +3428,24 @@ msgid "Choose blank vote" msgstr "Choisir de voter blanc" #: election/templates/election/election_detail.jinja:304 -#: election/templates/election/election_detail.jinja:342 +#: election/templates/election/election_detail.jinja:344 msgid "votes" msgstr "votes" -#: election/templates/election/election_detail.jinja:335 +#: election/templates/election/election_detail.jinja:337 #: launderette/templates/launderette/launderette_book.jinja:12 msgid "Choose" msgstr "Choisir" -#: election/templates/election/election_detail.jinja:358 +#: election/templates/election/election_detail.jinja:360 msgid "Submit the vote !" msgstr "Envoyer le vote !" -#: election/templates/election/election_detail.jinja:365 +#: election/templates/election/election_detail.jinja:368 msgid "Add a new list" msgstr "Ajouter une nouvelle liste" -#: election/templates/election/election_detail.jinja:368 +#: election/templates/election/election_detail.jinja:372 msgid "Add a new role" msgstr "Ajouter un nouveau rôle" @@ -3300,6 +3490,113 @@ msgstr "Début des candidatures" msgid "End candidature" msgstr "Fin des candidatures" +#: forum/models.py:27 +msgid "is a category" +msgstr "est une catégorie" + +#: forum/models.py:29 +msgid "owner club" +msgstr "club propriétaire" + +#: forum/models.py:35 +msgid "number to choose a specific forum ordering" +msgstr "numéro spécifiant l'ordre d'affichage" + +#: forum/models.py:79 +msgid "You can not make loops in forums" +msgstr "Vous ne pouvez pas faire de boucles dans les forums" + +#: forum/models.py:169 +msgid "message" +msgstr "message" + +#: forum/models.py:171 +msgid "readers" +msgstr "lecteurs" + +#: forum/models.py:217 +msgid "Message edited by" +msgstr "Message édité par" + +#: forum/models.py:218 +msgid "Message deleted by" +msgstr "Message supprimé par" + +#: forum/models.py:219 +msgid "Message undeleted by" +msgstr "Message restauré par" + +#: forum/models.py:226 +msgid "action" +msgstr "action" + +#: forum/models.py:235 +msgid "last read date" +msgstr "dernière date de lecture" + +#: forum/templates/forum/forum.jinja:19 forum/templates/forum/main.jinja:19 +msgid "New forum" +msgstr "Nouveau forum" + +#: forum/templates/forum/forum.jinja:21 +msgid "New topic" +msgstr "Nouveau sujet" + +#: forum/templates/forum/forum.jinja:30 +msgid "Topics" +msgstr "Sujets" + +#: forum/templates/forum/forum.jinja:33 forum/templates/forum/forum.jinja:54 +msgid "Last message" +msgstr "Dernier message" + +#: forum/templates/forum/forum.jinja:51 +msgid "Messages" +msgstr "Messages" + +#: forum/templates/forum/last_unread.jinja:5 +#: forum/templates/forum/last_unread.jinja:13 +msgid "Last unread messages" +msgstr "Derniers messages non lus" + +#: forum/templates/forum/last_unread.jinja:16 +msgid "Refresh" +msgstr "Rafraîchir" + +#: forum/templates/forum/macros.jinja:102 +msgid "Reply as quote" +msgstr "Répondre en citant" + +#: forum/templates/forum/macros.jinja:108 +msgid "Undelete" +msgstr "Restaurer" + +#: forum/templates/forum/macros.jinja:126 +msgid " the " +msgstr " le " + +#: forum/templates/forum/macros.jinja:137 +msgid "Deleted or unreadable message." +msgstr "Message supprimé ou non-visible." + +#: forum/templates/forum/main.jinja:15 +msgid "View last unread messages" +msgstr "Voir les derniers messages non lus" + +#: forum/templates/forum/reply.jinja:5 forum/templates/forum/reply.jinja:18 +#: forum/templates/forum/topic.jinja:39 forum/templates/forum/topic.jinja:58 +msgid "Reply" +msgstr "Répondre" + +#: forum/views.py:68 +msgid "Apply rights and club owner recursively" +msgstr "Appliquer les droits et le club propriétaire récursivement" + +#: forum/views.py:206 +#, python-format +msgid "%(author)s said" +msgstr "Citation de %(author)s" + #: launderette/models.py:17 #: launderette/templates/launderette/launderette_book.jinja:5 #: launderette/templates/launderette/launderette_book_choose.jinja:4 @@ -3356,12 +3653,12 @@ msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:454 +#: sith/settings.py:482 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:454 +#: sith/settings.py:482 msgid "Drying" msgstr "Séchage" @@ -3428,13 +3725,13 @@ msgstr "Utilisateur qui sera conservé" msgid "User that will be deleted" msgstr "Utilisateur qui sera supprimé" -#: sas/models.py:132 +#: sas/models.py:133 msgid "picture" msgstr "photo" #: sas/templates/sas/album.jinja:52 sas/templates/sas/album.jinja.py:54 -#: sas/templates/sas/main.jinja:17 sas/templates/sas/main.jinja.py:19 -#: sas/templates/sas/main.jinja:21 +#: sas/templates/sas/main.jinja:13 sas/templates/sas/main.jinja.py:15 +#: sas/templates/sas/main.jinja:17 msgid "preview" msgstr "miniature" @@ -3442,7 +3739,15 @@ msgstr "miniature" msgid "Upload" msgstr "Envoyer" +#: sas/templates/sas/main.jinja:34 +msgid "Latest albums" +msgstr "Derniers albums" + #: sas/templates/sas/main.jinja:41 +msgid "All categories" +msgstr "Toutes les catégories" + +#: sas/templates/sas/main.jinja:53 msgid "Create" msgstr "Créer" @@ -3454,27 +3759,27 @@ msgstr "Modération du SAS" msgid "Albums" msgstr "Albums" -#: sas/templates/sas/picture.jinja:60 +#: sas/templates/sas/picture.jinja:68 msgid "People" msgstr "Personne(s)" -#: sas/templates/sas/picture.jinja:89 +#: sas/templates/sas/picture.jinja:97 msgid "HD version" msgstr "Version HD" -#: sas/templates/sas/picture.jinja:93 +#: sas/templates/sas/picture.jinja:101 msgid "Rotate left" msgstr "Tourner vers la gauche" -#: sas/templates/sas/picture.jinja:94 +#: sas/templates/sas/picture.jinja:102 msgid "Rotate right" msgstr "Tourner vers la droite" -#: sas/templates/sas/picture.jinja:95 +#: sas/templates/sas/picture.jinja:103 msgid "Ask for removal" msgstr "Demander le retrait" -#: sas/templates/sas/picture.jinja:111 +#: sas/templates/sas/picture.jinja:119 msgid "Asked for removal" msgstr "Retrait demandé" @@ -3495,395 +3800,219 @@ msgstr "Erreur de création de l'album %(album)s : %(msg)s" msgid "Add user" msgstr "Ajouter une personne" -#: sas/views.py:256 -msgid "Apply rights recursively" -msgstr "Appliquer les droits récursivement" - -#: sith/settings.py:177 +#: sith/settings.py:180 msgid "English" msgstr "Anglais" -#: sith/settings.py:178 +#: sith/settings.py:181 msgid "French" msgstr "Français" -#: sith/settings.py:287 +#: sith/settings.py:298 msgid "TC" msgstr "TC" -#: sith/settings.py:288 +#: sith/settings.py:299 msgid "IMSI" msgstr "IMSI" -#: sith/settings.py:289 +#: sith/settings.py:300 msgid "IMAP" msgstr "IMAP" -#: sith/settings.py:290 +#: sith/settings.py:301 msgid "INFO" msgstr "INFO" -#: sith/settings.py:291 +#: sith/settings.py:302 msgid "GI" msgstr "GI" -#: sith/settings.py:292 +#: sith/settings.py:303 msgid "E" msgstr "E" -#: sith/settings.py:293 +#: sith/settings.py:304 msgid "EE" msgstr "EE" -#: sith/settings.py:294 +#: sith/settings.py:305 msgid "GESC" msgstr "GESC" -#: sith/settings.py:295 +#: sith/settings.py:306 msgid "GMC" msgstr "GMC" -#: sith/settings.py:296 +#: sith/settings.py:307 msgid "MC" msgstr "MC" -#: sith/settings.py:297 +#: sith/settings.py:308 msgid "EDIM" msgstr "EDIM" -#: sith/settings.py:298 +#: sith/settings.py:309 msgid "Humanities" msgstr "Humanités" -#: sith/settings.py:299 +#: sith/settings.py:310 msgid "N/A" msgstr "N/A" -#: sith/settings.py:303 sith/settings.py:310 sith/settings.py:331 +#: sith/settings.py:314 sith/settings.py:321 sith/settings.py:342 msgid "Check" msgstr "Chèque" -#: sith/settings.py:304 sith/settings.py:312 sith/settings.py:332 +#: sith/settings.py:315 sith/settings.py:323 sith/settings.py:343 msgid "Cash" msgstr "Espèces" -#: sith/settings.py:305 +#: sith/settings.py:316 msgid "Transfert" msgstr "Virement" -#: sith/settings.py:318 +#: sith/settings.py:329 msgid "Belfort" msgstr "Belfort" -#: sith/settings.py:319 +#: sith/settings.py:330 msgid "Sevenans" msgstr "Sevenans" -#: sith/settings.py:320 +#: sith/settings.py:331 msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:361 +#: sith/settings.py:375 msgid "One semester" msgstr "Un semestre, 15 €" -#: sith/settings.py:366 +#: sith/settings.py:380 msgid "Two semesters" msgstr "Deux semestres, 28 €" -#: sith/settings.py:371 +#: sith/settings.py:385 msgid "Common core cursus" msgstr "Cursus tronc commun, 45 €" -#: sith/settings.py:376 +#: sith/settings.py:390 msgid "Branch cursus" msgstr "Cursus branche, 45 €" -#: sith/settings.py:381 +#: sith/settings.py:395 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:386 +#: sith/settings.py:400 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:391 +#: sith/settings.py:405 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:396 +#: sith/settings.py:410 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:401 +#: sith/settings.py:415 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:406 +#: sith/settings.py:420 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:411 +#: sith/settings.py:425 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 15 €" -#: sith/settings.py:419 +#: sith/settings.py:447 msgid "President" msgstr "Président" -#: sith/settings.py:420 +#: sith/settings.py:448 msgid "Vice-President" msgstr "Vice-Président" -#: sith/settings.py:421 +#: sith/settings.py:449 msgid "Treasurer" msgstr "Trésorier" -#: sith/settings.py:422 +#: sith/settings.py:450 msgid "Communication supervisor" -msgstr "Responsable com" +msgstr "Responsable communication" -#: sith/settings.py:423 +#: sith/settings.py:451 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:424 +#: sith/settings.py:452 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:425 +#: sith/settings.py:453 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:426 +#: sith/settings.py:454 msgid "Active member" msgstr "Membre actif" -#: sith/settings.py:427 +#: sith/settings.py:455 msgid "Curious" msgstr "Curieux" -#: sith/settings.py:461 +#: sith/settings.py:489 msgid "A fresh new to be moderated" msgstr "Une nouvelle toute neuve à modérer" -#: sith/settings.py:462 +#: sith/settings.py:490 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:463 +#: sith/settings.py:491 msgid "New pictures/album to be moderated in the SAS" msgstr "Nouvelles photos/albums à modérer dans le SAS" -#: sith/settings.py:464 +#: sith/settings.py:492 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:465 +#: sith/settings.py:493 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s €" -#: sith/settings.py:466 +#: sith/settings.py:494 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:467 +#: sith/settings.py:495 msgid "You have a notification" msgstr "Vous avez une notification" -#: stock/models.py:30 -msgid "unit quantity" -msgstr "quantité unitaire " +#: sith/settings.py:499 +msgid "Success!" +msgstr "Succès !" -#: stock/models.py:30 -msgid "number of element in one box" -msgstr "nombre d'éléments dans une boite" +#: sith/settings.py:500 +msgid "Fail!" +msgstr "Échec !" -#: stock/models.py:31 -msgid "effective quantity" -msgstr "quantité effective" +#: sith/settings.py:501 +msgid "You successfully posted an article in the Weekmail" +msgstr "Article posté avec succès dans le Weekmail" -#: stock/models.py:31 -msgid "number of box" -msgstr "nombre de boites" +#: sith/settings.py:502 +msgid "You successfully edited an article in the Weekmail" +msgstr "Article édité avec succès dans le Weekmail" -#: stock/models.py:32 -msgid "minimal quantity" -msgstr "quantité minimale" - -#: stock/models.py:33 -msgid "" -"if the effective quantity is less than the minimal, item is added to the " -"shopping list" -msgstr "" -"si la quantité effective est plus petite que la minimale, l'élément est " -"ajouté à la liste de courses" - -#: stock/models.py:53 -msgid "todo" -msgstr "à faire" - -#: stock/models.py:70 -msgid "shopping lists" -msgstr "Listes de courses" - -#: stock/models.py:75 -msgid "quantity to buy" -msgstr "quantité à acheter" - -#: stock/models.py:75 -msgid "quantity to buy during the next shopping session" -msgstr "quantité à acheter pendant les prochaines courses" - -#: stock/models.py:76 -msgid "quantity bought" -msgstr "quantité achetée" - -#: stock/models.py:76 -msgid "quantity bought during the last shopping session" -msgstr "quantité achetée pendant les dernières courses" - -#: stock/templates/stock/shopping_list_items.jinja:4 -#, python-format -msgid "%(shoppinglist)s's items" -msgstr "Éléments de %(shoppinglist)s" - -#: stock/templates/stock/shopping_list_items.jinja:21 -msgid "Quantity asked" -msgstr "Quantité demandée" - -#: stock/templates/stock/shopping_list_items.jinja:22 -msgid "Quantity bought" -msgstr "Quantité achetée" - -#: stock/templates/stock/shopping_list_items.jinja:42 stock/views.py:181 -msgid "Comments" -msgstr "Commentaires" - -#: stock/templates/stock/shopping_list_quantity.jinja:4 -#: stock/templates/stock/shopping_list_quantity.jinja:8 -#, python-format -msgid "%(s)s's quantity to buy" -msgstr "Quantité à acheter de %(s)s" - -#: stock/templates/stock/shopping_list_quantity.jinja:13 -#: stock/templates/stock/stock_shopping_list.jinja:9 -msgid "Create shopping list" -msgstr "Créer une liste de courses" - -#: stock/templates/stock/stock_item_list.jinja:10 -msgid "New item" -msgstr "Nouvel élément" - -#: stock/templates/stock/stock_item_list.jinja:23 -msgid "Others" -msgstr "Autres" - -#: stock/templates/stock/stock_item_list.jinja:30 -msgid "There is no items in this stock." -msgstr "Il n'y a pas d'éléments dans ce stock." - -#: stock/templates/stock/stock_list.jinja:4 -#: stock/templates/stock/stock_list.jinja:9 -msgid "Stock list" -msgstr "Liste des stocks" - -#: stock/templates/stock/stock_list.jinja:22 -msgid "There is no stocks in this website." -msgstr "Il n'y a pas de stock sur ce site web." - -#: stock/templates/stock/stock_shopping_list.jinja:11 -#, python-format -msgid "Shopping lists history for %(s)s" -msgstr "Historique des listes de courses de %(s)s" - -#: stock/templates/stock/stock_shopping_list.jinja:14 -msgid "Information :" -msgstr "Information :" - -#: stock/templates/stock/stock_shopping_list.jinja:16 -msgid "" -"Use the \"update stock\" action when you get back from shopping to add the " -"effective quantity bought for each shopping list item." -msgstr "" -"Utiliser l'action \"Mettre le stock à jour\" quand vous revenez des courses " -"pour ajouter les quantités réellement achétées pour chacun des éléments de " -"la liste" - -#: stock/templates/stock/stock_shopping_list.jinja:18 -msgid "" -"For example, 3 Cheeseburger (boxes) are aksing in the list, but there were " -"only 2 so, 2 have to be added in the stock quantity." -msgstr "" -"Par exemple, 3 Cheeseburger (boites) sont demandées dans la liste, mais il " -"n'en restait plus que 2, donc seulement 2 doivent être ajoutés au stock" - -#: stock/templates/stock/stock_shopping_list.jinja:21 -msgid "To do" -msgstr "À faire" - -#: stock/templates/stock/stock_shopping_list.jinja:27 -#: stock/templates/stock/stock_shopping_list.jinja:56 -msgid "Number of items" -msgstr "Nombre d'éléments" - -#: stock/templates/stock/stock_shopping_list.jinja:37 -msgid "Update stock" -msgstr "Mettre le stock à jour" - -#: stock/templates/stock/stock_shopping_list.jinja:40 -msgid "Mark as done" -msgstr "Marquer comme ancien" - -#: stock/templates/stock/stock_shopping_list.jinja:66 -msgid "Mark as to do" -msgstr "Marquer comme à faire" - -#: stock/templates/stock/stock_take_items.jinja:5 -#: stock/templates/stock/stock_take_items.jinja:9 -#, python-format -msgid "Take items from %(s)s" -msgstr "Prendre des éléments dans %(s)s" - -#: stock/templates/stock/stock_take_items.jinja:14 -msgid "Take items" -msgstr "Prendre un élément" - -#: stock/templates/stock/update_after_shopping.jinja:4 -#: stock/templates/stock/update_after_shopping.jinja:8 -#, python-format -msgid "Update %(s)s's quantity after shopping" -msgstr "Mettre à jour les quantités de %(s)s après les courses" - -#: stock/templates/stock/update_after_shopping.jinja:13 -msgid "Update stock quantities" -msgstr "Mettre à jour les quantités du stock" - -#: stock/views.py:173 -msgid "Shopping list name" -msgstr "Nom de la liste de courses" - -#: stock/views.py:179 -msgid " left" -msgstr " restant(s)" - -#: stock/views.py:180 -msgid "" -"Add here, items to buy that are not reference as a stock item (example : " -"sponge, knife, mugs ...)" -msgstr "" -"Ajouter ici les éléments à acheter qui ne sont pas référencés comme un " -"élément du stock (exemple : éponges, couteaux, tasses ...)" - -#: stock/views.py:309 -msgid " asked" -msgstr " demandé(s)" - -#: stock/views.py:376 -msgid "(" -msgstr "(" +#: sith/settings.py:503 +msgid "You successfully sent the Weekmail" +msgstr "Weekmail envoyé avec succès" #: subscription/models.py:16 msgid "Bad subscription type" @@ -3925,3 +4054,6 @@ msgstr "Un utilisateur avec cette adresse email existe déjà" msgid "You must either choose an existing user or create a new one properly" msgstr "" "Vous devez soit choisir un utilisateur existant, soit en créer un proprement" + +#~ msgid "CB refillilngs" +#~ msgstr "Rechargements Carte Bancaire" diff --git a/manage.py b/manage.py index a28324fa..0e28fd20 100755 --- a/manage.py +++ b/manage.py @@ -1,4 +1,28 @@ #!/usr/bin/env python3 +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + import os import sys diff --git a/migrate.py b/migrate.py index 997e9261..e0ab9269 100644 --- a/migrate.py +++ b/migrate.py @@ -1,3 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + import MySQLdb import os import django diff --git a/requirements.txt b/requirements.txt index e723a6a7..88ba05c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,6 @@ django-ajax-selects reportlab django-haystack whoosh +django-debug-toolbar # mysqlclient diff --git a/rootplace/__init__.py b/rootplace/__init__.py index e69de29b..0a9419f8 100644 --- a/rootplace/__init__.py +++ b/rootplace/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/rootplace/admin.py b/rootplace/admin.py index 8c38f3f3..84bb227c 100644 --- a/rootplace/admin.py +++ b/rootplace/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin # Register your models here. diff --git a/rootplace/models.py b/rootplace/models.py index 71a83623..5877743e 100644 --- a/rootplace/models.py +++ b/rootplace/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.db import models # Create your models here. diff --git a/rootplace/tests.py b/rootplace/tests.py index 7ce503c2..b8fbbe1e 100644 --- a/rootplace/tests.py +++ b/rootplace/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase # Create your tests here. diff --git a/rootplace/urls.py b/rootplace/urls.py index 19b6aca6..fcacb5f1 100644 --- a/rootplace/urls.py +++ b/rootplace/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from rootplace.views import * diff --git a/rootplace/views.py b/rootplace/views.py index 3b8d8159..c9ba404d 100644 --- a/rootplace/views.py +++ b/rootplace/views.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.shortcuts import render from django.utils.translation import ugettext as _ from django.views.generic.edit import FormView diff --git a/sas/__init__.py b/sas/__init__.py index e69de29b..0a9419f8 100644 --- a/sas/__init__.py +++ b/sas/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/sas/admin.py b/sas/admin.py index c49ee007..f504eec4 100644 --- a/sas/admin.py +++ b/sas/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin from sas.models import * diff --git a/sas/models.py b/sas/models.py index 0cd7c685..10412b9e 100644 --- a/sas/models.py +++ b/sas/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.db import models from django.core.urlresolvers import reverse_lazy, reverse from django.conf import settings @@ -7,6 +31,7 @@ from django.core.files.base import ContentFile from PIL import Image from io import BytesIO +import os from core.models import SithFile, User from core.utils import resize_image, exif_auto_rotate @@ -17,7 +42,7 @@ class Picture(SithFile): @property def is_vertical(self): - with open((settings.MEDIA_ROOT + self.file.name).encode('utf-8'), 'rb') as f: + with open(os.path.join(settings.MEDIA_ROOT, self.file.name).encode('utf-8'), 'rb') as f: im = Image.open(BytesIO(f.read())) (w, h) = im.size return (w / h) < 1 @@ -30,7 +55,7 @@ class Picture(SithFile): def can_be_viewed_by(self, user): # file = SithFile.objects.filter(id=self.id).first() return self.can_be_edited_by(user) or (self.is_in_sas and self.is_moderated and - user.was_subscribed())# or user.can_view(file) + user.was_subscribed)# or user.can_view(file) def get_download_url(self): return reverse('sas:download', kwargs={'picture_id': self.id}) @@ -67,7 +92,7 @@ class Picture(SithFile): def rotate(self, degree): for attr in ['file', 'compressed', 'thumbnail']: name = self.__getattribute__(attr).name - with open((settings.MEDIA_ROOT + name).encode('utf-8'), 'r+b') as file: + with open(os.path.join(settings.MEDIA_ROOT, name).encode('utf-8'), 'r+b') as file: if file: im = Image.open(BytesIO(file.read())) file.seek(0) @@ -107,7 +132,7 @@ class Album(SithFile): def can_be_viewed_by(self, user): # file = SithFile.objects.filter(id=self.id).first() return self.can_be_edited_by(user) or (self.is_in_sas and self.is_moderated and - user.was_subscribed())# or user.can_view(file) + user.was_subscribed)# or user.can_view(file) def get_absolute_url(self): return reverse('sas:album', kwargs={'album_id': self.id}) diff --git a/sas/templates/sas/album.jinja b/sas/templates/sas/album.jinja index a7ccd7d1..eb18a4e1 100644 --- a/sas/templates/sas/album.jinja +++ b/sas/templates/sas/album.jinja @@ -39,7 +39,7 @@ {% endif %} {% endif %}
      - {% for a in album.children_albums.order_by('-id') %} + {% for a in album.children_albums.order_by('-date') %}
      {% if edit_mode %} @@ -142,7 +142,7 @@ $("form#upload_form").submit(function (event) { var imagesCount = images.length; var completeCount = 0; - var poolSize = 5; + var poolSize = 1; var imagePool = []; while(images.length > 0 && imagePool.length < poolSize) { diff --git a/sas/templates/sas/main.jinja b/sas/templates/sas/main.jinja index d6d93f40..30cd0797 100644 --- a/sas/templates/sas/main.jinja +++ b/sas/templates/sas/main.jinja @@ -4,32 +4,44 @@ {% trans %}SAS{% endtrans %} {% endblock %} +{% macro display_album(a) %} +{% if a.is_moderated %} + +
      +
      + {% if a.file %} + {% trans %}preview{% endtrans %} + {% elif a.children.filter(is_folder=False, is_moderated=True).exists() %} + {% trans %}preview{% endtrans %} + {% else %} + {% trans %}preview{% endtrans %} + {% endif %} +
      + {{ a.name }} +
      +
      +{% elif user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID) %} + +{% endif %} +{% endmacro %} + {% block content %}

      {% trans %}SAS{% endtrans %}


      +

      {% trans %}Latest albums{% endtrans %}

      +
      + {% for a in latest %} + {{ display_album(a) }} + {% endfor %} +
      +
      +

      {% trans %}All categories{% endtrans %}

      {% for a in root_file.children.filter(is_folder=True).order_by('date') %} - {% if a.is_moderated %} - -
      -
      - {% if a.file %} - {% trans %}preview{% endtrans %} - {% elif a.children.filter(is_folder=False, is_moderated=True).exists() %} - {% trans %}preview{% endtrans %} - {% else %} - {% trans %}preview{% endtrans %} - {% endif %} -
      - {{ a.name }} -
      -
      - {% elif user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID) %} - - {% endif %} + {{ display_album(a) }} {% endfor %}
      {% if user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID) %} diff --git a/sas/templates/sas/picture.jinja b/sas/templates/sas/picture.jinja index f634cae2..68253ea4 100644 --- a/sas/templates/sas/picture.jinja +++ b/sas/templates/sas/picture.jinja @@ -20,6 +20,14 @@ max-width: 100%; } + +{% if picture.get_previous() %} + +{% endif %} +{% if picture.get_next() %} + +{% endif %} + {% endblock %} {% block title %} diff --git a/sas/tests.py b/sas/tests.py index 7ce503c2..b8fbbe1e 100644 --- a/sas/tests.py +++ b/sas/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase # Create your tests here. diff --git a/sas/urls.py b/sas/urls.py index aa64f704..40f5d3b7 100644 --- a/sas/urls.py +++ b/sas/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from sas.views import * diff --git a/sas/views.py b/sas/views.py index bf4fd903..38ab54dc 100644 --- a/sas/views.py +++ b/sas/views.py @@ -1,9 +1,33 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.shortcuts import render, redirect from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse_lazy, reverse from django.views.generic import ListView, DetailView, RedirectView, TemplateView from django.views.generic.edit import UpdateView, CreateView, DeleteView, ProcessFormView, FormMixin, FormView -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.conf import settings from django.forms.models import modelform_factory @@ -85,6 +109,7 @@ class SASMainView(FormView): def get_context_data(self, **kwargs): kwargs = super(SASMainView, self).get_context_data(**kwargs) kwargs['root_file'] = SithFile.objects.filter(id=settings.SITH_SAS_ROOT_DIR_ID).first() + kwargs['latest'] = SithFile.objects.filter(is_in_sas=True, is_folder=True, is_moderated=True).order_by('-id')[:5] return kwargs class PictureView(CanViewMixin, DetailView, FormMixin): @@ -156,10 +181,12 @@ class AlbumUploadView(CanViewMixin, DetailView, FormMixin): def post(self, request, *args, **kwargs): self.object = self.get_object() + if not self.object.file: + self.object.generate_thumbnail() self.form = self.get_form() parent = SithFile.objects.filter(id=self.object.id).first() files = request.FILES.getlist('images') - if request.user.is_authenticated() and request.user.is_subscribed(): + if request.user.is_authenticated() and request.user.is_subscribed: if self.form.is_valid(): self.form.process(parent=parent, owner=request.user, files=files, automodere=request.user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID)) @@ -191,7 +218,7 @@ class AlbumView(CanViewMixin, DetailView, FormMixin): FileView.handle_clipboard(request, self.object) parent = SithFile.objects.filter(id=self.object.id).first() files = request.FILES.getlist('images') - if request.user.is_authenticated() and request.user.is_subscribed(): + if request.user.is_authenticated() and request.user.is_subscribed: if self.form.is_valid(): self.form.process(parent=parent, owner=request.user, files=files, automodere=request.user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID)) @@ -250,7 +277,8 @@ class PictureEditForm(forms.ModelForm): class AlbumEditForm(forms.ModelForm): class Meta: model = Album - fields=['name', 'file', 'parent', 'edit_groups'] + fields = ['name', 'date', 'file', 'parent', 'edit_groups'] + date = forms.DateField(label=_("Date"), widget=SelectDate, required=True) parent = make_ajax_field(Album, 'parent', 'files', help_text="") edit_groups = make_ajax_field(Album, 'edit_groups', 'groups', help_text="") recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False) @@ -272,3 +300,4 @@ class AlbumEditView(CanEditMixin, UpdateView): if form.cleaned_data['recursive']: self.object.apply_rights_recursively(True) return ret + diff --git a/sith/__init__.py b/sith/__init__.py index e69de29b..0a9419f8 100644 --- a/sith/__init__.py +++ b/sith/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/sith/settings.py b/sith/settings.py index 78966035..5ebf4b0d 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + """ Django settings for sith project. @@ -27,6 +51,7 @@ SECRET_KEY = '(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False +INTERNAL_IPS = ['127.0.0.1'] ALLOWED_HOSTS = ['*'] @@ -59,7 +84,7 @@ INSTALLED_APPS = ( 'sas', 'com', 'election', - 'stock', + 'forum', ) MIDDLEWARE_CLASSES = ( @@ -154,6 +179,8 @@ HAYSTACK_CONNECTIONS = { }, } +HAYSTACK_SIGNAL_PROCESSOR = 'core.search_indexes.UserOnlySignalProcessor' + WSGI_APPLICATION = 'sith.wsgi.application' @@ -232,6 +259,7 @@ SITH_URL = "my.url.git.an" SITH_NAME = "Sith website" # AE configuration +SITH_MAIN_CLUB_ID = 1 # TODO: keep only that first setting, with the ID, and do the same for the other clubs SITH_MAIN_CLUB = { 'name': "AE", 'unix_name': "ae", @@ -261,18 +289,25 @@ SITH_SCHOOL_START_YEAR = 1999 SITH_GROUP_ROOT_ID = 1 SITH_GROUP_PUBLIC_ID = 2 -SITH_GROUP_ACCOUNTING_ADMIN_ID = 3 -SITH_GROUP_COM_ADMIN_ID = 4 -SITH_GROUP_COUNTER_ADMIN_ID = 5 -SITH_GROUP_BANNED_ALCOHOL_ID = 6 -SITH_GROUP_BANNED_COUNTER_ID = 7 -SITH_GROUP_BANNED_SUBSCRIPTION_ID = 8 -SITH_GROUP_SAS_ADMIN_ID = 9 +SITH_GROUP_SUBSCRIBERS_ID = 3 +SITH_GROUP_OLD_SUBSCRIBERS_ID = 4 +SITH_GROUP_ACCOUNTING_ADMIN_ID = 5 +SITH_GROUP_COM_ADMIN_ID = 6 +SITH_GROUP_COUNTER_ADMIN_ID = 7 +SITH_GROUP_BANNED_ALCOHOL_ID = 8 +SITH_GROUP_BANNED_COUNTER_ID = 9 +SITH_GROUP_BANNED_SUBSCRIPTION_ID = 10 +SITH_GROUP_SAS_ADMIN_ID = 11 +SITH_GROUP_FORUM_ADMIN_ID = 12 + SITH_CLUB_REFOUND_ID = 89 SITH_COUNTER_REFOUND_ID = 38 SITH_PRODUCT_REFOUND_ID = 5 +# Forum + +SITH_FORUM_PAGE_LENGTH = 30 # SAS variables SITH_SAS_ROOT_DIR_ID = 4 @@ -346,6 +381,9 @@ SITH_COUNTER_BANK = [ ('LA-POSTE', 'La Poste'), ] +# Defines pagination for cash summary +SITH_COUNTER_CASH_SUMMARY_LENGTH = 50 + # Defines which product type is the refilling type, and thus increases the account amount SITH_COUNTER_PRODUCTTYPE_REFILLING = 11 @@ -415,17 +453,31 @@ SITH_SUBSCRIPTIONS = { # To be completed.... } +SITH_CLUB_ROLES = {} + +SITH_CLUB_ROLES_ID = { + 'President': 10, + 'Vice-President': 9, + 'Treasurer': 7, + 'Communication supervisor': 5, + 'Secretary': 4, + 'IT supervisor': 3, + 'Board member': 2, + 'Active member': 1, + 'Curious': 0, +} + SITH_CLUB_ROLES = { - 10: _('President'), - 9: _('Vice-President'), - 7: _('Treasurer'), - 5: _('Communication supervisor'), - 4: _('Secretary'), - 3: _('IT supervisor'), - 2: _('Board member'), - 1: _('Active member'), - 0: _('Curious'), - } + 10: _('President'), + 9: _('Vice-President'), + 7: _('Treasurer'), + 5: _('Communication supervisor'), + 4: _('Secretary'), + 3: _('IT supervisor'), + 2: _('Board member'), + 1: _('Active member'), + 0: _('Curious'), +} # This corresponds to the maximum role a user can freely subscribe to # In this case, SITH_MAXIMUM_FREE_ROLE=1 means that a user can set himself as "Membre actif" or "Curieux", but not higher @@ -435,7 +487,7 @@ SITH_MAXIMUM_FREE_ROLE=1 SITH_BARMAN_TIMEOUT=20 # Minutes to delete the last operations -SITH_LAST_OPERATIONS_LIMIT=5 +SITH_LAST_OPERATIONS_LIMIT=10 # Minutes for a counter to be inactive SITH_COUNTER_MINUTE_INACTIVE=10 @@ -467,8 +519,34 @@ SITH_NOTIFICATIONS = [ ('GENERIC', _("You have a notification")), ] +SITH_QUICK_NOTIF = { + 'qn_success': _("Success!"), + 'qn_fail': _("Fail!"), + 'qn_weekmail_new_article': _("You successfully posted an article in the Weekmail"), + 'qn_weekmail_article_edit': _("You successfully edited an article in the Weekmail"), + 'qn_weekmail_send_success': _("You successfully sent the Weekmail"), + } + try: from .settings_custom import * print("Custom settings imported") except: print("Custom settings failed") + +if DEBUG: + INSTALLED_APPS += ("debug_toolbar",) + MIDDLEWARE_CLASSES = ('debug_toolbar.middleware.DebugToolbarMiddleware',) + MIDDLEWARE_CLASSES + DEBUG_TOOLBAR_PANELS = [ + 'debug_toolbar.panels.versions.VersionsPanel', + 'debug_toolbar.panels.timer.TimerPanel', + 'debug_toolbar.panels.settings.SettingsPanel', + 'debug_toolbar.panels.headers.HeadersPanel', + 'debug_toolbar.panels.request.RequestPanel', + 'debug_toolbar.panels.sql.SQLPanel', + 'debug_toolbar.panels.staticfiles.StaticFilesPanel', + 'sith.toolbar_debug.TemplatesPanel', + 'debug_toolbar.panels.cache.CachePanel', + 'debug_toolbar.panels.signals.SignalsPanel', + 'debug_toolbar.panels.logging.LoggingPanel', + 'debug_toolbar.panels.redirects.RedirectsPanel', + ] diff --git a/sith/toolbar_debug.py b/sith/toolbar_debug.py new file mode 100644 index 00000000..f30c74c2 --- /dev/null +++ b/sith/toolbar_debug.py @@ -0,0 +1,32 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from debug_toolbar.panels.templates import TemplatesPanel as BaseTemplatesPanel + +class TemplatesPanel(BaseTemplatesPanel): + def generate_stats(self, *args): + template = self.templates[0]['template'] + if not hasattr(template, 'engine') and hasattr(template, 'backend'): + template.engine = template.backend + return super().generate_stats(*args) diff --git a/sith/urls.py b/sith/urls.py index 075e2f20..4ba65ca8 100644 --- a/sith/urls.py +++ b/sith/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + """sith URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: @@ -41,6 +65,7 @@ urlpatterns = [ url(r'^sas/', include('sas.urls', namespace="sas", app_name="sas")), url(r'^api/v1/', include('api.urls', namespace="api", app_name="api")), url(r'^election/', include('election.urls', namespace="election", app_name="election")), + url(r'^forum/', include('forum.urls', namespace="forum", app_name="forum")), url(r'^admin/', include(admin.site.urls)), url(r'^ajax_select/', include(ajax_select_urls)), url(r'^i18n/', include('django.conf.urls.i18n')), @@ -50,4 +75,8 @@ urlpatterns = [ if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + import debug_toolbar + urlpatterns += [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] diff --git a/sith/wsgi.py b/sith/wsgi.py index dc50fae3..be0024b7 100644 --- a/sith/wsgi.py +++ b/sith/wsgi.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + """ WSGI config for sith project. diff --git a/subscription/__init__.py b/subscription/__init__.py index e69de29b..0a9419f8 100644 --- a/subscription/__init__.py +++ b/subscription/__init__.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + diff --git a/subscription/admin.py b/subscription/admin.py index 48488ed2..2bb80007 100644 --- a/subscription/admin.py +++ b/subscription/admin.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.contrib import admin from subscription.models import Subscription diff --git a/subscription/models.py b/subscription/models.py index 1b6b5f52..8697a41d 100644 --- a/subscription/models.py +++ b/subscription/models.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from datetime import date, timedelta from django.db import models from django.utils import timezone diff --git a/subscription/tests.py b/subscription/tests.py index 7ce503c2..b8fbbe1e 100644 --- a/subscription/tests.py +++ b/subscription/tests.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.test import TestCase # Create your tests here. diff --git a/subscription/urls.py b/subscription/urls.py index 19085045..4e8b22a7 100644 --- a/subscription/urls.py +++ b/subscription/urls.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.conf.urls import url, include from subscription.views import * diff --git a/subscription/views.py b/subscription/views.py index 70753103..aacd69ed 100644 --- a/subscription/views.py +++ b/subscription/views.py @@ -1,3 +1,27 @@ +# -*- coding:utf-8 -* +# +# Copyright 2016,2017 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + from django.shortcuts import render from django.views.generic.edit import UpdateView, CreateView from django.views.generic.base import View
      {% trans %}Club{% endtrans %}