diff --git a/.gitignore b/.gitignore index db3884f..f87a8ca 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,10 @@ *.pyc __pycache__/ local_settings.py -db.sqlite3 -db.sqlite3-journal +# db.sqlite3 +# db.sqlite3-journal media +csvstorage/ # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # in your Git repository. Update and uncomment the following line accordingly. @@ -167,7 +168,6 @@ sketch # End of https://www.toptal.com/developers/gitignore/api/django,react {"mode":"full","isActive":false} -<<<<<<< HEAD /client/node_modules # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. @@ -194,6 +194,6 @@ sketch npm-debug.log* yarn-debug.log* yarn-error.log* -======= -client/node_modules ->>>>>>> 41c413222912585d191bff8f01499aa1ade3307f +/requests.txt +/requests.http +/.vscode \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + 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/Pipfile b/Pipfile new file mode 100644 index 0000000..5d44a48 --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.8" diff --git a/requests.http b/requests.http new file mode 100644 index 0000000..0391eb8 --- /dev/null +++ b/requests.http @@ -0,0 +1 @@ +PUT http://localhost:8000/api/users/user HTTP/1.1 \ No newline at end of file diff --git a/server/api/__init__.py b/server/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/api/admin.py b/server/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/server/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/api/apps.py b/server/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/server/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/server/api/migrations/__init__.py b/server/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/api/models.py b/server/api/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/server/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/server/api/tests.py b/server/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/api/views.py b/server/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/server/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/server/attendance/admin.py b/server/attendance/admin.py index 91d72c3..d606991 100644 --- a/server/attendance/admin.py +++ b/server/attendance/admin.py @@ -1,24 +1,55 @@ from django.contrib import admin -from .models import * +from .models import * + + +class TripAdmin(admin.ModelAdmin): + list_display = ( + "id", + "vehicle", + "custodian_1", + "custodian_2", + "custodian_3", + "entry_time", + "exit_time", + "start_location", + "end_location", + "branch_id", + "added_by", + ) + list_display_links = ("id", "custodian_1", "custodian_2", "custodian_3") + search_fields = ("id", "vehicle", "custodian_1", "custodian_2", "custodian_3") + list_per_page = 20 + class AttendanceSheetAdmin(admin.ModelAdmin): - list_display = ('id', 'sheet_created') - list_display_links = ('id', 'sheet_created') + list_display = ("id", "sheet_created") + list_display_links = ("id", "sheet_created") list_per_page = 20 class AttendanceAdmin(admin.ModelAdmin): - list_display = ('id', 'first_name', 'last_name','entry_time', 'exit_time', 'vendor', 'branch_id') - list_display_links = ('id', 'first_name', 'last_name') - search_fields = ('first_name', 'last_name','vendor') - list_per_page = 20 + list_display = ("id", "custodian", "entry_time", "exit_time", "branch_id") + list_display_links = ("id", "custodian") + search_fields = ("custodian",) + list_per_page = 20 + + +class AttendanceVehicleAdmin(admin.ModelAdmin): + list_display = ("id", "vehicle", "entry_time", "exit_time", "branch_id") + list_display_links = ("id", "vehicle") + search_fields = ("vehicle",) + list_per_page = 20 + class IssueAdmin(admin.ModelAdmin): - list_display = ('id', 'comment', 'vendor', 'reverted_by', 'sheet', 'created_at') - list_display_links = ('id','comment') - list_per_page = 20 + list_display = ("id", "comment", "vendor", "reverted_by", "sheet", "created_at") + list_display_links = ("id", "comment") + list_per_page = 20 + admin.site.register(Attendance, AttendanceAdmin) +admin.site.register(AttendanceVehicle, AttendanceVehicleAdmin) +admin.site.register(Trip, TripAdmin) admin.site.register(AttendanceSheet, AttendanceSheetAdmin) admin.site.register(Issue, IssueAdmin) diff --git a/server/attendance/apps.py b/server/attendance/apps.py index 177ba15..c5eba55 100644 --- a/server/attendance/apps.py +++ b/server/attendance/apps.py @@ -2,4 +2,4 @@ class AttendanceConfig(AppConfig): - name = 'attendance' + name = "attendance" diff --git a/server/attendance/cron.py b/server/attendance/cron.py new file mode 100644 index 0000000..d646371 --- /dev/null +++ b/server/attendance/cron.py @@ -0,0 +1,73 @@ +from attendance.models import Attendance, AttendanceVehicle +from vendors.models import Vendor +from attendance.utils import qs_to_local_csv +from django.conf import settings +from datetime import datetime + +from django.core.mail import EmailMessage + +BASE_DIR = settings.BASE_DIR + + +def send_attendance_report(): + today = datetime.today() + + for vendor in Vendor.objects.all(): + attendance_custodian = Attendance.objects.filter( + custodian__vendor = vendor, + entry_time__month = today.month, + entry_time__year = today.year, + ).order_by('entry_time').all() + + if attendance_custodian: + attendance_custodian_path = qs_to_local_csv( + attendance_custodian, + fields=[ + "id", + "custodian__first_name", + "custodian__last_name", + "entry_time", + "exit_time", + ], + ) + + mail = EmailMessage( + subject= f"Monthly Custodian Attendance Report: {today.strftime('%B %Y')}", + body= f"PFA the attendance reports for the custodians for {today.strftime('%B %Y')}", + from_email= settings.EMAIL_HOST_USER, + to=[vendor.email] + ) + + attendance_file = open(attendance_custodian_path) + mail.attach(f"attendance_report_{ today.strftime('%B_%Y') }.csv", attendance_file.read()) + mail.send() + + + attendance_vehicle = AttendanceVehicle.objects.filter( + custodian__vendor = vendor, + entry_time__month = today.month, + entry_time__year = today.year, + ).order_by('entry_time').all() + + if attendance_vehicle: + attendance_vehicle_path = qs_to_local_csv( + attendance_vehicle, + fields=[ + "id", + "vehicle__model_name", + "custodian__number_plate", + "entry_time", + "exit_time", + ], + ) + + mail = EmailMessage( + subject= f"Monthly Vehicle Attendance Report: {today.strftime('%B %Y')}", + body= f"PFA the attendance reports for the vehicles for {today.strftime('%B %Y')}", + from_email= settings.EMAIL_HOST_USER, + to=[vendor.email] + ) + + attendance_file = open(attendance_vehicle_path) + mail.attach(attendance_file.name, attendance_file.read()) + mail.send() diff --git a/server/attendance/migrations/0002_auto_20210712_1636.py b/server/attendance/migrations/0002_auto_20210712_1636.py new file mode 100644 index 0000000..06bf869 --- /dev/null +++ b/server/attendance/migrations/0002_auto_20210712_1636.py @@ -0,0 +1,41 @@ +# Generated by Django 3.0.4 on 2021-07-12 16:36 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('vendors', '0001_initial'), + ('attendance', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='attendance', + name='first_name', + ), + migrations.RemoveField( + model_name='attendance', + name='last_name', + ), + migrations.RemoveField( + model_name='attendance', + name='vendor', + ), + migrations.CreateModel( + name='Gunmen', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=200)), + ('last_name', models.CharField(max_length=200)), + ('vendor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vendors.Vendor')), + ], + ), + migrations.AddField( + model_name='attendance', + name='gunmen', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='attendance.Gunmen'), + ), + ] diff --git a/server/attendance/migrations/0003_auto_20210712_1712.py b/server/attendance/migrations/0003_auto_20210712_1712.py new file mode 100644 index 0000000..73f2f14 --- /dev/null +++ b/server/attendance/migrations/0003_auto_20210712_1712.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.4 on 2021-07-12 17:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('vendors', '0002_gunmen'), + ('attendance', '0002_auto_20210712_1636'), + ] + + operations = [ + migrations.AlterField( + model_name='attendance', + name='gunmen', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vendors.Gunmen'), + ), + migrations.DeleteModel( + name='Gunmen', + ), + ] diff --git a/server/attendance/migrations/0004_trip.py b/server/attendance/migrations/0004_trip.py new file mode 100644 index 0000000..8406957 --- /dev/null +++ b/server/attendance/migrations/0004_trip.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0.4 on 2021-07-19 17:44 + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0006_auto_20210715_1609'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('vendors', '0005_custodian'), + ('attendance', '0003_auto_20210712_1712'), + ] + + operations = [ + migrations.CreateModel( + name='Trip', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('trip_code', models.CharField(blank=True, max_length=6, null=True)), + ('entry_time', models.DateTimeField(default=datetime.datetime.now)), + ('exit_time', models.DateTimeField(blank=True, null=True)), + ('start_location', models.CharField(max_length=100)), + ('end_location', models.CharField(max_length=100)), + ('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('branch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.Branch')), + ('custodian_1', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='custodian_1', to='vendors.Custodian')), + ('custodian_2', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='custodian_2', to='vendors.Custodian')), + ('custodian_3', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='custodian_3', to='vendors.Custodian')), + ('vehicle', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vendors.Vehicle')), + ], + ), + ] diff --git a/server/attendance/migrations/0005_auto_20210719_1756.py b/server/attendance/migrations/0005_auto_20210719_1756.py new file mode 100644 index 0000000..5682b8e --- /dev/null +++ b/server/attendance/migrations/0005_auto_20210719_1756.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.4 on 2021-07-19 17:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0004_trip'), + ] + + operations = [ + migrations.AlterField( + model_name='trip', + name='entry_time', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/server/attendance/migrations/0006_auto_20210720_1620.py b/server/attendance/migrations/0006_auto_20210720_1620.py new file mode 100644 index 0000000..43b488d --- /dev/null +++ b/server/attendance/migrations/0006_auto_20210720_1620.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.4 on 2021-07-20 16:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0005_auto_20210719_1756'), + ] + + operations = [ + migrations.AddField( + model_name='trip', + name='custodian_1_code', + field=models.CharField(blank=True, max_length=6, null=True), + ), + migrations.AddField( + model_name='trip', + name='custodian_2_code', + field=models.CharField(blank=True, max_length=6, null=True), + ), + migrations.AddField( + model_name='trip', + name='custodian_3_code', + field=models.CharField(blank=True, max_length=6, null=True), + ), + ] diff --git a/server/attendance/migrations/0007_auto_20210720_1801.py b/server/attendance/migrations/0007_auto_20210720_1801.py new file mode 100644 index 0000000..cb56610 --- /dev/null +++ b/server/attendance/migrations/0007_auto_20210720_1801.py @@ -0,0 +1,34 @@ +# Generated by Django 3.0.4 on 2021-07-20 18:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('vendors', '0005_custodian'), + ('attendance', '0006_auto_20210720_1620'), + ] + + operations = [ + migrations.RemoveField( + model_name='attendance', + name='gunmen', + ), + migrations.AddField( + model_name='attendance', + name='custodian', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vendors.Custodian'), + ), + migrations.AlterField( + model_name='trip', + name='end_location', + field=models.CharField(blank=True, max_length=300, null=True), + ), + migrations.AlterField( + model_name='trip', + name='start_location', + field=models.CharField(blank=True, max_length=300, null=True), + ), + ] diff --git a/server/attendance/migrations/0007_auto_20210720_2300.py b/server/attendance/migrations/0007_auto_20210720_2300.py new file mode 100644 index 0000000..4f5c7e3 --- /dev/null +++ b/server/attendance/migrations/0007_auto_20210720_2300.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.4 on 2021-07-20 17:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0006_auto_20210720_1620'), + ] + + operations = [ + migrations.AlterField( + model_name='trip', + name='end_location', + field=models.CharField(blank=True, max_length=300, null=True), + ), + migrations.AlterField( + model_name='trip', + name='start_location', + field=models.CharField(blank=True, max_length=300, null=True), + ), + ] diff --git a/server/attendance/migrations/0008_auto_20210720_1804.py b/server/attendance/migrations/0008_auto_20210720_1804.py new file mode 100644 index 0000000..0c07d6e --- /dev/null +++ b/server/attendance/migrations/0008_auto_20210720_1804.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.4 on 2021-07-20 18:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0007_auto_20210720_1801'), + ] + + operations = [ + migrations.AlterField( + model_name='trip', + name='custodian_1_code', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AlterField( + model_name='trip', + name='custodian_2_code', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AlterField( + model_name='trip', + name='custodian_3_code', + field=models.CharField(blank=True, max_length=20, null=True), + ), + ] diff --git a/server/attendance/migrations/0009_merge_20210721_0021.py b/server/attendance/migrations/0009_merge_20210721_0021.py new file mode 100644 index 0000000..103e6cd --- /dev/null +++ b/server/attendance/migrations/0009_merge_20210721_0021.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.4 on 2021-07-20 18:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0008_auto_20210720_1804'), + ('attendance', '0007_auto_20210720_2300'), + ] + + operations = [ + ] diff --git a/server/attendance/migrations/0010_trip_trip_start.py b/server/attendance/migrations/0010_trip_trip_start.py new file mode 100644 index 0000000..92be639 --- /dev/null +++ b/server/attendance/migrations/0010_trip_trip_start.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.4 on 2021-07-21 07:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0009_merge_20210721_0021'), + ] + + operations = [ + migrations.AddField( + model_name='trip', + name='trip_start', + field=models.BooleanField(default=False), + ), + ] diff --git a/server/attendance/migrations/0011_attendancevehicle.py b/server/attendance/migrations/0011_attendancevehicle.py new file mode 100644 index 0000000..7f4564c --- /dev/null +++ b/server/attendance/migrations/0011_attendancevehicle.py @@ -0,0 +1,31 @@ +# Generated by Django 3.0.4 on 2021-07-21 07:14 + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0006_auto_20210715_1609'), + ('vendors', '0005_custodian'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('attendance', '0010_trip_trip_start'), + ] + + operations = [ + migrations.CreateModel( + name='AttendanceVehicle', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('entry_time', models.DateTimeField(default=datetime.datetime.now)), + ('exit_time', models.DateTimeField(blank=True, null=True)), + ('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('attendance_sheet', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='attendance.AttendanceSheet')), + ('branch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.Branch')), + ('vehicle', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vendors.Vehicle')), + ], + ), + ] diff --git a/server/attendance/models.py b/server/attendance/models.py index 6446089..3d8e74a 100644 --- a/server/attendance/models.py +++ b/server/attendance/models.py @@ -1,38 +1,109 @@ from django.db import models from django.contrib.auth.models import User -from vendors.models import Vendor +from vendors.models import Vehicle, Vendor, Custodian from users.models import Branch from datetime import datetime +class Trip(models.Model): + trip_code = models.CharField(max_length=6, blank=True, null=True) + trip_start = models.BooleanField(default=False) + + vehicle = models.ForeignKey( + Vehicle, on_delete=models.SET_NULL, null=True, blank=True + ) + + custodian_1 = models.ForeignKey( + Custodian, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="custodian_1", + ) + custodian_1_code = models.CharField(max_length=20, blank=True, null=True) + + custodian_2 = models.ForeignKey( + Custodian, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="custodian_2", + ) + custodian_2_code = models.CharField(max_length=20, blank=True, null=True) + + custodian_3 = models.ForeignKey( + Custodian, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="custodian_3", + ) + custodian_3_code = models.CharField(max_length=20, blank=True, null=True) + + entry_time = models.DateTimeField(blank=True, null=True) + exit_time = models.DateTimeField(blank=True, null=True) + + start_location = models.CharField(max_length=300, blank=True, null=True) + end_location = models.CharField(max_length=300, blank=True, null=True) + + added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + branch = models.ForeignKey(Branch, on_delete=models.SET_NULL, null=True, blank=True) + + def __str__(self): + return f"{self.vehicle}: {self.custodian_1}, {self.custodian_2}, {self.custodian_3}" + + class AttendanceSheet(models.Model): sheet_created = models.DateField(default=datetime.now) - invoice = models.FileField(blank=True, null=True, upload_to='invoice') + invoice = models.FileField(blank=True, null=True, upload_to="invoice") verified = models.BooleanField(default=False) def __str__(self): - return f'{self.sheet_created} -> {self.verified}' + return f"{self.sheet_created} -> {self.verified}" class Attendance(models.Model): - first_name = models.CharField(max_length=200) - last_name = models.CharField(max_length=200) entry_time = models.DateTimeField(default=datetime.now) - exit_time = models.DateTimeField(blank=True,null=True) - vendor = models.ForeignKey(Vendor, on_delete=models.SET_NULL, null=True, blank=True) - added_by = models.ForeignKey(User, on_delete=models.SET_NULL,null=True, blank=True) + exit_time = models.DateTimeField(blank=True, null=True) + custodian = models.ForeignKey( + Custodian, on_delete=models.SET_NULL, null=True, blank=True + ) + added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) branch = models.ForeignKey(Branch, on_delete=models.SET_NULL, null=True, blank=True) - attendance_sheet = models.ForeignKey(AttendanceSheet, on_delete=models.SET_NULL, null=True, blank=True) + attendance_sheet = models.ForeignKey( + AttendanceSheet, on_delete=models.SET_NULL, null=True, blank=True + ) + + def __str__(self): + return f"{self.custodian.first_name} {self.custodian.last_name}" + + +class AttendanceVehicle(models.Model): + entry_time = models.DateTimeField(default=datetime.now) + exit_time = models.DateTimeField(blank=True, null=True) + vehicle = models.ForeignKey( + Vehicle, on_delete=models.SET_NULL, null=True, blank=True + ) + added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + branch = models.ForeignKey(Branch, on_delete=models.SET_NULL, null=True, blank=True) + attendance_sheet = models.ForeignKey( + AttendanceSheet, on_delete=models.SET_NULL, null=True, blank=True + ) + + def __str__(self): + return f"{self.vehicle.model_name} -> {self.vehicle.number_plate}" - def __str__(self) -> str: - return f'{self.first_name} {self.last_name}' class Issue(models.Model): comment = models.CharField(max_length=200) vendor = models.ForeignKey(Vendor, on_delete=models.SET_NULL, null=True, blank=True) - reverted_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) - sheet = models.ForeignKey(AttendanceSheet, on_delete=models.SET_NULL, null=True, blank=True) + reverted_by = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, blank=True + ) + sheet = models.ForeignKey( + AttendanceSheet, on_delete=models.SET_NULL, null=True, blank=True + ) created_at = models.DateTimeField(default=datetime.now) - + def __str__(self) -> str: - return f'{self.comment} {self.vendor} {self.reverted_by} -> {self.sheet}' + return f"{self.comment} {self.vendor} {self.reverted_by} -> {self.sheet}" diff --git a/server/attendance/serializers.py b/server/attendance/serializers.py new file mode 100644 index 0000000..7ce0aaf --- /dev/null +++ b/server/attendance/serializers.py @@ -0,0 +1,264 @@ +from datetime import datetime +from secrets import token_hex +from rest_framework import serializers +from rest_framework.response import Response +from users.serializers import BranchSerializer, UserSerializer +from vendors.serializers import ( + VendorSerializer, + CustodianSerializer, + VehicleSerializer, +) + +from .models import Attendance, AttendanceSheet, AttendanceVehicle, Issue, Trip +from users.models import Branch, User +from vendors.models import Custodian, Vendor, Vehicle +from attendance.models import AttendanceVehicle + + +class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField): + def __init__(self, **kwargs): + + self.serializer = kwargs.pop("serializer", None) + if self.serializer is not None and not issubclass( + self.serializer, serializers.Serializer + ): + raise TypeError('"serializer" is not a valid serializer class') + # if self.serializer: + # print(self.serializer) + super().__init__(**kwargs) + + def use_pk_only_optimization(self): + return False if self.serializer else True + + def to_representation(self, instance): + if self.serializer: + return self.serializer(instance, context=self.context).data + return super().to_representation(instance) + + +class AttendanceSheetSerializer(serializers.ModelSerializer): + class Meta: + model = AttendanceSheet + fields = ["id", "sheet_created", "invoice", "verified"] + + +class AttendanceVehicleSerializer(serializers.ModelSerializer): + vehicle = RelatedFieldAlternative( + queryset=Vehicle.objects.all(), serializer=VehicleSerializer + ) + attendance_sheet = RelatedFieldAlternative( + queryset=AttendanceSheet.objects.all(), serializer=AttendanceSheetSerializer + ) + branch = RelatedFieldAlternative( + queryset=Branch.objects.all(), serializer=BranchSerializer + ) + added_by = RelatedFieldAlternative( + queryset=User.objects.all(), serializer=UserSerializer + ) + + class Meta: + model = AttendanceVehicle + fields = [ + "id", + "vehicle", + "entry_time", + "exit_time", + "branch", + "added_by", + "attendance_sheet", + ] + + +class AttendanceSerializer(serializers.ModelSerializer): + custodian = RelatedFieldAlternative( + queryset=Custodian.objects.all(), serializer=CustodianSerializer + ) + attendance_sheet = RelatedFieldAlternative( + queryset=AttendanceSheet.objects.all(), serializer=AttendanceSheetSerializer + ) + branch = RelatedFieldAlternative( + queryset=Branch.objects.all(), serializer=BranchSerializer + ) + added_by = RelatedFieldAlternative( + queryset=User.objects.all(), serializer=UserSerializer + ) + + class Meta: + model = Attendance + fields = [ + "id", + "custodian", + "entry_time", + "exit_time", + "branch", + "added_by", + "attendance_sheet", + ] + + +class TripSerializer(serializers.ModelSerializer): + vehicle = RelatedFieldAlternative( + queryset=Vehicle.objects.all(), serializer=VehicleSerializer + ) + custodian_1 = RelatedFieldAlternative( + queryset=Custodian.objects.all(), serializer=CustodianSerializer + ) + custodian_2 = RelatedFieldAlternative( + queryset=Custodian.objects.all(), serializer=CustodianSerializer + ) + custodian_3 = RelatedFieldAlternative( + queryset=Custodian.objects.all(), serializer=CustodianSerializer + ) + branch = RelatedFieldAlternative( + queryset=Branch.objects.all(), serializer=BranchSerializer + ) + added_by = RelatedFieldAlternative( + queryset=User.objects.all(), serializer=UserSerializer + ) + + class Meta: + model = Trip + fields = [ + "id", + "vehicle", + "trip_code", + "trip_start", + "custodian_1", + "custodian_2", + "custodian_3", + "custodian_1_code", + "custodian_2_code", + "custodian_3_code", + "entry_time", + "exit_time", + "start_location", + "end_location", + "branch", + "added_by", + ] + + def create(self, validated_data): + print(validated_data) + validated_data["trip_code"] = token_hex(6).upper() + + custodian_1 = validated_data.get("custodian_1") + custodian_2 = validated_data.get("custodian_2") + custodian_3 = validated_data.get("custodian_3") + + if custodian_1: + validated_data["custodian_1_code"] = token_hex(6).upper() + if custodian_2 != custodian_2: + validated_data["custodian_2_code"] = token_hex(6).upper() + if custodian_3 != custodian_1: + validated_data["custodian_3_code"] = token_hex(6).upper() + + return super().create(validated_data) + + def update(self, instance, validated_data): + custodian_1 = validated_data.get("custodian_1") + custodian_2 = validated_data.get("custodian_2") + custodian_3 = validated_data.get("custodian_3") + custodian_1_code = validated_data.get("custodian_1_code") + custodian_2_code = validated_data.get("custodian_2_code") + custodian_3_code = validated_data.get("custodian_3_code") + trip_start = validated_data.get("trip_start") + if trip_start and trip_start == True and instance.trip_start == False: + AttendanceVehicle.objects.create( + vehicle=instance.vehicle, + branch=instance.branch, + ) + + if instance.trip_start == False and not trip_start: + if custodian_1_code: + if custodian_1_code != instance.custodian_1_code: + return serializers.ValidationError( + {"Error": "Code does not match !"} + ) + else: + Attendance.objects.create( + custodian=custodian_1, + branch=instance.branch, + ) + + if custodian_2_code and custodian_2 != custodian_1: + if custodian_2_code != instance.custodian_2_code: + return serializers.ValidationError( + {"error": "Code does not match !"} + ) + else: + Attendance.objects.create( + custodian=custodian_2, + branch=instance.branch, + ) + + if custodian_3_code and custodian_3 != custodian_1: + if custodian_3_code != instance.custodian_3_code: + return serializers.ValidationError( + {"error": "Code does not match !"} + ) + else: + Attendance.objects.create( + custodian=custodian_3, + branch=instance.branch, + ) + if instance.trip_start == False: + today = datetime.now() + attendance_1 = Attendance.objects.filter( + entry_time__year=today.date().year, + entry_time__month=today.date().month, + entry_time__day=today.date().day, + custodian=custodian_1, + branch=instance.branch, + ).first() + attendance_2 = Attendance.objects.filter( + entry_time__year=today.date().year, + entry_time__month=today.date().month, + entry_time__day=today.date().day, + custodian=custodian_2, + branch=instance.branch, + ).first() + attendance_3 = Attendance.objects.filter( + entry_time__year=today.date().year, + entry_time__month=today.date().month, + entry_time__day=today.date().day, + custodian=custodian_3, + branch=instance.branch, + ).first() + vehicle_attendance = AttendanceVehicle.objects.filter( + entry_time__year=today.date().year, + entry_time__month=today.date().month, + entry_time__day=today.date().day, + vehicle=instance.vehicle, + branch=instance.branch, + ).first() + if attendance_1: + attendance_1.exit_time = today + attendance_1.save() + if attendance_2: + attendance_2.exit_time = today + attendance_2.save() + if attendance_3: + attendance_3.exit_time = today + attendance_3.save() + if vehicle_attendance: + vehicle_attendance.exit_time = today + vehicle_attendance.save() + + return super().update(instance, validated_data) + + +class IssueSerializer(serializers.ModelSerializer): + reverted_by = RelatedFieldAlternative( + queryset=User.objects.all(), serializer=UserSerializer + ) + vendor = RelatedFieldAlternative( + queryset=Vendor.objects.all(), serializer=VendorSerializer + ) + sheet = RelatedFieldAlternative( + queryset=AttendanceSheet.objects.all(), serializer=AttendanceSheetSerializer + ) + + class Meta: + model = Issue + fields = ["id", "comment", "reverted_by", "vendor", "sheet", "created_at"] + diff --git a/server/attendance/urls.py b/server/attendance/urls.py new file mode 100644 index 0000000..0a4b307 --- /dev/null +++ b/server/attendance/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns +from . import views + +urlpatterns = [ + path("attendance/", views.AttendanceList.as_view()), + path("attendance//", views.AttendanceDetail.as_view()), + path("attendance/vehicle/", views.AttendanceVehicleList.as_view()), + path("attendance/vehicle//", views.AttendanceVehicleDetail.as_view()), + path("trip/", views.TripList.as_view()), + path("trip//", views.TripDetail.as_view()), + path("issue/", views.IssueList.as_view()), + path("issue//", views.IssueDetail.as_view()), +] + +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/server/attendance/utils.py b/server/attendance/utils.py new file mode 100644 index 0000000..1ac08ab --- /dev/null +++ b/server/attendance/utils.py @@ -0,0 +1,108 @@ +import pandas as pd +import os +import csv +from django.conf import settings +from django.utils.text import slugify +from datetime import datetime + +BASE_DIR = settings.BASE_DIR + + +def get_model_field_names(model, ignore_fields=["content_object"]): + """ + ::param model is a Django model class + ::param ignore_fields is a list of field names to ignore by default + This method gets all model field names (as strings) and returns a list + of them ignoring the ones we know don't work (like the 'content_object' field) + """ + model_fields = model._meta.get_fields() + model_field_names = list( + set([f.name for f in model_fields if f.name not in ignore_fields]) + ) + return model_field_names + + +def get_lookup_fields(model, fields=None): + """ + ::param model is a Django model class + ::param fields is a list of field name strings. + This method compares the lookups we want vs the lookups + that are available. It ignores the unavailable fields we passed. + """ + model_field_names = get_model_field_names(model) + if fields is not None: + """ + we'll iterate through all the passed field_names + and verify they are valid by only including the valid ones + """ + lookup_fields = [] + for x in fields: + if "__" in x: + # the __ is for ForeignKey lookups + lookup_fields.append(x) + elif x in model_field_names: + lookup_fields.append(x) + else: + """ + No field names were passed, use the default model fields + """ + lookup_fields = model_field_names + return lookup_fields + + +def qs_to_dataset(qs, fields=None): + """ + ::param qs is any Django queryset + ::param fields is a list of field name strings, ignoring non-model field names + This method is the final step, simply calling the fields we formed on the queryset + and turning it into a list of dictionaries with key/value pairs. + """ + + lookup_fields = get_lookup_fields(qs.model, fields=fields) + return list(qs.values(*lookup_fields)) + + +def convert_to_dataframe(qs, fields=None, index=None): + """ + ::param qs is an QuerySet from Django + ::fields is a list of field names from the Model of the QuerySet + ::index is the preferred index column we want our dataframe to be set to + + Using the methods from above, we can easily build a dataframe + from this data. + """ + lookup_fields = get_lookup_fields(qs.model, fields=fields) + index_col = None + if index in lookup_fields: + index_col = index + elif "id" in lookup_fields: + index_col = "id" + values = qs_to_dataset(qs, fields=fields) + df = pd.DataFrame.from_records(values, columns=lookup_fields, index=index_col) + return df + + +def qs_to_local_csv(qs, fields=None, path=None): + if path is None: + path = os.path.join(os.path.dirname(BASE_DIR), "csvstorage") + print(path) + if not os.path.exists(path): + """ + CSV storage folder doesn't exist, make it! + """ + os.mkdir(path) + model_name = slugify(qs.model.__name__) + today = datetime.today() + filename = "{}_{}_{}.csv".format(model_name, today.month, today.year) + filepath = os.path.join(path, filename) + lookups = get_lookup_fields(qs.model, fields=fields) + dataset = qs_to_dataset(qs, fields) + rows_done = 0 + with open(filepath, "w") as my_file: + writer = csv.DictWriter(my_file, fieldnames=lookups) + writer.writeheader() + for data_item in dataset: + writer.writerow(data_item) + rows_done += 1 + + return filepath diff --git a/server/attendance/views.py b/server/attendance/views.py index 91ea44a..0aec2a1 100644 --- a/server/attendance/views.py +++ b/server/attendance/views.py @@ -1,3 +1,284 @@ -from django.shortcuts import render +from datetime import datetime +from secrets import token_hex -# Create your views here. +from django.shortcuts import get_object_or_404 + +from rest_framework import mixins +from rest_framework import generics +from rest_framework import filters + +from rest_framework.permissions import IsAuthenticated +from rest_framework.authentication import ( + BasicAuthentication, + SessionAuthentication, + TokenAuthentication, +) +from rest_framework.response import Response +from rest_framework.pagination import PageNumberPagination +from django_filters.rest_framework import DjangoFilterBackend + +from users.serializers import BranchSerializer, UserSerializer +from vendors.serializers import CustodianSerializer, VehicleSerializer +from attendance.serializers import ( + AttendanceSerializer, + IssueSerializer, + TripSerializer, + AttendanceVehicleSerializer, +) + +from users.models import Branch, User +from vendors.models import Custodian, Vehicle, Gunmen +from attendance.models import ( + Attendance, + AttendanceSheet, + AttendanceVehicle, + Issue, + Trip, +) +from attendance.utils import qs_to_local_csv + + +class CustomPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = "page_size" + max_page_size = 1000 + + def get_paginated_response(self, data): + return Response( + { + "links": { + "next": self.get_next_link(), + "previous": self.get_previous_link(), + }, + "count": self.page.paginator.count, + "page_size": self.page_size, + "results": data, + } + ) + + +# @csrf_exempt +class TripList(generics.ListCreateAPIView): + + # authentication_classes = [ + # TokenAuthentication, + # SessionAuthentication, + # BasicAuthentication, + # ] + permission_classes = [IsAuthenticated] + queryset = Trip.objects.all() + serializer_class = TripSerializer + pagination_class = CustomPagination + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = [ + "^custodian_1__first_name", + "^custodian_1__last_name", + "^custodian_2__first_name", + "^custodian_2__last_name", + "^custodian_3__first_name", + "^custodian_3__last_name", + "^vehicle__number_plate", + "^vehicle__model_name", + ] + + filterset_fields = { + "entry_time": ["gte", "lte", "exact", "gt", "lt"], + "exit_time": ["gte", "lte", "exact", "gt", "lt"], + "custodian_1": ["exact"], + "custodian_2": ["exact"], + "custodian_3": ["exact"], + "vehicle": ["exact"], + "added_by": ["exact"], + "branch": ["exact"], + } + ordering_fields = "__all__" + + def post(self, request, *args, **kwargs): + validated_data = request.data + + custodian_1 = validated_data.get("custodian_1") + custodian_2 = validated_data.get("custodian_2") + custodian_3 = validated_data.get("custodian_3") + + if custodian_1 and custodian_2 and custodian_1 == custodian_2: + return Response( + "!! ERR !!: The same custodian cannot be added more than once" + ) + if custodian_1 and custodian_3 and custodian_1 == custodian_3: + return Response( + "!! ERR !!: The same custodian cannot be added more than once" + ) + if custodian_3 and custodian_2 and custodian_3 == custodian_2: + return Response( + "!! ERR !!: The same custodian cannot be added more than once" + ) + + if not custodian_2: + request.data["custodian_2"] = request.data["custodian_1"] + if not custodian_3: + request.data["custodian_3"] = request.data["custodian_1"] + + return self.create(request, *args, **kwargs) + + +class TripDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Trip.objects.all() + serializer_class = TripSerializer + + def put(self, request, *args, **kwargs): + validated_data = request.data + + custodian_1 = validated_data.get("custodian_1") + custodian_2 = validated_data.get("custodian_2") + custodian_3 = validated_data.get("custodian_3") + + if custodian_1 and custodian_2 and custodian_1 == custodian_2: + return Response( + "!! ERR !!: The same custodian cannot be added more than once" + ) + if custodian_1 and custodian_3 and custodian_1 == custodian_3: + return Response( + "!! ERR !!: The same custodian cannot be added more than once" + ) + if custodian_3 and custodian_2 and custodian_3 == custodian_2: + return Response( + "!! ERR !!: The same custodian cannot be added more than once" + ) + + if not custodian_2: + request.data["custodian_2"] = request.data["custodian_1"] + if not custodian_3: + request.data["custodian_3"] = request.data["custodian_1"] + return self.update(request, *args, **kwargs) + + +class AttendanceVehicleList(generics.ListCreateAPIView): + queryset = AttendanceVehicle.objects.all() + serializer_class = AttendanceVehicleSerializer + pagination_class = CustomPagination + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ["^vehicle__model_name", "^vehicle__number_plate"] + + filterset_fields = { + "entry_time": ["gte", "lte", "exact", "gt", "lt"], + "exit_time": ["gte", "lte", "exact", "gt", "lt"], + "vehicle": ["exact"], + "added_by": ["exact"], + "branch": ["exact"], + "attendance_sheet": ["exact"], + } + + ordering_fields = "__all__" + + +class AttendanceVehicleDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = AttendanceVehicle.objects.all() + serializer_class = AttendanceVehicleSerializer + + +class AttendanceList(generics.ListCreateAPIView): + queryset = Attendance.objects.all() + serializer_class = AttendanceSerializer + pagination_class = CustomPagination + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ["^custodian__first_name", "^custodian__last_name"] + + filterset_fields = { + "entry_time": ["gte", "lte", "exact", "gt", "lt"], + "exit_time": ["gte", "lte", "exact", "gt", "lt"], + "custodian": ["exact"], + "added_by": ["exact"], + "branch": ["exact"], + "attendance_sheet": ["exact"], + } + + ordering_fields = "__all__" + + def get(self, request, *args, **kwargs): + params = request.query_params + start_date = params.get("start_date", datetime.min) + end_date = params.get("end_date", datetime.max) + + queryset = self.filter_queryset(self.get_queryset()) + queryset = queryset.filter(entry_time__date__range=[ + start_date, end_date]) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + def post(self, request, *args, **kwargs): + data = request.data + branch_id = data.get("branch") + added_by_id = data.get("added_by") + custodian_ids = data.get("custodian_id") + attendance_sheet = data.get("attendance_sheet") + + result = [] + + for custodian_id in custodian_ids: + data = { + "custodian": custodian_id, + "branch": branch_id, + "added_by": added_by_id, + "attendance_sheet": attendance_sheet, + } + new_attendance = AttendanceSerializer(data=data) + if not new_attendance.is_valid(): + return Response(data="invalid request") + + today = datetime.now() + attendance = Attendance.objects.filter( + entry_time__year=today.date().year, + entry_time__month=today.date().month, + entry_time__day=today.date().day, + custodian=new_attendance.data["custodian"]["id"], + branch=new_attendance.data["branch"]["id"], + ).first() + + if attendance: + attendance.entry_time = today + attendance.exit_time = new_attendance.data.get( + "exit_time", attendance.exit_time + ) + attendance.save() + result.append(AttendanceSerializer(attendance).data) + else: + custodian_ = get_object_or_404( + Custodian, pk=data.pop("custodian")) + added_by_ = get_object_or_404(User, pk=data.pop("added_by")) + branch_ = get_object_or_404(Branch, pk=data.pop("branch")) + attendance_sheet_ = get_object_or_404( + AttendanceSheet, pk=data.pop("attendance_sheet") + ) + attendance = Attendance( + custodian=custodian_, + added_by=added_by_, + branch=branch_, + attendance_sheet=attendance_sheet_, + ) + attendance.save() + result.append(AttendanceSerializer(attendance).data) + + return Response(data=result) + + +class AttendanceDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Attendance.objects.all() + serializer_class = AttendanceSerializer + + +class IssueList(generics.ListCreateAPIView): + queryset = Issue.objects.all() + serializer_class = IssueSerializer + pagination_class = CustomPagination + ordering_fields = "__all__" + + +class IssueDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Issue.objects.all() + serializer_class = IssueSerializer diff --git a/server/db.sqlite3 b/server/db.sqlite3 new file mode 100644 index 0000000..33b7a04 Binary files /dev/null and b/server/db.sqlite3 differ diff --git a/server/manage.py b/server/manage.py index 8b46ee6..9856e02 100644 --- a/server/manage.py +++ b/server/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/server/requirements.txt b/server/requirements.txt index e896f9c..f9b46d1 100644 Binary files a/server/requirements.txt and b/server/requirements.txt differ diff --git a/server/server/asgi.py b/server/server/asgi.py index 501c9a9..cfe3f88 100644 --- a/server/server/asgi.py +++ b/server/server/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") application = get_asgi_application() diff --git a/server/server/settings.py b/server/server/settings.py index c5c9404..ab6cee1 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -12,7 +12,8 @@ from pathlib import Path from datetime import timedelta -import os +import os + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -22,7 +23,7 @@ # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'm7+v4@(z0^_4@c(7o48%d1n7$hy=rbr2@p)zv%6ta4*j2s&btq' +SECRET_KEY = "m7+v4@(z0^_4@c(7o48%d1n7$hy=rbr2@p)zv%6ta4*j2s&btq" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -33,56 +34,62 @@ # Application definition INSTALLED_APPS = [ - 'jazzmin', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'users.apps.UsersConfig', - 'vendors.apps.VendorsConfig', - 'attendance.apps.AttendanceConfig', + "rest_framework", + "django_crontab", + "jazzmin", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_filters", + "corsheaders", + "rest_framework.authtoken", + "users.apps.UsersConfig", + "vendors.apps.VendorsConfig", + "attendance.apps.AttendanceConfig", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'server.urls' +ROOT_URLCONF = "server.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'server.wsgi.application' +WSGI_APPLICATION = "server.wsgi.application" # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR,'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -92,16 +99,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -109,9 +116,9 @@ # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -123,41 +130,51 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ -STATIC_URL = '/static/' - -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +STATIC_URL = "/static/" -MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = "/media/" - -CORS_ALLOWED_ORIGINS = [ - "http://localhost:3000" -] +CORS_ALLOWED_ORIGINS = ["http://localhost:3000"] SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=10), - 'ROTATE_REFRESH_TOKENS': True, - 'BLACKLIST_AFTER_ROTATION': False, - 'ALGORITHM': 'HS256', - 'SIGNING_KEY': SECRET_KEY, - 'VERIFYING_KEY': None, - 'AUTH_HEADER_TYPES': ('JWT',), - 'USER_ID_FIELD': 'id', - 'USER_ID_CLAIM': 'user_id', - 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), - 'TOKEN_TYPE_CLAIM': 'token_type', + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=10), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": False, + "ALGORITHM": "HS256", + "SIGNING_KEY": SECRET_KEY, + "VERIFYING_KEY": None, + "AUTH_HEADER_TYPES": ("JWT", "Bearer"), + "USER_ID_FIELD": "id", + "USER_ID_CLAIM": "user_id", + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + "TOKEN_TYPE_CLAIM": "token_type", } REST_FRAMEWORK = { - 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.AllowAny', + "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.AllowAny", ], - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework_simplejwt.authentication.JWTAuthentication', + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework_simplejwt.authentication.JWTAuthentication", + "rest_framework.permissions.IsAuthenticated", ], } + +# Run once a month at midnight of the first day of the month +# 0 0 20 * * + +# for now once every 10 minutes +CRONJOBS = [("*/10 * * * *", "attendance.cron.send_attendance_report")] + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.gmail.com" +EMAIL_USE_TLS = True +EMAIL_PORT = 587 +EMAIL_HOST_USER = "" +EMAIL_HOST_PASSWORD = "" diff --git a/server/server/urls.py b/server/server/urls.py index c8321c8..96c81df 100644 --- a/server/server/urls.py +++ b/server/server/urls.py @@ -14,14 +14,17 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include from rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView, ) urlpatterns = [ - path('admin/', admin.site.urls), - path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), - path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path("admin/", admin.site.urls), + path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("api/attendance/", include("attendance.urls")), + path("api/vendor/", include("vendors.urls")), + path("api/users/", include("users.urls")), ] diff --git a/server/server/wsgi.py b/server/server/wsgi.py index 3798fda..171e1c1 100644 --- a/server/server/wsgi.py +++ b/server/server/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") application = get_wsgi_application() diff --git a/server/users/admin.py b/server/users/admin.py index 0bc8499..a115015 100644 --- a/server/users/admin.py +++ b/server/users/admin.py @@ -1,20 +1,21 @@ from django.contrib import admin -from .models import Profile,Branch,Region +from .models import Profile, Branch, Region + class BranchAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'address', 'branch_manager', 'region') - list_display_links = ('id', 'name') - search_fields = ('id', 'name', 'region', 'branch_manager') + list_display = ("id", "name", "address", "branch_manager", "region") + list_display_links = ("id", "name") + search_fields = ("id", "name", "region", "branch_manager") list_per_page = 20 class RegionAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'address', 'regional_officer') - list_display_links = ('id', 'name') - search_fields = ('id', 'name', 'regional_officer') + list_display = ("id", "name", "address", "regional_officer") + list_display_links = ("id", "name") + search_fields = ("id", "name", "regional_officer") list_per_page = 20 admin.site.register(Branch, BranchAdmin) admin.site.register(Region, RegionAdmin) -admin.site.register(Profile) \ No newline at end of file +admin.site.register(Profile) diff --git a/server/users/apps.py b/server/users/apps.py index f910dd7..1e547f5 100644 --- a/server/users/apps.py +++ b/server/users/apps.py @@ -1,8 +1,8 @@ - from django.apps import AppConfig class UsersConfig(AppConfig): - name = 'users' - def ready(self): - import users.signals \ No newline at end of file + name = "users" + + # def ready(self): + # import users.signals diff --git a/server/users/migrations/0003_auto_20210714_2341.py b/server/users/migrations/0003_auto_20210714_2341.py new file mode 100644 index 0000000..6c04793 --- /dev/null +++ b/server/users/migrations/0003_auto_20210714_2341.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.4 on 2021-07-14 18:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_branch_region'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='branch', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.Branch'), + ), + migrations.AddField( + model_name='profile', + name='gender', + field=models.CharField(choices=[('M', 'Male'), ('F', 'Female'), ('O', 'Other')], default='male', max_length=6, unique=True), + preserve_default=False, + ), + ] diff --git a/server/users/migrations/0004_auto_20210715_1603.py b/server/users/migrations/0004_auto_20210715_1603.py new file mode 100644 index 0000000..6cd4039 --- /dev/null +++ b/server/users/migrations/0004_auto_20210715_1603.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.4 on 2021-07-15 10:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('users', '0003_auto_20210714_2341'), + ] + + operations = [ + migrations.RemoveField( + model_name='profile', + name='id', + ), + migrations.AlterField( + model_name='profile', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/server/users/migrations/0005_auto_20210715_1604.py b/server/users/migrations/0005_auto_20210715_1604.py new file mode 100644 index 0000000..90cf79e --- /dev/null +++ b/server/users/migrations/0005_auto_20210715_1604.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.4 on 2021-07-15 10:34 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('users', '0004_auto_20210715_1603'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='id', + field=models.AutoField(auto_created=True, default=1, primary_key=True, serialize=False, verbose_name='ID'), + preserve_default=False, + ), + migrations.AlterField( + model_name='profile', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/server/users/migrations/0006_auto_20210715_1609.py b/server/users/migrations/0006_auto_20210715_1609.py new file mode 100644 index 0000000..c6e4cdf --- /dev/null +++ b/server/users/migrations/0006_auto_20210715_1609.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.4 on 2021-07-15 10:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_auto_20210715_1604'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='gender', + field=models.CharField(choices=[('M', 'Male'), ('F', 'Female'), ('O', 'Other')], max_length=6), + ), + ] diff --git a/server/users/models.py b/server/users/models.py index f168758..5cc96bd 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -1,33 +1,46 @@ from django.db import models from django.contrib.auth.models import User - +from django.db.models.signals import post_save +from django.dispatch import receiver # Create your models here. -class Profile(models.Model): - user = models.OneToOneField(User,on_delete = models.CASCADE) - is_superuser = models.BooleanField(default=False) - is_incharge = models.BooleanField(default=False) - - def __str__(self): - return f'{self.user.username} Profile' - class Region(models.Model): name = models.CharField(max_length=200) address = models.CharField(max_length=1000) - regional_officer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + regional_officer = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, blank=True + ) def __str__(self): - return f'{self.name}' + return f"{self.name}" class Branch(models.Model): name = models.CharField(max_length=200) address = models.CharField(max_length=1000) - branch_manager = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + branch_manager = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, blank=True + ) region = models.ForeignKey(Region, on_delete=models.SET_NULL, null=True, blank=True) def __str__(self) -> str: - return f'{self.name} -> {self.region}' + return f"{self.name} -> {self.region}" + + +class Profile(models.Model): + GENDER_CHOICES = ( + ("M", "Male"), + ("F", "Female"), + ("O", "Other"), + ) + user = models.OneToOneField(User, on_delete=models.CASCADE) + gender = models.CharField(max_length=6, choices=GENDER_CHOICES) + branch = models.ForeignKey(Branch, on_delete=models.SET_NULL, null=True, blank=True) + is_superuser = models.BooleanField(default=False) + is_incharge = models.BooleanField(default=False) + + def __str__(self): + return f"{self.user.username} Profile" diff --git a/server/users/serializers.py b/server/users/serializers.py new file mode 100644 index 0000000..e632c64 --- /dev/null +++ b/server/users/serializers.py @@ -0,0 +1,115 @@ +from secrets import token_hex +from rest_framework import serializers +from django.contrib.auth.models import User +from django.contrib.auth.hashers import make_password +from .models import Profile, Region, Branch + + +class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField): + def __init__(self, **kwargs): + self.serializer = kwargs.pop("serializer", None) + if self.serializer is not None and not issubclass( + self.serializer, serializers.Serializer + ): + raise TypeError('"serializer" is not a valid serializer class') + super().__init__(**kwargs) + + def use_pk_only_optimization(self): + return False if self.serializer else True + + def to_representation(self, instance): + if self.serializer: + return self.serializer(instance, context=self.context).data + return super().to_representation(instance) + + +class ProfileSerializer(serializers.ModelSerializer): + branch = RelatedFieldAlternative(queryset=Branch.objects.all()) + + class Meta: + model = Profile + fields = ["id", "gender", "branch", "is_superuser", "is_incharge"] + + +class UserSerializer(serializers.ModelSerializer): + # profile = RelatedFieldAlternative( + # queryset=Profile.objects.all(), serializer=ProfileSerializer + # ) + profile = ProfileSerializer(required=False) + + class Meta: + model = User + fields = ( + "id", + "first_name", + "last_name", + "username", + "email", + "profile", + "is_staff", + "is_active", + "is_superuser", + "last_login", + "date_joined", + ) + # extra_kwargs = {"password": {"write_only": True}} + + def create(self, validated_data): + profile_data = validated_data.pop("profile") + random_password = token_hex(6).upper() + encoded_password = make_password(random_password) + user = User.objects.create(**validated_data, password=encoded_password) + profile = Profile.objects.create(user=user, **profile_data) + + # mail( + # subject='CMS Login Credentials', + # message=f'Username: {user.username} Password: {random_password}', + # to_mail= [user.email], + # ) + + return user + + def update(self, instance, validated_data): + instance.first_name = validated_data.get("first_name", instance.first_name) + instance.last_name = validated_data.get("last_name", instance.last_name) + instance.email = validated_data.get("email", instance.email) + instance.username = validated_data.get("username", instance.username) + + profile = Profile.objects.filter(user=instance).first() + profile_data = validated_data.pop("profile") + if profile: + profile.gender = profile_data.get("gender", profile.gender) + profile.branch = profile_data.get("branch", profile.branch) + profile.is_superuser = profile_data.get( + "is_superuser", profile.is_superuser + ) + profile.is_incharge = profile_data.get("is_incharge", profile.is_incharge) + profile.save() + else: + profile = Profile.objects.create(user=instance, **profile_data) + profile.save() + instance.save() + return instance + + +class RegionSerializer(serializers.ModelSerializer): + regional_officer = RelatedFieldAlternative( + queryset=User.objects.all(), serializer=UserSerializer + ) + + class Meta: + model = Region + fields = ["id", "name", "address", "regional_officer"] + + +class BranchSerializer(serializers.ModelSerializer): + branch_manager = RelatedFieldAlternative( + queryset=User.objects.all(), serializer=UserSerializer + ) + region = RelatedFieldAlternative( + queryset=Region.objects.all(), serializer=RegionSerializer + ) + + class Meta: + model = Branch + fields = ["id", "name", "address", "branch_manager", "region"] diff --git a/server/users/signals.py b/server/users/signals.py index 492c8c7..e513185 100644 --- a/server/users/signals.py +++ b/server/users/signals.py @@ -1,17 +1,23 @@ from django.db.models.signals import post_save from django.contrib.auth.models import User from django.dispatch import receiver -from .models import Profile +from .models import Profile - -@receiver(post_save,sender = User) -def created_profile(sender,instance,created,**kwargs): +@receiver(post_save, sender=User) +def created_profile(sender, instance, created, **kwargs): if created: - Profile.objects.create(user =instance) - + profile_data = instance.pop("profile") + # create profile + Profile.objects.get_or_create( + user=instance, + gender=profile_data["gender"], + branch=profile_data["branch"], + # etc... + ) + # Profile.objects.create(user=instance) -@receiver(post_save,sender = User) -def save_profile(sender,instance,**kwargs): - instance.profile.save() \ No newline at end of file +@receiver(post_save, sender=User) +def save_profile(sender, instance, **kwargs): + instance.profile.save() diff --git a/server/users/urls.py b/server/users/urls.py new file mode 100644 index 0000000..25c699a --- /dev/null +++ b/server/users/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns +from . import views + +urlpatterns = [ + path("user/", views.UserList.as_view()), + path("user//", views.UserDetail.as_view()), + path("branch/", views.BranchList.as_view()), + path("branch//", views.BranchDetail.as_view()), + path("region/", views.RegionList.as_view()), + path("region//", views.RegionDetail.as_view()), + path("current/", views.get_current_user), +] + +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/server/users/views.py b/server/users/views.py index 91ea44a..b891be0 100644 --- a/server/users/views.py +++ b/server/users/views.py @@ -1,3 +1,96 @@ -from django.shortcuts import render +from django.contrib.auth.models import User +from .models import Branch, Region +from .serializers import BranchSerializer, RegionSerializer, UserSerializer +from rest_framework import mixins +from rest_framework import generics +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response +from rest_framework import status +from rest_framework.decorators import api_view +from django.shortcuts import get_object_or_404 -# Create your views here. + +class CustomPagination(PageNumberPagination): + page_size = 100 + page_size_query_param = "page_size" + max_page_size = 1000 + + def get_paginated_response(self, data): + return Response( + { + "links": { + "next": self.get_next_link(), + "previous": self.get_previous_link(), + }, + "count": self.page.paginator.count, + "page_size": self.page_size, + "results": data, + } + ) + + +class UserList(generics.ListCreateAPIView): + # permission_classes = (IsAuthenticatedOrWriteOnly,) + queryset = User.objects.all() + serializer_class = UserSerializer + pagination_class = CustomPagination + + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ["^first_name", "^last_name", "^username", "^email"] + filterset_fields = [ + "first_name", + "last_name", + "username", + "email", + "profile__gender", + "is_staff", + "is_active", + "is_superuser", + ] + ordering_fields = "__all__" + + +class UserDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer + + +class BranchList(generics.ListCreateAPIView): + queryset = Branch.objects.all() + serializer_class = BranchSerializer + pagination_class = CustomPagination + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ["^name", "^address"] + filterset_fields = ["name", "address", "branch_manager", "region"] + ordering_fields = "__all__" + + +class BranchDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Branch.objects.all() + serializer_class = BranchSerializer + + +class RegionList(generics.ListCreateAPIView): + queryset = Region.objects.all() + serializer_class = RegionSerializer + pagination_class = CustomPagination + + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ["^name", "^address"] + filterset_fields = ["name", "address", "regional_officer"] + ordering_fields = "__all__" + + +class RegionDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Region.objects.all() + serializer_class = RegionSerializer + + +@ api_view(['GET']) +def get_current_user(request): + print(request.user.id) + user = get_object_or_404(User, pk=request.user.id) + serializer = UserSerializer(user) + return Response(serializer.data) diff --git a/server/vendors/admin.py b/server/vendors/admin.py index eba28dc..89cafed 100644 --- a/server/vendors/admin.py +++ b/server/vendors/admin.py @@ -1,13 +1,44 @@ from django.contrib import admin -from django.db import models +from .models import * -from .models import Vendor class VendorAdmin(admin.ModelAdmin): - list_display = ('id', 'name','address','email', 'contact', 'officer_incharge', 'created_at') - list_display_links = ('id', 'name') - search_fields = ('id', 'name', 'email', 'officer_incharge') + list_display = ( + "id", + "name", + "address", + "email", + "contact", + "officer_incharge", + "created_at", + ) + list_display_links = ("id", "name") + search_fields = ("id", "name", "email", "officer_incharge") + list_per_page = 20 + + +class CustodianAdmin(admin.ModelAdmin): + list_display = ( + "id", + "first_name", + "last_name", + "custodian_type", + "email", + "phone_number", + "vendor", + ) + list_display_links = ("id", "vendor", "custodian_type") + search_fields = ("first_name", "last_name", "vendor", "custodian_type") list_per_page = 20 -admin.site.register(Vendor, VendorAdmin) +class VehicleAdmin(admin.ModelAdmin): + list_display = ("id", "model_name", "number_plate", "vendor") + list_display_links = ("vendor",) + search_fields = ("id", "model_name", "vendor", "number_plate") + list_per_page = 20 + + +admin.site.register(Custodian, CustodianAdmin) +admin.site.register(Vendor, VendorAdmin) +admin.site.register(Vehicle, VehicleAdmin) diff --git a/server/vendors/apps.py b/server/vendors/apps.py index 1cc958b..5d12bd8 100644 --- a/server/vendors/apps.py +++ b/server/vendors/apps.py @@ -2,4 +2,4 @@ class VendorsConfig(AppConfig): - name = 'vendors' + name = "vendors" diff --git a/server/vendors/migrations/0002_gunmen.py b/server/vendors/migrations/0002_gunmen.py new file mode 100644 index 0000000..f4ac8df --- /dev/null +++ b/server/vendors/migrations/0002_gunmen.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.4 on 2021-07-12 17:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('vendors', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Gunmen', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=200)), + ('last_name', models.CharField(max_length=200)), + ('vendor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vendors.Vendor')), + ], + ), + ] diff --git a/server/vendors/migrations/0003_gunmen_email.py b/server/vendors/migrations/0003_gunmen_email.py new file mode 100644 index 0000000..a55d112 --- /dev/null +++ b/server/vendors/migrations/0003_gunmen_email.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.4 on 2021-07-13 18:18 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('vendors', '0002_gunmen'), + ] + + operations = [ + migrations.AddField( + model_name='gunmen', + name='email', + field=models.EmailField(default=django.utils.timezone.now, max_length=254), + preserve_default=False, + ), + ] diff --git a/server/vendors/migrations/0004_vehicle.py b/server/vendors/migrations/0004_vehicle.py new file mode 100644 index 0000000..d47b0bb --- /dev/null +++ b/server/vendors/migrations/0004_vehicle.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.4 on 2021-07-14 11:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('vendors', '0003_gunmen_email'), + ] + + operations = [ + migrations.CreateModel( + name='Vehicle', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('model_name', models.CharField(max_length=200)), + ('number_plate', models.CharField(max_length=200)), + ('vendor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vendors.Vendor')), + ], + ), + ] diff --git a/server/vendors/migrations/0005_custodian.py b/server/vendors/migrations/0005_custodian.py new file mode 100644 index 0000000..7d81434 --- /dev/null +++ b/server/vendors/migrations/0005_custodian.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.4 on 2021-07-19 17:39 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('vendors', '0004_vehicle'), + ] + + operations = [ + migrations.CreateModel( + name='Custodian', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('custodian_type', models.CharField(choices=[('C', 'Custodian'), ('G', 'Gunmen')], default='C', max_length=1)), + ('first_name', models.CharField(max_length=200)), + ('last_name', models.CharField(max_length=200)), + ('email', models.EmailField(max_length=254)), + ('phone_number', models.CharField(blank=True, max_length=17, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", regex='^\\+?1?\\d{9,15}$')])), + ('vendor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='vendors.Vendor')), + ], + ), + ] diff --git a/server/vendors/models.py b/server/vendors/models.py index 2c7f93c..2f013a7 100644 --- a/server/vendors/models.py +++ b/server/vendors/models.py @@ -1,15 +1,65 @@ from django.db import models +from django.core.validators import RegexValidator from django.contrib.auth.models import User +from django.core.validators import RegexValidator from datetime import datetime + class Vendor(models.Model): name = models.CharField(max_length=200) address = models.CharField(max_length=1000) email = models.EmailField() contact = models.CharField(max_length=20) officer_incharge = models.CharField(max_length=100) - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) + created_by = models.ForeignKey( + User, on_delete=models.SET_NULL, blank=True, null=True + ) created_at = models.DateTimeField(default=datetime.now) - + def __str__(self): - return f'{self.name} -> {self.email}' \ No newline at end of file + return f"{self.name} -> {self.email}" + + +class Custodian(models.Model): + CUSTODIAN_TYPE_CHOICES = [("C", "Custodian"), ("G", "Gunmen")] + custodian_type = models.CharField( + max_length=1, default="C", choices=CUSTODIAN_TYPE_CHOICES + ) + + first_name = models.CharField(max_length=200) + last_name = models.CharField(max_length=200) + + email = models.EmailField() + phone_regex = RegexValidator( + regex=r"^\+?1?\d{9,15}$", + message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", + ) + phone_number = models.CharField( + validators=[phone_regex], max_length=17, blank=True) + + vendor = models.ForeignKey( + Vendor, on_delete=models.SET_NULL, null=True, blank=True) + + def __str__(self) -> str: + return f"{self.custodian_type}: {self.first_name} {self.last_name}" + + +class Vehicle(models.Model): + model_name = models.CharField(max_length=200) + number_plate = models.CharField(max_length=200) + vendor = models.ForeignKey( + Vendor, on_delete=models.SET_NULL, null=True, blank=True) + + def __str__(self) -> str: + return f"{self.model_name} -> {self.number_plate}" + + +class Gunmen(models.Model): + first_name = models.CharField(max_length=200) + last_name = models.CharField(max_length=200) + email = models.EmailField() + vendor = models.ForeignKey( + Vendor, on_delete=models.SET_NULL, null=True, blank=True) + + def __str__(self) -> str: + return f"{self.first_name} {self.last_name}" diff --git a/server/vendors/serializers.py b/server/vendors/serializers.py new file mode 100644 index 0000000..d0a5a20 --- /dev/null +++ b/server/vendors/serializers.py @@ -0,0 +1,73 @@ +from rest_framework import serializers +from .models import Vehicle, Vendor, Gunmen, Custodian + + +class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField): + def __init__(self, **kwargs): + self.serializer = kwargs.pop("serializer", None) + if self.serializer is not None and not issubclass( + self.serializer, serializers.Serializer + ): + raise TypeError('"serializer" is not a valid serializer class') + super().__init__(**kwargs) + + def use_pk_only_optimization(self): + return False if self.serializer else True + + def to_representation(self, instance): + if self.serializer: + return self.serializer(instance, context=self.context).data + return super().to_representation(instance) + + +class VendorSerializer(serializers.ModelSerializer): + class Meta: + model = Vendor + fields = [ + "id", + "name", + "address", + "email", + "contact", + "officer_incharge", + "created_by", + "created_at", + ] + + +class GunmenSerializer(serializers.ModelSerializer): + vendor = RelatedFieldAlternative( + queryset=Vendor.objects.all(), serializer=VendorSerializer + ) + + class Meta: + model = Gunmen + fields = ["id", "first_name", "last_name", "email", "vendor"] + + +class CustodianSerializer(serializers.ModelSerializer): + vendor = RelatedFieldAlternative( + queryset=Vendor.objects.all(), serializer=VendorSerializer + ) + + class Meta: + model = Custodian + fields = [ + "id", + "custodian_type", + "first_name", + "last_name", + "email", + "phone_number", + "vendor", + ] + + +class VehicleSerializer(serializers.ModelSerializer): + vendor = RelatedFieldAlternative( + queryset=Vendor.objects.all(), serializer=VendorSerializer + ) + + class Meta: + model = Vehicle + fields = ["id", "model_name", "number_plate", "vendor"] diff --git a/server/vendors/urls.py b/server/vendors/urls.py new file mode 100644 index 0000000..f73d86d --- /dev/null +++ b/server/vendors/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from rest_framework.generics import CreateAPIView +from rest_framework.urlpatterns import format_suffix_patterns +from . import views + +urlpatterns = [ + path("gunmen/", views.GunmenList.as_view()), + path("gunmen//", views.GunmenDetail.as_view()), + path("custodian/", views.CustodianList.as_view()), + path("custodian//", views.CustodianDetail.as_view()), + path("vendor/", views.VendorList.as_view()), + path("vendor//", views.VendorDetail.as_view()), + path("vehicle/", views.VehicleList.as_view()), + path("vehicle//", views.VehicleDetail.as_view()), +] + +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/server/vendors/views.py b/server/vendors/views.py index 91ea44a..7fe6a2d 100644 --- a/server/vendors/views.py +++ b/server/vendors/views.py @@ -1,3 +1,115 @@ -from django.shortcuts import render +from rest_framework import generics +from rest_framework import filters +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response -# Create your views here. +from .models import Custodian, Vehicle, Vendor, Gunmen +from .serializers import ( + VehicleSerializer, + VendorSerializer, + GunmenSerializer, + CustodianSerializer, +) + + +class CustomPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = "page_size" + max_page_size = 1000 + + def get_paginated_response(self, data): + return Response( + { + "links": { + "next": self.get_next_link(), + "previous": self.get_previous_link(), + }, + "count": self.page.paginator.count, + "page_size": self.page_size, + "results": data, + } + ) + + +class CustodianList(generics.ListCreateAPIView): + queryset = Custodian.objects.all() + serializer_class = CustodianSerializer + pagination_class = CustomPagination + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = [ + "^first_name", + "^last_name", + "^phone_number", + "^email", + "^vendor__name", + ] + filterset_fields = [ + "custodian_type", + "first_name", + "last_name", + "email", + "phone_number", + "vendor", + ] + ordering_fields = "__all__" + + +class CustodianDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Custodian.objects.all() + serializer_class = CustodianSerializer + ordering_fields = "__all__" + + +class VendorList(generics.ListCreateAPIView): + queryset = Vendor.objects.all() + serializer_class = VendorSerializer + pagination_class = CustomPagination + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ["^name", "^address", "^contact", "^officer_incharge"] + filterset_fields = [ + "name", + "address", + "email", + "contact", + "officer_incharge", + "created_by", + "created_at", + ] + ordering_fields = "__all__" + + +class VendorDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Vendor.objects.all() + serializer_class = VendorSerializer + ordering_fields = "__all__" + + +class GunmenList(generics.ListCreateAPIView): + queryset = Gunmen.objects.all() + serializer_class = GunmenSerializer + pagination_class = CustomPagination + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ["^first_name", "^last_name"] + filterset_fields = ["first_name", "last_name", "vendor"] + ordering_fields = "__all__" + + +class GunmenDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Gunmen.objects.all() + serializer_class = GunmenSerializer + + +class VehicleList(generics.ListCreateAPIView): + queryset = Vehicle.objects.all() + serializer_class = VehicleSerializer + pagination_class = CustomPagination + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ["^model_name", "^number_plate"] + filterset_fields = ["model_name", "vendor", "number_plate"] + ordering_fields = "__all__" + + +class VehicleDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Vehicle.objects.all() + serializer_class = VehicleSerializer